From c3ba232989300874552f87c9c00abef855421241 Mon Sep 17 00:00:00 2001 From: Xiao Liang Yu <45890886+xiaoly8@users.noreply.github.com> Date: Sat, 12 Jun 2021 06:52:16 +0800 Subject: [PATCH] commit --- .gitignore | 4 + README.md | 23 +- build.sbt | 23 +- project/plugins.sbt | 2 + .../resources/external/src/ddmlib/.classpath | 11 + .../resources/external/src/ddmlib/.gitignore | 2 + .../resources/external/src/ddmlib/.project | 17 + .../.settings/org.eclipse.jdt.core.prefs | 98 ++ src/main/resources/external/src/ddmlib/NOTICE | 190 +++ .../external/src/ddmlib/build.gradle | 28 + .../ddmlib/AdbCommandRejectedException.java | 55 + .../java/com/android/ddmlib/AdbHelper.java | 894 ++++++++++++ .../java/com/android/ddmlib/AdbVersion.java | 100 ++ .../com/android/ddmlib/AllocationInfo.java | 244 ++++ .../com/android/ddmlib/AllocationsParser.java | 198 +++ .../android/ddmlib/AndroidDebugBridge.java | 1183 +++++++++++++++ .../android/ddmlib/BadPacketException.java | 35 + .../com/android/ddmlib/BatteryFetcher.java | 237 +++ .../com/android/ddmlib/ByteBufferUtil.java | 56 + .../com/android/ddmlib/CanceledException.java | 40 + .../java/com/android/ddmlib/ChunkHandler.java | 206 +++ .../main/java/com/android/ddmlib/Client.java | 948 ++++++++++++ .../java/com/android/ddmlib/ClientData.java | 843 +++++++++++ .../ddmlib/CollectingOutputReceiver.java | 71 + .../java/com/android/ddmlib/DdmConstants.java | 65 + .../com/android/ddmlib/DdmPreferences.java | 220 +++ .../com/android/ddmlib/DebugPortManager.java | 70 + .../java/com/android/ddmlib/Debugger.java | 353 +++++ .../main/java/com/android/ddmlib/Device.java | 1295 +++++++++++++++++ .../com/android/ddmlib/DeviceMonitor.java | 884 +++++++++++ .../com/android/ddmlib/EmulatorConsole.java | 783 ++++++++++ .../android/ddmlib/FileListingService.java | 852 +++++++++++ .../com/android/ddmlib/HandleAppName.java | 116 ++ .../java/com/android/ddmlib/HandleExit.java | 76 + .../java/com/android/ddmlib/HandleHeap.java | 401 +++++ .../java/com/android/ddmlib/HandleHello.java | 230 +++ .../com/android/ddmlib/HandleNativeHeap.java | 351 +++++ .../com/android/ddmlib/HandleProfiling.java | 354 +++++ .../java/com/android/ddmlib/HandleTest.java | 86 ++ .../java/com/android/ddmlib/HandleThread.java | 379 +++++ .../com/android/ddmlib/HandleViewDebug.java | 363 +++++ .../java/com/android/ddmlib/HandleWait.java | 91 ++ .../java/com/android/ddmlib/HeapSegment.java | 448 ++++++ .../main/java/com/android/ddmlib/IDevice.java | 639 ++++++++ .../android/ddmlib/IShellEnabledDevice.java | 78 + .../android/ddmlib/IShellOutputReceiver.java | 44 + .../com/android/ddmlib/IStackTraceInfo.java | 29 + .../com/android/ddmlib/InstallException.java | 46 + .../java/com/android/ddmlib/JdwpPacket.java | 371 +++++ .../src/main/java/com/android/ddmlib/Log.java | 359 +++++ .../com/android/ddmlib/MonitorThread.java | 790 ++++++++++ .../com/android/ddmlib/MultiLineReceiver.java | 131 ++ .../android/ddmlib/NativeAllocationInfo.java | 312 ++++ .../android/ddmlib/NativeLibraryMapInfo.java | 73 + .../android/ddmlib/NativeStackCallInfo.java | 117 ++ .../android/ddmlib/NullOutputReceiver.java | 53 + .../com/android/ddmlib/PropertyFetcher.java | 192 +++ .../java/com/android/ddmlib/RawImage.java | 223 +++ .../android/ddmlib/ScreenRecorderOptions.java | 72 + .../ShellCommandUnresponsiveException.java | 27 + .../com/android/ddmlib/SyncException.java | 97 ++ .../java/com/android/ddmlib/SyncService.java | 916 ++++++++++++ .../java/com/android/ddmlib/ThreadInfo.java | 140 ++ .../com/android/ddmlib/TimeoutException.java | 41 + .../android/ddmlib/log/EventContainer.java | 462 ++++++ .../android/ddmlib/log/EventLogParser.java | 585 ++++++++ .../ddmlib/log/EventValueDescription.java | 216 +++ .../android/ddmlib/log/GcEventContainer.java | 347 +++++ .../ddmlib/log/InvalidTypeException.java | 74 + .../ddmlib/log/InvalidValueTypeException.java | 78 + .../com/android/ddmlib/log/LogReceiver.java | 247 ++++ .../android/ddmlib/logcat/LogCatFilter.java | 231 +++ .../android/ddmlib/logcat/LogCatListener.java | 23 + .../android/ddmlib/logcat/LogCatMessage.java | 105 ++ .../ddmlib/logcat/LogCatMessageParser.java | 101 ++ .../ddmlib/logcat/LogCatReceiverTask.java | 136 ++ .../testrunner/IRemoteAndroidTestRunner.java | 255 ++++ .../ddmlib/testrunner/ITestRunListener.java | 117 ++ .../InstrumentationResultParser.java | 629 ++++++++ .../testrunner/RemoteAndroidTestRunner.java | 328 +++++ .../ddmlib/testrunner/TestIdentifier.java | 91 ++ .../android/ddmlib/testrunner/TestResult.java | 145 ++ .../ddmlib/testrunner/TestRunResult.java | 307 ++++ .../ddmlib/testrunner/XmlTestRunListener.java | 323 ++++ .../com/android/ddmlib/utils/ArrayHelper.java | 90 ++ .../android/ddmlib/utils/DebuggerPorts.java | 74 + .../external/src/ddmlib/src/test/.classpath | 9 + .../external/src/ddmlib/src/test/.project | 17 + .../com/android/ddmlib/AdbVersionTest.java | 50 + .../ddmlib/AndroidDebugBridgeTest.java | 70 + .../android/ddmlib/BatteryFetcherTest.java | 90 ++ .../com/android/ddmlib/DeviceMonitorTest.java | 67 + .../java/com/android/ddmlib/DeviceTest.java | 84 ++ .../android/ddmlib/EmulatorConsoleTest.java | 45 + .../android/ddmlib/PropertyFetcherTest.java | 163 +++ .../ddmlib/SysFsBatteryLevelReceiverTest.java | 86 ++ .../allocations/AllocationsParserTest.java | 183 +++ .../ddmlib/logcat/LogCatFilterTest.java | 165 +++ .../logcat/LogCatMessageParserTest.java | 101 ++ .../InstrumentationResultParserTest.java | 533 +++++++ .../RemoteAndroidTestRunnerTest.java | 153 ++ .../ddmlib/testrunner/TestRunResultTest.java | 40 + .../testrunner/XmlTestRunListenerTest.java | 193 +++ .../ddmlib/utils/DebuggerPortsTest.java | 39 + src/main/scala/org/ftd/Master/Config.scala | 25 + .../scala/org/ftd/Master/FTDDebugger.scala | 461 ++++++ src/main/scala/org/ftd/Master/Main.scala | 183 +++ src/main/scala/org/ftd/Master/Message.scala | 29 + .../scala/org/ftd/Master/MessageStore.scala | 51 + src/main/scala/org/ftd/Master/Profiler.scala | 237 +++ .../scala/org/ftd/Master/ProfilerRunner.scala | 104 ++ .../Master/Scheduler/AdaptiveScheduler.scala | 5 + .../Scheduler/DescendingDelayScheduler.scala | 44 + .../Master/Scheduler/MaxDelayScheduler.scala | 32 + .../org/ftd/Master/Scheduler/Scheduler.scala | 16 + .../Master/Scheduler/SchedulerRunner.scala | 61 + .../scala/org/ftd/Master/SourceLine.scala | 22 + .../scala/org/ftd/Master/StackTrace.scala | 31 + .../Strategy/NaturalProfilingStrategy.scala | 41 + .../ftd/Master/Strategy/RerunStrategy.scala | 44 + .../org/ftd/Master/Strategy/Strategy.scala | 12 + .../ftd/Master/Strategy/XiaoScheduling.scala | 46 + .../scala/org/ftd/Master/TestRunner.scala | 83 ++ src/main/scala/org/ftd/Master/utils/CLI.scala | 106 ++ .../scala/org/ftd/Master/utils/Retry.scala | 18 + .../org/ftd/Master/utils/RetryException.scala | 3 + .../org/yxliang01/ftd/MessageTracer.scala | 3 - template.dockerignore | 6 + 128 files changed, 26383 insertions(+), 7 deletions(-) create mode 100644 project/plugins.sbt create mode 100644 src/main/resources/external/src/ddmlib/.classpath create mode 100644 src/main/resources/external/src/ddmlib/.gitignore create mode 100644 src/main/resources/external/src/ddmlib/.project create mode 100644 src/main/resources/external/src/ddmlib/.settings/org.eclipse.jdt.core.prefs create mode 100644 src/main/resources/external/src/ddmlib/NOTICE create mode 100644 src/main/resources/external/src/ddmlib/build.gradle create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbCommandRejectedException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbVersion.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationInfo.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationsParser.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AndroidDebugBridge.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BadPacketException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BatteryFetcher.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ByteBufferUtil.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CanceledException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ChunkHandler.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Client.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ClientData.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmConstants.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmPreferences.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DebugPortManager.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Debugger.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Device.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DeviceMonitor.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/EmulatorConsole.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/FileListingService.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleAppName.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleExit.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHeap.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHello.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleNativeHeap.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleProfiling.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleThread.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleViewDebug.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleWait.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HeapSegment.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IDevice.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellEnabledDevice.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellOutputReceiver.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IStackTraceInfo.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/InstallException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/JdwpPacket.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Log.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MonitorThread.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MultiLineReceiver.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeAllocationInfo.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeLibraryMapInfo.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NullOutputReceiver.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/PropertyFetcher.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/RawImage.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ScreenRecorderOptions.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ShellCommandUnresponsiveException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncService.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ThreadInfo.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/TimeoutException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventContainer.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventLogParser.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventValueDescription.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/GcEventContainer.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidTypeException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidValueTypeException.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/LogReceiver.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatFilter.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatListener.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessage.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessageParser.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatReceiverTask.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/IRemoteAndroidTestRunner.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/ITestRunListener.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/InstrumentationResultParser.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestIdentifier.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestResult.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestRunResult.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/XmlTestRunListener.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/ArrayHelper.java create mode 100644 src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/DebuggerPorts.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/.classpath create mode 100644 src/main/resources/external/src/ddmlib/src/test/.project create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AdbVersionTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AndroidDebugBridgeTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/BatteryFetcherTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceMonitorTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/EmulatorConsoleTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/PropertyFetcherTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/SysFsBatteryLevelReceiverTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/allocations/AllocationsParserTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatFilterTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatMessageParserTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/TestRunResultTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java create mode 100644 src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/utils/DebuggerPortsTest.java create mode 100644 src/main/scala/org/ftd/Master/Config.scala create mode 100644 src/main/scala/org/ftd/Master/FTDDebugger.scala create mode 100644 src/main/scala/org/ftd/Master/Main.scala create mode 100644 src/main/scala/org/ftd/Master/Message.scala create mode 100644 src/main/scala/org/ftd/Master/MessageStore.scala create mode 100644 src/main/scala/org/ftd/Master/Profiler.scala create mode 100644 src/main/scala/org/ftd/Master/ProfilerRunner.scala create mode 100644 src/main/scala/org/ftd/Master/Scheduler/AdaptiveScheduler.scala create mode 100644 src/main/scala/org/ftd/Master/Scheduler/DescendingDelayScheduler.scala create mode 100644 src/main/scala/org/ftd/Master/Scheduler/MaxDelayScheduler.scala create mode 100644 src/main/scala/org/ftd/Master/Scheduler/Scheduler.scala create mode 100644 src/main/scala/org/ftd/Master/Scheduler/SchedulerRunner.scala create mode 100644 src/main/scala/org/ftd/Master/SourceLine.scala create mode 100644 src/main/scala/org/ftd/Master/StackTrace.scala create mode 100644 src/main/scala/org/ftd/Master/Strategy/NaturalProfilingStrategy.scala create mode 100644 src/main/scala/org/ftd/Master/Strategy/RerunStrategy.scala create mode 100644 src/main/scala/org/ftd/Master/Strategy/Strategy.scala create mode 100644 src/main/scala/org/ftd/Master/Strategy/XiaoScheduling.scala create mode 100644 src/main/scala/org/ftd/Master/TestRunner.scala create mode 100644 src/main/scala/org/ftd/Master/utils/CLI.scala create mode 100644 src/main/scala/org/ftd/Master/utils/Retry.scala create mode 100644 src/main/scala/org/ftd/Master/utils/RetryException.scala delete mode 100644 src/scala/org/yxliang01/ftd/MessageTracer.scala create mode 100644 template.dockerignore diff --git a/.gitignore b/.gitignore index a60bfc5..59c843a 100644 --- a/.gitignore +++ b/.gitignore @@ -313,3 +313,7 @@ $RECYCLE.BIN/ *.lnk # End of https://www.gitignore.io/api/git,sbt,code,java,linux,macos,scala,android,windows,intellij + +# Custom by xiao +.idea +.dockerignore \ No newline at end of file diff --git a/README.md b/README.md index 5e0019e..6ee7e00 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,28 @@ +FlakeScanner +=== + +A tool for detecting Android flaky tests. This is considered alpha version, active development and improvements are on going and will be pushed soon. We would like to also welcome any contributions from you! + +This is an implementation of the paper "Flaky Test Detection in Android via Event Order Exploration" to appear in FSE 2021. + Setup === -Require `sbt` installed (ver 1.3.7 tested). +Requirement +--- +- `java` (> 1.8) +- `sbt` (ver 1.3.7 tested) +- `gradle` -Run command `sbt` to build. +Run command `sbt` to build. Instruction for using `sbt` can be found online. Development Note === - Please work on personal branches, instead of always pushing to master -- For pushing work to master, please use PR \ No newline at end of file +- For pushing work to master, please use PR + +Developed by +=== +Xiao Liang Yu + +Zhen Dong diff --git a/build.sbt b/build.sbt index 1b50c7b..71cbb33 100644 --- a/build.sbt +++ b/build.sbt @@ -1 +1,22 @@ -name := "xiao-ftd" \ No newline at end of file +organization := "org.ftd" +name := "xiao-ftd" + +scalaVersion := "2.13.1" + +libraryDependencies += "com.github.scopt" %% "scopt" % "4.0.0-RC2" +libraryDependencies += "com.google.guava" % "guava" % "28.2-jre" +libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.13.0" +libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.13.0" + +mainClass in Compile := Some("org.ftd.Master.utils.CLI") + +enablePlugins(JavaAppPackaging) +enablePlugins(DockerPlugin) + +maintainer in Docker := "xiao" +daemonUser in Docker := "ftd-user" +defaultLinuxInstallLocation in Docker := "/ftd" +dockerAlias := dockerAlias.value.withName("ftd").withTag(Option("intermediate-runner")) +dockerBaseImage := "openjdk:11" +dockerAutoremoveMultiStageIntermediateImages in Docker := false + diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..0595b24 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.1") diff --git a/src/main/resources/external/src/ddmlib/.classpath b/src/main/resources/external/src/ddmlib/.classpath new file mode 100644 index 0000000..a5a8aaa --- /dev/null +++ b/src/main/resources/external/src/ddmlib/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/resources/external/src/ddmlib/.gitignore b/src/main/resources/external/src/ddmlib/.gitignore new file mode 100644 index 0000000..81631c6 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/.gitignore @@ -0,0 +1,2 @@ +/bin +/build diff --git a/src/main/resources/external/src/ddmlib/.project b/src/main/resources/external/src/ddmlib/.project new file mode 100644 index 0000000..fea25c7 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/.project @@ -0,0 +1,17 @@ + + + ddmlib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/src/main/resources/external/src/ddmlib/.settings/org.eclipse.jdt.core.prefs b/src/main/resources/external/src/ddmlib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..ea66196 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,98 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/src/main/resources/external/src/ddmlib/NOTICE b/src/main/resources/external/src/ddmlib/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/src/main/resources/external/src/ddmlib/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/src/main/resources/external/src/ddmlib/build.gradle b/src/main/resources/external/src/ddmlib/build.gradle new file mode 100644 index 0000000..fd300c1 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'java' +apply plugin: 'jacoco' +apply plugin: 'sdk-java-lib' + +group = 'com.android.tools.ddms' +archivesBaseName = 'ddmlib' +version = rootProject.ext.baseVersion + +dependencies { + compile project(':base:common') + + compile 'net.sf.kxml:kxml2:2.3.0' + + testCompile 'org.easymock:easymock:3.1' + testCompile 'junit:junit:4.12' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + test.resources.srcDir 'src/test/java' +} + +project.ext.pomName = 'Android Tools ddmlib' +project.ext.pomDesc = 'Library providing APIs to talk to Android devices' + +apply from: "$rootDir/buildSrc/base/publish.gradle" +apply from: "$rootDir/buildSrc/base/bintray.gradle" +apply from: "$rootDir/buildSrc/base/javadoc.gradle" diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbCommandRejectedException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbCommandRejectedException.java new file mode 100644 index 0000000..ae7d014 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbCommandRejectedException.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + + +/** + * Exception thrown when adb refuses a command. + */ +public class AdbCommandRejectedException extends Exception { + private static final long serialVersionUID = 1L; + private final boolean mIsDeviceOffline; + private final boolean mErrorDuringDeviceSelection; + + AdbCommandRejectedException(String message) { + super(message); + mIsDeviceOffline = "device offline".equals(message); + mErrorDuringDeviceSelection = false; + } + + AdbCommandRejectedException(String message, boolean errorDuringDeviceSelection) { + super(message); + mErrorDuringDeviceSelection = errorDuringDeviceSelection; + mIsDeviceOffline = "device offline".equals(message); + } + + /** + * Returns true if the error is due to the device being offline. + */ + public boolean isDeviceOffline() { + return mIsDeviceOffline; + } + + /** + * Returns whether adb refused to target a given device for the command. + *

If false, adb refused the command itself, if true, it refused to target the given + * device. + */ + public boolean wasErrorDuringDeviceSelection() { + return mErrorDuringDeviceSelection; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java new file mode 100644 index 0000000..b390f93 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java @@ -0,0 +1,894 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.Nullable; +import com.android.ddmlib.log.LogReceiver; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; +import java.util.concurrent.TimeUnit; + +/** + * Helper class to handle requests and connections to adb. + *

{@link AndroidDebugBridge} is the public API to connection to adb, while {@link AdbHelper} + * does the low level stuff. + *

This currently uses spin-wait non-blocking I/O. A Selector would be more efficient, + * but seems like overkill for what we're doing here. + */ +final class AdbHelper { + + // public static final long kOkay = 0x59414b4fL; + // public static final long kFail = 0x4c494146L; + + static final int WAIT_TIME = 5; // spin-wait sleep, in ms + + static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ + + /** do not instantiate */ + private AdbHelper() { + } + + /** + * Response from ADB. + */ + static class AdbResponse { + public AdbResponse() { + message = ""; + } + + public boolean okay; // first 4 bytes in response were "OKAY"? + + public String message; // diagnostic string if #okay is false + } + + /** + * Create and connect a new pass-through socket, from the host to a port on + * the device. + * + * @param adbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param devicePort the port we're opening + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + */ + public static SocketChannel open(InetSocketAddress adbSockAddr, + Device device, int devicePort) + throws IOException, TimeoutException, AdbCommandRejectedException { + + SocketChannel adbChan = SocketChannel.open(adbSockAddr); + try { + adbChan.socket().setTcpNoDelay(true); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk to a specific device + setDevice(adbChan, device); + + byte[] req = createAdbForwardRequest(null, devicePort); + // Log.hexDump(req); + + write(adbChan, req); + + AdbResponse resp = readAdbResponse(adbChan, false); + if (!resp.okay) { + throw new AdbCommandRejectedException(resp.message); + } + + adbChan.configureBlocking(true); + } catch (TimeoutException e) { + adbChan.close(); + throw e; + } catch (IOException e) { + adbChan.close(); + throw e; + } catch (AdbCommandRejectedException e) { + adbChan.close(); + throw e; + } + + return adbChan; + } + + /** + * Creates and connects a new pass-through socket, from the host to a port on + * the device. + * + * @param adbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param pid the process pid to connect to. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr, + Device device, int pid) + throws TimeoutException, AdbCommandRejectedException, IOException { + + SocketChannel adbChan = SocketChannel.open(adbSockAddr); + try { + adbChan.socket().setTcpNoDelay(true); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk to a specific device + setDevice(adbChan, device); + + byte[] req = createJdwpForwardRequest(pid); + // Log.hexDump(req); + + write(adbChan, req); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) { + throw new AdbCommandRejectedException(resp.message); + } + + adbChan.configureBlocking(true); + } catch (TimeoutException e) { + adbChan.close(); + throw e; + } catch (IOException e) { + adbChan.close(); + throw e; + } catch (AdbCommandRejectedException e) { + adbChan.close(); + throw e; + } + + return adbChan; + } + + /** + * Creates a port forwarding request for adb. This returns an array + * containing "####tcp:{port}:{addStr}". + * @param addrStr the host. Can be null. + * @param port the port on the device. This does not need to be numeric. + */ + private static byte[] createAdbForwardRequest(String addrStr, int port) { + String reqStr; + + if (addrStr == null) + reqStr = "tcp:" + port; + else + reqStr = "tcp:" + port + ":" + addrStr; + return formAdbRequest(reqStr); + } + + /** + * Creates a port forwarding request to a jdwp process. This returns an array + * containing "####jwdp:{pid}". + * @param pid the jdwp process pid on the device. + */ + private static byte[] createJdwpForwardRequest(int pid) { + String reqStr = String.format("jdwp:%1$d", pid); //$NON-NLS-1$ + return formAdbRequest(reqStr); + } + + /** + * Create an ASCII string preceded by four hex digits. The opening "####" + * is the length of the rest of the string, encoded as ASCII hex (case + * doesn't matter). + */ + public static byte[] formAdbRequest(String req) { + String resultStr = String.format("%04X%s", req.length(), req); //$NON-NLS-1$ + byte[] result; + try { + result = resultStr.getBytes(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + return null; + } + assert result.length == req.length() + 4; + return result; + } + + /** + * Reads the response from ADB after a command. + * @param chan The socket channel that is connected to adb. + * @param readDiagString If true, we're expecting an OKAY response to be + * followed by a diagnostic string. Otherwise, we only expect the + * diagnostic string to follow a FAIL. + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString) + throws TimeoutException, IOException { + + AdbResponse resp = new AdbResponse(); + + byte[] reply = new byte[4]; + read(chan, reply); + + if (isOkay(reply)) { + resp.okay = true; + } else { + readDiagString = true; // look for a reason after the FAIL + resp.okay = false; + } + + // not a loop -- use "while" so we can use "break" + try { + while (readDiagString) { + // length string is in next 4 bytes + byte[] lenBuf = new byte[4]; + read(chan, lenBuf); + + String lenStr = replyToString(lenBuf); + + int len; + try { + len = Integer.parseInt(lenStr, 16); + } catch (NumberFormatException nfe) { + Log.w("ddms", "Expected digits, got '" + lenStr + "': " + + lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " " + + lenBuf[3]); + Log.w("ddms", "reply was " + replyToString(reply)); + break; + } + + byte[] msg = new byte[len]; + read(chan, msg); + + resp.message = replyToString(msg); + Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='" + + resp.message + "'"); + + break; + } + } catch (Exception e) { + // ignore those, since it's just reading the diagnose string, the response will + // contain okay==false anyway. + } + + return resp; + } + + /** + * Retrieve the frame buffer from the device with the given timeout. A timeout of 0 indicates + * that it will wait forever. + * + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device, long timeout, + TimeUnit unit) + throws TimeoutException, AdbCommandRejectedException, IOException { + + RawImage imageParams = new RawImage(); + byte[] request = formAdbRequest("framebuffer:"); //$NON-NLS-1$ + byte[] nudge = { + 0 + }; + byte[] reply; + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + write(adbChan, request); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) { + throw new AdbCommandRejectedException(resp.message); + } + + // first the protocol version. + reply = new byte[4]; + read(adbChan, reply); + + ByteBuffer buf = ByteBuffer.wrap(reply); + buf.order(ByteOrder.LITTLE_ENDIAN); + + int version = buf.getInt(); + + // get the header size (this is a count of int) + int headerSize = RawImage.getHeaderSize(version); + + // read the header + reply = new byte[headerSize * 4]; + read(adbChan, reply); + + buf = ByteBuffer.wrap(reply); + buf.order(ByteOrder.LITTLE_ENDIAN); + + // fill the RawImage with the header + if (!imageParams.readHeader(version, buf)) { + Log.e("Screenshot", "Unsupported protocol: " + version); + return null; + } + + Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size=" + + imageParams.size + ", width=" + imageParams.width + + ", height=" + imageParams.height); + + write(adbChan, nudge); + + reply = new byte[imageParams.size]; + read(adbChan, reply, imageParams.size, unit.toMillis(timeout)); + + imageParams.data = reply; + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + + return imageParams; + } + + /** + * @deprecated Use {@link #executeRemoteCommand(java.net.InetSocketAddress, String, IDevice, IShellOutputReceiver, long, java.util.concurrent.TimeUnit)}. + */ + @Deprecated + static void executeRemoteCommand(InetSocketAddress adbSockAddr, + String command, IDevice device, IShellOutputReceiver rcvr, int maxTimeToOutputResponse) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + executeRemoteCommand(adbSockAddr, command, device, rcvr, maxTimeToOutputResponse, + TimeUnit.MILLISECONDS); + } + + /** + * Executes a shell command on the device and retrieve the output. The output is + * handed to rcvr as it arrives. + * + * @param adbSockAddr the {@link InetSocketAddress} to adb. + * @param command the shell command to execute + * @param device the {@link IDevice} on which to execute the command. + * @param rcvr the {@link IShellOutputReceiver} that will receives the output of the shell + * command + * @param maxTimeToOutputResponse max time between command output. If more time passes + * between command output, the method will throw + * {@link ShellCommandUnresponsiveException}. A value of 0 means the method will + * wait forever for command output and never throw. + * @param maxTimeUnits Units for non-zero {@code maxTimeToOutputResponse} values. + * @throws TimeoutException in case of timeout on the connection when sending the command. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output + * for a period longer than maxTimeToOutputResponse. + * @throws IOException in case of I/O error on the connection. + * + * @see DdmPreferences#getTimeOut() + */ + static void executeRemoteCommand(InetSocketAddress adbSockAddr, + String command, IDevice device, IShellOutputReceiver rcvr, long maxTimeToOutputResponse, + TimeUnit maxTimeUnits) throws TimeoutException, AdbCommandRejectedException, + ShellCommandUnresponsiveException, IOException { + + executeRemoteCommand(adbSockAddr, AdbService.SHELL, command, device, rcvr, maxTimeToOutputResponse, + maxTimeUnits, null /* inputStream */); + } + + /** + * Identify which adb service the command should target. + */ + public enum AdbService { + /** + * the shell service + */ + SHELL, + + /** + * The exec service. + */ + EXEC + } + + /** + * Executes a remote command on the device and retrieve the output. The output is + * handed to rcvr as it arrives. The command is execute by the remote service + * identified by the adbService parameter. + * + * @param adbSockAddr the {@link InetSocketAddress} to adb. + * @param adbService the {@link com.android.ddmlib.AdbHelper.AdbService} to use to run the + * command. + * @param command the shell command to execute + * @param device the {@link IDevice} on which to execute the command. + * @param rcvr the {@link IShellOutputReceiver} that will receives the output of the shell + * command + * @param maxTimeToOutputResponse max time between command output. If more time passes + * between command output, the method will throw + * {@link ShellCommandUnresponsiveException}. A value of 0 means the method will + * wait forever for command output and never throw. + * @param maxTimeUnits Units for non-zero {@code maxTimeToOutputResponse} values. + * @param is a optional {@link InputStream} to be streamed up after invoking the command + * and before retrieving the response. + * @throws TimeoutException in case of timeout on the connection when sending the command. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output + * for a period longer than maxTimeToOutputResponse. + * @throws IOException in case of I/O error on the connection. + * + * @see DdmPreferences#getTimeOut() + */ + static void executeRemoteCommand(InetSocketAddress adbSockAddr, AdbService adbService, + String command, IDevice device, IShellOutputReceiver rcvr, long maxTimeToOutputResponse, + TimeUnit maxTimeUnits, + @Nullable InputStream is) throws TimeoutException, AdbCommandRejectedException, + ShellCommandUnresponsiveException, IOException { + + long maxTimeToOutputMs = 0; + if (maxTimeToOutputResponse > 0) { + if (maxTimeUnits == null) { + throw new NullPointerException("Time unit must not be null for non-zero max."); + } + maxTimeToOutputMs = maxTimeUnits.toMillis(maxTimeToOutputResponse); + } + + Log.v("ddms", "execute: running " + command); + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk + // to a specific device + setDevice(adbChan, device); + + byte[] request = formAdbRequest(adbService.name().toLowerCase() + ":" + command); //$NON-NLS-1$ + write(adbChan, request); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) { + Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message); + throw new AdbCommandRejectedException(resp.message); + } + + byte[] data = new byte[16384]; + + // stream the input file if present. + if (is != null) { + int read; + while ((read = is.read(data)) != -1) { + ByteBuffer buf = ByteBuffer.wrap(data, 0, read); + int written = 0; + while (buf.hasRemaining()) { + written += adbChan.write(buf); + } + if (written != read) { + Log.e("ddms", + "ADB write inconsistency, wrote " + written + "expected " + read); + throw new AdbCommandRejectedException("write failed"); + } + } + } + + ByteBuffer buf = ByteBuffer.wrap(data); + buf.clear(); + long timeToResponseCount = 0; + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + Log.v("ddms", "execute: cancelled"); + break; + } + + count = adbChan.read(buf); + if (count < 0) { + // we're at the end, we flush the output + rcvr.flush(); + Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: " + + count); + break; + } else if (count == 0) { + try { + int wait = WAIT_TIME * 5; + timeToResponseCount += wait; + if (maxTimeToOutputMs > 0 && timeToResponseCount > maxTimeToOutputMs) { + throw new ShellCommandUnresponsiveException(); + } + Thread.sleep(wait); + } catch (InterruptedException ie) { + } + } else { + // reset timeout + timeToResponseCount = 0; + + // send data to receiver if present + if (rcvr != null) { + rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + Log.v("ddms", "execute: returning"); + } + } + + /** + * Runs the Event log service on the {@link Device}, and provides its output to the + * {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * @param adbSockAddr the socket address to connect to adb + * @param device the Device on which to run the service + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void runEventLogService(InetSocketAddress adbSockAddr, Device device, + LogReceiver rcvr) throws TimeoutException, AdbCommandRejectedException, IOException { + runLogService(adbSockAddr, device, "events", rcvr); //$NON-NLS-1$ + } + + /** + * Runs a log service on the {@link Device}, and provides its output to the {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * @param adbSockAddr the socket address to connect to adb + * @param device the Device on which to run the service + * @param logName the name of the log file to output + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName, + LogReceiver rcvr) throws TimeoutException, AdbCommandRejectedException, IOException { + SocketChannel adbChan = null; + + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + byte[] request = formAdbRequest("log:" + logName); + write(adbChan, request); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) { + throw new AdbCommandRejectedException(resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + break; + } + + count = adbChan.read(buf); + if (count < 0) { + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + } + + /** + * Creates a port forwarding between a local and a remote port. + * @param adbSockAddr the socket address to connect to adb + * @param device the device on which to do the port forwarding + * @param localPortSpec specification of the local port to forward, should be of format + * tcp: + * @param remotePortSpec specification of the remote port to forward to, one of: + * tcp: + * localabstract: + * localreserved: + * localfilesystem: + * dev: + * jdwp: (remote only) + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void createForward(InetSocketAddress adbSockAddr, Device device, + String localPortSpec, String remotePortSpec) + throws TimeoutException, AdbCommandRejectedException, IOException { + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:forward:%2$s;%3$s", //$NON-NLS-1$ + device.getSerialNumber(), localPortSpec, remotePortSpec)); + + write(adbChan, request); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) { + Log.w("create-forward", "Error creating forward: " + resp.message); + throw new AdbCommandRejectedException(resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + } + + /** + * Remove a port forwarding between a local and a remote port. + * @param adbSockAddr the socket address to connect to adb + * @param device the device on which to remove the port forwarding + * @param localPortSpec specification of the local port that was forwarded, should be of format + * tcp: + * @param remotePortSpec specification of the remote port forwarded to, one of: + * tcp: + * localabstract: + * localreserved: + * localfilesystem: + * dev: + * jdwp: (remote only) + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void removeForward(InetSocketAddress adbSockAddr, Device device, + String localPortSpec, String remotePortSpec) + throws TimeoutException, AdbCommandRejectedException, IOException { + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:killforward:%2$s", //$NON-NLS-1$ + device.getSerialNumber(), localPortSpec)); + + write(adbChan, request); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) { + Log.w("remove-forward", "Error creating forward: " + resp.message); + throw new AdbCommandRejectedException(resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + } + + /** + * Checks to see if the first four bytes in "reply" are OKAY. + */ + static boolean isOkay(byte[] reply) { + return reply[0] == (byte)'O' && reply[1] == (byte)'K' + && reply[2] == (byte)'A' && reply[3] == (byte)'Y'; + } + + /** + * Converts an ADB reply to a string. + */ + static String replyToString(byte[] reply) { + String result; + try { + result = new String(reply, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + result = ""; + } + return result; + } + + /** + * Reads from the socket until the array is filled, or no more data is coming (because + * the socket closed or the timeout expired). + *

This uses the default time out value. + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static void read(SocketChannel chan, byte[] data) throws TimeoutException, IOException { + read(chan, data, -1, DdmPreferences.getTimeOut()); + } + + /** + * Reads from the socket until the array is filled, the optional length + * is reached, or no more data is coming (because the socket closed or the + * timeout expired). After "timeout" milliseconds since the + * previous successful read, this will return whether or not new data has + * been found. + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @param length the length to read or -1 to fill the data buffer completely + * @param timeout The timeout value in ms. A timeout of zero means "wait forever". + */ + static void read(SocketChannel chan, byte[] data, int length, long timeout) + throws TimeoutException, IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.read(buf); + if (count < 0) { + Log.d("ddms", "read: channel EOF"); + throw new IOException("EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.d("ddms", "read: timeout"); + throw new TimeoutException(); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * Write until all data in "data" is written or the connection fails or times out. + *

This uses the default time out value. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static void write(SocketChannel chan, byte[] data) throws TimeoutException, IOException { + write(chan, data, -1, DdmPreferences.getTimeOut()); + } + + /** + * Write until all data in "data" is written, the optional length is reached, + * the timeout expires, or the connection fails. Returns "true" if all + * data was written. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @param length the length to write or -1 to send the whole buffer. + * @param timeout The timeout value. A timeout of zero means "wait forever". + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static void write(SocketChannel chan, byte[] data, int length, int timeout) + throws TimeoutException, IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.write(buf); + if (count < 0) { + Log.d("ddms", "write: channel EOF"); + throw new IOException("channel EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.d("ddms", "write: timeout"); + throw new TimeoutException(); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * tells adb to talk to a specific device + * + * @param adbChan the socket connection to adb + * @param device The device to talk to. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + static void setDevice(SocketChannel adbChan, IDevice device) + throws TimeoutException, AdbCommandRejectedException, IOException { + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + if (device != null) { + String msg = "host:transport:" + device.getSerialNumber(); //$NON-NLS-1$ + byte[] device_query = formAdbRequest(msg); + + write(adbChan, device_query); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) { + throw new AdbCommandRejectedException(resp.message, + true/*errorDuringDeviceSelection*/); + } + } + } + + /** + * Reboot the device. + * + * @param into what to reboot into (recovery, bootloader). Or null to just reboot. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void reboot(String into, InetSocketAddress adbSockAddr, + Device device) throws TimeoutException, AdbCommandRejectedException, IOException { + byte[] request; + if (into == null) { + request = formAdbRequest("reboot:"); //$NON-NLS-1$ + } else { + request = formAdbRequest("reboot:" + into); //$NON-NLS-1$ + } + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + write(adbChan, request); + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbVersion.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbVersion.java new file mode 100644 index 0000000..92de6ee --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AdbVersion.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import com.android.annotations.NonNull; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AdbVersion implements Comparable { + public static final AdbVersion UNKNOWN = new AdbVersion(-1, -1, -1); + + /** Matches e.g. ".... 1.0.32" */ + private static final Pattern ADB_VERSION_PATTERN = Pattern.compile( + "^.*(\\d+)\\.(\\d+)\\.(\\d+).*"); + + public final int major; + public final int minor; + public final int micro; + + private AdbVersion(int major, int minor, int micro) { + this.major = major; + this.minor = minor; + this.micro = micro; + } + + @Override + public String toString() { + return String.format(Locale.US, "%1$d.%2$d.%3$d", major, minor, micro); + } + + @Override + public int compareTo(AdbVersion o) { + if (major != o.major) { + return major - o.major; + } + + if (minor != o.minor) { + return minor - o.minor; + } + + return micro - o.micro; + } + + @NonNull + public static AdbVersion parseFrom(@NonNull String input) { + Matcher matcher = ADB_VERSION_PATTERN.matcher(input); + if (matcher.matches()) { + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + int micro = Integer.parseInt(matcher.group(3)); + return new AdbVersion(major, minor, micro); + } else { + return UNKNOWN; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AdbVersion version = (AdbVersion) o; + + if (major != version.major) { + return false; + } + if (minor != version.minor) { + return false; + } + return micro == version.micro; + + } + + @Override + public int hashCode() { + int result = major; + result = 31 * result + minor; + result = 31 * result + micro; + return result; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationInfo.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationInfo.java new file mode 100644 index 0000000..4aa6d1e --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationInfo.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +/** + * Holds an Allocation information. + */ +public class AllocationInfo implements IStackTraceInfo { + private final String mAllocatedClass; + private final int mAllocNumber; + private final int mAllocationSize; + private final short mThreadId; + private final StackTraceElement[] mStackTrace; + + public enum SortMode { + NUMBER, SIZE, CLASS, THREAD, ALLOCATION_SITE, IN_CLASS, IN_METHOD + } + + public static final class AllocationSorter implements Comparator { + + private SortMode mSortMode = SortMode.SIZE; + private boolean mDescending = true; + + public AllocationSorter() { + } + + public void setSortMode(@NonNull SortMode mode) { + if (mSortMode == mode) { + mDescending = !mDescending; + } else { + mSortMode = mode; + } + } + + public void setSortMode(@NonNull SortMode mode, boolean descending) { + mSortMode = mode; + mDescending = descending; + } + + @NonNull + public SortMode getSortMode() { + return mSortMode; + } + + public boolean isDescending() { + return mDescending; + } + + @Override + public int compare(AllocationInfo o1, AllocationInfo o2) { + int diff = 0; + switch (mSortMode) { + case NUMBER: + diff = o1.mAllocNumber - o2.mAllocNumber; + break; + case SIZE: + // pass, since diff is init with 0, we'll use SIZE compare below + // as a back up anyway. + break; + case CLASS: + diff = o1.mAllocatedClass.compareTo(o2.mAllocatedClass); + break; + case THREAD: + diff = o1.mThreadId - o2.mThreadId; + break; + case IN_CLASS: + String class1 = o1.getFirstTraceClassName(); + String class2 = o2.getFirstTraceClassName(); + diff = compareOptionalString(class1, class2); + break; + case IN_METHOD: + String method1 = o1.getFirstTraceMethodName(); + String method2 = o2.getFirstTraceMethodName(); + diff = compareOptionalString(method1, method2); + break; + case ALLOCATION_SITE: + String desc1 = o1.getAllocationSite(); + String desc2 = o2.getAllocationSite(); + diff = compareOptionalString(desc1, desc2); + break; + } + + if (diff == 0) { + // same? compare on size + diff = o1.mAllocationSize - o2.mAllocationSize; + } + + if (mDescending) { + diff = -diff; + } + + return diff; + } + + /** compares two strings that could be null */ + private static int compareOptionalString(String str1, String str2) { + if (str1 != null) { + if (str2 == null) { + return -1; + } else { + return str1.compareTo(str2); + } + } else { + if (str2 == null) { + return 0; + } else { + return 1; + } + } + } + } + + /* + * Simple constructor. + */ + AllocationInfo(int allocNumber, String allocatedClass, int allocationSize, + short threadId, StackTraceElement[] stackTrace) { + mAllocNumber = allocNumber; + mAllocatedClass = allocatedClass; + mAllocationSize = allocationSize; + mThreadId = threadId; + mStackTrace = stackTrace; + } + + /** + * Returns the allocation number. Allocations are numbered as they happen with the most + * recent one having the highest number + */ + public int getAllocNumber() { + return mAllocNumber; + } + + /** + * Returns the name of the allocated class. + */ + public String getAllocatedClass() { + return mAllocatedClass; + } + + /** + * Returns the size of the allocation. + */ + public int getSize() { + return mAllocationSize; + } + + /** + * Returns the id of the thread that performed the allocation. + */ + public short getThreadId() { + return mThreadId; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IStackTraceInfo#getStackTrace() + */ + @Override + public StackTraceElement[] getStackTrace() { + return mStackTrace; + } + + public int compareTo(AllocationInfo otherAlloc) { + return otherAlloc.mAllocationSize - mAllocationSize; + } + + @Nullable + public String getAllocationSite() { + if (mStackTrace.length > 0) { + return mStackTrace[0].toString(); + } + return null; + } + + public String getFirstTraceClassName() { + if (mStackTrace.length > 0) { + return mStackTrace[0].getClassName(); + } + + return null; + } + + public String getFirstTraceMethodName() { + if (mStackTrace.length > 0) { + return mStackTrace[0].getMethodName(); + } + + return null; + } + + /** + * Returns true if the given filter matches case insensitively (according to + * the given locale) this allocation info. + */ + public boolean filter(String filter, boolean fullTrace, Locale locale) { + return allocatedClassMatches(filter, locale) || !getMatchingStackFrames(filter, fullTrace, locale).isEmpty(); + } + + public boolean allocatedClassMatches(@NonNull String pattern, @NonNull Locale locale) { + return mAllocatedClass.toLowerCase(locale).contains(pattern.toLowerCase(locale)); + } + + @NonNull + public List getMatchingStackFrames(@NonNull String filter, boolean fullTrace, @NonNull Locale locale) { + filter = filter.toLowerCase(locale); + // check the top of the stack trace always + if (mStackTrace.length > 0) { + final int length = fullTrace ? mStackTrace.length : 1; + List matchingFrames = Lists.newArrayListWithExpectedSize(length); + for (int i = 0; i < length; ++i) { + String frameString = mStackTrace[i].toString(); + if (frameString.toLowerCase(locale).contains(filter)) { + matchingFrames.add(frameString); + } + } + return matchingFrames; + } else { + return Collections.emptyList(); + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationsParser.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationsParser.java new file mode 100644 index 0000000..7d6aa0b --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AllocationsParser.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import com.android.annotations.NonNull; + +import java.nio.ByteBuffer; + +public class AllocationsParser { + /** + * Converts a VM class descriptor string ("Landroid/os/Debug;") to + * a dot-notation class name ("android.os.Debug"). + */ + private static String descriptorToDot(String str) { + // count the number of arrays. + int array = 0; + while (str.startsWith("[")) { + str = str.substring(1); + array++; + } + + int len = str.length(); + + /* strip off leading 'L' and trailing ';' if appropriate */ + if (len >= 2 && str.charAt(0) == 'L' && str.charAt(len - 1) == ';') { + str = str.substring(1, len-1); + str = str.replace('/', '.'); + } else { + // convert the basic types + if ("C".equals(str)) { + str = "char"; + } else if ("B".equals(str)) { + str = "byte"; + } else if ("Z".equals(str)) { + str = "boolean"; + } else if ("S".equals(str)) { + str = "short"; + } else if ("I".equals(str)) { + str = "int"; + } else if ("J".equals(str)) { + str = "long"; + } else if ("F".equals(str)) { + str = "float"; + } else if ("D".equals(str)) { + str = "double"; + } + } + + // now add the array part + for (int a = 0 ; a < array; a++) { + str += "[]"; + } + + return str; + } + + /** + * Reads a string table out of "data". + * + * This is just a serial collection of strings, each of which is a + * four-byte length followed by UTF-16 data. + */ + private static void readStringTable(ByteBuffer data, String[] strings) { + int count = strings.length; + int i; + + for (i = 0; i < count; i++) { + int nameLen = data.getInt(); + String descriptor = ByteBufferUtil.getString(data, nameLen); + strings[i] = descriptorToDot(descriptor); + } + } + + /* + * Message format: + * Message header (all values big-endian): + * (1b) message header len (to allow future expansion); includes itself + * (1b) entry header len + * (1b) stack frame len + * (2b) number of entries + * (4b) offset to string table from start of message + * (2b) number of class name strings + * (2b) number of method name strings + * (2b) number of source file name strings + * For each entry: + * (4b) total allocation size + * (2b) threadId + * (2b) allocated object's class name index + * (1b) stack depth + * For each stack frame: + * (2b) method's class name + * (2b) method name + * (2b) method source file + * (2b) line number, clipped to 32767; -2 if native; -1 if no source + * (xb) class name strings + * (xb) method name strings + * (xb) source file strings + * + * As with other DDM traffic, strings are sent as a 4-byte length + * followed by UTF-16 data. + */ + @NonNull + public static AllocationInfo[] parse(@NonNull ByteBuffer data) { + int messageHdrLen, entryHdrLen, stackFrameLen; + int numEntries, offsetToStrings; + int numClassNames, numMethodNames, numFileNames; + + /* + * Read the header. + */ + messageHdrLen = (data.get() & 0xff); + entryHdrLen = (data.get() & 0xff); + stackFrameLen = (data.get() & 0xff); + numEntries = (data.getShort() & 0xffff); + offsetToStrings = data.getInt(); + numClassNames = (data.getShort() & 0xffff); + numMethodNames = (data.getShort() & 0xffff); + numFileNames = (data.getShort() & 0xffff); + + + /* + * Skip forward to the strings and read them. + */ + data.position(offsetToStrings); + + String[] classNames = new String[numClassNames]; + String[] methodNames = new String[numMethodNames]; + String[] fileNames = new String[numFileNames]; + + readStringTable(data, classNames); + readStringTable(data, methodNames); + readStringTable(data, fileNames); + + /* + * Skip back to a point just past the header and start reading + * entries. + */ + data.position(messageHdrLen); + + AllocationInfo[] allocations = new AllocationInfo[numEntries]; + for (int i = 0; i < numEntries; i++) { + int totalSize; + int threadId, classNameIndex, stackDepth; + + totalSize = data.getInt(); + threadId = (data.getShort() & 0xffff); + classNameIndex = (data.getShort() & 0xffff); + stackDepth = (data.get() & 0xff); + /* we've consumed 9 bytes; gobble up any extra */ + for (int skip = 9; skip < entryHdrLen; skip++) + data.get(); + + StackTraceElement[] steArray = new StackTraceElement[stackDepth]; + + /* + * Pull out the stack trace. + */ + for (int sti = 0; sti < stackDepth; sti++) { + int methodClassNameIndex, methodNameIndex; + int methodSourceFileIndex; + short lineNumber; + String methodClassName, methodName, methodSourceFile; + + methodClassNameIndex = (data.getShort() & 0xffff); + methodNameIndex = (data.getShort() & 0xffff); + methodSourceFileIndex = (data.getShort() & 0xffff); + lineNumber = data.getShort(); + + methodClassName = classNames[methodClassNameIndex]; + methodName = methodNames[methodNameIndex]; + methodSourceFile = fileNames[methodSourceFileIndex]; + + steArray[sti] = new StackTraceElement(methodClassName, + methodName, methodSourceFile, lineNumber); + + /* we've consumed 8 bytes; gobble up any extra */ + for (int skip = 8; skip < stackFrameLen; skip++) + data.get(); + } + + allocations[i] = new AllocationInfo(numEntries - i, classNames[classNameIndex], totalSize, (short) threadId, steArray); + } + return allocations; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AndroidDebugBridge.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AndroidDebugBridge.java new file mode 100644 index 0000000..13dec70 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/AndroidDebugBridge.java @@ -0,0 +1,1183 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.ddmlib.Log.LogLevel; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.Thread.State; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * A connection to the host-side android debug bridge (adb) + *

This is the central point to communicate with any devices, emulators, or the applications + * running on them. + *

{@link #init(boolean)} must be called before anything is done. + */ +public final class AndroidDebugBridge { + + /* + * Minimum and maximum version of adb supported. This correspond to + * ADB_SERVER_VERSION found in //device/tools/adb/adb.h + */ + private static final AdbVersion MIN_ADB_VERSION = AdbVersion.parseFrom("1.0.20"); + + private static final String ADB = "adb"; //$NON-NLS-1$ + private static final String DDMS = "ddms"; //$NON-NLS-1$ + private static final String SERVER_PORT_ENV_VAR = "ANDROID_ADB_SERVER_PORT"; //$NON-NLS-1$ + + // Where to find the ADB bridge. + static final String DEFAULT_ADB_HOST = "127.0.0.1"; //$NON-NLS-1$ + static final int DEFAULT_ADB_PORT = 5037; + + /** Port where adb server will be started **/ + private static int sAdbServerPort = 0; + + private static InetAddress sHostAddr; + private static InetSocketAddress sSocketAddr; + + private static AndroidDebugBridge sThis; + private static boolean sInitialized = false; + private static boolean sClientSupport; + + /** Full path to adb. */ + private String mAdbOsLocation = null; + + private boolean mVersionCheck; + + private boolean mStarted = false; + + private DeviceMonitor mDeviceMonitor; + + private static final ArrayList sBridgeListeners = + new ArrayList(); + private static final ArrayList sDeviceListeners = + new ArrayList(); + private static final ArrayList sClientListeners = + new ArrayList(); + + // lock object for synchronization + private static final Object sLock = sBridgeListeners; + + /** + * Classes which implement this interface provide a method that deals + * with {@link AndroidDebugBridge} changes. + */ + public interface IDebugBridgeChangeListener { + /** + * Sent when a new {@link AndroidDebugBridge} is connected. + *

+ * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + */ + void bridgeChanged(AndroidDebugBridge bridge); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link IDevice} addition, deletion, and changes. + */ + public interface IDeviceChangeListener { + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + */ + void deviceConnected(IDevice device); + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + */ + void deviceDisconnected(IDevice device); + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + *

+ * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask describing what changed. It can contain any of the following + * values: {@link IDevice#CHANGE_BUILD_INFO}, {@link IDevice#CHANGE_STATE}, + * {@link IDevice#CHANGE_CLIENT_LIST} + */ + void deviceChanged(IDevice device, int changeMask); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link Client} changes. + */ + public interface IClientChangeListener { + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + */ + void clientChanged(Client client, int changeMask); + } + + /** + * Initialized the library only if needed. + * + * @param clientSupport Indicates whether the library should enable the monitoring and + * interaction with applications running on the devices. + * + * @see #init(boolean) + */ + public static synchronized void initIfNeeded(boolean clientSupport) { + if (sInitialized) { + return; + } + + init(clientSupport); + } + + /** + * Initializes the ddm library. + *

This must be called once before any call to + * {@link #createBridge(String, boolean)}. + *

The library can be initialized in 2 ways: + *

+ *

Only one tool can run in mode 1 at the same time. + *

Note that mode 1 does not prevent debugging of applications running on devices. Mode 1 + * lets debuggers connect to ddmlib which acts as a proxy between the debuggers and + * the applications to debug. See {@link Client#getDebuggerListenPort()}. + *

The preferences of ddmlib should also be initialized with whatever default + * values were changed from the default values. + *

When the application quits, {@link #terminate()} should be called. + * @param clientSupport Indicates whether the library should enable the monitoring and + * interaction with applications running on the devices. + * @see AndroidDebugBridge#createBridge(String, boolean) + * @see DdmPreferences + */ + public static synchronized void init(boolean clientSupport) { + if (sInitialized) { + throw new IllegalStateException("AndroidDebugBridge.init() has already been called."); + } + sInitialized = true; + sClientSupport = clientSupport; + + // Determine port and instantiate socket address. + initAdbSocketAddr(); + + MonitorThread monitorThread = MonitorThread.createInstance(); + monitorThread.start(); + + HandleHello.register(monitorThread); + HandleAppName.register(monitorThread); + HandleTest.register(monitorThread); + HandleThread.register(monitorThread); + HandleHeap.register(monitorThread); + HandleWait.register(monitorThread); + HandleProfiling.register(monitorThread); + HandleNativeHeap.register(monitorThread); + HandleViewDebug.register(monitorThread); + } + + /** + * Terminates the ddm library. This must be called upon application termination. + */ + public static synchronized void terminate() { + // kill the monitoring services + if (sThis != null && sThis.mDeviceMonitor != null) { + sThis.mDeviceMonitor.stop(); + sThis.mDeviceMonitor = null; + } + + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.quit(); + } + + sInitialized = false; + } + + /** + * Returns whether the ddmlib is setup to support monitoring and interacting with + * {@link Client}s running on the {@link IDevice}s. + */ + static boolean getClientSupport() { + return sClientSupport; + } + + /** + * Returns the socket address of the ADB server on the host. + */ + public static InetSocketAddress getSocketAddress() { + return sSocketAddr; + } + + /** + * Creates a {@link AndroidDebugBridge} that is not linked to any particular executable. + *

This bridge will expect adb to be running. It will not be able to start/stop/restart + * adb. + *

If a bridge has already been started, it is directly returned with no changes (similar + * to calling {@link #getBridge()}). + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge() { + synchronized (sLock) { + if (sThis != null) { + return sThis; + } + + try { + sThis = new AndroidDebugBridge(); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + + /** + * Creates a new debug bridge from the location of the command line tool. + *

+ * Any existing server will be disconnected, unless the location is the same and + * forceNewBridge is set to false. + * @param osLocation the location of the command line tool 'adb' + * @param forceNewBridge force creation of a new bridge even if one with the same location + * already exists. + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge(String osLocation, boolean forceNewBridge) { + synchronized (sLock) { + if (sThis != null) { + if (sThis.mAdbOsLocation != null && sThis.mAdbOsLocation.equals(osLocation) && + !forceNewBridge) { + return sThis; + } else { + // stop the current server + sThis.stop(); + } + } + + try { + sThis = new AndroidDebugBridge(osLocation); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + /** + * Returns the current debug bridge. Can be null if none were created. + */ + public static AndroidDebugBridge getBridge() { + return sThis; + } + + /** + * Disconnects the current debug bridge, and destroy the object. + *

This also stops the current adb host server. + *

+ * A new object will have to be created with {@link #createBridge(String, boolean)}. + */ + public static void disconnectBridge() { + synchronized (sLock) { + if (sThis != null) { + sThis.stop(); + sThis = null; + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners. + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is connected, by sending it one of the messages defined + * in the {@link IDebugBridgeChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + if (!sBridgeListeners.contains(listener)) { + sBridgeListeners.add(listener); + if (sThis != null) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is started. + * @param listener The listener which should no longer be notified. + */ + public static void removeDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + sBridgeListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link IDevice} + * is connected, disconnected, or when its properties or its {@link Client} list changed, + * by sending it one of the messages defined in the {@link IDeviceChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + if (!sDeviceListeners.contains(listener)) { + sDeviceListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link IDevice} is connected, disconnected, or when its properties or its {@link Client} + * list changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + sDeviceListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link Client} + * property changed, by sending it one of the messages defined in the + * {@link IClientChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + if (!sClientListeners.contains(listener)) { + sClientListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link Client} property changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + sClientListeners.remove(listener); + } + } + + + /** + * Returns the devices. + * @see #hasInitialDeviceList() + */ + @NonNull + public IDevice[] getDevices() { + synchronized (sLock) { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getDevices(); + } + } + + return new IDevice[0]; + } + + /** + * Returns whether the bridge has acquired the initial list from adb after being created. + *

Calling {@link #getDevices()} right after {@link #createBridge(String, boolean)} will + * generally result in an empty list. This is due to the internal asynchronous communication + * mechanism with adb that does not guarantee that the {@link IDevice} list has been + * built before the call to {@link #getDevices()}. + *

The recommended way to get the list of {@link IDevice} objects is to create a + * {@link IDeviceChangeListener} object. + */ + public boolean hasInitialDeviceList() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.hasInitialDeviceList(); + } + + return false; + } + + /** + * Sets the client to accept debugger connection on the custom "Selected debug port". + * @param selectedClient the client. Can be null. + */ + public void setSelectedClient(Client selectedClient) { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setSelectedClient(selectedClient); + } + } + + /** + * Returns whether the {@link AndroidDebugBridge} object is still connected to the adb daemon. + */ + public boolean isConnected() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (mDeviceMonitor != null && monitorThread != null) { + return mDeviceMonitor.isMonitoring() && monitorThread.getState() != State.TERMINATED; + } + return false; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to connect + * to the adb daemon. + */ + public int getConnectionAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getConnectionAttemptCount(); + } + return -1; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to restart + * the adb daemon. + */ + public int getRestartAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getRestartAttemptCount(); + } + return -1; + } + + /** + * Creates a new bridge. + * @param osLocation the location of the command line tool + * @throws InvalidParameterException + */ + private AndroidDebugBridge(String osLocation) throws InvalidParameterException { + if (osLocation == null || osLocation.isEmpty()) { + throw new InvalidParameterException(); + } + mAdbOsLocation = osLocation; + + try { + checkAdbVersion(); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Creates a new bridge not linked to any particular adb executable. + */ + private AndroidDebugBridge() { + } + + /** + * Queries adb for its version number and checks that it is atleast {@link #MIN_ADB_VERSION}. + */ + private void checkAdbVersion() throws IOException { + // default is bad check + mVersionCheck = false; + + if (mAdbOsLocation == null) { + return; + } + + File adb = new File(mAdbOsLocation); + ListenableFuture future = getAdbVersion(adb); + AdbVersion version; + try { + version = future.get(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return; + } catch (java.util.concurrent.TimeoutException e) { + String msg = "Unable to obtain result of 'adb version'"; + Log.logAndDisplay(LogLevel.ERROR, ADB, msg); + return; + } catch (ExecutionException e) { + Log.logAndDisplay(LogLevel.ERROR, ADB, e.getCause().getMessage()); + Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); + return; + } + + if (version.compareTo(MIN_ADB_VERSION) > 0) { + mVersionCheck = true; + } else { + String message = String.format( + "Required minimum version of adb: %1$s." + + "Current version is %2$s", MIN_ADB_VERSION, version); + Log.logAndDisplay(LogLevel.ERROR, ADB, message); + } + } + + public static ListenableFuture getAdbVersion(@NonNull final File adb) { + final SettableFuture future = SettableFuture.create(); + new Thread(new Runnable() { + @Override + public void run() { + ProcessBuilder pb = new ProcessBuilder(adb.getPath(), "version"); + pb.redirectErrorStream(true); + + Process p = null; + try { + p = pb.start(); + } catch (IOException e) { + future.setException(e); + return; + } + + StringBuilder sb = new StringBuilder(); + BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); + try { + String line; + while ((line = br.readLine()) != null) { + AdbVersion version = AdbVersion.parseFrom(line); + if (version != AdbVersion.UNKNOWN) { + future.set(version); + return; + } + sb.append(line); + sb.append('\n'); + } + } catch (IOException e) { + future.setException(e); + return; + } finally { + try { + br.close(); + } catch (IOException e) { + future.setException(e); + } + } + + future.setException(new RuntimeException( + "Unable to detect adb version, adb output: " + sb.toString())); + } + }, "Obtaining adb version").start(); + return future; + } + + /** + * Starts the debug bridge. + * + * @return true if success. + */ + boolean start() { + if (mAdbOsLocation != null && sAdbServerPort != 0 && (!mVersionCheck || !startAdb())) { + return false; + } + + mStarted = true; + + // now that the bridge is connected, we start the underlying services. + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + + return true; + } + + /** + * Kills the debug bridge, and the adb host server. + * @return true if success + */ + boolean stop() { + // if we haven't started we return false; + if (!mStarted) { + return false; + } + + // kill the monitoring services + if (mDeviceMonitor != null) { + mDeviceMonitor.stop(); + mDeviceMonitor = null; + } + + if (!stopAdb()) { + return false; + } + + mStarted = false; + return true; + } + + /** + * Restarts adb, but not the services around it. + * @return true if success. + */ + public boolean restart() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot restart adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + if (sAdbServerPort == 0) { + Log.e(ADB, "ADB server port for restarting AndroidDebugBridge is not set."); //$NON-NLS-1$ + return false; + } + + if (!mVersionCheck) { + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Attempting to restart adb, but version check failed!"); //$NON-NLS-1$ + return false; + } + synchronized (this) { + stopAdb(); + + boolean restart = startAdb(); + + if (restart && mDeviceMonitor == null) { + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + } + + return restart; + } + } + + /** + * Notify the listener of a new {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the new IDevice. + * @see #getLock() + */ + void deviceConnected(IDevice device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceConnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a disconnected {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the disconnected IDevice. + * @see #getLock() + */ + void deviceDisconnected(IDevice device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceDisconnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified IDevice. + * @see #getLock() + */ + void deviceChanged(IDevice device, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceChanged(device, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link Client}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param client the modified Client. + * @param changeMask the mask indicating what changed in the Client + * @see #getLock() + */ + void clientChanged(Client client, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IClientChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sClientListeners.toArray( + new IClientChangeListener[sClientListeners.size()]); + + } + + // Notify the listeners + for (IClientChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.clientChanged(client, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Returns the {@link DeviceMonitor} object. + */ + DeviceMonitor getDeviceMonitor() { + return mDeviceMonitor; + } + + /** + * Starts the adb host side server. + * @return true if success + */ + synchronized boolean startAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot start adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + if (sAdbServerPort == 0) { + Log.w(ADB, "ADB server port for starting AndroidDebugBridge is not set."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + String[] command = getAdbLaunchCommand("start-server"); + String commandString = Joiner.on(',').join(command); + try { + Log.d(DDMS, String.format("Launching '%1$s' to ensure ADB is running.", commandString)); + ProcessBuilder processBuilder = new ProcessBuilder(command); + if (DdmPreferences.getUseAdbHost()) { + String adbHostValue = DdmPreferences.getAdbHostValue(); + if (adbHostValue != null && !adbHostValue.isEmpty()) { + //TODO : check that the String is a valid IP address + Map env = processBuilder.environment(); + env.put("ADBHOST", adbHostValue); + } + } + proc = processBuilder.start(); + + ArrayList errorOutput = new ArrayList(); + ArrayList stdOutput = new ArrayList(); + status = grabProcessOutput(proc, errorOutput, stdOutput, false /* waitForReaders */); + } catch (IOException ioe) { + Log.e(DDMS, "Unable to run 'adb': " + ioe.getMessage()); //$NON-NLS-1$ + // we'll return false; + } catch (InterruptedException ie) { + Log.e(DDMS, "Unable to run 'adb': " + ie.getMessage()); //$NON-NLS-1$ + // we'll return false; + } + + if (status != 0) { + Log.e(DDMS, + String.format("'%1$s' failed -- run manually if necessary", commandString)); //$NON-NLS-1$ + return false; + } else { + Log.d(DDMS, String.format("'%1$s' succeeded", commandString)); //$NON-NLS-1$ + return true; + } + } + + private String[] getAdbLaunchCommand(String option) { + List command = new ArrayList(4); + command.add(mAdbOsLocation); + if (sAdbServerPort != DEFAULT_ADB_PORT) { + command.add("-P"); //$NON-NLS-1$ + command.add(Integer.toString(sAdbServerPort)); + } + command.add(option); + return command.toArray(new String[command.size()]); + } + + /** + * Stops the adb host side server. + * + * @return true if success + */ + private synchronized boolean stopAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot stop adb when AndroidDebugBridge is created without the location of adb."); + return false; + } + + if (sAdbServerPort == 0) { + Log.e(ADB, "ADB server port for restarting AndroidDebugBridge is not set"); + return false; + } + + Process proc; + int status = -1; + + String[] command = getAdbLaunchCommand("kill-server"); //$NON-NLS-1$ + try { + proc = Runtime.getRuntime().exec(command); + status = proc.waitFor(); + } + catch (IOException ioe) { + // we'll return false; + } + catch (InterruptedException ie) { + // we'll return false; + } + + String commandString = Joiner.on(',').join(command); + if (status != 0) { + Log.w(DDMS, String.format("'%1$s' failed -- run manually if necessary", commandString)); + return false; + } else { + Log.d(DDMS, String.format("'%1$s' succeeded", commandString)); + return true; + } + } + + /** + * Get the stderr/stdout outputs of a process and return when the process is done. + * Both must be read or the process will block on windows. + * @param process The process to get the output from + * @param errorOutput The array to store the stderr output. cannot be null. + * @param stdOutput The array to store the stdout output. cannot be null. + * @param waitForReaders if true, this will wait for the reader threads. + * @return the process return code. + * @throws InterruptedException + */ + private int grabProcessOutput(final Process process, final ArrayList errorOutput, + final ArrayList stdOutput, boolean waitForReaders) + throws InterruptedException { + assert errorOutput != null; + assert stdOutput != null; + // read the lines as they come. if null is returned, it's + // because the process finished + Thread t1 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + Log.e(ADB, line); + errorOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + Thread t2 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + Log.d(ADB, line); + stdOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + t1.start(); + t2.start(); + + // it looks like on windows process#waitFor() can return + // before the thread have filled the arrays, so we wait for both threads and the + // process itself. + if (waitForReaders) { + try { + t1.join(); + } catch (InterruptedException e) { + } + try { + t2.join(); + } catch (InterruptedException e) { + } + } + + // get the return code from the process + return process.waitFor(); + } + + /** + * Returns the singleton lock used by this class to protect any access to the listener. + *

+ * This includes adding/removing listeners, but also notifying listeners of new bridges, + * devices, and clients. + */ + private static Object getLock() { + return sLock; + } + + /** + * Instantiates sSocketAddr with the address of the host's adb process. + */ + private static void initAdbSocketAddr() { + try { + sAdbServerPort = getAdbServerPort(); + sHostAddr = InetAddress.getByName(DEFAULT_ADB_HOST); + sSocketAddr = new InetSocketAddress(sHostAddr, sAdbServerPort); + } catch (UnknownHostException e) { + // localhost should always be known. + } + } + + /** + * Returns the port where adb server should be launched. This looks at: + *

    + *
  1. The system property ANDROID_ADB_SERVER_PORT
  2. + *
  3. The environment variable ANDROID_ADB_SERVER_PORT
  4. + *
  5. Defaults to {@link #DEFAULT_ADB_PORT} if neither the system property nor the env var + * are set.
  6. + *
+ * + * @return The port number where the host's adb should be expected or started. + */ + private static int getAdbServerPort() { + // check system property + Integer prop = Integer.getInteger(SERVER_PORT_ENV_VAR); + if (prop != null) { + try { + return validateAdbServerPort(prop.toString()); + } catch (IllegalArgumentException e) { + String msg = String.format( + "Invalid value (%1$s) for ANDROID_ADB_SERVER_PORT system property.", + prop); + Log.w(DDMS, msg); + } + } + + // when system property is not set or is invalid, parse environment property + try { + String env = System.getenv(SERVER_PORT_ENV_VAR); + if (env != null) { + return validateAdbServerPort(env); + } + } catch (SecurityException ex) { + // A security manager has been installed that doesn't allow access to env vars. + // So an environment variable might have been set, but we can't tell. + // Let's log a warning and continue with ADB's default port. + // The issue is that adb would be started (by the forked process having access + // to the env vars) on the desired port, but within this process, we can't figure out + // what that port is. However, a security manager not granting access to env vars + // but allowing to fork is a rare and interesting configuration, so the right + // thing seems to be to continue using the default port, as forking is likely to + // fail later on in the scenario of the security manager. + Log.w(DDMS, + "No access to env variables allowed by current security manager. " + + "If you've set ANDROID_ADB_SERVER_PORT: it's being ignored."); + } catch (IllegalArgumentException e) { + String msg = String.format( + "Invalid value (%1$s) for ANDROID_ADB_SERVER_PORT environment variable (%2$s).", + prop, e.getMessage()); + Log.w(DDMS, msg); + } + + // use default port if neither are set + return DEFAULT_ADB_PORT; + } + + /** + * Returns the integer port value if it is a valid value for adb server port + * @param adbServerPort adb server port to validate + * @return {@code adbServerPort} as a parsed integer + * @throws IllegalArgumentException when {@code adbServerPort} is not bigger than 0 or it is + * not a number at all + */ + private static int validateAdbServerPort(@NonNull String adbServerPort) + throws IllegalArgumentException { + try { + // C tools (adb, emulator) accept hex and octal port numbers, so need to accept them too + int port = Integer.decode(adbServerPort); + if (port <= 0 || port >= 65535) { + throw new IllegalArgumentException("Should be > 0 and < 65535"); + } + return port; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Not a valid port number"); + } + } + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BadPacketException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BadPacketException.java new file mode 100644 index 0000000..129b312 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BadPacketException.java @@ -0,0 +1,35 @@ +/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddmlib; + +/** + * Thrown if the contents of a packet are bad. + */ +@SuppressWarnings("serial") +class BadPacketException extends RuntimeException { + public BadPacketException() + { + super(); + } + + public BadPacketException(String msg) + { + super(msg); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BatteryFetcher.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BatteryFetcher.java new file mode 100644 index 0000000..842f102 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/BatteryFetcher.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import com.android.annotations.Nullable; +import com.google.common.util.concurrent.SettableFuture; + +import java.io.IOException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Fetches battery level from device. + */ +class BatteryFetcher { + + private static final String LOG_TAG = "BatteryFetcher"; + + /** the amount of time to wait between unsuccessful battery fetch attempts */ + private static final long FETCH_BACKOFF_MS = 5 * 1000; // 5 seconds + private static final long BATTERY_TIMEOUT = 2 * 1000; // 2 seconds + + /** + * Output receiver for "cat /sys/class/power_supply/.../capacity" command line. + */ + static final class SysFsBatteryLevelReceiver extends MultiLineReceiver { + + private static final Pattern BATTERY_LEVEL = Pattern.compile("^(\\d+)[.\\s]*"); + private Integer mBatteryLevel = null; + + /** + * Get the parsed battery level. + * @return battery level or null if it cannot be determined + */ + @Nullable + public Integer getBatteryLevel() { + return mBatteryLevel; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + Matcher batteryMatch = BATTERY_LEVEL.matcher(line); + if (batteryMatch.matches()) { + if (mBatteryLevel == null) { + mBatteryLevel = Integer.parseInt(batteryMatch.group(1)); + } else { + // multiple matches, check if they are different + Integer tmpLevel = Integer.parseInt(batteryMatch.group(1)); + if (!mBatteryLevel.equals(tmpLevel)) { + Log.w(LOG_TAG, String.format( + "Multiple lines matched with different value; " + + "Original: %s, Current: %s (keeping original)", + mBatteryLevel.toString(), tmpLevel.toString())); + } + } + } + } + } + } + + /** + * Output receiver for "dumpsys battery" command line. + */ + private static final class BatteryReceiver extends MultiLineReceiver { + private static final Pattern BATTERY_LEVEL = Pattern.compile("\\s*level: (\\d+)"); + private static final Pattern SCALE = Pattern.compile("\\s*scale: (\\d+)"); + + private Integer mBatteryLevel = null; + private Integer mBatteryScale = null; + + /** + * Get the parsed percent battery level. + * @return + */ + public Integer getBatteryLevel() { + if (mBatteryLevel != null && mBatteryScale != null) { + return (mBatteryLevel * 100) / mBatteryScale; + } + return null; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + Matcher batteryMatch = BATTERY_LEVEL.matcher(line); + if (batteryMatch.matches()) { + try { + mBatteryLevel = Integer.parseInt(batteryMatch.group(1)); + } catch (NumberFormatException e) { + Log.w(LOG_TAG, String.format("Failed to parse %s as an integer", + batteryMatch.group(1))); + } + } + Matcher scaleMatch = SCALE.matcher(line); + if (scaleMatch.matches()) { + try { + mBatteryScale = Integer.parseInt(scaleMatch.group(1)); + } catch (NumberFormatException e) { + Log.w(LOG_TAG, String.format("Failed to parse %s as an integer", + batteryMatch.group(1))); + } + } + } + } + + @Override + public boolean isCancelled() { + return false; + } + } + + private Integer mBatteryLevel = null; + private final IDevice mDevice; + private long mLastSuccessTime = 0; + private SettableFuture mPendingRequest = null; + + public BatteryFetcher(IDevice device) { + mDevice = device; + } + + /** + * Make a possibly asynchronous request for the device's battery level + * + * @param freshness the desired recentness of battery level + * @param timeUnit the {@link TimeUnit} of freshness + * @return a {@link Future} that can be used to retrieve the battery level + */ + public synchronized Future getBattery(long freshness, TimeUnit timeUnit) { + SettableFuture result; + if (mBatteryLevel == null || isFetchRequired(freshness, timeUnit)) { + if (mPendingRequest == null) { + // no request underway - start a new one + mPendingRequest = SettableFuture.create(); + initiateBatteryQuery(); + } else { + // fall through - return the already created future from the request already + // underway + } + result = mPendingRequest; + } else { + // cache is populated within desired freshness + result = SettableFuture.create(); + result.set(mBatteryLevel); + } + return result; + } + + private boolean isFetchRequired(long freshness, TimeUnit timeUnit) { + long freshnessMs = timeUnit.toMillis(freshness); + return (System.currentTimeMillis() - mLastSuccessTime) > freshnessMs; + } + + private void initiateBatteryQuery() { + String threadName = String.format("query-battery-%s", mDevice.getSerialNumber()); + Thread fetchThread = new Thread(threadName) { + @Override + public void run() { + Exception exception = null; + try { + // first try to get it from sysfs + SysFsBatteryLevelReceiver sysBattReceiver = new SysFsBatteryLevelReceiver(); + mDevice.executeShellCommand("cat /sys/class/power_supply/*/capacity", + sysBattReceiver, BATTERY_TIMEOUT, TimeUnit.MILLISECONDS); + if (!setBatteryLevel(sysBattReceiver.getBatteryLevel())) { + // failed! try dumpsys + BatteryReceiver receiver = new BatteryReceiver(); + mDevice.executeShellCommand("dumpsys battery", receiver, BATTERY_TIMEOUT, + TimeUnit.MILLISECONDS); + if (setBatteryLevel(receiver.getBatteryLevel())) { + return; + } + } + exception = new IOException("Unrecognized response to battery level queries"); + } catch (TimeoutException e) { + exception = e; + } catch (AdbCommandRejectedException e) { + exception = e; + } catch (ShellCommandUnresponsiveException e) { + exception = e; + } catch (IOException e) { + exception = e; + } + handleBatteryLevelFailure(exception); + } + }; + fetchThread.setDaemon(true); + fetchThread.start(); + } + + private synchronized boolean setBatteryLevel(Integer batteryLevel) { + if (batteryLevel == null) { + return false; + } + mLastSuccessTime = System.currentTimeMillis(); + mBatteryLevel = batteryLevel; + if (mPendingRequest != null) { + mPendingRequest.set(mBatteryLevel); + } + mPendingRequest = null; + return true; + } + + private synchronized void handleBatteryLevelFailure(Exception e) { + Log.w(LOG_TAG, String.format( + "%s getting battery level for device %s: %s", + e.getClass().getSimpleName(), mDevice.getSerialNumber(), e.getMessage())); + if (mPendingRequest != null) { + if (!mPendingRequest.setException(e)) { + // should never happen + Log.e(LOG_TAG, "Future.setException failed"); + mPendingRequest.set(null); + } + } + mPendingRequest = null; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ByteBufferUtil.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ByteBufferUtil.java new file mode 100644 index 0000000..d419dfe --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ByteBufferUtil.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import com.android.annotations.NonNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +public class ByteBufferUtil { + + @NonNull + public static ByteBuffer mapFile(@NonNull File f, long offset, @NonNull ByteOrder byteOrder) throws IOException { + FileInputStream dataFile = new FileInputStream(f); + try { + FileChannel fc = dataFile.getChannel(); + MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, f.length() - offset); + buffer.order(byteOrder); + return buffer; + } finally { + dataFile.close(); // this *also* closes the associated channel, fc + } + } + + @NonNull + public static String getString(@NonNull ByteBuffer buf, int len) { + char[] data = new char[len]; + for (int i = 0; i < len; i++) + data[i] = buf.getChar(); + return new String(data); + } + + public static void putString(@NonNull ByteBuffer buf, @NonNull String str) { + int len = str.length(); + for (int i = 0; i < len; i++) + buf.putChar(str.charAt(i)); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CanceledException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CanceledException.java new file mode 100644 index 0000000..84eda03 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CanceledException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Abstract exception for exception that can be thrown when a user input cancels the action. + *

+ * {@link #wasCanceled()} returns whether the action was canceled because of user input. + * + */ +public abstract class CanceledException extends Exception { + private static final long serialVersionUID = 1L; + + CanceledException(String message) { + super(message); + } + + CanceledException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Returns true if the action was canceled by user input. + */ + public abstract boolean wasCanceled(); +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ChunkHandler.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ChunkHandler.java new file mode 100644 index 0000000..36e24a3 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ChunkHandler.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Subclass this with a class that handles one or more chunk types. + */ +abstract class ChunkHandler { + + public static final int CHUNK_HEADER_LEN = 8; // 4-byte type, 4-byte len + public static final ByteOrder CHUNK_ORDER = ByteOrder.BIG_ENDIAN; + + public static final int CHUNK_FAIL = type("FAIL"); + + ChunkHandler() {} + + /** + * Client is ready. The monitor thread calls this method on all + * handlers when the client is determined to be DDM-aware (usually + * after receiving a HELO response.) + * + * The handler can use this opportunity to initialize client-side + * activity. Because there's a fair chance we'll want to send a + * message to the client, this method can throw an IOException. + */ + abstract void clientReady(Client client) throws IOException; + + /** + * Client has gone away. Can be used to clean up any resources + * associated with this client connection. + */ + abstract void clientDisconnected(Client client); + + /** + * Handle an incoming chunk. The data, of chunk type "type", begins + * at the start of "data" and continues to data.limit(). + * + * If "isReply" is set, then "msgId" will be the ID of the request + * we sent to the client. Otherwise, it's the ID generated by the + * client for this event. Note that it's possible to receive chunks + * in reply packets for which we are not registered. + * + * The handler may not modify the contents of "data". + */ + abstract void handleChunk(Client client, int type, + ByteBuffer data, boolean isReply, int msgId); + + /** + * Handle chunks not recognized by handlers. The handleChunk() method + * in sub-classes should call this if the chunk type isn't recognized. + */ + protected void handleUnknownChunk(Client client, int type, + ByteBuffer data, boolean isReply, int msgId) { + if (type == CHUNK_FAIL) { + int errorCode, msgLen; + String msg; + + errorCode = data.getInt(); + msgLen = data.getInt(); + msg = ByteBufferUtil.getString(data, msgLen); + Log.w("ddms", "WARNING: failure code=" + errorCode + " msg=" + msg); + } else { + Log.w("ddms", "WARNING: received unknown chunk " + name(type) + + ": len=" + data.limit() + ", reply=" + isReply + + ", msgId=0x" + Integer.toHexString(msgId)); + } + Log.w("ddms", " client " + client + ", handler " + this); + } + + /** + * Utility function to copy a String out of a ByteBuffer. + */ + public static String getString(ByteBuffer buf, int len) { + return ByteBufferUtil.getString(buf, len); + } + + /** + * Convert a 4-character string to a 32-bit type. + */ + static int type(String typeName) { + int val = 0; + + if (typeName.length() != 4) { + Log.e("ddms", "Type name must be 4 letter long"); + throw new RuntimeException("Type name must be 4 letter long"); + } + + for (int i = 0; i < 4; i++) { + val <<= 8; + val |= (byte) typeName.charAt(i); + } + + return val; + } + + /** + * Convert an integer type to a 4-character string. + */ + static String name(int type) { + char[] ascii = new char[4]; + + ascii[0] = (char) ((type >> 24) & 0xff); + ascii[1] = (char) ((type >> 16) & 0xff); + ascii[2] = (char) ((type >> 8) & 0xff); + ascii[3] = (char) (type & 0xff); + + return new String(ascii); + } + + /** + * Allocate a ByteBuffer with enough space to hold the JDWP packet + * header and one chunk header in addition to the demands of the + * chunk being created. + * + * "maxChunkLen" indicates the size of the chunk contents only. + */ + static ByteBuffer allocBuffer(int maxChunkLen) { + ByteBuffer buf = + ByteBuffer.allocate(JdwpPacket.JDWP_HEADER_LEN + 8 +maxChunkLen); + buf.order(CHUNK_ORDER); + return buf; + } + + /** + * Return the slice of the JDWP packet buffer that holds just the + * chunk data. + */ + static ByteBuffer getChunkDataBuf(ByteBuffer jdwpBuf) { + ByteBuffer slice; + + assert jdwpBuf.position() == 0; + + jdwpBuf.position(JdwpPacket.JDWP_HEADER_LEN + CHUNK_HEADER_LEN); + slice = jdwpBuf.slice(); + slice.order(CHUNK_ORDER); + jdwpBuf.position(0); + + return slice; + } + + /** + * Write the chunk header at the start of the chunk. + * + * Pass in the byte buffer returned by JdwpPacket.getPayload(). + */ + static void finishChunkPacket(JdwpPacket packet, int type, int chunkLen) { + ByteBuffer buf = packet.getPayload(); + + buf.putInt(0x00, type); + buf.putInt(0x04, chunkLen); + + packet.finishPacket(CHUNK_HEADER_LEN + chunkLen); + } + + /** + * Check that the client is opened with the proper debugger port for the + * specified application name, and if not, reopen it. + * @param client + * @param uiThread + * @param appName + * @return + */ + protected static Client checkDebuggerPortForAppName(Client client, String appName) { + IDebugPortProvider provider = DebugPortManager.getProvider(); + if (provider != null) { + Device device = client.getDeviceImpl(); + int newPort = provider.getPort(device, appName); + + if (newPort != IDebugPortProvider.NO_STATIC_PORT && + newPort != client.getDebuggerListenPort()) { + + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + DeviceMonitor deviceMonitor = bridge.getDeviceMonitor(); + if (deviceMonitor != null) { + deviceMonitor.addClientToDropAndReopen(client, newPort); + client = null; + } + } + } + } + + return client; + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Client.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Client.java new file mode 100644 index 0000000..d2b1488 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Client.java @@ -0,0 +1,948 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +/** + * This represents a single client, usually a Dalvik VM process. + *

This class gives access to basic client information, as well as methods to perform actions + * on the client. + *

More detailed information, usually updated in real time, can be access through the + * {@link ClientData} class. Each Client object has its own ClientData + * accessed through {@link #getClientData()}. + */ +public class Client { + + private static final int SERVER_PROTOCOL_VERSION = 1; + + /** Client change bit mask: application name change */ + public static final int CHANGE_NAME = 0x0001; + /** Client change bit mask: debugger status change */ + public static final int CHANGE_DEBUGGER_STATUS = 0x0002; + /** Client change bit mask: debugger port change */ + public static final int CHANGE_PORT = 0x0004; + /** Client change bit mask: thread update flag change */ + public static final int CHANGE_THREAD_MODE = 0x0008; + /** Client change bit mask: thread data updated */ + public static final int CHANGE_THREAD_DATA = 0x0010; + /** Client change bit mask: heap update flag change */ + public static final int CHANGE_HEAP_MODE = 0x0020; + /** Client change bit mask: head data updated */ + public static final int CHANGE_HEAP_DATA = 0x0040; + /** Client change bit mask: native heap data updated */ + public static final int CHANGE_NATIVE_HEAP_DATA = 0x0080; + /** Client change bit mask: thread stack trace updated */ + public static final int CHANGE_THREAD_STACKTRACE = 0x0100; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_HEAP_ALLOCATIONS = 0x0200; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_HEAP_ALLOCATION_STATUS = 0x0400; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_METHOD_PROFILING_STATUS = 0x0800; + /** Client change bit mask: hprof data updated */ + public static final int CHANGE_HPROF = 0x1000; + + /** Client change bit mask: combination of {@link Client#CHANGE_NAME}, + * {@link Client#CHANGE_DEBUGGER_STATUS}, and {@link Client#CHANGE_PORT}. + */ + public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_STATUS | CHANGE_PORT; + + private SocketChannel mChan; + + // debugger we're associated with, if any + private Debugger mDebugger; + private int mDebuggerListenPort; + + // list of IDs for requests we have sent to the client + private HashMap mOutstandingReqs; + + // chunk handlers stash state data in here + private ClientData mClientData; + + // User interface state. Changing the value causes a message to be + // sent to the client. + private boolean mThreadUpdateEnabled; + private boolean mHeapInfoUpdateEnabled; + private boolean mHeapSegmentUpdateEnabled; + + /* + * Read/write buffers. We can get large quantities of data from the + * client, e.g. the response to a "give me the list of all known classes" + * request from the debugger. Requests from the debugger, and from us, + * are much smaller. + * + * Pass-through debugger traffic is sent without copying. "mWriteBuffer" + * is only used for data generated within Client. + */ + private static final int INITIAL_BUF_SIZE = 2*1024; + private static final int MAX_BUF_SIZE = 800*1024*1024; + private ByteBuffer mReadBuffer; + + private static final int WRITE_BUF_SIZE = 256; + private ByteBuffer mWriteBuffer; + + private Device mDevice; + + private int mConnState; + + private static final int ST_INIT = 1; + private static final int ST_NOT_JDWP = 2; + private static final int ST_AWAIT_SHAKE = 10; + private static final int ST_NEED_DDM_PKT = 11; + private static final int ST_NOT_DDM = 12; + private static final int ST_READY = 13; + private static final int ST_ERROR = 20; + private static final int ST_DISCONNECTED = 21; + + + /** + * Create an object for a new client connection. + * + * @param device the device this client belongs to + * @param chan the connected {@link SocketChannel}. + * @param pid the client pid. + */ + Client(Device device, SocketChannel chan, int pid) { + mDevice = device; + mChan = chan; + + mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE); + mWriteBuffer = ByteBuffer.allocate(WRITE_BUF_SIZE); + + mOutstandingReqs = new HashMap(); + + mConnState = ST_INIT; + + mClientData = new ClientData(pid); + + mThreadUpdateEnabled = DdmPreferences.getInitialThreadUpdate(); + mHeapInfoUpdateEnabled = DdmPreferences.getInitialHeapUpdate(); + mHeapSegmentUpdateEnabled = DdmPreferences.getInitialHeapUpdate(); + } + + /** + * Returns a string representation of the {@link Client} object. + */ + @Override + public String toString() { + return "[Client pid: " + mClientData.getPid() + "]"; + } + + /** + * Returns the {@link IDevice} on which this Client is running. + */ + public IDevice getDevice() { + return mDevice; + } + + /** Returns the {@link Device} on which this Client is running. + */ + Device getDeviceImpl() { + return mDevice; + } + + /** + * Returns the debugger port for this client. + */ + public int getDebuggerListenPort() { + return mDebuggerListenPort; + } + + /** + * Returns true if the client VM is DDM-aware. + * + * Calling here is only allowed after the connection has been + * established. + */ + public boolean isDdmAware() { + switch (mConnState) { + case ST_INIT: + case ST_NOT_JDWP: + case ST_AWAIT_SHAKE: + case ST_NEED_DDM_PKT: + case ST_NOT_DDM: + case ST_ERROR: + case ST_DISCONNECTED: + return false; + case ST_READY: + return true; + default: + assert false; + return false; + } + } + + /** + * Returns true if a debugger is currently attached to the client. + */ + public boolean isDebuggerAttached() { + return mDebugger.isDebuggerAttached(); + } + + /** + * Return the Debugger object associated with this client. + */ + Debugger getDebugger() { + return mDebugger; + } + + /** + * Returns the {@link ClientData} object containing this client information. + */ + @NonNull + public ClientData getClientData() { + return mClientData; + } + + /** + * Forces the client to execute its garbage collector. + */ + public void executeGarbageCollector() { + try { + HandleHeap.sendHPGC(this); + } catch (IOException ioe) { + Log.w("ddms", "Send of HPGC message failed"); + // ignore + } + } + + /** + * Makes the VM dump an HPROF file + */ + public void dumpHprof() { + boolean canStream = mClientData.hasFeature(ClientData.FEATURE_HPROF_STREAMING); + try { + if (canStream) { + HandleHeap.sendHPDS(this); + } else { + String file = "/sdcard/" + mClientData.getClientDescription().replaceAll( + "\\:.*", "") + ".hprof"; + HandleHeap.sendHPDU(this, file); + } + } catch (IOException e) { + Log.w("ddms", "Send of HPDU message failed"); + // ignore + } + } + + /** + * Toggles method profiling state. + * @deprecated Use {@link #startMethodTracer()}, {@link #stopMethodTracer()}, + * {@link #startSamplingProfiler(int, java.util.concurrent.TimeUnit)} or + * {@link #stopSamplingProfiler()} instead. + */ + public void toggleMethodProfiling() { + try { + switch (mClientData.getMethodProfilingStatus()) { + case TRACER_ON: + stopMethodTracer(); + break; + case SAMPLER_ON: + stopSamplingProfiler(); + break; + case OFF: + startMethodTracer(); + break; + } + } catch (IOException e) { + Log.w("ddms", "Toggle method profiling failed"); + // ignore + } + } + + private int getProfileBufferSize() { + return DdmPreferences.getProfilerBufferSizeMb() * 1024 * 1024; + } + + public void startMethodTracer() throws IOException { + boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING); + int bufferSize = getProfileBufferSize(); + if (canStream) { + HandleProfiling.sendMPSS(this, bufferSize, 0 /*flags*/); + } else { + String file = "/sdcard/" + + mClientData.getClientDescription().replaceAll("\\:.*", "") + + DdmConstants.DOT_TRACE; + HandleProfiling.sendMPRS(this, file, bufferSize, 0 /*flags*/); + } + } + + public void stopMethodTracer() throws IOException { + boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING); + + if (canStream) { + HandleProfiling.sendMPSE(this); + } else { + HandleProfiling.sendMPRE(this); + } + } + + public void startSamplingProfiler(int samplingInterval, TimeUnit timeUnit) throws IOException { + int bufferSize = getProfileBufferSize(); + HandleProfiling.sendSPSS(this, bufferSize, samplingInterval, timeUnit); + } + + public void stopSamplingProfiler() throws IOException { + HandleProfiling.sendSPSE(this); + } + + public boolean startOpenGlTracing() { + boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING); + if (!canTraceOpenGl) { + return false; + } + + try { + HandleViewDebug.sendStartGlTracing(this); + return true; + } catch (IOException e) { + Log.w("ddms", "Start OpenGL Tracing failed"); + return false; + } + } + + public boolean stopOpenGlTracing() { + boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING); + if (!canTraceOpenGl) { + return false; + } + + try { + HandleViewDebug.sendStopGlTracing(this); + return true; + } catch (IOException e) { + Log.w("ddms", "Stop OpenGL Tracing failed"); + return false; + } + } + + /** + * Sends a request to the VM to send the enable status of the method profiling. + * This is asynchronous. + *

The allocation status can be accessed by {@link ClientData#getAllocationStatus()}. + * The notification that the new status is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}. + */ + public void requestMethodProfilingStatus() { + try { + HandleHeap.sendREAQ(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + + /** + * Enables or disables the thread update. + *

If true the VM will be able to send thread information. Thread information + * must be requested with {@link #requestThreadUpdate()}. + * @param enabled the enable flag. + */ + public void setThreadUpdateEnabled(boolean enabled) { + mThreadUpdateEnabled = enabled; + if (!enabled) { + mClientData.clearThreads(); + } + + try { + HandleThread.sendTHEN(this, enabled); + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + ioe.printStackTrace(); + } + + update(CHANGE_THREAD_MODE); + } + + /** + * Returns whether the thread update is enabled. + */ + public boolean isThreadUpdateEnabled() { + return mThreadUpdateEnabled; + } + + /** + * Sends a thread update request. This is asynchronous. + *

The thread info can be accessed by {@link ClientData#getThreads()}. The notification + * that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_THREAD_DATA}. + */ + public void requestThreadUpdate() { + HandleThread.requestThreadUpdate(this); + } + + /** + * Sends a thread stack trace update request. This is asynchronous. + *

The thread info can be accessed by {@link ClientData#getThreads()} and + * {@link ThreadInfo#getStackTrace()}. + *

The notification that the new data is available + * will be received through {@link IClientChangeListener#clientChanged(Client, int)} + * with a changeMask containing the mask {@link #CHANGE_THREAD_STACKTRACE}. + */ + public void requestThreadStackTrace(int threadId) { + HandleThread.requestThreadStackCallRefresh(this, threadId); + } + + /** + * Enables or disables the heap update. + *

If true, any GC will cause the client to send its heap information. + *

The heap information can be accessed by {@link ClientData#getVmHeapData()}. + *

The notification that the new data is available + * will be received through {@link IClientChangeListener#clientChanged(Client, int)} + * with a changeMask containing the value {@link #CHANGE_HEAP_DATA}. + * @param enabled the enable flag + */ + public void setHeapUpdateEnabled(boolean enabled) { + setHeapInfoUpdateEnabled(enabled); + setHeapSegmentUpdateEnabled(enabled); + } + + public void setHeapInfoUpdateEnabled(boolean enabled) { + mHeapInfoUpdateEnabled = enabled; + + try { + HandleHeap.sendHPIF(this, + enabled ? HandleHeap.HPIF_WHEN_EVERY_GC : HandleHeap.HPIF_WHEN_NEVER); + + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + } + + update(CHANGE_HEAP_MODE); + } + + public void setHeapSegmentUpdateEnabled(boolean enabled) { + mHeapSegmentUpdateEnabled = enabled; + + try { + HandleHeap.sendHPSG(this, + enabled ? HandleHeap.WHEN_GC : HandleHeap.WHEN_DISABLE, + HandleHeap.WHAT_MERGE); + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + } + + update(CHANGE_HEAP_MODE); + } + + void initializeHeapUpdateStatus() throws IOException { + setHeapInfoUpdateEnabled(mHeapInfoUpdateEnabled); + } + + /** + * Fires a single heap update. + */ + public void updateHeapInfo() { + try { + HandleHeap.sendHPIF(this, HandleHeap.HPIF_WHEN_NOW); + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + } + } + + /** + * Returns whether any heap update is enabled. + * @see #setHeapUpdateEnabled(boolean) + */ + public boolean isHeapUpdateEnabled() { + return mHeapInfoUpdateEnabled || mHeapSegmentUpdateEnabled; + } + + /** + * Sends a native heap update request. this is asynchronous. + *

The native heap info can be accessed by {@link ClientData#getNativeAllocationList()}. + * The notification that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_NATIVE_HEAP_DATA}. + */ + public boolean requestNativeHeapInformation() { + try { + HandleNativeHeap.sendNHGT(this); + return true; + } catch (IOException e) { + Log.e("ddmlib", e); + } + + return false; + } + + /** + * Enables or disables the Allocation tracker for this client. + *

If enabled, the VM will start tracking allocation information. A call to + * {@link #requestAllocationDetails()} will make the VM sends the information about all the + * allocations that happened between the enabling and the request. + * @param enable + * @see #requestAllocationDetails() + */ + public void enableAllocationTracker(boolean enable) { + try { + HandleHeap.sendREAE(this, enable); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a request to the VM to send the enable status of the allocation tracking. + * This is asynchronous. + *

The allocation status can be accessed by {@link ClientData#getAllocationStatus()}. + * The notification that the new status is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}. + */ + public void requestAllocationStatus() { + try { + HandleHeap.sendREAQ(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a request to the VM to send the information about all the allocations that have + * happened since the call to {@link #enableAllocationTracker(boolean)} with enable + * set to null. This is asynchronous. + *

The allocation information can be accessed by {@link ClientData#getAllocations()}. + * The notification that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_HEAP_ALLOCATIONS}. + */ + public void requestAllocationDetails() { + try { + HandleHeap.sendREAL(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a kill message to the VM. + */ + public void kill() { + try { + HandleExit.sendEXIT(this, 1); + } catch (IOException ioe) { + Log.w("ddms", "Send of EXIT message failed"); + // ignore + } + } + + /** + * Registers the client with a Selector. + */ + void register(Selector sel) throws IOException { + if (mChan != null) { + mChan.register(sel, SelectionKey.OP_READ, this); + } + } + + /** + * Sets the client to accept debugger connection on the "selected debugger port". + * + * @see AndroidDebugBridge#setSelectedClient(Client) + * @see DdmPreferences#setSelectedDebugPort(int) + */ + public void setAsSelectedClient() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setSelectedClient(this); + } + } + + /** + * Returns whether this client is the current selected client, accepting debugger connection + * on the "selected debugger port". + * + * @see #setAsSelectedClient() + * @see AndroidDebugBridge#setSelectedClient(Client) + * @see DdmPreferences#setSelectedDebugPort(int) + */ + public boolean isSelectedClient() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + return monitorThread.getSelectedClient() == this; + } + + return false; + } + + /** + * Tell the client to open a server socket channel and listen for + * connections on the specified port. + */ + void listenForDebugger(int listenPort) throws IOException { + mDebuggerListenPort = listenPort; + mDebugger = new Debugger(this, listenPort); + } + + /** + * Initiate the JDWP handshake. + * + * On failure, closes the socket and returns false. + */ + boolean sendHandshake() { + assert mWriteBuffer.position() == 0; + + try { + // assume write buffer can hold 14 bytes + JdwpPacket.putHandshake(mWriteBuffer); + int expectedLen = mWriteBuffer.position(); + mWriteBuffer.flip(); + if (mChan.write(mWriteBuffer) != expectedLen) + throw new IOException("partial handshake write"); + } + catch (IOException ioe) { + Log.e("ddms-client", "IO error during handshake: " + ioe.getMessage()); + mConnState = ST_ERROR; + close(true /* notify */); + return false; + } + finally { + mWriteBuffer.clear(); + } + + mConnState = ST_AWAIT_SHAKE; + + return true; + } + + + /** + * Send a non-DDM packet to the client. + * + * Equivalent to sendAndConsume(packet, null). + */ + void sendAndConsume(JdwpPacket packet) throws IOException { + sendAndConsume(packet, null); + } + + /** + * Send a DDM packet to the client. + * + * Ideally, we can do this with a single channel write. If that doesn't + * happen, we have to prevent anybody else from writing to the channel + * until this packet completes, so we synchronize on the channel. + * + * Another goal is to avoid unnecessary buffer copies, so we write + * directly out of the JdwpPacket's ByteBuffer. + */ + void sendAndConsume(JdwpPacket packet, ChunkHandler replyHandler) + throws IOException { + + // Fix to avoid a race condition on mChan. This should be better synchronized + // but just capturing the channel here, avoids a NPE. + SocketChannel chan = mChan; + if (chan == null) { + // can happen for e.g. THST packets + Log.v("ddms", "Not sending packet -- client is closed"); + return; + } + + if (replyHandler != null) { + /* + * Add the ID to the list of outstanding requests. We have to do + * this before sending the packet, in case the response comes back + * before our thread returns from the packet-send function. + */ + addRequestId(packet.getId(), replyHandler); + } + + // Synchronizing on this variable is still useful as we do not want to threads + // reading at the same time from the same channel, and the only change that + // can happen to this channel is to be closed and mChan become null. + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (chan) { + try { + packet.writeAndConsume(chan); + } + catch (IOException ioe) { + removeRequestId(packet.getId()); + throw ioe; + } + } + } + + /** + * Forward the packet to the debugger (if still connected to one). + * + * Consumes the packet. + */ + void forwardPacketToDebugger(JdwpPacket packet) + throws IOException { + + Debugger dbg = mDebugger; + + if (dbg == null) { + Log.d("ddms", "Discarding packet"); + packet.consume(); + } else { + dbg.sendAndConsume(packet); + } + } + + /** + * Read data from our channel. + * + * This is called when data is known to be available, and we don't yet + * have a full packet in the buffer. If the buffer is at capacity, + * expand it. + */ + void read() + throws IOException, BufferOverflowException { + + int count; + + if (mReadBuffer.position() == mReadBuffer.capacity()) { + if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) { + Log.e("ddms", "Exceeded MAX_BUF_SIZE!"); + throw new BufferOverflowException(); + } + Log.d("ddms", "Expanding read buffer to " + + mReadBuffer.capacity() * 2); + + ByteBuffer newBuffer = ByteBuffer.allocate(mReadBuffer.capacity() * 2); + + // copy entire buffer to new buffer + mReadBuffer.position(0); + newBuffer.put(mReadBuffer); // leaves "position" at end of copied + + mReadBuffer = newBuffer; + } + + count = mChan.read(mReadBuffer); + if (count < 0) + throw new IOException("read failed"); + + if (Log.Config.LOGV) Log.v("ddms", "Read " + count + " bytes from " + this); + //Log.hexDump("ddms", Log.DEBUG, mReadBuffer.array(), + // mReadBuffer.arrayOffset(), mReadBuffer.position()); + } + + /** + * Return information for the first full JDWP packet in the buffer. + * + * If we don't yet have a full packet, return null. + * + * If we haven't yet received the JDWP handshake, we watch for it here + * and consume it without admitting to have done so. Upon receipt + * we send out the "HELO" message, which is why this can throw an + * IOException. + */ + JdwpPacket getJdwpPacket() throws IOException { + + /* + * On entry, the data starts at offset 0 and ends at "position". + * "limit" is set to the buffer capacity. + */ + if (mConnState == ST_AWAIT_SHAKE) { + /* + * The first thing we get from the client is a response to our + * handshake. It doesn't look like a packet, so we have to + * handle it specially. + */ + int result; + + result = JdwpPacket.findHandshake(mReadBuffer); + //Log.v("ddms", "findHand: " + result); + switch (result) { + case JdwpPacket.HANDSHAKE_GOOD: + Log.d("ddms", + "Good handshake from client, sending HELO to " + mClientData.getPid()); + JdwpPacket.consumeHandshake(mReadBuffer); + mConnState = ST_NEED_DDM_PKT; + HandleHello.sendHelloCommands(this, SERVER_PROTOCOL_VERSION); + // see if we have another packet in the buffer + return getJdwpPacket(); + case JdwpPacket.HANDSHAKE_BAD: + Log.d("ddms", "Bad handshake from client"); + if (MonitorThread.getInstance().getRetryOnBadHandshake()) { + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + mDevice.getMonitor().addClientToDropAndReopen(this, + IDebugPortProvider.NO_STATIC_PORT); + } else { + // mark it as bad, close the socket, and don't retry + mConnState = ST_NOT_JDWP; + close(true /* notify */); + } + break; + case JdwpPacket.HANDSHAKE_NOTYET: + Log.d("ddms", "No handshake from client yet."); + break; + default: + Log.e("ddms", "Unknown packet while waiting for client handshake"); + } + return null; + } else if (mConnState == ST_NEED_DDM_PKT || + mConnState == ST_NOT_DDM || + mConnState == ST_READY) { + /* + * Normal packet traffic. + */ + if (mReadBuffer.position() != 0) { + if (Log.Config.LOGV) Log.v("ddms", + "Checking " + mReadBuffer.position() + " bytes"); + } + return JdwpPacket.findPacket(mReadBuffer); + } else { + /* + * Not expecting data when in this state. + */ + Log.e("ddms", "Receiving data in state = " + mConnState); + } + + return null; + } + + /* + * Add the specified ID to the list of request IDs for which we await + * a response. + */ + private void addRequestId(int id, ChunkHandler handler) { + synchronized (mOutstandingReqs) { + if (Log.Config.LOGV) Log.v("ddms", + "Adding req 0x" + Integer.toHexString(id) +" to set"); + mOutstandingReqs.put(id, handler); + } + } + + /* + * Remove the specified ID from the list, if present. + */ + void removeRequestId(int id) { + synchronized (mOutstandingReqs) { + if (Log.Config.LOGV) Log.v("ddms", + "Removing req 0x" + Integer.toHexString(id) + " from set"); + mOutstandingReqs.remove(id); + } + + //Log.w("ddms", "Request " + Integer.toHexString(id) + // + " could not be removed from " + this); + } + + /** + * Determine whether this is a response to a request we sent earlier. + * If so, return the ChunkHandler responsible. + */ + ChunkHandler isResponseToUs(int id) { + + synchronized (mOutstandingReqs) { + ChunkHandler handler = mOutstandingReqs.get(id); + if (handler != null) { + if (Log.Config.LOGV) Log.v("ddms", + "Found 0x" + Integer.toHexString(id) + + " in request set - " + handler); + return handler; + } + } + + return null; + } + + /** + * An earlier request resulted in a failure. This is the expected + * response to a HELO message when talking to a non-DDM client. + */ + void packetFailed(JdwpPacket reply) { + if (mConnState == ST_NEED_DDM_PKT) { + Log.d("ddms", "Marking " + this + " as non-DDM client"); + mConnState = ST_NOT_DDM; + } else if (mConnState != ST_NOT_DDM) { + Log.w("ddms", "WEIRD: got JDWP failure packet on DDM req"); + } + } + + /** + * The MonitorThread calls this when it sees a DDM request or reply. + * If we haven't seen a DDM packet before, we advance the state to + * ST_READY and return "false". Otherwise, just return true. + * + * The idea is to let the MonitorThread know when we first see a DDM + * packet, so we can send a broadcast to the handlers when a client + * connection is made. This method is synchronized so that we only + * send the broadcast once. + */ + synchronized boolean ddmSeen() { + if (mConnState == ST_NEED_DDM_PKT) { + mConnState = ST_READY; + return false; + } else if (mConnState != ST_READY) { + Log.w("ddms", "WEIRD: in ddmSeen with state=" + mConnState); + } + return true; + } + + /** + * Close the client socket channel. If there is a debugger associated + * with us, close that too. + * + * Closing a channel automatically unregisters it from the selector. + * However, we have to iterate through the selector loop before it + * actually lets them go and allows the file descriptors to close. + * The caller is expected to manage that. + * @param notify Whether or not to notify the listeners of a change. + */ + void close(boolean notify) { + Log.d("ddms", "Closing " + this.toString()); + + mOutstandingReqs.clear(); + + try { + if (mChan != null) { + mChan.close(); + mChan = null; + } + + if (mDebugger != null) { + mDebugger.close(); + mDebugger = null; + } + } + catch (IOException ioe) { + Log.w("ddms", "failed to close " + this); + // swallow it -- not much else to do + } + + mDevice.removeClient(this, notify); + } + + /** + * Returns whether this {@link Client} has a valid connection to the application VM. + */ + public boolean isValid() { + return mChan != null; + } + + void update(int changeMask) { + mDevice.update(this, changeMask); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ClientData.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ClientData.java new file mode 100644 index 0000000..2362a44 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ClientData.java @@ -0,0 +1,843 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; + + +/** + * Contains the data of a {@link Client}. + */ +public class ClientData { + /* This is a place to stash data associated with a Client, such as thread + * states or heap data. ClientData maps 1:1 to Client, but it's a little + * cleaner if we separate the data out. + * + * Message handlers are welcome to stash arbitrary data here. + * + * IMPORTANT: The data here is written by HandleFoo methods and read by + * FooPanel methods, which run in different threads. All non-trivial + * access should be synchronized against the ClientData object. + */ + + + /** Temporary name of VM to be ignored. */ + private static final String PRE_INITIALIZED = ""; //$NON-NLS-1$ + + public enum DebuggerStatus { + /** Debugger connection status: not waiting on one, not connected to one, but accepting + * new connections. This is the default value. */ + DEFAULT, + /** + * Debugger connection status: the application's VM is paused, waiting for a debugger to + * connect to it before resuming. */ + WAITING, + /** Debugger connection status : Debugger is connected */ + ATTACHED, + /** Debugger connection status: The listening port for debugger connection failed to listen. + * No debugger will be able to connect. */ + ERROR + } + + public enum AllocationTrackingStatus { + /** + * Allocation tracking status: unknown. + *

This happens right after a {@link Client} is discovered + * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query + * regarding its allocation tracking status. + * @see Client#requestAllocationStatus() + */ + UNKNOWN, + /** Allocation tracking status: the {@link Client} is not tracking allocations. */ + OFF, + /** Allocation tracking status: the {@link Client} is tracking allocations. */ + ON + } + + public enum MethodProfilingStatus { + /** + * Method profiling status: unknown. + *

This happens right after a {@link Client} is discovered + * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query + * regarding its method profiling status. + * @see Client#requestMethodProfilingStatus() + */ + UNKNOWN, + /** Method profiling status: the {@link Client} is not profiling method calls. */ + OFF, + /** Method profiling status: the {@link Client} is tracing method calls. */ + TRACER_ON, + /** Method profiling status: the {@link Client} is being profiled via sampling. */ + SAMPLER_ON + } + + /** + * String for feature enabling starting/stopping method profiling + * @see #hasFeature(String) + */ + public static final String FEATURE_PROFILING = "method-trace-profiling"; //$NON-NLS-1$ + + /** + * String for feature enabling direct streaming of method profiling data + * @see #hasFeature(String) + */ + public static final String FEATURE_PROFILING_STREAMING = "method-trace-profiling-streaming"; //$NON-NLS-1$ + + /** + * String for feature enabling sampling profiler. + * @see #hasFeature(String) + */ + public static final String FEATURE_SAMPLING_PROFILER = "method-sample-profiling"; //$NON-NLS-1$ + + /** + * String for feature indicating support for tracing OpenGL calls. + * @see #hasFeature(String) + */ + public static final String FEATURE_OPENGL_TRACING = "opengl-tracing"; //$NON-NLS-1$ + + /** + * String for feature indicating support for providing view hierarchy. + * @see #hasFeature(String) + */ + public static final String FEATURE_VIEW_HIERARCHY = "view-hierarchy"; //$NON-NLS-1$ + + /** + * String for feature allowing to dump hprof files + * @see #hasFeature(String) + */ + public static final String FEATURE_HPROF = "hprof-heap-dump"; //$NON-NLS-1$ + + /** + * String for feature allowing direct streaming of hprof dumps + * @see #hasFeature(String) + */ + public static final String FEATURE_HPROF_STREAMING = "hprof-heap-dump-streaming"; //$NON-NLS-1$ + + @Deprecated + private static IHprofDumpHandler sHprofDumpHandler; + private static IMethodProfilingHandler sMethodProfilingHandler; + private static IAllocationTrackingHandler sAllocationTrackingHandler; + + // is this a DDM-aware client? + private boolean mIsDdmAware; + + // the client's process ID + private final int mPid; + + // Java VM identification string + private String mVmIdentifier; + + // client's self-description + private String mClientDescription; + + // client's user id (on device in a multi user environment) + private int mUserId; + + // client's user id is valid + private boolean mValidUserId; + + // client's ABI + private String mAbi; + + // jvm flag: currently only indicates whether checkJni is enabled + private String mJvmFlags; + + // how interested are we in a debugger? + private DebuggerStatus mDebuggerInterest; + + // List of supported features by the client. + private final HashSet mFeatures = new HashSet(); + + // Thread tracking (THCR, THDE). + private TreeMap mThreadMap; + + /** VM Heap data */ + private final HeapData mHeapData = new HeapData(); + /** Native Heap data */ + private final HeapData mNativeHeapData = new HeapData(); + + /** Hprof data */ + private HprofData mHprofData = null; + + private HashMap mHeapInfoMap = new HashMap(); + + /** library map info. Stored here since the backtrace data + * is computed on a need to display basis. + */ + private ArrayList mNativeLibMapInfo = + new ArrayList(); + + /** Native Alloc info list */ + private ArrayList mNativeAllocationList = + new ArrayList(); + private int mNativeTotalMemory; + + private AllocationInfo[] mAllocations; + private AllocationTrackingStatus mAllocationStatus = AllocationTrackingStatus.UNKNOWN; + + @Deprecated + private String mPendingHprofDump; + + private MethodProfilingStatus mProfilingStatus = MethodProfilingStatus.UNKNOWN; + private String mPendingMethodProfiling; + + /** + * Heap Information. + *

The heap is composed of several {@link HeapSegment} objects. + *

A call to {@link #isHeapDataComplete()} will indicate if the segments (available through + * {@link #getHeapSegments()}) represent the full heap. + */ + public static class HeapData { + private TreeSet mHeapSegments = new TreeSet(); + private boolean mHeapDataComplete = false; + private byte[] mProcessedHeapData; + private Map> mProcessedHeapMap; + + /** + * Abandon the current list of heap segments. + */ + public synchronized void clearHeapData() { + /* Abandon the old segments instead of just calling .clear(). + * This lets the user hold onto the old set if it wants to. + */ + mHeapSegments = new TreeSet(); + mHeapDataComplete = false; + } + + /** + * Add raw HPSG chunk data to the list of heap segments. + * + * @param data The raw data from an HPSG chunk. + */ + synchronized void addHeapData(ByteBuffer data) { + HeapSegment hs; + + if (mHeapDataComplete) { + clearHeapData(); + } + + try { + hs = new HeapSegment(data); + } catch (BufferUnderflowException e) { + System.err.println("Discarding short HPSG data (length " + data.limit() + ")"); + return; + } + + mHeapSegments.add(hs); + } + + /** + * Called when all heap data has arrived. + */ + synchronized void sealHeapData() { + mHeapDataComplete = true; + } + + /** + * Returns whether the heap data has been sealed. + */ + public boolean isHeapDataComplete() { + return mHeapDataComplete; + } + + /** + * Get the collected heap data, if sealed. + * + * @return The list of heap segments if the heap data has been sealed, or null if it hasn't. + */ + public Collection getHeapSegments() { + if (isHeapDataComplete()) { + return mHeapSegments; + } + return null; + } + + /** + * Sets the processed heap data. + * + * @param heapData The new heap data (can be null) + */ + public void setProcessedHeapData(byte[] heapData) { + mProcessedHeapData = heapData; + } + + /** + * Get the processed heap data, if present. + * + * @return the processed heap data, or null. + */ + public byte[] getProcessedHeapData() { + return mProcessedHeapData; + } + + public void setProcessedHeapMap(Map> heapMap) { + mProcessedHeapMap = heapMap; + } + + public Map> getProcessedHeapMap() { + return mProcessedHeapMap; + } + } + + public static class HeapInfo { + public long maxSizeInBytes; + public long sizeInBytes; + public long bytesAllocated; + public long objectsAllocated; + public long timeStamp; + public byte reason; + + public HeapInfo(long maxSizeInBytes, + long sizeInBytes, + long bytesAllocated, + long objectsAllocated, + long timeStamp, + byte reason) { + this.maxSizeInBytes = maxSizeInBytes; + this.sizeInBytes = sizeInBytes; + this.bytesAllocated = bytesAllocated; + this.objectsAllocated = objectsAllocated; + this.timeStamp = timeStamp; + this.reason = reason; + } + } + + public static class HprofData { + public enum Type { + FILE, + DATA + } + + public final Type type; + public final String filename; + public final byte[] data; + + public HprofData(@NonNull String filename) { + type = Type.FILE; + this.filename = filename; + this.data = null; + } + + public HprofData(@NonNull byte[] data) { + type = Type.DATA; + this.data = data; + this.filename = null; + } + } + + /** + * Handlers able to act on HPROF dumps. + */ + @Deprecated + public interface IHprofDumpHandler { + /** + * Called when a HPROF dump succeeded. + * @param remoteFilePath the device-side path of the HPROF file. + * @param client the client for which the HPROF file was. + */ + void onSuccess(String remoteFilePath, Client client); + + /** + * Called when a HPROF dump was successful. + * @param data the data containing the HPROF file, streamed from the VM + * @param client the client that was profiled. + */ + void onSuccess(byte[] data, Client client); + + /** + * Called when a hprof dump failed to end on the VM side + * @param client the client that was profiled. + * @param message an optional (null ok) error message to be displayed. + */ + void onEndFailure(Client client, String message); + } + + /** + * Handlers able to act on Method profiling info + */ + public interface IMethodProfilingHandler { + /** + * Called when a method tracing was successful. + * @param remoteFilePath the device-side path of the trace file. + * @param client the client that was profiled. + */ + void onSuccess(String remoteFilePath, Client client); + + /** + * Called when a method tracing was successful. + * @param data the data containing the trace file, streamed from the VM + * @param client the client that was profiled. + */ + void onSuccess(byte[] data, Client client); + + /** + * Called when method tracing failed to start + * @param client the client that was profiled. + * @param message an optional (null ok) error message to be displayed. + */ + void onStartFailure(Client client, String message); + + /** + * Called when method tracing failed to end on the VM side + * @param client the client that was profiled. + * @param message an optional (null ok) error message to be displayed. + */ + void onEndFailure(Client client, String message); + } + + /* + * Handlers able to act on allocation tracking info + */ + public interface IAllocationTrackingHandler { + /** + * Called when an allocation tracking was successful. + * @param data the data containing the encoded allocations. + * See {@link AllocationsParser#parse(java.nio.ByteBuffer)} for parsing this data. + * @param client the client for which allocations were tracked. + */ + void onSuccess(@NonNull byte[] data, @NonNull Client client); + } + + public void setHprofData(byte[] data) { + mHprofData = new HprofData(data); + } + + public void setHprofData(String filename) { + mHprofData = new HprofData(filename); + } + + public void clearHprofData() { + mHprofData = null; + } + + public HprofData getHprofData() { + return mHprofData; + } + + /** + * Sets the handler to receive notifications when an HPROF dump succeeded or failed. + * This method is deprecated, please register a client listener and listen for CHANGE_HPROF. + */ + @Deprecated + public static void setHprofDumpHandler(IHprofDumpHandler handler) { + sHprofDumpHandler = handler; + } + + @Deprecated + static IHprofDumpHandler getHprofDumpHandler() { + return sHprofDumpHandler; + } + + /** + * Sets the handler to receive notifications when an HPROF dump succeeded or failed. + * This method is deprecated, please register a client listener and listen for CHANGE_HPROF. + */ + public static void setMethodProfilingHandler(IMethodProfilingHandler handler) { + sMethodProfilingHandler = handler; + } + + static IMethodProfilingHandler getMethodProfilingHandler() { + return sMethodProfilingHandler; + } + + public static void setAllocationTrackingHandler(@NonNull IAllocationTrackingHandler handler) { + sAllocationTrackingHandler = handler; + } + + @Nullable + static IAllocationTrackingHandler getAllocationTrackingHandler() { + return sAllocationTrackingHandler; + } + + /** + * Generic constructor. + */ + ClientData(int pid) { + mPid = pid; + + mDebuggerInterest = DebuggerStatus.DEFAULT; + mThreadMap = new TreeMap(); + } + + /** + * Returns whether the process is DDM-aware. + */ + public boolean isDdmAware() { + return mIsDdmAware; + } + + /** + * Sets DDM-aware status. + */ + void isDdmAware(boolean aware) { + mIsDdmAware = aware; + } + + /** + * Returns the process ID. + */ + public int getPid() { + return mPid; + } + + /** + * Returns the Client's VM identifier. + */ + public String getVmIdentifier() { + return mVmIdentifier; + } + + /** + * Sets VM identifier. + */ + void setVmIdentifier(String ident) { + mVmIdentifier = ident; + } + + /** + * Returns the client description. + *

This is generally the name of the package defined in the + * AndroidManifest.xml. + * + * @return the client description or null if not the description was not yet + * sent by the client. + */ + public String getClientDescription() { + return mClientDescription; + } + + /** + * Returns the client's user id. + * @return user id if set, -1 otherwise + */ + public int getUserId() { + return mUserId; + } + + /** + * Returns true if the user id of this client was set. Only devices that support multiple + * users will actually return the user id to ddms. For other/older devices, this will not + * be set. + */ + public boolean isValidUserId() { + return mValidUserId; + } + + /** Returns the abi flavor (32-bit or 64-bit) of the application, null if unknown or not set. */ + @Nullable + public String getAbi() { + return mAbi; + } + + /** Returns the VM flags in use, or null if unknown. */ + public String getJvmFlags() { + return mJvmFlags; + } + + /** + * Sets client description. + * + * There may be a race between HELO and APNM. Rather than try + * to enforce ordering on the device, we just don't allow an empty + * name to replace a specified one. + */ + void setClientDescription(String description) { + if (mClientDescription == null && !description.isEmpty()) { + /* + * The application VM is first named before being assigned + * its real name. + * Depending on the timing, we can get an APNM chunk setting this name before + * another one setting the final actual name. So if we get a SetClientDescription + * with this value we ignore it. + */ + if (!PRE_INITIALIZED.equals(description)) { + mClientDescription = description; + } + } + } + + void setUserId(int id) { + mUserId = id; + mValidUserId = true; + } + + void setAbi(String abi) { + mAbi = abi; + } + + void setJvmFlags(String jvmFlags) { + mJvmFlags = jvmFlags; + } + + /** + * Returns the debugger connection status. + */ + public DebuggerStatus getDebuggerConnectionStatus() { + return mDebuggerInterest; + } + + /** + * Sets debugger connection status. + */ + void setDebuggerConnectionStatus(DebuggerStatus status) { + mDebuggerInterest = status; + } + + /** + * Sets the current heap info values for the specified heap. + * @param heapId The heap whose info to update + * @param sizeInBytes The size of the heap, in bytes + * @param bytesAllocated The number of bytes currently allocated in the heap + * @param objectsAllocated The number of objects currently allocated in + * @param timeStamp + * @param reason + */ + synchronized void setHeapInfo(int heapId, + long maxSizeInBytes, + long sizeInBytes, + long bytesAllocated, + long objectsAllocated, + long timeStamp, + byte reason) { + mHeapInfoMap.put(heapId, new HeapInfo(maxSizeInBytes, sizeInBytes, bytesAllocated, + objectsAllocated, timeStamp, reason)); + } + + /** + * Returns the {@link HeapData} object for the VM. + */ + public HeapData getVmHeapData() { + return mHeapData; + } + + /** + * Returns the {@link HeapData} object for the native code. + */ + HeapData getNativeHeapData() { + return mNativeHeapData; + } + + /** + * Returns an iterator over the list of known VM heap ids. + *

+ * The caller must synchronize on the {@link ClientData} object while iterating. + * + * @return an iterator over the list of heap ids + */ + public synchronized Iterator getVmHeapIds() { + return mHeapInfoMap.keySet().iterator(); + } + + /** + * Returns the most-recent info values for the specified VM heap. + * + * @param heapId The heap whose info should be returned + * @return a map containing the info values for the specified heap. + * Returns null if the heap ID is unknown. + */ + public synchronized HeapInfo getVmHeapInfo(int heapId) { + return mHeapInfoMap.get(heapId); + } + + /** + * Adds a new thread to the list. + */ + synchronized void addThread(int threadId, String threadName) { + ThreadInfo attr = new ThreadInfo(threadId, threadName); + mThreadMap.put(threadId, attr); + } + + /** + * Removes a thread from the list. + */ + synchronized void removeThread(int threadId) { + mThreadMap.remove(threadId); + } + + /** + * Returns the list of threads as {@link ThreadInfo} objects. + *

The list is empty until a thread update was requested with + * {@link Client#requestThreadUpdate()}. + */ + public synchronized ThreadInfo[] getThreads() { + Collection threads = mThreadMap.values(); + return threads.toArray(new ThreadInfo[threads.size()]); + } + + /** + * Returns the {@link ThreadInfo} by thread id. + */ + synchronized ThreadInfo getThread(int threadId) { + return mThreadMap.get(threadId); + } + + synchronized void clearThreads() { + mThreadMap.clear(); + } + + /** + * Returns the list of {@link NativeAllocationInfo}. + * @see Client#requestNativeHeapInformation() + */ + public synchronized List getNativeAllocationList() { + return Collections.unmodifiableList(mNativeAllocationList); + } + + /** + * adds a new {@link NativeAllocationInfo} to the {@link Client} + * @param allocInfo The {@link NativeAllocationInfo} to add. + */ + synchronized void addNativeAllocation(NativeAllocationInfo allocInfo) { + mNativeAllocationList.add(allocInfo); + } + + /** + * Clear the current malloc info. + */ + synchronized void clearNativeAllocationInfo() { + mNativeAllocationList.clear(); + } + + /** + * Returns the total native memory. + * @see Client#requestNativeHeapInformation() + */ + public synchronized int getTotalNativeMemory() { + return mNativeTotalMemory; + } + + synchronized void setTotalNativeMemory(int totalMemory) { + mNativeTotalMemory = totalMemory; + } + + synchronized void addNativeLibraryMapInfo(long startAddr, long endAddr, String library) { + mNativeLibMapInfo.add(new NativeLibraryMapInfo(startAddr, endAddr, library)); + } + + /** + * Returns the list of native libraries mapped in memory for this client. + */ + public synchronized List getMappedNativeLibraries() { + return Collections.unmodifiableList(mNativeLibMapInfo); + } + + synchronized void setAllocationStatus(AllocationTrackingStatus status) { + mAllocationStatus = status; + } + + /** + * Returns the allocation tracking status. + * @see Client#requestAllocationStatus() + */ + public synchronized AllocationTrackingStatus getAllocationStatus() { + return mAllocationStatus; + } + + synchronized void setAllocations(AllocationInfo[] allocs) { + mAllocations = allocs; + } + + /** + * Returns the list of tracked allocations. + * @see Client#requestAllocationDetails() + */ + @Nullable + public synchronized AllocationInfo[] getAllocations() { + return mAllocations; + } + + void addFeature(String feature) { + mFeatures.add(feature); + } + + /** + * Returns true if the {@link Client} supports the given feature + * @param feature The feature to test. + * @return true if the feature is supported + * + * @see ClientData#FEATURE_PROFILING + * @see ClientData#FEATURE_HPROF + */ + public boolean hasFeature(String feature) { + return mFeatures.contains(feature); + } + + /** + * Sets the device-side path to the hprof file being written + * @param pendingHprofDump the file to the hprof file + */ + @Deprecated + void setPendingHprofDump(String pendingHprofDump) { + mPendingHprofDump = pendingHprofDump; + } + + /** + * Returns the path to the device-side hprof file being written. + */ + @Deprecated + String getPendingHprofDump() { + return mPendingHprofDump; + } + + @Deprecated + public boolean hasPendingHprofDump() { + return mPendingHprofDump != null; + } + + synchronized void setMethodProfilingStatus(MethodProfilingStatus status) { + mProfilingStatus = status; + } + + /** + * Returns the method profiling status. + * @see Client#requestMethodProfilingStatus() + */ + public synchronized MethodProfilingStatus getMethodProfilingStatus() { + return mProfilingStatus; + } + + /** + * Sets the device-side path to the method profile file being written + * @param pendingMethodProfiling the file being written + */ + void setPendingMethodProfiling(String pendingMethodProfiling) { + mPendingMethodProfiling = pendingMethodProfiling; + } + + /** + * Returns the path to the device-side method profiling file being written. + */ + String getPendingMethodProfiling() { + return mPendingMethodProfiling; + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java new file mode 100644 index 0000000..903eb2d --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + + +import com.google.common.base.Charsets; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A {@link IShellOutputReceiver} which collects the whole shell output into one + * {@link String}. + */ +public class CollectingOutputReceiver implements IShellOutputReceiver { + private CountDownLatch mCompletionLatch; + private StringBuffer mOutputBuffer = new StringBuffer(); + private AtomicBoolean mIsCanceled = new AtomicBoolean(false); + + public CollectingOutputReceiver() { + } + + public CollectingOutputReceiver(CountDownLatch commandCompleteLatch) { + mCompletionLatch = commandCompleteLatch; + } + + public String getOutput() { + return mOutputBuffer.toString(); + } + + @Override + public boolean isCancelled() { + return mIsCanceled.get(); + } + + /** + * Cancel the output collection + */ + public void cancel() { + mIsCanceled.set(true); + } + + @Override + public void addOutput(byte[] data, int offset, int length) { + if (!isCancelled()) { + String s; + s = new String(data, offset, length, Charsets.UTF_8); + mOutputBuffer.append(s); + } + } + + @Override + public void flush() { + if (mCompletionLatch != null) { + mCompletionLatch.countDown(); + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmConstants.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmConstants.java new file mode 100644 index 0000000..f998ef7 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmConstants.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +public final class DdmConstants { + + public static final int PLATFORM_UNKNOWN = 0; + public static final int PLATFORM_LINUX = 1; + public static final int PLATFORM_WINDOWS = 2; + public static final int PLATFORM_DARWIN = 3; + + /** + * Returns current platform, one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + public static final int CURRENT_PLATFORM = currentPlatform(); + + public static final String EXTENSION = "trace"; + /** + * Extension for Traceview files. + */ + public static final String DOT_TRACE = "." + EXTENSION; + + /** hprof-conv executable (with extension for the current OS) */ + public static final String FN_HPROF_CONVERTER = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "hprof-conv.exe" : "hprof-conv"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** traceview executable (with extension for the current OS) */ + public static final String FN_TRACEVIEW = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "traceview.bat" : "traceview"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** + * Returns current platform + * + * @return one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + public static int currentPlatform() { + String os = System.getProperty("os.name"); //$NON-NLS-1$ + if (os.startsWith("Mac OS")) { //$NON-NLS-1$ + return PLATFORM_DARWIN; + } else if (os.startsWith("Windows")) { //$NON-NLS-1$ + return PLATFORM_WINDOWS; + } else if (os.startsWith("Linux")) { //$NON-NLS-1$ + return PLATFORM_LINUX; + } + + return PLATFORM_UNKNOWN; + } + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmPreferences.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmPreferences.java new file mode 100644 index 0000000..b0072ec --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DdmPreferences.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +/** + * Preferences for the ddm library. + *

This class does not handle storing the preferences. It is merely a central point for + * applications using the ddmlib to override the default values. + *

Various components of the ddmlib query this class to get their values. + *

Calls to some set##() methods will update the components using the values + * right away, while other methods will have no effect once {@link AndroidDebugBridge#init(boolean)} + * has been called. + *

Check the documentation of each method. + */ +public final class DdmPreferences { + + /** Default value for thread update flag upon client connection. */ + public static final boolean DEFAULT_INITIAL_THREAD_UPDATE = false; + /** Default value for heap update flag upon client connection. */ + public static final boolean DEFAULT_INITIAL_HEAP_UPDATE = false; + /** Default value for the selected client debug port */ + public static final int DEFAULT_SELECTED_DEBUG_PORT = 8700; + /** Default value for the debug port base */ + public static final int DEFAULT_DEBUG_PORT_BASE = 8600; + /** Default value for the logcat {@link LogLevel} */ + public static final LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR; + /** Default timeout values for adb connection (milliseconds) */ + public static final int DEFAULT_TIMEOUT = 5000; // standard delay, in ms + /** Default profiler buffer size (megabytes) */ + public static final int DEFAULT_PROFILER_BUFFER_SIZE_MB = 8; + /** Default values for the use of the ADBHOST environment variable. */ + public static final boolean DEFAULT_USE_ADBHOST = false; + public static final String DEFAULT_ADBHOST_VALUE = "127.0.0.1"; + + private static boolean sThreadUpdate = DEFAULT_INITIAL_THREAD_UPDATE; + private static boolean sInitialHeapUpdate = DEFAULT_INITIAL_HEAP_UPDATE; + + private static int sSelectedDebugPort = DEFAULT_SELECTED_DEBUG_PORT; + private static int sDebugPortBase = DEFAULT_DEBUG_PORT_BASE; + private static LogLevel sLogLevel = DEFAULT_LOG_LEVEL; + private static int sTimeOut = DEFAULT_TIMEOUT; + private static int sProfilerBufferSizeMb = DEFAULT_PROFILER_BUFFER_SIZE_MB; + + private static boolean sUseAdbHost = DEFAULT_USE_ADBHOST; + private static String sAdbHostValue = DEFAULT_ADBHOST_VALUE; + + /** + * Returns the initial {@link Client} flag for thread updates. + * @see #setInitialThreadUpdate(boolean) + */ + public static boolean getInitialThreadUpdate() { + return sThreadUpdate; + } + + /** + * Sets the initial {@link Client} flag for thread updates. + *

This change takes effect right away, for newly created {@link Client} objects. + */ + public static void setInitialThreadUpdate(boolean state) { + sThreadUpdate = state; + } + + /** + * Returns the initial {@link Client} flag for heap updates. + * @see #setInitialHeapUpdate(boolean) + */ + public static boolean getInitialHeapUpdate() { + return sInitialHeapUpdate; + } + + /** + * Sets the initial {@link Client} flag for heap updates. + *

If true, the {@link ClientData} will automatically be updated with + * the VM heap information whenever a GC happens. + *

This change takes effect right away, for newly created {@link Client} objects. + */ + public static void setInitialHeapUpdate(boolean state) { + sInitialHeapUpdate = state; + } + + /** + * Returns the debug port used by the selected {@link Client}. + */ + public static int getSelectedDebugPort() { + return sSelectedDebugPort; + } + + /** + * Sets the debug port used by the selected {@link Client}. + *

This change takes effect right away. + * @param port the new port to use. + */ + public static void setSelectedDebugPort(int port) { + sSelectedDebugPort = port; + + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setDebugSelectedPort(port); + } + } + + /** + * Returns the debug port used by the first {@link Client}. Following clients, will use the + * next port. + */ + public static int getDebugPortBase() { + return sDebugPortBase; + } + + /** + * Sets the debug port used by the first {@link Client}. + *

Once a port is used, the next Client will use port + 1. Quitting applications will + * release their debug port, and new clients will be able to reuse them. + *

This must be called before {@link AndroidDebugBridge#init(boolean)}. + */ + public static void setDebugPortBase(int port) { + sDebugPortBase = port; + } + + /** + * Returns the minimum {@link LogLevel} being displayed. + */ + public static LogLevel getLogLevel() { + return sLogLevel; + } + + /** + * Sets the minimum {@link LogLevel} to display. + *

This change takes effect right away. + */ + public static void setLogLevel(String value) { + sLogLevel = LogLevel.getByString(value); + + Log.setLevel(sLogLevel); + } + + /** + * Returns the timeout to be used in adb connections (milliseconds). + */ + public static int getTimeOut() { + return sTimeOut; + } + + /** + * Sets the timeout value for adb connection. + *

This change takes effect for newly created connections only. + * @param timeOut the timeout value (milliseconds). + */ + public static void setTimeOut(int timeOut) { + sTimeOut = timeOut; + } + + /** + * Returns the profiler buffer size (megabytes). + */ + public static int getProfilerBufferSizeMb() { + return sProfilerBufferSizeMb; + } + + /** + * Sets the profiler buffer size value. + * @param bufferSizeMb the buffer size (megabytes). + */ + public static void setProfilerBufferSizeMb(int bufferSizeMb) { + sProfilerBufferSizeMb = bufferSizeMb; + } + + /** + * Returns a boolean indicating that the user uses or not the variable ADBHOST. + */ + public static boolean getUseAdbHost() { + return sUseAdbHost; + } + + /** + * Sets the value of the boolean indicating that the user uses or not the variable ADBHOST. + * @param useAdbHost true if the user uses ADBHOST + */ + public static void setUseAdbHost(boolean useAdbHost) { + sUseAdbHost = useAdbHost; + } + + /** + * Returns the value of the ADBHOST variable set by the user. + */ + public static String getAdbHostValue() { + return sAdbHostValue; + } + + /** + * Sets the value of the ADBHOST variable. + * @param adbHostValue + */ + public static void setAdbHostValue(String adbHostValue) { + sAdbHostValue = adbHostValue; + } + + /** + * Non accessible constructor. + */ + private DdmPreferences() { + // pass, only static methods in the class. + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DebugPortManager.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DebugPortManager.java new file mode 100644 index 0000000..2d2d081 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DebugPortManager.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Centralized point to provide a {@link IDebugPortProvider} to ddmlib. + * + *

When {@link Client} objects are created, they start listening for debuggers on a specific + * port. The default behavior is to start with {@link DdmPreferences#getDebugPortBase()} and + * increment this value for each new Client. + * + *

This {@link DebugPortManager} allows applications using ddmlib to provide a custom + * port provider on a per-Client basis, depending on the device/emulator they are + * running on, and/or their names. + */ +public class DebugPortManager { + + /** + * Classes which implement this interface provide a method that provides a non random + * debugger port for a newly created {@link Client}. + */ + public interface IDebugPortProvider { + + int NO_STATIC_PORT = -1; + + /** + * Returns a non-random debugger port for the specified application running on the + * specified {@link Device}. + * @param device The device the application is running on. + * @param appName The application name, as defined in the AndroidManifest.xml + * package attribute of the manifest node. + * @return The non-random debugger port or {@link #NO_STATIC_PORT} if the {@link Client} + * should use the automatic debugger port provider. + */ + int getPort(IDevice device, String appName); + } + + private static IDebugPortProvider sProvider = null; + + /** + * Sets the {@link IDebugPortProvider} that will be used when a new {@link Client} requests + * a debugger port. + * @param provider the IDebugPortProvider to use. + */ + public static void setProvider(IDebugPortProvider provider) { + sProvider = provider; + } + + /** + * Returns the + * @return + */ + static IDebugPortProvider getProvider() { + return sProvider; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Debugger.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Debugger.java new file mode 100644 index 0000000..9356c13 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Debugger.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.ClientData.DebuggerStatus; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +/** + * This represents a pending or established connection with a JDWP debugger. + */ +class Debugger { + + /* + * Messages from the debugger should be pretty small; may not even + * need an expanding-buffer implementation for this. + */ + private static final int INITIAL_BUF_SIZE = 1 * 1024; + private static final int MAX_BUF_SIZE = 32 * 1024; + private ByteBuffer mReadBuffer; + + private static final int PRE_DATA_BUF_SIZE = 256; + private ByteBuffer mPreDataBuffer; + + /* connection state */ + private int mConnState; + private static final int ST_NOT_CONNECTED = 1; + private static final int ST_AWAIT_SHAKE = 2; + private static final int ST_READY = 3; + + /* peer */ + private Client mClient; // client we're forwarding to/from + private int mListenPort; // listen to me + private ServerSocketChannel mListenChannel; + + /* this goes up and down; synchronize methods that access the field */ + private SocketChannel mChannel; + + /** + * Create a new Debugger object, configured to listen for connections + * on a specific port. + */ + Debugger(Client client, int listenPort) throws IOException { + + mClient = client; + mListenPort = listenPort; + + mListenChannel = ServerSocketChannel.open(); + mListenChannel.configureBlocking(false); // required for Selector + + InetSocketAddress addr = new InetSocketAddress( + InetAddress.getByName("localhost"), //$NON-NLS-1$ + listenPort); + mListenChannel.socket().setReuseAddress(true); // enable SO_REUSEADDR + mListenChannel.socket().bind(addr); + + mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE); + mPreDataBuffer = ByteBuffer.allocate(PRE_DATA_BUF_SIZE); + mConnState = ST_NOT_CONNECTED; + + Log.d("ddms", "Created: " + this.toString()); + } + + /** + * Returns "true" if a debugger is currently attached to us. + */ + boolean isDebuggerAttached() { + return mChannel != null; + } + + /** + * Represent the Debugger as a string. + */ + @Override + public String toString() { + // mChannel != null means we have connection, ST_READY means it's going + return "[Debugger " + mListenPort + "-->" + mClient.getClientData().getPid() + + ((mConnState != ST_READY) ? " inactive]" : " active]"); + } + + /** + * Register the debugger's listen socket with the Selector. + */ + void registerListener(Selector sel) throws IOException { + mListenChannel.register(sel, SelectionKey.OP_ACCEPT, this); + } + + /** + * Return the Client being debugged. + */ + Client getClient() { + return mClient; + } + + /** + * Accept a new connection, but only if we don't already have one. + * + * Must be synchronized with other uses of mChannel and mPreBuffer. + * + * Returns "null" if we're already talking to somebody. + */ + synchronized SocketChannel accept() throws IOException { + return accept(mListenChannel); + } + + /** + * Accept a new connection from the specified listen channel. This + * is so we can listen on a dedicated port for the "current" client, + * where "current" is constantly in flux. + * + * Must be synchronized with other uses of mChannel and mPreBuffer. + * + * Returns "null" if we're already talking to somebody. + */ + synchronized SocketChannel accept(ServerSocketChannel listenChan) + throws IOException { + + if (listenChan != null) { + SocketChannel newChan; + + newChan = listenChan.accept(); + if (mChannel != null) { + Log.w("ddms", "debugger already talking to " + mClient + + " on " + mListenPort); + newChan.close(); + return null; + } + mChannel = newChan; + mChannel.configureBlocking(false); // required for Selector + mConnState = ST_AWAIT_SHAKE; + return mChannel; + } + + return null; + } + + /** + * Close the data connection only. + */ + synchronized void closeData() { + try { + if (mChannel != null) { + mChannel.close(); + mChannel = null; + mConnState = ST_NOT_CONNECTED; + + ClientData cd = mClient.getClientData(); + cd.setDebuggerConnectionStatus(DebuggerStatus.DEFAULT); + mClient.update(Client.CHANGE_DEBUGGER_STATUS); + } + } catch (IOException ioe) { + Log.w("ddms", "Failed to close data " + this); + } + } + + /** + * Close the socket that's listening for new connections and (if + * we're connected) the debugger data socket. + */ + synchronized void close() { + try { + if (mListenChannel != null) { + mListenChannel.close(); + } + mListenChannel = null; + closeData(); + } catch (IOException ioe) { + Log.w("ddms", "Failed to close listener " + this); + } + } + + // TODO: ?? add a finalizer that verifies the channel was closed + + /** + * Read data from our channel. + * + * This is called when data is known to be available, and we don't yet + * have a full packet in the buffer. If the buffer is at capacity, + * expand it. + */ + void read() throws IOException { + int count; + + if (mReadBuffer.position() == mReadBuffer.capacity()) { + if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) { + throw new BufferOverflowException(); + } + Log.d("ddms", "Expanding read buffer to " + + mReadBuffer.capacity() * 2); + + ByteBuffer newBuffer = + ByteBuffer.allocate(mReadBuffer.capacity() * 2); + mReadBuffer.position(0); + newBuffer.put(mReadBuffer); // leaves "position" at end + + mReadBuffer = newBuffer; + } + + count = mChannel.read(mReadBuffer); + Log.v("ddms", "Read " + count + " bytes from " + this); + if (count < 0) throw new IOException("read failed"); + } + + /** + * Return information for the first full JDWP packet in the buffer. + * + * If we don't yet have a full packet, return null. + * + * If we haven't yet received the JDWP handshake, we watch for it here + * and consume it without admitting to have done so. We also send + * the handshake response to the debugger, along with any pending + * pre-connection data, which is why this can throw an IOException. + */ + JdwpPacket getJdwpPacket() throws IOException { + /* + * On entry, the data starts at offset 0 and ends at "position". + * "limit" is set to the buffer capacity. + */ + if (mConnState == ST_AWAIT_SHAKE) { + int result; + + result = JdwpPacket.findHandshake(mReadBuffer); + //Log.v("ddms", "findHand: " + result); + switch (result) { + case JdwpPacket.HANDSHAKE_GOOD: + Log.d("ddms", "Good handshake from debugger"); + JdwpPacket.consumeHandshake(mReadBuffer); + sendHandshake(); + mConnState = ST_READY; + + ClientData cd = mClient.getClientData(); + cd.setDebuggerConnectionStatus(DebuggerStatus.ATTACHED); + mClient.update(Client.CHANGE_DEBUGGER_STATUS); + + // see if we have another packet in the buffer + return getJdwpPacket(); + case JdwpPacket.HANDSHAKE_BAD: + // not a debugger, throw an exception so we drop the line + Log.d("ddms", "Bad handshake from debugger"); + throw new IOException("bad handshake"); + case JdwpPacket.HANDSHAKE_NOTYET: + break; + default: + Log.e("ddms", "Unknown packet while waiting for client handshake"); + } + return null; + } else if (mConnState == ST_READY) { + if (mReadBuffer.position() != 0) { + Log.v("ddms", "Checking " + mReadBuffer.position() + " bytes"); + } + return JdwpPacket.findPacket(mReadBuffer); + } else { + Log.e("ddms", "Receiving data in state = " + mConnState); + } + + return null; + } + + /** + * Forward a packet to the client. + * + * "mClient" will never be null, though it's possible that the channel + * in the client has closed and our send attempt will fail. + * + * Consumes the packet. + */ + void forwardPacketToClient(JdwpPacket packet) throws IOException { + mClient.sendAndConsume(packet); + } + + /** + * Send the handshake to the debugger. We also send along any packets + * we already received from the client (usually just a VM_START event, + * if anything at all). + */ + private synchronized void sendHandshake() throws IOException { + ByteBuffer tempBuffer = ByteBuffer.allocate(JdwpPacket.HANDSHAKE_LEN); + JdwpPacket.putHandshake(tempBuffer); + int expectedLength = tempBuffer.position(); + tempBuffer.flip(); + if (mChannel.write(tempBuffer) != expectedLength) { + throw new IOException("partial handshake write"); + } + + expectedLength = mPreDataBuffer.position(); + if (expectedLength > 0) { + Log.d("ddms", "Sending " + mPreDataBuffer.position() + + " bytes of saved data"); + mPreDataBuffer.flip(); + if (mChannel.write(mPreDataBuffer) != expectedLength) { + throw new IOException("partial pre-data write"); + } + mPreDataBuffer.clear(); + } + } + + /** + * Send a packet to the debugger. + * + * Ideally, we can do this with a single channel write. If that doesn't + * happen, we have to prevent anybody else from writing to the channel + * until this packet completes, so we synchronize on the channel. + * + * Another goal is to avoid unnecessary buffer copies, so we write + * directly out of the JdwpPacket's ByteBuffer. + * + * We must synchronize on "mChannel" before writing to it. We want to + * coordinate the buffered data with mChannel creation, so this whole + * method is synchronized. + */ + synchronized void sendAndConsume(JdwpPacket packet) + throws IOException { + + if (mChannel == null) { + /* + * Buffer this up so we can send it to the debugger when it + * finally does connect. This is essential because the VM_START + * message might be telling the debugger that the VM is + * suspended. The alternative approach would be for us to + * capture and interpret VM_START and send it later if we + * didn't choose to un-suspend the VM for our own purposes. + */ + Log.d("ddms", "Saving packet 0x" + + Integer.toHexString(packet.getId())); + packet.movePacket(mPreDataBuffer); + } else { + packet.writeAndConsume(mChannel); + } + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Device.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Device.java new file mode 100644 index 0000000..1cb1a4c --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Device.java @@ -0,0 +1,1295 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.concurrency.GuardedBy; +import com.android.ddmlib.log.LogReceiver; +import com.google.common.base.CharMatcher; +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.Atomics; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * A Device. It can be a physical device or an emulator. + */ +final class Device implements IDevice { + /** Emulator Serial Number regexp. */ + static final String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$ + + /** Serial number of the device */ + private final String mSerialNumber; + + /** Name of the AVD */ + private String mAvdName = null; + + /** State of the device. */ + private DeviceState mState = null; + + /** Device properties. */ + private final PropertyFetcher mPropFetcher = new PropertyFetcher(this); + private final Map mMountPoints = new HashMap(); + + private final BatteryFetcher mBatteryFetcher = new BatteryFetcher(this); + + @GuardedBy("mClients") + private final List mClients = new ArrayList(); + + /** Maps pid's of clients in {@link #mClients} to their package name. */ + private final Map mClientInfo = new ConcurrentHashMap(); + + private DeviceMonitor mMonitor; + + private static final String LOG_TAG = "Device"; + private static final char SEPARATOR = '-'; + private static final String UNKNOWN_PACKAGE = ""; //$NON-NLS-1$ + + private static final long GET_PROP_TIMEOUT_MS = 100; + private static final long INSTALL_TIMEOUT_MINUTES; + + static { + String installTimeout = System.getenv("ADB_INSTALL_TIMEOUT"); + long time = 4; + if (installTimeout != null) { + try { + time = Long.parseLong(installTimeout); + } catch (NumberFormatException e) { + // use default value + } + } + INSTALL_TIMEOUT_MINUTES = time; + } + + /** + * Socket for the connection monitoring client connection/disconnection. + */ + private SocketChannel mSocketChannel; + + private Integer mLastBatteryLevel = null; + private long mLastBatteryCheckTime = 0; + + /** Path to the screen recorder binary on the device. */ + private static final String SCREEN_RECORDER_DEVICE_PATH = "/system/bin/screenrecord"; + private static final long LS_TIMEOUT_SEC = 2; + + /** Flag indicating whether the device has the screen recorder binary. */ + private Boolean mHasScreenRecorder; + + /** Cached list of hardware characteristics */ + private Set mHardwareCharacteristics; + + private int mApiLevel; + private String mName; + + /** + * Output receiver for "pm install package.apk" command line. + */ + private static final class InstallReceiver extends MultiLineReceiver { + + private static final String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$ + private static final Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$ + + private String mErrorMessage = null; + + public InstallReceiver() { + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (!line.isEmpty()) { + if (line.startsWith(SUCCESS_OUTPUT)) { + mErrorMessage = null; + } else { + Matcher m = FAILURE_PATTERN.matcher(line); + if (m.matches()) { + mErrorMessage = m.group(1); + } else { + mErrorMessage = "Unknown failure"; + } + } + } + } + } + + @Override + public boolean isCancelled() { + return false; + } + + public String getErrorMessage() { + return mErrorMessage; + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getSerialNumber() + */ + @NonNull + @Override + public String getSerialNumber() { + return mSerialNumber; + } + + @Override + public String getAvdName() { + return mAvdName; + } + + /** + * Sets the name of the AVD + */ + void setAvdName(String avdName) { + if (!isEmulator()) { + throw new IllegalArgumentException( + "Cannot set the AVD name of the device is not an emulator"); + } + + mAvdName = avdName; + } + + @Override + public String getName() { + if (mName != null) { + return mName; + } + + if (isOnline()) { + // cache name only if device is online + mName = constructName(); + return mName; + } else { + return constructName(); + } + } + + private String constructName() { + if (isEmulator()) { + String avdName = getAvdName(); + if (avdName != null) { + return String.format("%s [%s]", avdName, getSerialNumber()); + } else { + return getSerialNumber(); + } + } else { + String manufacturer = null; + String model = null; + + try { + manufacturer = cleanupStringForDisplay(getProperty(PROP_DEVICE_MANUFACTURER)); + model = cleanupStringForDisplay(getProperty(PROP_DEVICE_MODEL)); + } catch (Exception e) { + // If there are exceptions thrown while attempting to get these properties, + // we can just use the serial number, so ignore these exceptions. + } + + StringBuilder sb = new StringBuilder(20); + + if (manufacturer != null) { + sb.append(manufacturer); + sb.append(SEPARATOR); + } + + if (model != null) { + sb.append(model); + sb.append(SEPARATOR); + } + + sb.append(getSerialNumber()); + return sb.toString(); + } + } + + private String cleanupStringForDisplay(String s) { + if (s == null) { + return null; + } + + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (Character.isLetterOrDigit(c)) { + sb.append(Character.toLowerCase(c)); + } else { + sb.append('_'); + } + } + + return sb.toString(); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getState() + */ + @Override + public DeviceState getState() { + return mState; + } + + /** + * Changes the state of the device. + */ + void setState(DeviceState state) { + mState = state; + } + + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getProperties() + */ + @Override + public Map getProperties() { + return Collections.unmodifiableMap(mPropFetcher.getProperties()); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getPropertyCount() + */ + @Override + public int getPropertyCount() { + return mPropFetcher.getProperties().size(); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getProperty(java.lang.String) + */ + @Override + public String getProperty(String name) { + Future future = mPropFetcher.getProperty(name); + try { + return future.get(GET_PROP_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // ignore + } catch (ExecutionException e) { + // ignore + } catch (java.util.concurrent.TimeoutException e) { + // ignore + } + return null; + } + + @Override + public boolean arePropertiesSet() { + return mPropFetcher.arePropertiesSet(); + } + + @Override + public String getPropertyCacheOrSync(String name) throws TimeoutException, + AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { + Future future = mPropFetcher.getProperty(name); + try { + return future.get(); + } catch (InterruptedException e) { + // ignore + } catch (ExecutionException e) { + // ignore + } + return null; + } + + @Override + public String getPropertySync(String name) throws TimeoutException, + AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { + Future future = mPropFetcher.getProperty(name); + try { + return future.get(); + } catch (InterruptedException e) { + // ignore + } catch (ExecutionException e) { + // ignore + } + return null; + } + + @NonNull + @Override + public Future getSystemProperty(@NonNull String name) { + return mPropFetcher.getProperty(name); + } + + @Override + public boolean supportsFeature(@NonNull Feature feature) { + switch (feature) { + case SCREEN_RECORD: + if (getApiLevel() < 19) { + return false; + } + if (mHasScreenRecorder == null) { + mHasScreenRecorder = hasBinary(SCREEN_RECORDER_DEVICE_PATH); + } + return mHasScreenRecorder; + case PROCSTATS: + return getApiLevel() >= 19; + default: + return false; + } + } + + // The full list of features can be obtained from /etc/permissions/features* + // However, the smaller set of features we are interested in can be obtained by + // reading the build characteristics property. + @Override + public boolean supportsFeature(@NonNull HardwareFeature feature) { + if (mHardwareCharacteristics == null) { + try { + String characteristics = getProperty(PROP_BUILD_CHARACTERISTICS); + if (characteristics == null) { + return false; + } + + mHardwareCharacteristics = Sets.newHashSet(Splitter.on(',').split(characteristics)); + } catch (Exception e) { + mHardwareCharacteristics = Collections.emptySet(); + } + } + + return mHardwareCharacteristics.contains(feature.getCharacteristic()); + } + + private int getApiLevel() { + if (mApiLevel > 0) { + return mApiLevel; + } + + try { + String buildApi = getProperty(PROP_BUILD_API_LEVEL); + mApiLevel = buildApi == null ? -1 : Integer.parseInt(buildApi); + return mApiLevel; + } catch (Exception e) { + return -1; + } + } + + private boolean hasBinary(String path) { + CountDownLatch latch = new CountDownLatch(1); + CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch); + try { + executeShellCommand("ls " + path, receiver); + } catch (Exception e) { + return false; + } + + try { + latch.await(LS_TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } + + String value = receiver.getOutput().trim(); + return !value.endsWith("No such file or directory"); + } + + @Nullable + @Override + public String getMountPoint(@NonNull String name) { + String mount = mMountPoints.get(name); + if (mount == null) { + try { + mount = queryMountPoint(name); + mMountPoints.put(name, mount); + } catch (TimeoutException ignored) { + } catch (AdbCommandRejectedException ignored) { + } catch (ShellCommandUnresponsiveException ignored) { + } catch (IOException ignored) { + } + } + return mount; + } + + @Nullable + private String queryMountPoint(@NonNull final String name) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + + final AtomicReference ref = Atomics.newReference(); + executeShellCommand("echo $" + name, new MultiLineReceiver() { //$NON-NLS-1$ + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (!line.isEmpty()) { + // this should be the only one. + ref.set(line); + } + } + } + }); + return ref.get(); + } + + @Override + public String toString() { + return mSerialNumber; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isOnline() + */ + @Override + public boolean isOnline() { + return mState == DeviceState.ONLINE; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isEmulator() + */ + @Override + public boolean isEmulator() { + return mSerialNumber.matches(RE_EMULATOR_SN); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isOffline() + */ + @Override + public boolean isOffline() { + return mState == DeviceState.OFFLINE; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isBootLoader() + */ + @Override + public boolean isBootLoader() { + return mState == DeviceState.BOOTLOADER; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getSyncService() + */ + @Override + public SyncService getSyncService() + throws TimeoutException, AdbCommandRejectedException, IOException { + SyncService syncService = new SyncService(AndroidDebugBridge.getSocketAddress(), this); + if (syncService.openSync()) { + return syncService; + } + + return null; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getFileListingService() + */ + @Override + public FileListingService getFileListingService() { + return new FileListingService(this); + } + + @Override + public RawImage getScreenshot() + throws TimeoutException, AdbCommandRejectedException, IOException { + return getScreenshot(0, TimeUnit.MILLISECONDS); + } + + @Override + public RawImage getScreenshot(long timeout, TimeUnit unit) + throws TimeoutException, AdbCommandRejectedException, IOException { + return AdbHelper.getFrameBuffer(AndroidDebugBridge.getSocketAddress(), this, timeout, unit); + } + + @Override + public void startScreenRecorder(String remoteFilePath, ScreenRecorderOptions options, + IShellOutputReceiver receiver) throws TimeoutException, AdbCommandRejectedException, + IOException, ShellCommandUnresponsiveException { + executeShellCommand(getScreenRecorderCommand(remoteFilePath, options), receiver, 0, null); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + static String getScreenRecorderCommand(@NonNull String remoteFilePath, + @NonNull ScreenRecorderOptions options) { + StringBuilder sb = new StringBuilder(); + + sb.append("screenrecord"); + sb.append(' '); + + if (options.width > 0 && options.height > 0) { + sb.append("--size "); + sb.append(options.width); + sb.append('x'); + sb.append(options.height); + sb.append(' '); + } + + if (options.bitrateMbps > 0) { + sb.append("--bit-rate "); + sb.append(options.bitrateMbps * 1000000); + sb.append(' '); + } + + if (options.timeLimit > 0) { + sb.append("--time-limit "); + long seconds = TimeUnit.SECONDS.convert(options.timeLimit, options.timeLimitUnits); + if (seconds > 180) { + seconds = 180; + } + sb.append(seconds); + sb.append(' '); + } + + sb.append(remoteFilePath); + + return sb.toString(); + } + + @Override + public void executeShellCommand(String command, IShellOutputReceiver receiver) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this, + receiver, DdmPreferences.getTimeOut()); + } + + @Override + public void executeShellCommand(String command, IShellOutputReceiver receiver, + int maxTimeToOutputResponse) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this, + receiver, maxTimeToOutputResponse); + } + + @Override + public void executeShellCommand(String command, IShellOutputReceiver receiver, + long maxTimeToOutputResponse, TimeUnit maxTimeUnits) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this, + receiver, maxTimeToOutputResponse, maxTimeUnits); + } + + @Override + public void runEventLogService(LogReceiver receiver) + throws TimeoutException, AdbCommandRejectedException, IOException { + AdbHelper.runEventLogService(AndroidDebugBridge.getSocketAddress(), this, receiver); + } + + @Override + public void runLogService(String logname, LogReceiver receiver) + throws TimeoutException, AdbCommandRejectedException, IOException { + AdbHelper.runLogService(AndroidDebugBridge.getSocketAddress(), this, logname, receiver); + } + + @Override + public void createForward(int localPort, int remotePort) + throws TimeoutException, AdbCommandRejectedException, IOException { + AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this, + String.format("tcp:%d", localPort), //$NON-NLS-1$ + String.format("tcp:%d", remotePort)); //$NON-NLS-1$ + } + + @Override + public void createForward(int localPort, String remoteSocketName, + DeviceUnixSocketNamespace namespace) throws TimeoutException, + AdbCommandRejectedException, IOException { + AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this, + String.format("tcp:%d", localPort), //$NON-NLS-1$ + String.format("%s:%s", namespace.getType(), remoteSocketName)); //$NON-NLS-1$ + } + + @Override + public void removeForward(int localPort, int remotePort) + throws TimeoutException, AdbCommandRejectedException, IOException { + AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this, + String.format("tcp:%d", localPort), //$NON-NLS-1$ + String.format("tcp:%d", remotePort)); //$NON-NLS-1$ + } + + @Override + public void removeForward(int localPort, String remoteSocketName, + DeviceUnixSocketNamespace namespace) throws TimeoutException, + AdbCommandRejectedException, IOException { + AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this, + String.format("tcp:%d", localPort), //$NON-NLS-1$ + String.format("%s:%s", namespace.getType(), remoteSocketName)); //$NON-NLS-1$ + } + + Device(DeviceMonitor monitor, String serialNumber, DeviceState deviceState) { + mMonitor = monitor; + mSerialNumber = serialNumber; + mState = deviceState; + } + + DeviceMonitor getMonitor() { + return mMonitor; + } + + @Override + public boolean hasClients() { + synchronized (mClients) { + return !mClients.isEmpty(); + } + } + + @Override + public Client[] getClients() { + synchronized (mClients) { + return mClients.toArray(new Client[mClients.size()]); + } + } + + @Override + public Client getClient(String applicationName) { + synchronized (mClients) { + for (Client c : mClients) { + if (applicationName.equals(c.getClientData().getClientDescription())) { + return c; + } + } + } + + return null; + } + + void addClient(Client client) { + synchronized (mClients) { + mClients.add(client); + } + + addClientInfo(client); + } + + List getClientList() { + return mClients; + } + + void clearClientList() { + synchronized (mClients) { + mClients.clear(); + } + + clearClientInfo(); + } + + /** + * Removes a {@link Client} from the list. + * @param client the client to remove. + * @param notify Whether or not to notify the listeners of a change. + */ + void removeClient(Client client, boolean notify) { + mMonitor.addPortToAvailableList(client.getDebuggerListenPort()); + synchronized (mClients) { + mClients.remove(client); + } + if (notify) { + mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST); + } + + removeClientInfo(client); + } + + /** Sets the socket channel on which a track-jdwp command for this device has been sent. */ + void setClientMonitoringSocket(@NonNull SocketChannel socketChannel) { + mSocketChannel = socketChannel; + } + + /** + * Returns the channel on which responses to the track-jdwp command will be available if it + * has been set, null otherwise. The channel is set via {@link #setClientMonitoringSocket(SocketChannel)}, + * which is usually invoked when the device goes online. + */ + @Nullable + SocketChannel getClientMonitoringSocket() { + return mSocketChannel; + } + + void update(int changeMask) { + mMonitor.getServer().deviceChanged(this, changeMask); + } + + void update(Client client, int changeMask) { + mMonitor.getServer().clientChanged(client, changeMask); + updateClientInfo(client, changeMask); + } + + void setMountingPoint(String name, String value) { + mMountPoints.put(name, value); + } + + private void addClientInfo(Client client) { + ClientData cd = client.getClientData(); + setClientInfo(cd.getPid(), cd.getClientDescription()); + } + + private void updateClientInfo(Client client, int changeMask) { + if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) { + addClientInfo(client); + } + } + + private void removeClientInfo(Client client) { + int pid = client.getClientData().getPid(); + mClientInfo.remove(pid); + } + + private void clearClientInfo() { + mClientInfo.clear(); + } + + private void setClientInfo(int pid, String pkgName) { + if (pkgName == null) { + pkgName = UNKNOWN_PACKAGE; + } + + mClientInfo.put(pid, pkgName); + } + + @Override + public String getClientName(int pid) { + String pkgName = mClientInfo.get(pid); + return pkgName == null ? UNKNOWN_PACKAGE : pkgName; + } + + @Override + public void pushFile(String local, String remote) + throws IOException, AdbCommandRejectedException, TimeoutException, SyncException { + SyncService sync = null; + try { + String targetFileName = getFileName(local); + + Log.d(targetFileName, String.format("Uploading %1$s onto device '%2$s'", + targetFileName, getSerialNumber())); + + sync = getSyncService(); + if (sync != null) { + String message = String.format("Uploading file onto device '%1$s'", + getSerialNumber()); + Log.d(LOG_TAG, message); + sync.pushFile(local, remote, SyncService.getNullProgressMonitor()); + } else { + throw new IOException("Unable to open sync connection!"); + } + } catch (TimeoutException e) { + Log.e(LOG_TAG, "Error during Sync: timeout."); + throw e; + + } catch (SyncException e) { + Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); + throw e; + + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); + throw e; + + } finally { + if (sync != null) { + sync.close(); + } + } + } + + @Override + public void pullFile(String remote, String local) + throws IOException, AdbCommandRejectedException, TimeoutException, SyncException { + SyncService sync = null; + try { + String targetFileName = getFileName(remote); + + Log.d(targetFileName, String.format("Downloading %1$s from device '%2$s'", + targetFileName, getSerialNumber())); + + sync = getSyncService(); + if (sync != null) { + String message = String.format("Downloading file from device '%1$s'", + getSerialNumber()); + Log.d(LOG_TAG, message); + sync.pullFile(remote, local, SyncService.getNullProgressMonitor()); + } else { + throw new IOException("Unable to open sync connection!"); + } + } catch (TimeoutException e) { + Log.e(LOG_TAG, "Error during Sync: timeout."); + throw e; + + } catch (SyncException e) { + Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); + throw e; + + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); + throw e; + + } finally { + if (sync != null) { + sync.close(); + } + } + } + + @Override + public String installPackage(String packageFilePath, boolean reinstall, + String... extraArgs) + throws InstallException { + try { + String remoteFilePath = syncPackageToDevice(packageFilePath); + String result = installRemotePackage(remoteFilePath, reinstall, extraArgs); + removeRemotePackage(remoteFilePath); + return result; + } catch (IOException e) { + throw new InstallException(e); + } catch (AdbCommandRejectedException e) { + throw new InstallException(e); + } catch (TimeoutException e) { + throw new InstallException(e); + } catch (SyncException e) { + throw new InstallException(e); + } + } + + @Override + public void installPackages(List apkFilePaths, int timeOutInMs, boolean reinstall, + String... extraArgs) throws InstallException { + + assert(!apkFilePaths.isEmpty()); + if (getApiLevel() < 21) { + Log.w("Internal error : installPackages invoked with device < 21 for %s", + Joiner.on(",").join(apkFilePaths)); + + if (apkFilePaths.size() == 1) { + installPackage(apkFilePaths.get(0), reinstall, extraArgs); + return; + } + Log.e("Internal error : installPackages invoked with device < 21 for multiple APK : %s", + Joiner.on(",").join(apkFilePaths)); + throw new InstallException( + "Internal error : installPackages invoked with device < 21 for multiple APK : " + + Joiner.on(",").join(apkFilePaths)); + } + String mainPackageFilePath = apkFilePaths.get(0); + Log.d(mainPackageFilePath, + String.format("Uploading main %1$s and %2$s split APKs onto device '%3$s'", + mainPackageFilePath, Joiner.on(',').join(apkFilePaths), + getSerialNumber())); + + try { + // create a installation session. + + List extraArgsList = extraArgs != null + ? ImmutableList.copyOf(extraArgs) + : ImmutableList.of(); + + String sessionId = createMultiInstallSession(apkFilePaths, extraArgsList, reinstall); + if (sessionId == null) { + Log.d(mainPackageFilePath, "Failed to establish session, quit installation"); + throw new InstallException("Failed to establish session"); + } + Log.d(mainPackageFilePath, String.format("Established session id=%1$s", sessionId)); + + // now upload each APK in turn. + int index = 0; + boolean allUploadSucceeded = true; + while (allUploadSucceeded && index < apkFilePaths.size()) { + allUploadSucceeded = uploadAPK(sessionId, apkFilePaths.get(index), index++); + } + + // if all files were upload successfully, commit otherwise abandon the installation. + String command = allUploadSucceeded + ? "pm install-commit " + sessionId + : "pm install-abandon " + sessionId; + InstallReceiver receiver = new InstallReceiver(); + executeShellCommand(command, receiver, timeOutInMs, TimeUnit.MILLISECONDS); + String errorMessage = receiver.getErrorMessage(); + if (errorMessage != null) { + String message = String.format("Failed to finalize session : %1$s", errorMessage); + Log.e(mainPackageFilePath, message); + throw new InstallException(message); + } + // in case not all files were upload and we abandoned the install, make sure to + // notifier callers. + if (!allUploadSucceeded) { + throw new InstallException("Unable to upload some APKs"); + } + } catch (TimeoutException e) { + Log.e(LOG_TAG, "Error during Sync: timeout."); + throw new InstallException(e); + + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); + throw new InstallException(e); + + } catch (AdbCommandRejectedException e) { + throw new InstallException(e); + } catch (ShellCommandUnresponsiveException e) { + Log.e(LOG_TAG, String.format("Error during shell execution: %1$s", e.getMessage())); + throw new InstallException(e); + } + } + + /** + * Implementation of {@link com.android.ddmlib.MultiLineReceiver} that can receive a + * Success message from ADB followed by a session ID. + */ + private static class MultiInstallReceiver extends MultiLineReceiver { + + private static final Pattern successPattern = Pattern.compile("Success: .*\\[(\\d*)\\]"); + + @Nullable String sessionId = null; + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + Matcher matcher = successPattern.matcher(line); + if (matcher.matches()) { + sessionId = matcher.group(1); + } + } + + } + + @Nullable + public String getSessionId() { + return sessionId; + } + } + + @Nullable + private String createMultiInstallSession(List apkFileNames, + @NonNull Collection extraArgs, boolean reinstall) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + + List apkFiles = Lists.transform(apkFileNames, new Function() { + @Override + public File apply(String input) { + return new File(input); + } + }); + + long totalFileSize = 0L; + for (File apkFile : apkFiles) { + if (apkFile.exists() && apkFile.isFile()) { + totalFileSize += apkFile.length(); + } else { + throw new IllegalArgumentException(apkFile.getAbsolutePath() + " is not a file"); + } + } + StringBuilder parameters = new StringBuilder(); + if (reinstall) { + parameters.append(("-r ")); + } + parameters.append(Joiner.on(' ').join(extraArgs)); + MultiInstallReceiver receiver = new MultiInstallReceiver(); + String cmd = String.format("pm install-create %1$s -S %2$d", + parameters.toString(), + totalFileSize); + executeShellCommand(cmd, receiver, DdmPreferences.getTimeOut()); + return receiver.getSessionId(); + } + + private static final CharMatcher UNSAFE_PM_INSTALL_SESSION_SPLIT_NAME_CHARS = + CharMatcher.inRange('a','z').or(CharMatcher.inRange('A','Z')) + .or(CharMatcher.anyOf("_-")).negate(); + + private boolean uploadAPK(final String sessionId, String apkFilePath, int uniqueId) { + Log.d(sessionId, String.format("Uploading APK %1$s ", apkFilePath)); + File fileToUpload = new File(apkFilePath); + if (!fileToUpload.exists()) { + Log.e(sessionId, String.format("File not found: %1$s", apkFilePath)); + return false; + } + if (fileToUpload.isDirectory()) { + Log.e(sessionId, String.format("Directory upload not supported: %1$s", apkFilePath)); + return false; + } + String baseName = fileToUpload.getName().lastIndexOf('.') != -1 + ? fileToUpload.getName().substring(0, fileToUpload.getName().lastIndexOf('.')) + : fileToUpload.getName(); + + baseName = UNSAFE_PM_INSTALL_SESSION_SPLIT_NAME_CHARS.replaceFrom(baseName, '_'); + + String command = String.format("pm install-write -S %d %s %d_%s -", + fileToUpload.length(), sessionId, uniqueId, baseName); + + Log.d(sessionId, String.format("Executing : %1$s", command)); + InputStream inputStream = null; + try { + inputStream = new BufferedInputStream(new FileInputStream(fileToUpload)); + InstallReceiver receiver = new InstallReceiver(); + AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), + AdbHelper.AdbService.EXEC, command, this, + receiver, DdmPreferences.getTimeOut(), TimeUnit.MILLISECONDS, inputStream); + if (receiver.getErrorMessage() != null) { + Log.e(sessionId, String.format("Error while uploading %1$s : %2$s", fileToUpload.getName(), + receiver.getErrorMessage())); + } else { + Log.d(sessionId, String.format("Successfully uploaded %1$s", fileToUpload.getName())); + } + return receiver.getErrorMessage() == null; + } catch (Exception e) { + Log.e(sessionId, e); + return false; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.e(sessionId, e); + } + } + + } + } + + @Override + public String syncPackageToDevice(String localFilePath) + throws IOException, AdbCommandRejectedException, TimeoutException, SyncException { + SyncService sync = null; + try { + String packageFileName = getFileName(localFilePath); + String remoteFilePath = String.format("/data/local/tmp/%1$s", packageFileName); //$NON-NLS-1$ + + Log.d(packageFileName, String.format("Uploading %1$s onto device '%2$s'", + packageFileName, getSerialNumber())); + + sync = getSyncService(); + if (sync != null) { + String message = String.format("Uploading file onto device '%1$s'", + getSerialNumber()); + Log.d(LOG_TAG, message); + sync.pushFile(localFilePath, remoteFilePath, SyncService.getNullProgressMonitor()); + } else { + throw new IOException("Unable to open sync connection!"); + } + return remoteFilePath; + } catch (TimeoutException e) { + Log.e(LOG_TAG, "Error during Sync: timeout."); + throw e; + + } catch (SyncException e) { + Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); + throw e; + + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); + throw e; + + } finally { + if (sync != null) { + sync.close(); + } + } + } + + /** + * Helper method to retrieve the file name given a local file path + * @param filePath full directory path to file + * @return {@link String} file name + */ + private static String getFileName(String filePath) { + return new File(filePath).getName(); + } + + @Override + public String installRemotePackage(String remoteFilePath, boolean reinstall, + String... extraArgs) throws InstallException { + try { + InstallReceiver receiver = new InstallReceiver(); + StringBuilder optionString = new StringBuilder(); + if (reinstall) { + optionString.append("-r "); + } + if (extraArgs != null) { + optionString.append(Joiner.on(' ').join(extraArgs)); + } + String cmd = String.format("pm install %1$s \"%2$s\"", optionString.toString(), + remoteFilePath); + executeShellCommand(cmd, receiver, INSTALL_TIMEOUT_MINUTES, TimeUnit.MINUTES); + return receiver.getErrorMessage(); + } catch (TimeoutException e) { + throw new InstallException(e); + } catch (AdbCommandRejectedException e) { + throw new InstallException(e); + } catch (ShellCommandUnresponsiveException e) { + throw new InstallException(e); + } catch (IOException e) { + throw new InstallException(e); + } + } + + @Override + public void removeRemotePackage(String remoteFilePath) throws InstallException { + try { + executeShellCommand(String.format("rm \"%1$s\"", remoteFilePath), + new NullOutputReceiver(), INSTALL_TIMEOUT_MINUTES, TimeUnit.MINUTES); + } catch (IOException e) { + throw new InstallException(e); + } catch (TimeoutException e) { + throw new InstallException(e); + } catch (AdbCommandRejectedException e) { + throw new InstallException(e); + } catch (ShellCommandUnresponsiveException e) { + throw new InstallException(e); + } + } + + @Override + public String uninstallPackage(String packageName) throws InstallException { + try { + InstallReceiver receiver = new InstallReceiver(); + executeShellCommand("pm uninstall " + packageName, receiver, INSTALL_TIMEOUT_MINUTES, + TimeUnit.MINUTES); + return receiver.getErrorMessage(); + } catch (TimeoutException e) { + throw new InstallException(e); + } catch (AdbCommandRejectedException e) { + throw new InstallException(e); + } catch (ShellCommandUnresponsiveException e) { + throw new InstallException(e); + } catch (IOException e) { + throw new InstallException(e); + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#reboot() + */ + @Override + public void reboot(String into) + throws TimeoutException, AdbCommandRejectedException, IOException { + AdbHelper.reboot(into, AndroidDebugBridge.getSocketAddress(), this); + } + + @Override + public Integer getBatteryLevel() throws TimeoutException, AdbCommandRejectedException, + IOException, ShellCommandUnresponsiveException { + // use default of 5 minutes + return getBatteryLevel(5 * 60 * 1000); + } + + @Override + public Integer getBatteryLevel(long freshnessMs) throws TimeoutException, + AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException { + Future futureBattery = getBattery(freshnessMs, TimeUnit.MILLISECONDS); + try { + return futureBattery.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + return null; + } + } + + @NonNull + @Override + public Future getBattery() { + return getBattery(5, TimeUnit.MINUTES); + } + + @NonNull + @Override + public Future getBattery(long freshnessTime, @NonNull TimeUnit timeUnit) { + return mBatteryFetcher.getBattery(freshnessTime, timeUnit); + } + + @NonNull + @Override + public List getAbis() { + /* Try abiList (implemented in L onwards) otherwise fall back to abi and abi2. */ + String abiList = getProperty(IDevice.PROP_DEVICE_CPU_ABI_LIST); + if(abiList != null) { + return Lists.newArrayList(abiList.split(",")); + } else { + List abis = Lists.newArrayListWithExpectedSize(2); + String abi = getProperty(IDevice.PROP_DEVICE_CPU_ABI); + if (abi != null) { + abis.add(abi); + } + + abi = getProperty(IDevice.PROP_DEVICE_CPU_ABI2); + if (abi != null) { + abis.add(abi); + } + + return abis; + } + } + + @Override + public int getDensity() { + String densityValue = getProperty(IDevice.PROP_DEVICE_DENSITY); + if (densityValue != null) { + try { + return Integer.parseInt(densityValue); + } catch (NumberFormatException e) { + return -1; + } + } + + return -1; + } + + @Override + public String getLanguage() { + return getProperties().get(IDevice.PROP_DEVICE_LANGUAGE); + } + + @Override + public String getRegion() { + return getProperty(IDevice.PROP_DEVICE_REGION); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DeviceMonitor.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DeviceMonitor.java new file mode 100644 index 0000000..4aa0daa --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/DeviceMonitor.java @@ -0,0 +1,884 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.ddmlib.AdbHelper.AdbResponse; +import com.android.ddmlib.ClientData.DebuggerStatus; +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.IDevice.DeviceState; +import com.android.ddmlib.utils.DebuggerPorts; +import com.android.utils.Pair; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Queues; +import com.google.common.util.concurrent.Uninterruptibles; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * The {@link DeviceMonitor} monitors devices attached to adb. + * + * On one thread, it runs the {@link com.android.ddmlib.DeviceMonitor.DeviceListMonitorTask}. + * This establishes a socket connection to the adb host, and issues a + * {@link #ADB_TRACK_DEVICES_COMMAND}. It then monitors that socket for all changes about device + * connection and device state. + * + * For each device that is detected to be online, it then opens a new socket connection to adb, + * and issues a "track-jdwp" command to that device. On this connection, it monitors active + * clients on the device. Note: a single thread monitors jdwp connections from all devices. + * The different socket connections to adb (one per device) are multiplexed over a single selector. + */ +final class DeviceMonitor { + private static final String ADB_TRACK_DEVICES_COMMAND = "host:track-devices"; + private static final String ADB_TRACK_JDWP_COMMAND = "track-jdwp"; + + private final byte[] mLengthBuffer2 = new byte[4]; + + private volatile boolean mQuit = false; + + private final AndroidDebugBridge mServer; + private DeviceListMonitorTask mDeviceListMonitorTask; + + private Selector mSelector; + + private final List mDevices = Lists.newCopyOnWriteArrayList(); + private final DebuggerPorts mDebuggerPorts = + new DebuggerPorts(DdmPreferences.getDebugPortBase()); + private final Map mClientsToReopen = new HashMap(); + private final BlockingQueue> mChannelsToRegister = + Queues.newLinkedBlockingQueue(); + + /** + * Creates a new {@link DeviceMonitor} object and links it to the running + * {@link AndroidDebugBridge} object. + * @param server the running {@link AndroidDebugBridge}. + */ + DeviceMonitor(@NonNull AndroidDebugBridge server) { + mServer = server; + } + + /** + * Starts the monitoring. + */ + void start() { + mDeviceListMonitorTask = new DeviceListMonitorTask(mServer, new DeviceListUpdateListener()); + new Thread(mDeviceListMonitorTask, "Device List Monitor").start(); //$NON-NLS-1$ + } + + /** + * Stops the monitoring. + */ + void stop() { + mQuit = true; + + if (mDeviceListMonitorTask != null) { + mDeviceListMonitorTask.stop(); + } + + // wake up the secondary loop by closing the selector. + if (mSelector != null) { + mSelector.wakeup(); + } + } + + /** + * Returns whether the monitor is currently connected to the debug bridge server. + */ + boolean isMonitoring() { + return mDeviceListMonitorTask != null && mDeviceListMonitorTask.isMonitoring(); + } + + int getConnectionAttemptCount() { + return mDeviceListMonitorTask == null ? 0 + : mDeviceListMonitorTask.getConnectionAttemptCount(); + } + + int getRestartAttemptCount() { + return mDeviceListMonitorTask == null ? 0 : mDeviceListMonitorTask.getRestartAttemptCount(); + } + + boolean hasInitialDeviceList() { + return mDeviceListMonitorTask != null && mDeviceListMonitorTask.hasInitialDeviceList(); + } + + /** + * Returns the devices. + */ + @NonNull Device[] getDevices() { + // Since this is a copy of write array list, we don't want to do a compound operation + // (toArray with an appropriate size) without locking, so we just let the container provide + // an appropriately sized array + //noinspection ToArrayCallWithZeroLengthArrayArgument + return mDevices.toArray(new Device[0]); + } + + @NonNull + AndroidDebugBridge getServer() { + return mServer; + } + + void addClientToDropAndReopen(Client client, int port) { + synchronized (mClientsToReopen) { + Log.d("DeviceMonitor", + "Adding " + client + " to list of client to reopen (" + port + ")."); + if (mClientsToReopen.get(client) == null) { + mClientsToReopen.put(client, port); + } + } + mSelector.wakeup(); + } + + /** + * Attempts to connect to the debug bridge server. + * @return a connect socket if success, null otherwise + */ + @Nullable + private static SocketChannel openAdbConnection() { + try { + SocketChannel adbChannel = SocketChannel.open(AndroidDebugBridge.getSocketAddress()); + adbChannel.socket().setTcpNoDelay(true); + return adbChannel; + } catch (IOException e) { + return null; + } + } + + /** + * Updates the device list with the new items received from the monitoring service. + */ + private void updateDevices(@NonNull List newList) { + DeviceListComparisonResult result = DeviceListComparisonResult.compare(mDevices, newList); + for (IDevice device : result.removed) { + removeDevice((Device) device); + mServer.deviceDisconnected(device); + } + + List newlyOnline = Lists.newArrayListWithExpectedSize(mDevices.size()); + + for (Map.Entry entry : result.updated.entrySet()) { + Device device = (Device) entry.getKey(); + device.setState(entry.getValue()); + device.update(Device.CHANGE_STATE); + + if (device.isOnline()) { + newlyOnline.add(device); + } + } + + for (IDevice device : result.added) { + mDevices.add((Device) device); + mServer.deviceConnected(device); + if (device.isOnline()) { + newlyOnline.add((Device) device); + } + } + + if (AndroidDebugBridge.getClientSupport()) { + for (Device device : newlyOnline) { + if (!startMonitoringDevice(device)) { + Log.e("DeviceMonitor", "Failed to start monitoring " + + device.getSerialNumber()); + } + } + } + + for (Device device : newlyOnline) { + queryAvdName(device); + } + } + + private void removeDevice(@NonNull Device device) { + device.clearClientList(); + mDevices.remove(device); + + SocketChannel channel = device.getClientMonitoringSocket(); + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + // doesn't really matter if the close fails. + } + } + } + + private static void queryAvdName(@NonNull Device device) { + if (!device.isEmulator()) { + return; + } + + EmulatorConsole console = EmulatorConsole.getConsole(device); + if (console != null) { + device.setAvdName(console.getAvdName()); + console.close(); + } + } + + /** + * Starts a monitoring service for a device. + * @param device the device to monitor. + * @return true if success. + */ + private boolean startMonitoringDevice(@NonNull Device device) { + SocketChannel socketChannel = openAdbConnection(); + + if (socketChannel != null) { + try { + boolean result = sendDeviceMonitoringRequest(socketChannel, device); + if (result) { + + if (mSelector == null) { + startDeviceMonitorThread(); + } + + device.setClientMonitoringSocket(socketChannel); + + socketChannel.configureBlocking(false); + + try { + mChannelsToRegister.put(Pair.of(socketChannel, device)); + } catch (InterruptedException e) { + // the queue is unbounded, and isn't going to block + } + mSelector.wakeup(); + + return true; + } + } catch (TimeoutException e) { + try { + // attempt to close the socket if needed. + socketChannel.close(); + } catch (IOException e1) { + // we can ignore that one. It may already have been closed. + } + Log.d("DeviceMonitor", + "Connection Failure when starting to monitor device '" + + device + "' : timeout"); + } catch (AdbCommandRejectedException e) { + try { + // attempt to close the socket if needed. + socketChannel.close(); + } catch (IOException e1) { + // we can ignore that one. It may already have been closed. + } + Log.d("DeviceMonitor", + "Adb refused to start monitoring device '" + + device + "' : " + e.getMessage()); + } catch (IOException e) { + try { + // attempt to close the socket if needed. + socketChannel.close(); + } catch (IOException e1) { + // we can ignore that one. It may already have been closed. + } + Log.d("DeviceMonitor", + "Connection Failure when starting to monitor device '" + + device + "' : " + e.getMessage()); + } + } + + return false; + } + + private void startDeviceMonitorThread() throws IOException { + mSelector = Selector.open(); + new Thread("Device Client Monitor") { //$NON-NLS-1$ + @Override + public void run() { + deviceClientMonitorLoop(); + } + }.start(); + } + + private void deviceClientMonitorLoop() { + do { + try { + int count = mSelector.select(); + + if (mQuit) { + return; + } + + synchronized (mClientsToReopen) { + if (!mClientsToReopen.isEmpty()) { + Set clients = mClientsToReopen.keySet(); + MonitorThread monitorThread = MonitorThread.getInstance(); + + for (Client client : clients) { + Device device = client.getDeviceImpl(); + int pid = client.getClientData().getPid(); + + monitorThread.dropClient(client, false /* notify */); + + // This is kinda bad, but if we don't wait a bit, the client + // will never answer the second handshake! + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + + int port = mClientsToReopen.get(client); + + if (port == IDebugPortProvider.NO_STATIC_PORT) { + port = getNextDebuggerPort(); + } + Log.d("DeviceMonitor", "Reopening " + client); + openClient(device, pid, port, monitorThread); + device.update(Device.CHANGE_CLIENT_LIST); + } + + mClientsToReopen.clear(); + } + } + + // register any new channels + while (!mChannelsToRegister.isEmpty()) { + try { + Pair data = mChannelsToRegister.take(); + data.getFirst().register(mSelector, SelectionKey.OP_READ, data.getSecond()); + } catch (InterruptedException e) { + // doesn't block: this thread is the only reader and it reads only when + // there is data + } + } + + if (count == 0) { + continue; + } + + Set keys = mSelector.selectedKeys(); + Iterator iter = keys.iterator(); + + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + + if (key.isValid() && key.isReadable()) { + Object attachment = key.attachment(); + + if (attachment instanceof Device) { + Device device = (Device)attachment; + + SocketChannel socket = device.getClientMonitoringSocket(); + + if (socket != null) { + try { + int length = readLength(socket, mLengthBuffer2); + + processIncomingJdwpData(device, socket, length); + } catch (IOException ioe) { + Log.d("DeviceMonitor", + "Error reading jdwp list: " + ioe.getMessage()); + socket.close(); + + // restart the monitoring of that device + if (mDevices.contains(device)) { + Log.d("DeviceMonitor", + "Restarting monitoring service for " + device); + startMonitoringDevice(device); + } + } + } + } + } + } + } catch (IOException e) { + Log.e("DeviceMonitor", "Connection error while monitoring clients."); + } + + } while (!mQuit); + } + + private static boolean sendDeviceMonitoringRequest(@NonNull SocketChannel socket, + @NonNull Device device) + throws TimeoutException, AdbCommandRejectedException, IOException { + + try { + AdbHelper.setDevice(socket, device); + AdbHelper.write(socket, AdbHelper.formAdbRequest(ADB_TRACK_JDWP_COMMAND)); + AdbResponse resp = AdbHelper.readAdbResponse(socket, false); + + if (!resp.okay) { + // request was refused by adb! + Log.e("DeviceMonitor", "adb refused request: " + resp.message); + } + + return resp.okay; + } catch (TimeoutException e) { + Log.e("DeviceMonitor", "Sending jdwp tracking request timed out!"); + throw e; + } catch (IOException e) { + Log.e("DeviceMonitor", "Sending jdwp tracking request failed!"); + throw e; + } + } + + private void processIncomingJdwpData(@NonNull Device device, + @NonNull SocketChannel monitorSocket, int length) throws IOException { + + // This methods reads @length bytes from the @monitorSocket channel. + // These bytes correspond to the pids of the current set of processes on the device. + // It takes this set of pids and compares them with the existing set of clients + // for the device. Clients that correspond to pids that are not alive anymore are + // dropped, and new clients are created for pids that don't have a corresponding Client. + + if (length >= 0) { + // array for the current pids. + Set newPids = new HashSet(); + + // get the string data if there are any + if (length > 0) { + byte[] buffer = new byte[length]; + String result = read(monitorSocket, buffer); + + // split each line in its own list and create an array of integer pid + String[] pids = result == null ? new String[0] : result.split("\n"); //$NON-NLS-1$ + + for (String pid : pids) { + try { + newPids.add(Integer.valueOf(pid)); + } catch (NumberFormatException nfe) { + // looks like this pid is not really a number. Lets ignore it. + continue; + } + } + } + + MonitorThread monitorThread = MonitorThread.getInstance(); + + List clients = device.getClientList(); + Map existingClients = new HashMap(); + + synchronized (clients) { + for (Client c : clients) { + existingClients.put(c.getClientData().getPid(), c); + } + } + + Set clientsToRemove = new HashSet(); + for (Integer pid : existingClients.keySet()) { + if (!newPids.contains(pid)) { + clientsToRemove.add(existingClients.get(pid)); + } + } + + Set pidsToAdd = new HashSet(newPids); + pidsToAdd.removeAll(existingClients.keySet()); + + monitorThread.dropClients(clientsToRemove, false); + + // at this point whatever pid is left in the list needs to be converted into Clients. + for (int newPid : pidsToAdd) { + openClient(device, newPid, getNextDebuggerPort(), monitorThread); + } + + if (!pidsToAdd.isEmpty() || !clientsToRemove.isEmpty()) { + mServer.deviceChanged(device, Device.CHANGE_CLIENT_LIST); + } + } + } + + /** Opens and creates a new client. */ + private static void openClient(@NonNull Device device, int pid, int port, + @NonNull MonitorThread monitorThread) { + + SocketChannel clientSocket; + try { + clientSocket = AdbHelper.createPassThroughConnection( + AndroidDebugBridge.getSocketAddress(), device, pid); + + // required for Selector + clientSocket.configureBlocking(false); + } catch (UnknownHostException uhe) { + Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid); + return; + } catch (TimeoutException e) { + Log.w("DeviceMonitor", + "Failed to connect to client '" + pid + "': timeout"); + return; + } catch (AdbCommandRejectedException e) { + Log.w("DeviceMonitor", + "Adb rejected connection to client '" + pid + "': " + e.getMessage()); + return; + + } catch (IOException ioe) { + Log.w("DeviceMonitor", + "Failed to connect to client '" + pid + "': " + ioe.getMessage()); + return ; + } + + createClient(device, pid, clientSocket, port, monitorThread); + } + + /** Creates a client and register it to the monitor thread */ + private static void createClient(@NonNull Device device, int pid, @NonNull SocketChannel socket, + int debuggerPort, @NonNull MonitorThread monitorThread) { + + /* + * Successfully connected to something. Create a Client object, add + * it to the list, and initiate the JDWP handshake. + */ + + Client client = new Client(device, socket, pid); + + if (client.sendHandshake()) { + try { + if (AndroidDebugBridge.getClientSupport()) { + client.listenForDebugger(debuggerPort); + } + } catch (IOException ioe) { + client.getClientData().setDebuggerConnectionStatus(DebuggerStatus.ERROR); + Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger"); + // oh well + } + + client.requestAllocationStatus(); + } else { + Log.e("ddms", "Handshake with " + client + " failed!"); + /* + * The handshake send failed. We could remove it now, but if the + * failure is "permanent" we'll just keep banging on it and + * getting the same result. Keep it in the list with its "error" + * state so we don't try to reopen it. + */ + } + + if (client.isValid()) { + device.addClient(client); + monitorThread.addClient(client); + } + } + + private int getNextDebuggerPort() { + return mDebuggerPorts.next(); + } + + void addPortToAvailableList(int port) { + mDebuggerPorts.free(port); + } + + /** + * Reads the length of the next message from a socket. + * @param socket The {@link SocketChannel} to read from. + * @return the length, or 0 (zero) if no data is available from the socket. + * @throws IOException if the connection failed. + */ + private static int readLength(@NonNull SocketChannel socket, @NonNull byte[] buffer) + throws IOException { + String msg = read(socket, buffer); + + if (msg != null) { + try { + return Integer.parseInt(msg, 16); + } catch (NumberFormatException nfe) { + // we'll throw an exception below. + } + } + + // we receive something we can't read. It's better to reset the connection at this point. + throw new IOException("Unable to read length"); + } + + /** + * Fills a buffer by reading data from a socket. + * @return the content of the buffer as a string, or null if it failed to convert the buffer. + * @throws IOException if there was not enough data to fill the buffer + */ + @Nullable + private static String read(@NonNull SocketChannel socket, @NonNull byte[] buffer) + throws IOException { + ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length); + + while (buf.position() != buf.limit()) { + int count; + + count = socket.read(buf); + if (count < 0) { + throw new IOException("EOF"); + } + } + + try { + return new String(buffer, 0, buf.position(), AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + } + + private class DeviceListUpdateListener implements DeviceListMonitorTask.UpdateListener { + @Override + public void connectionError(@NonNull Exception e) { + for (Device device : mDevices) { + removeDevice(device); + mServer.deviceDisconnected(device); + } + } + + @Override + public void deviceListUpdate(@NonNull Map devices) { + List l = Lists.newArrayListWithExpectedSize(devices.size()); + for (Map.Entry entry : devices.entrySet()) { + l.add(new Device(DeviceMonitor.this, entry.getKey(), entry.getValue())); + } + // now merge the new devices with the old ones. + updateDevices(l); + } + } + + @VisibleForTesting + static class DeviceListComparisonResult { + @NonNull public final Map updated; + @NonNull public final List added; + @NonNull public final List removed; + + private DeviceListComparisonResult(@NonNull Map updated, + @NonNull List added, + @NonNull List removed) { + this.updated = updated; + this.added = added; + this.removed = removed; + } + + @NonNull + public static DeviceListComparisonResult compare(@NonNull List previous, + @NonNull List current) { + current = Lists.newArrayList(current); + + final Map updated = Maps.newHashMapWithExpectedSize(current.size()); + final List added = Lists.newArrayListWithExpectedSize(1); + final List removed = Lists.newArrayListWithExpectedSize(1); + + for (IDevice device : previous) { + IDevice currentDevice = find(current, device); + if (currentDevice != null) { + if (currentDevice.getState() != device.getState()) { + updated.put(device, currentDevice.getState()); + } + current.remove(currentDevice); + } else { + removed.add(device); + } + } + + added.addAll(current); + + return new DeviceListComparisonResult(updated, added, removed); + } + + @Nullable + private static IDevice find(@NonNull List devices, + @NonNull IDevice device) { + for (IDevice d : devices) { + if (d.getSerialNumber().equals(device.getSerialNumber())) { + return d; + } + } + + return null; + } + } + + @VisibleForTesting + static class DeviceListMonitorTask implements Runnable { + private final byte[] mLengthBuffer = new byte[4]; + + private final AndroidDebugBridge mBridge; + private final UpdateListener mListener; + + private SocketChannel mAdbConnection = null; + private boolean mMonitoring = false; + private int mConnectionAttempt = 0; + private int mRestartAttemptCount = 0; + private boolean mInitialDeviceListDone = false; + + private volatile boolean mQuit; + + private interface UpdateListener { + void connectionError(@NonNull Exception e); + void deviceListUpdate(@NonNull Map devices); + } + + public DeviceListMonitorTask(@NonNull AndroidDebugBridge bridge, + @NonNull UpdateListener listener) { + mBridge = bridge; + mListener = listener; + } + + @Override + public void run() { + do { + if (mAdbConnection == null) { + Log.d("DeviceMonitor", "Opening adb connection"); + mAdbConnection = openAdbConnection(); + if (mAdbConnection == null) { + mConnectionAttempt++; + Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt); + if (mConnectionAttempt > 10) { + if (!mBridge.startAdb()) { + mRestartAttemptCount++; + Log.e("DeviceMonitor", + "adb restart attempts: " + mRestartAttemptCount); + } else { + Log.i("DeviceMonitor", "adb restarted"); + mRestartAttemptCount = 0; + } + } + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + } else { + Log.d("DeviceMonitor", "Connected to adb for device monitoring"); + mConnectionAttempt = 0; + } + } + + try { + if (mAdbConnection != null && !mMonitoring) { + mMonitoring = sendDeviceListMonitoringRequest(); + } + + if (mMonitoring) { + int length = readLength(mAdbConnection, mLengthBuffer); + + if (length >= 0) { + // read the incoming message + processIncomingDeviceData(length); + + // flag the fact that we have build the list at least once. + mInitialDeviceListDone = true; + } + } + } catch (AsynchronousCloseException ace) { + // this happens because of a call to Quit. We do nothing, and the loop will break. + } catch (TimeoutException ioe) { + handleExceptionInMonitorLoop(ioe); + } catch (IOException ioe) { + handleExceptionInMonitorLoop(ioe); + } + } while (!mQuit); + } + + private boolean sendDeviceListMonitoringRequest() throws TimeoutException, IOException { + byte[] request = AdbHelper.formAdbRequest(ADB_TRACK_DEVICES_COMMAND); + + try { + AdbHelper.write(mAdbConnection, request); + AdbResponse resp = AdbHelper.readAdbResponse(mAdbConnection, false); + if (!resp.okay) { + // request was refused by adb! + Log.e("DeviceMonitor", "adb refused request: " + resp.message); + } + + return resp.okay; + } catch (IOException e) { + Log.e("DeviceMonitor", "Sending Tracking request failed!"); + mAdbConnection.close(); + throw e; + } + } + + private void handleExceptionInMonitorLoop(@NonNull Exception e) { + if (!mQuit) { + if (e instanceof TimeoutException) { + Log.e("DeviceMonitor", "Adb connection Error: timeout"); + } else { + Log.e("DeviceMonitor", "Adb connection Error:" + e.getMessage()); + } + mMonitoring = false; + if (mAdbConnection != null) { + try { + mAdbConnection.close(); + } catch (IOException ioe) { + // we can safely ignore that one. + } + mAdbConnection = null; + + mListener.connectionError(e); + } + } + } + + /** Processes an incoming device message from the socket */ + private void processIncomingDeviceData(int length) throws IOException { + Map result; + if (length <= 0) { + result = Collections.emptyMap(); + } else { + String response = read(mAdbConnection, new byte[length]); + result = parseDeviceListResponse(response); + } + + mListener.deviceListUpdate(result); + } + + @VisibleForTesting + static Map parseDeviceListResponse(@Nullable String result) { + Map deviceStateMap = Maps.newHashMap(); + String[] devices = result == null ? new String[0] : result.split("\n"); //$NON-NLS-1$ + + for (String d : devices) { + String[] param = d.split("\t"); //$NON-NLS-1$ + if (param.length == 2) { + // new adb uses only serial numbers to identify devices + deviceStateMap.put(param[0], DeviceState.getState(param[1])); + } + } + return deviceStateMap; + } + + boolean isMonitoring() { + return mMonitoring; + } + + boolean hasInitialDeviceList() { + return mInitialDeviceListDone; + } + + int getConnectionAttemptCount() { + return mConnectionAttempt; + } + + int getRestartAttemptCount() { + return mRestartAttemptCount; + } + + public void stop() { + mQuit = true; + + // wakeup the main loop thread by closing the main connection to adb. + if (mAdbConnection != null) { + try { + mAdbConnection.close(); + } catch (IOException ignored) { + } + } + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/EmulatorConsole.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/EmulatorConsole.java new file mode 100644 index 0000000..d7d4650 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/EmulatorConsole.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.concurrency.GuardedBy; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.InvalidParameterException; +import java.util.Formatter; +import java.util.HashMap; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides control over emulated hardware of the Android emulator. + *

This is basically a wrapper around the command line console normally used with telnet. + *

+ * Regarding line termination handling:
+ * One of the issues is that the telnet protocol requires usage of \r\n. Most + * implementations don't enforce it (the dos one does). In this particular case, this is mostly + * irrelevant since we don't use telnet in Java, but that means we want to make + * sure we use the same line termination than what the console expects. The console + * code removes \r and waits for \n. + *

However this means you may receive \r\n when reading from the console. + *

+ * This API will change in the near future. + */ +public final class EmulatorConsole { + + private static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ + + private static final int WAIT_TIME = 5; // spin-wait sleep, in ms + + private static final int STD_TIMEOUT = 5000; // standard delay, in ms + + private static final String HOST = "127.0.0.1"; //$NON-NLS-1$ + + private static final String COMMAND_PING = "help\r\n"; //$NON-NLS-1$ + private static final String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$ + private static final String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$ + private static final String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$ + private static final String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$ + private static final String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$ + private static final String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$ + private static final String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$ + private static final String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$ + private static final String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$ + private static final String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$ + private static final String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$ + private static final String COMMAND_GPS = "geo fix %1$f %2$f %3$f\r\n"; //$NON-NLS-1$ + + private static final Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$ + + /** + * Array of delay values: no delay, gprs, edge/egprs, umts/3d + */ + public static final int[] MIN_LATENCIES = new int[] { + 0, // No delay + 150, // gprs + 80, // edge/egprs + 35 // umts/3g + }; + + /** + * Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa. + */ + public static final int[] DOWNLOAD_SPEEDS = new int[] { + 0, // full speed + 14400, // gsm + 43200, // hscsd + 80000, // gprs + 236800, // edge/egprs + 1920000, // umts/3g + 14400000 // hsdpa + }; + + /** Arrays of valid network speeds */ + public static final String[] NETWORK_SPEEDS = new String[] { + "full", //$NON-NLS-1$ + "gsm", //$NON-NLS-1$ + "hscsd", //$NON-NLS-1$ + "gprs", //$NON-NLS-1$ + "edge", //$NON-NLS-1$ + "umts", //$NON-NLS-1$ + "hsdpa", //$NON-NLS-1$ + }; + + /** Arrays of valid network latencies */ + public static final String[] NETWORK_LATENCIES = new String[] { + "none", //$NON-NLS-1$ + "gprs", //$NON-NLS-1$ + "edge", //$NON-NLS-1$ + "umts", //$NON-NLS-1$ + }; + + /** Gsm Mode enum. */ + public enum GsmMode { + UNKNOWN((String)null), + UNREGISTERED(new String[] { "unregistered", "off" }), + HOME(new String[] { "home", "on" }), + ROAMING("roaming"), + SEARCHING("searching"), + DENIED("denied"); + + private final String[] tags; + + GsmMode(String tag) { + if (tag != null) { + this.tags = new String[] { tag }; + } else { + this.tags = new String[0]; + } + } + + GsmMode(String[] tags) { + this.tags = tags; + } + + public static GsmMode getEnum(String tag) { + for (GsmMode mode : values()) { + for (String t : mode.tags) { + if (t.equals(tag)) { + return mode; + } + } + } + return UNKNOWN; + } + + /** + * Returns the first tag of the enum. + */ + public String getTag() { + if (tags.length > 0) { + return tags[0]; + } + return null; + } + } + + public static final String RESULT_OK = null; + + private static final Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN); + private static final Pattern sVoiceStatusRegexp = Pattern.compile( + "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private static final Pattern sDataStatusRegexp = Pattern.compile( + "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private static final Pattern sDownloadSpeedRegexp = Pattern.compile( + "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private static final Pattern sMinLatencyRegexp = Pattern.compile( + "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + @GuardedBy(value = "sEmulators") + private static final HashMap sEmulators = + new HashMap(); + + private static final String LOG_TAG = "EmulatorConsole"; + + /** Gsm Status class */ + public static class GsmStatus { + /** Voice status. */ + public GsmMode voice = GsmMode.UNKNOWN; + /** Data status. */ + public GsmMode data = GsmMode.UNKNOWN; + } + + /** Network Status class */ + public static class NetworkStatus { + /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */ + public int speed = -1; + /** network latency status. This is an index in the {@link #MIN_LATENCIES} array. */ + public int latency = -1; + } + + private int mPort = -1; + + private SocketChannel mSocketChannel; + + private byte[] mBuffer = new byte[1024]; + + /** + * Returns an {@link EmulatorConsole} object for the given {@link Device}. This can + * be an already existing console, or a new one if it hadn't been created yet. + * Note: emulator consoles don't automatically close when an emulator exists. It is the + * responsibility of higher level code to explicitly call {@link #close()} when the emulator + * corresponding to a open console is killed. + * @param d The device that the console links to. + * @return an EmulatorConsole object or null if the connection failed. + */ + @Nullable + public static EmulatorConsole getConsole(IDevice d) { + // we need to make sure that the device is an emulator + // get the port number. This is the console port. + Integer port = getEmulatorPort(d.getSerialNumber()); + if (port == null) { + Log.w(LOG_TAG, "Failed to find emulator port from serial: " + d.getSerialNumber()); + return null; + } + + EmulatorConsole console = retrieveConsole(port); + + if (!console.checkConnection()) { + removeConsole(console.mPort); + console = null; + } + + return console; + } + + /** + * Return port of emulator given its serial number. + * + * @param serialNumber the emulator's serial number + * @return the integer port or null if it could not be determined + */ + public static Integer getEmulatorPort(String serialNumber) { + Matcher m = sEmulatorRegexp.matcher(serialNumber); + if (m.matches()) { + // get the port number. This is the console port. + int port; + try { + port = Integer.parseInt(m.group(1)); + if (port > 0) { + return port; + } + } catch (NumberFormatException e) { + // looks like we failed to get the port number. This is a bit strange since + // it's coming from a regexp that only accept digit, but we handle the case + // and return null. + } + } + return null; + } + + /** + * Retrieve a console object for this port, creating if necessary. + */ + @NonNull + private static EmulatorConsole retrieveConsole(int port) { + synchronized (sEmulators) { + EmulatorConsole console = sEmulators.get(port); + if (console == null) { + Log.v(LOG_TAG, "Creating emulator console for " + Integer.toString(port)); + console = new EmulatorConsole(port); + sEmulators.put(port, console); + } + return console; + } + } + + /** + * Removes the console object associated with a port from the map. + * @param port The port of the console to remove. + */ + private static void removeConsole(int port) { + synchronized (sEmulators) { + Log.v(LOG_TAG, "Removing emulator console for " + Integer.toString(port)); + sEmulators.remove(port); + } + } + + private EmulatorConsole(int port) { + mPort = port; + } + + /** + * Determine if connection to emulator console is functioning. Starts the connection if + * necessary + * @return true if success. + */ + private synchronized boolean checkConnection() { + if (mSocketChannel == null) { + // connection not established, try to connect + InetSocketAddress socketAddr; + try { + InetAddress hostAddr = InetAddress.getByName(HOST); + socketAddr = new InetSocketAddress(hostAddr, mPort); + mSocketChannel = SocketChannel.open(socketAddr); + mSocketChannel.configureBlocking(false); + // read initial output from console + readLines(); + } catch (IOException e) { + Log.w(LOG_TAG, "Failed to start Emulator console for " + Integer.toString(mPort)); + return false; + } + } + + return ping(); + } + + /** + * Ping the emulator to check if the connection is still alive. + * @return true if the connection is alive. + */ + private synchronized boolean ping() { + // it looks like we can send stuff, even when the emulator quit, but we can't read + // from the socket. So we check the return of readLines() + if (sendCommand(COMMAND_PING)) { + return readLines() != null; + } + + return false; + } + + /** + * Sends a KILL command to the emulator. + */ + public synchronized void kill() { + if (sendCommand(COMMAND_KILL)) { + close(); + } + } + + /** + * Closes this instance of the emulator console. + */ + public synchronized void close() { + if (mPort == -1) { + return; + } + + removeConsole(mPort); + try { + if (mSocketChannel != null) { + mSocketChannel.close(); + } + mSocketChannel = null; + mPort = -1; + } catch (IOException e) { + Log.w(LOG_TAG, "Failed to close EmulatorConsole channel"); + } + } + + public synchronized String getAvdName() { + if (sendCommand(COMMAND_AVD_NAME)) { + String[] result = readLines(); + if (result != null && result.length == 2) { // this should be the name on first line, + // and ok on 2nd line + return result[0]; + } else { + // try to see if there's a message after KO + Matcher m = RE_KO.matcher(result[result.length-1]); + if (m.matches()) { + return m.group(1); + } + Log.w(LOG_TAG, "avd name result did not match expected"); + for (int i=0; i < result.length; i++) { + Log.d(LOG_TAG, result[i]); + } + } + } + + return null; + } + + /** + * Get the network status of the emulator. + * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or + * null if the query failed. + */ + public synchronized NetworkStatus getNetworkStatus() { + if (sendCommand(COMMAND_NETWORK_STATUS)) { + /* Result is in the format + Current network status: + download speed: 14400 bits/s (1.8 KB/s) + upload speed: 14400 bits/s (1.8 KB/s) + minimum latency: 0 ms + maximum latency: 0 ms + */ + String[] result = readLines(); + + if (isValid(result)) { + // we only compare against the min latency and the download speed + // let's not rely on the order of the output, and simply loop through + // the line testing the regexp. + NetworkStatus status = new NetworkStatus(); + for (String line : result) { + Matcher m = sDownloadSpeedRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.speed = getSpeedIndex(value); + + // move on to next line. + continue; + } + + m = sMinLatencyRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.latency = getLatencyIndex(value); + + // move on to next line. + continue; + } + } + + return status; + } + } + + return null; + } + + /** + * Returns the current gsm status of the emulator + * @return a {@link GsmStatus} object containing the gms status, or null + * if the query failed. + */ + public synchronized GsmStatus getGsmStatus() { + if (sendCommand(COMMAND_GSM_STATUS)) { + /* + * result is in the format: + * gsm status + * gsm voice state: home + * gsm data state: home + */ + + String[] result = readLines(); + if (isValid(result)) { + + GsmStatus status = new GsmStatus(); + + // let's not rely on the order of the output, and simply loop through + // the line testing the regexp. + for (String line : result) { + Matcher m = sVoiceStatusRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.voice = GsmMode.getEnum(value.toLowerCase(Locale.US)); + + // move on to next line. + continue; + } + + m = sDataStatusRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.data = GsmMode.getEnum(value.toLowerCase(Locale.US)); + + // move on to next line. + continue; + } + } + + return status; + } + } + + return null; + } + + /** + * Sets the GSM voice mode. + * @param mode the {@link GsmMode} value. + * @return RESULT_OK if success, an error String otherwise. + * @throws InvalidParameterException if mode is an invalid value. + */ + public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException { + if (mode == GsmMode.UNKNOWN) { + throw new InvalidParameterException(); + } + + String command = String.format(COMMAND_GSM_VOICE, mode.getTag()); + return processCommand(command); + } + + /** + * Sets the GSM data mode. + * @param mode the {@link GsmMode} value + * @return {@link #RESULT_OK} if success, an error String otherwise. + * @throws InvalidParameterException if mode is an invalid value. + */ + public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException { + if (mode == GsmMode.UNKNOWN) { + throw new InvalidParameterException(); + } + + String command = String.format(COMMAND_GSM_DATA, mode.getTag()); + return processCommand(command); + } + + /** + * Initiate an incoming call on the emulator. + * @param number a string representing the calling number. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String call(String number) { + String command = String.format(COMMAND_GSM_CALL, number); + return processCommand(command); + } + + /** + * Cancels a current call. + * @param number the number of the call to cancel + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String cancelCall(String number) { + String command = String.format(COMMAND_GSM_CANCEL_CALL, number); + return processCommand(command); + } + + /** + * Sends an SMS to the emulator + * @param number The sender phone number + * @param message The SMS message. \ characters must be escaped. The carriage return is + * the 2 character sequence {'\', 'n' } + * + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String sendSms(String number, String message) { + String command = String.format(COMMAND_SMS_SEND, number, message); + return processCommand(command); + } + + /** + * Sets the network speed. + * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String setNetworkSpeed(int selectionIndex) { + String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]); + return processCommand(command); + } + + /** + * Sets the network latency. + * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String setNetworkLatency(int selectionIndex) { + String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]); + return processCommand(command); + } + + public synchronized String sendLocation(double longitude, double latitude, double elevation) { + + // need to make sure the string format uses dot and not comma + Formatter formatter = new Formatter(Locale.US); + try { + formatter.format(COMMAND_GPS, longitude, latitude, elevation); + + return processCommand(formatter.toString()); + } finally { + formatter.close(); + } + } + + /** + * Sends a command to the emulator console. + * @param command The command string. MUST BE TERMINATED BY \n. + * @return true if success + */ + private boolean sendCommand(String command) { + boolean result = false; + try { + byte[] bCommand; + try { + bCommand = command.getBytes(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + Log.w(LOG_TAG, "wrong encoding when sending " + command + " to " + + Integer.toString(mPort)); + // wrong encoding... + return result; + } + + // write the command + AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut()); + + result = true; + } catch (Exception e) { + Log.d(LOG_TAG, "Exception sending command " + command + " to " + + Integer.toString(mPort)); + return false; + } finally { + if (!result) { + // FIXME connection failed somehow, we need to disconnect the console. + removeConsole(mPort); + } + } + + return result; + } + + /** + * Sends a command to the emulator and parses its answer. + * @param command the command to send. + * @return {@link #RESULT_OK} if success, an error message otherwise. + */ + private String processCommand(String command) { + if (sendCommand(command)) { + String[] result = readLines(); + + if (result != null && result.length > 0) { + Matcher m = RE_KO.matcher(result[result.length-1]); + if (m.matches()) { + return m.group(1); + } + return RESULT_OK; + } + + return "Unable to communicate with the emulator"; + } + + return "Unable to send command to the emulator"; + } + + /** + * Reads line from the console socket. This call is blocking until we read the lines: + *

    + *
  • OK\r\n
  • + *
  • KO\r\n
  • + *
+ * @return the array of strings read from the emulator. + */ + private String[] readLines() { + try { + ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length); + int numWaits = 0; + boolean stop = false; + + while (buf.position() != buf.limit() && !stop) { + int count; + + count = mSocketChannel.read(buf); + if (count < 0) { + return null; + } else if (count == 0) { + if (numWaits * WAIT_TIME > STD_TIMEOUT) { + return null; + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + + // check the last few char aren't OK. For a valid message to test + // we need at least 4 bytes (OK/KO + \r\n) + if (buf.position() >= 4) { + int pos = buf.position(); + if (endsWithOK(pos) || lastLineIsKO(pos)) { + stop = true; + } + } + } + + String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING); + return msg.split("\r\n"); //$NON-NLS-1$ + } catch (IOException e) { + Log.d(LOG_TAG, "Exception reading lines for " + Integer.toString(mPort)); + return null; + } + } + + /** + * Returns true if the 4 characters *before* the current position are "OK\r\n" + * @param currentPosition The current position + */ + private boolean endsWithOK(int currentPosition) { + return mBuffer[currentPosition - 1] == '\n' && + mBuffer[currentPosition - 2] == '\r' && + mBuffer[currentPosition - 3] == 'K' && + mBuffer[currentPosition - 4] == 'O'; + + } + + /** + * Returns true if the last line starts with KO and is also terminated by \r\n + * @param currentPosition the current position + */ + private boolean lastLineIsKO(int currentPosition) { + // first check that the last 2 characters are CRLF + if (mBuffer[currentPosition-1] != '\n' || + mBuffer[currentPosition-2] != '\r') { + return false; + } + + // now loop backward looking for the previous CRLF, or the beginning of the buffer + int i = 0; + for (i = currentPosition-3 ; i >= 0; i--) { + if (mBuffer[i] == '\n') { + // found \n! + if (i > 0 && mBuffer[i-1] == '\r') { + // found \r! + break; + } + } + } + + // here it is either -1 if we reached the start of the buffer without finding + // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2 + if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') { + // found error! + return true; + } + + return false; + } + + /** + * Returns true if the last line of the result does not start with KO + */ + private boolean isValid(String[] result) { + if (result != null && result.length > 0) { + return !(RE_KO.matcher(result[result.length-1]).matches()); + } + return false; + } + + private int getLatencyIndex(String value) { + try { + // get the int value + int latency = Integer.parseInt(value); + + // check for the speed from the index + for (int i = 0 ; i < MIN_LATENCIES.length; i++) { + if (MIN_LATENCIES[i] == latency) { + return i; + } + } + } catch (NumberFormatException e) { + // Do nothing, we'll just return -1. + } + + return -1; + } + + private int getSpeedIndex(String value) { + try { + // get the int value + int speed = Integer.parseInt(value); + + // check for the speed from the index + for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) { + if (DOWNLOAD_SPEEDS[i] == speed) { + return i; + } + } + } catch (NumberFormatException e) { + // Do nothing, we'll just return -1. + } + + return -1; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/FileListingService.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/FileListingService.java new file mode 100644 index 0000000..52446ca --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/FileListingService.java @@ -0,0 +1,852 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides {@link Device} side file listing service. + *

To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}. + */ +public final class FileListingService { + + /** Pattern to find filenames that match "*.apk" */ + private static final Pattern sApkPattern = + Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + private static final String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$ + + /** Pattern to parse the output of the 'pm -lf' command.
+ * The output format looks like:
+ * /data/app/myapp.apk=com.mypackage.myapp */ + private static final Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$ + + /** Top level data folder. */ + public static final String DIRECTORY_DATA = "data"; //$NON-NLS-1$ + /** Top level sdcard folder. */ + public static final String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$ + /** Top level mount folder. */ + public static final String DIRECTORY_MNT = "mnt"; //$NON-NLS-1$ + /** Top level system folder. */ + public static final String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$ + /** Top level temp folder. */ + public static final String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$ + /** Application folder. */ + public static final String DIRECTORY_APP = "app"; //$NON-NLS-1$ + + public static final long REFRESH_RATE = 5000L; + /** + * Refresh test has to be slightly lower for precision issue. + */ + static final long REFRESH_TEST = (long)(REFRESH_RATE * .8); + + /** Entry type: File */ + public static final int TYPE_FILE = 0; + /** Entry type: Directory */ + public static final int TYPE_DIRECTORY = 1; + /** Entry type: Directory Link */ + public static final int TYPE_DIRECTORY_LINK = 2; + /** Entry type: Block */ + public static final int TYPE_BLOCK = 3; + /** Entry type: Character */ + public static final int TYPE_CHARACTER = 4; + /** Entry type: Link */ + public static final int TYPE_LINK = 5; + /** Entry type: Socket */ + public static final int TYPE_SOCKET = 6; + /** Entry type: FIFO */ + public static final int TYPE_FIFO = 7; + /** Entry type: Other */ + public static final int TYPE_OTHER = 8; + + /** Device side file separator. */ + public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$ + + private static final String FILE_ROOT = "/"; //$NON-NLS-1$ + + + /** + * Regexp pattern to parse the result from ls. + */ + private static final Pattern LS_L_PATTERN = Pattern.compile( + "^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+" + + "([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$ + + private static final Pattern LS_LD_PATTERN = Pattern.compile( + "d[rwx-]{9}\\s+\\S+\\s+\\S+\\s+[0-9-]{10}\\s+\\d{2}:\\d{2}$"); //$NON-NLS-1$ + + + private Device mDevice; + private FileEntry mRoot; + + private ArrayList mThreadList = new ArrayList(); + + /** + * Represents an entry in a directory. This can be a file or a directory. + */ + public static final class FileEntry { + /** Pattern to escape filenames for shell command consumption. + * This pattern identifies any special characters that need to be escaped with a + * backslash. */ + private static final Pattern sEscapePattern = Pattern.compile( + "([\\\\()*+?\"'&#/\\s])"); //$NON-NLS-1$ + + /** + * Comparator object for FileEntry + */ + private static Comparator sEntryComparator = new Comparator() { + @Override + public int compare(FileEntry o1, FileEntry o2) { + if (o1 instanceof FileEntry && o2 instanceof FileEntry) { + FileEntry fe1 = o1; + FileEntry fe2 = o2; + return fe1.name.compareTo(fe2.name); + } + return 0; + } + }; + + FileEntry parent; + String name; + String info; + String permissions; + String size; + String date; + String time; + String owner; + String group; + int type; + boolean isAppPackage; + + boolean isRoot; + + /** + * Indicates whether the entry content has been fetched yet, or not. + */ + long fetchTime = 0; + + final ArrayList mChildren = new ArrayList(); + + /** + * Creates a new file entry. + * @param parent parent entry or null if entry is root + * @param name name of the entry. + * @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE}, + * {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}. + */ + private FileEntry(FileEntry parent, String name, int type, boolean isRoot) { + this.parent = parent; + this.name = name; + this.type = type; + this.isRoot = isRoot; + + checkAppPackageStatus(); + } + + /** + * Returns the name of the entry + */ + public String getName() { + return name; + } + + /** + * Returns the size string of the entry, as returned by ls. + */ + public String getSize() { + return size; + } + + /** + * Returns the size of the entry. + */ + public int getSizeValue() { + return Integer.parseInt(size); + } + + /** + * Returns the date string of the entry, as returned by ls. + */ + public String getDate() { + return date; + } + + /** + * Returns the time string of the entry, as returned by ls. + */ + public String getTime() { + return time; + } + + /** + * Returns the permission string of the entry, as returned by ls. + */ + public String getPermissions() { + return permissions; + } + + /** + * Returns the owner string of the entry, as returned by ls. + */ + public String getOwner() { + return owner; + } + + /** + * Returns the group owner of the entry, as returned by ls. + */ + public String getGroup() { + return group; + } + + /** + * Returns the extra info for the entry. + *

For a link, it will be a description of the link. + *

For an application apk file it will be the application package as returned + * by the Package Manager. + */ + public String getInfo() { + return info; + } + + /** + * Return the full path of the entry. + * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator. + */ + public String getFullPath() { + if (isRoot) { + return FILE_ROOT; + } + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, false); + + return pathBuilder.toString(); + } + + /** + * Return the fully escaped path of the entry. This path is safe to use in a + * shell command line. + * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator + */ + public String getFullEscapedPath() { + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, true); + + return pathBuilder.toString(); + } + + /** + * Returns the path as a list of segments. + */ + public String[] getPathSegments() { + ArrayList list = new ArrayList(); + fillPathSegments(list); + + return list.toArray(new String[list.size()]); + } + + /** + * Returns the Entry type as an int, which will match one of the TYPE_(...) constants + */ + public int getType() { + return type; + } + + /** + * Sets a new type. + */ + public void setType(int type) { + this.type = type; + } + + /** + * Returns if the entry is a folder or a link to a folder. + */ + public boolean isDirectory() { + return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK; + } + + /** + * Returns the parent entry. + */ + public FileEntry getParent() { + return parent; + } + + /** + * Returns the cached children of the entry. This returns the cache created from calling + * FileListingService.getChildren(). + */ + public FileEntry[] getCachedChildren() { + return mChildren.toArray(new FileEntry[mChildren.size()]); + } + + /** + * Returns the child {@link FileEntry} matching the name. + * This uses the cached children list. + * @param name the name of the child to return. + * @return the FileEntry matching the name or null. + */ + public FileEntry findChild(String name) { + for (FileEntry entry : mChildren) { + if (entry.name.equals(name)) { + return entry; + } + } + return null; + } + + /** + * Returns whether the entry is the root. + */ + public boolean isRoot() { + return isRoot; + } + + void addChild(FileEntry child) { + mChildren.add(child); + } + + void setChildren(ArrayList newChildren) { + mChildren.clear(); + mChildren.addAll(newChildren); + } + + boolean needFetch() { + if (fetchTime == 0) { + return true; + } + long current = System.currentTimeMillis(); + return current - fetchTime > REFRESH_TEST; + + } + + /** + * Returns if the entry is a valid application package. + */ + public boolean isApplicationPackage() { + return isAppPackage; + } + + /** + * Returns if the file name is an application package name. + */ + public boolean isAppFileName() { + Matcher m = sApkPattern.matcher(name); + return m.matches(); + } + + /** + * Recursively fills the pathBuilder with the full path + * @param pathBuilder a StringBuilder used to create the path. + * @param escapePath Whether the path need to be escaped for consumption by + * a shell command line. + */ + protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) { + if (isRoot) { + return; + } + + if (parent != null) { + parent.fillPathBuilder(pathBuilder, escapePath); + } + pathBuilder.append(FILE_SEPARATOR); + pathBuilder.append(escapePath ? escape(name) : name); + } + + /** + * Recursively fills the segment list with the full path. + * @param list The list of segments to fill. + */ + protected void fillPathSegments(ArrayList list) { + if (isRoot) { + return; + } + + if (parent != null) { + parent.fillPathSegments(list); + } + + list.add(name); + } + + /** + * Sets the internal app package status flag. This checks whether the entry is in an app + * directory like /data/app or /system/app + */ + private void checkAppPackageStatus() { + isAppPackage = false; + + String[] segments = getPathSegments(); + if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) { + isAppPackage = DIRECTORY_APP.equals(segments[1]) && + (DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0])); + } + } + + /** + * Returns an escaped version of the entry name. + * @param entryName + */ + public static String escape(String entryName) { + return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$ + } + } + + private static class LsReceiver extends MultiLineReceiver { + + private ArrayList mEntryList; + private ArrayList mLinkList; + private FileEntry[] mCurrentChildren; + private FileEntry mParentEntry; + + /** + * Create an ls receiver/parser. + * @param currentChildren The list of current children. To prevent + * collapse during update, reusing the same FileEntry objects for + * files that were already there is paramount. + * @param entryList the list of new children to be filled by the + * receiver. + * @param linkList the list of link path to compute post ls, to figure + * out if the link pointed to a file or to a directory. + */ + public LsReceiver(FileEntry parentEntry, ArrayList entryList, + ArrayList linkList) { + mParentEntry = parentEntry; + mCurrentChildren = parentEntry.getCachedChildren(); + mEntryList = entryList; + mLinkList = linkList; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + // no need to handle empty lines. + if (line.isEmpty()) { + continue; + } + + // run the line through the regexp + Matcher m = LS_L_PATTERN.matcher(line); + if (!m.matches()) { + continue; + } + + // get the name + String name = m.group(7); + + // get the rest of the groups + String permissions = m.group(1); + String owner = m.group(2); + String group = m.group(3); + String size = m.group(4); + String date = m.group(5); + String time = m.group(6); + String info = null; + + // and the type + int objectType = TYPE_OTHER; + switch (permissions.charAt(0)) { + case '-' : + objectType = TYPE_FILE; + break; + case 'b' : + objectType = TYPE_BLOCK; + break; + case 'c' : + objectType = TYPE_CHARACTER; + break; + case 'd' : + objectType = TYPE_DIRECTORY; + break; + case 'l' : + objectType = TYPE_LINK; + break; + case 's' : + objectType = TYPE_SOCKET; + break; + case 'p' : + objectType = TYPE_FIFO; + break; + } + + + // now check what we may be linking to + if (objectType == TYPE_LINK) { + String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$ + + // we should have 2 segments + if (segments.length == 2) { + // update the entry name to not contain the link + name = segments[0]; + + // and the link name + info = segments[1]; + + // now get the path to the link + String[] pathSegments = info.split(FILE_SEPARATOR); + if (pathSegments.length == 1) { + // the link is to something in the same directory, + // unless the link is .. + if ("..".equals(pathSegments[0])) { //$NON-NLS-1$ + // set the type and we're done. + objectType = TYPE_DIRECTORY_LINK; + } else { + // either we found the object already + // or we'll find it later. + } + } + } + + // add an arrow in front to specify it's a link. + info = "-> " + info; //$NON-NLS-1$; + } + + // get the entry, either from an existing one, or a new one + FileEntry entry = getExistingEntry(name); + if (entry == null) { + entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */); + } + + // add some misc info + entry.permissions = permissions; + entry.size = size; + entry.date = date; + entry.time = time; + entry.owner = owner; + entry.group = group; + if (objectType == TYPE_LINK) { + entry.info = info; + } + + mEntryList.add(entry); + } + } + + /** + * Queries for an already existing Entry per name + * @param name the name of the entry + * @return the existing FileEntry or null if no entry with a matching + * name exists. + */ + private FileEntry getExistingEntry(String name) { + for (int i = 0 ; i < mCurrentChildren.length; i++) { + FileEntry e = mCurrentChildren[i]; + + // since we're going to "erase" the one we use, we need to + // check that the item is not null. + if (e != null) { + // compare per name, case-sensitive. + if (name.equals(e.name)) { + // erase from the list + mCurrentChildren[i] = null; + + // and return the object + return e; + } + } + } + + // couldn't find any matching object, return null + return null; + } + + @Override + public boolean isCancelled() { + return false; + } + + /** + * Determine if any symlinks in the list are links-to-directories, and if so + * mark them as such. This allows us to traverse them properly later on. + */ + public void finishLinks(IDevice device, ArrayList entries) + throws TimeoutException, AdbCommandRejectedException, + ShellCommandUnresponsiveException, IOException { + final int[] nLines = {0}; + MultiLineReceiver receiver = new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + Matcher m = LS_LD_PATTERN.matcher(line); + if (m.matches()) { + nLines[0]++; + } + } + } + + @Override + public boolean isCancelled() { + return false; + } + }; + + for (FileEntry entry : entries) { + if (entry.getType() != TYPE_LINK) continue; + + // We simply need to determine whether the referent is a directory or not. + // We do this by running `ls -ld ${link}/`. If the referent exists and is a + // directory, we'll see the normal directory listing. Otherwise, we'll see an + // error of some sort. + nLines[0] = 0; + + final String command = String.format("ls -l -d %s%s", entry.getFullEscapedPath(), + FILE_SEPARATOR); + + device.executeShellCommand(command, receiver); + + if (nLines[0] > 0) { + // We saw lines matching the directory pattern, so it's a directory! + entry.setType(TYPE_DIRECTORY_LINK); + } + } + } + } + + /** + * Classes which implement this interface provide a method that deals with asynchronous + * result from ls command on the device. + * + * @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver) + */ + public interface IListingReceiver { + void setChildren(FileEntry entry, FileEntry[] children); + + void refreshEntry(FileEntry entry); + } + + /** + * Creates a File Listing Service for a specified {@link Device}. + * @param device The Device the service is connected to. + */ + FileListingService(Device device) { + mDevice = device; + } + + /** + * Returns the root element. + * @return the {@link FileEntry} object representing the root element or + * null if the device is invalid. + */ + public FileEntry getRoot() { + if (mDevice != null) { + if (mRoot == null) { + mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY, + true /* isRoot */); + } + + return mRoot; + } + + return null; + } + + /** + * Returns the children of a {@link FileEntry}. + *

+ * This method supports a cache mechanism and synchronous and asynchronous modes. + *

+ * If receiver is null, the device side ls + * command is done synchronously, and the method will return upon completion of the command.
+ * If receiver is non null, the command is launched is a separate + * thread and upon completion, the receiver will be notified of the result. + *

+ * The result for each ls command is cached in the parent + * FileEntry. useCache allows usage of this cache, but only if the + * cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms. + * After that a new ls command is always executed. + *

+ * If the cache is valid and useCache == true, the method will always simply + * return the value of the cache, whether a {@link IListingReceiver} has been provided or not. + * + * @param entry The parent entry. + * @param useCache A flag to use the cache or to force a new ls command. + * @param receiver A receiver for asynchronous calls. + * @return The list of children or null for asynchronous calls. + * + * @see FileEntry#getCachedChildren() + */ + public FileEntry[] getChildren(final FileEntry entry, boolean useCache, + final IListingReceiver receiver) { + // first thing we do is check the cache, and if we already have a recent + // enough children list, we just return that. + if (useCache && !entry.needFetch()) { + return entry.getCachedChildren(); + } + + // if there's no receiver, then this is a synchronous call, and we + // return the result of ls + if (receiver == null) { + doLs(entry); + return entry.getCachedChildren(); + } + + // this is a asynchronous call. + // we launch a thread that will do ls and give the listing + // to the receiver + Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$ + @Override + public void run() { + doLs(entry); + + receiver.setChildren(entry, entry.getCachedChildren()); + + final FileEntry[] children = entry.getCachedChildren(); + if (children.length > 0 && children[0].isApplicationPackage()) { + final HashMap map = new HashMap(); + + for (FileEntry child : children) { + String path = child.getFullPath(); + map.put(path, child); + } + + // call pm. + String command = PM_FULL_LISTING; + try { + mDevice.executeShellCommand(command, new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (!line.isEmpty()) { + // get the filepath and package from the line + Matcher m = sPmPattern.matcher(line); + if (m.matches()) { + // get the children with that path + FileEntry entry = map.get(m.group(1)); + if (entry != null) { + entry.info = m.group(2); + receiver.refreshEntry(entry); + } + } + } + } + } + @Override + public boolean isCancelled() { + return false; + } + }); + } catch (Exception e) { + // adb failed somehow, we do nothing. + } + } + + + // if another thread is pending, launch it + synchronized (mThreadList) { + // first remove ourselves from the list + mThreadList.remove(this); + + // then launch the next one if applicable. + if (!mThreadList.isEmpty()) { + Thread t = mThreadList.get(0); + t.start(); + } + } + } + }; + + // we don't want to run multiple ls on the device at the same time, so we + // store the thread in a list and launch it only if there's no other thread running. + // the thread will launch the next one once it's done. + synchronized (mThreadList) { + // add to the list + mThreadList.add(t); + + // if it's the only one, launch it. + if (mThreadList.size() == 1) { + t.start(); + } + } + + // and we return null. + return null; + } + + /** + * Returns the children of a {@link FileEntry}. + *

+ * This method is the explicit synchronous version of + * {@link #getChildren(FileEntry, boolean, IListingReceiver)}. It is roughly equivalent to + * calling + * getChildren(FileEntry, false, null) + * + * @param entry The parent entry. + * @return The list of children + * @throws TimeoutException in case of timeout on the connection when sending the command. + * @throws AdbCommandRejectedException if adb rejects the command. + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output + * for a period longer than maxTimeToOutputResponse. + * @throws IOException in case of I/O error on the connection. + */ + public FileEntry[] getChildrenSync(final FileEntry entry) throws TimeoutException, + AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { + doLsAndThrow(entry); + return entry.getCachedChildren(); + } + + private void doLs(FileEntry entry) { + try { + doLsAndThrow(entry); + } catch (Exception e) { + // do nothing + } + } + + private void doLsAndThrow(FileEntry entry) throws TimeoutException, + AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { + // create a list that will receive the list of the entries + ArrayList entryList = new ArrayList(); + + // create a list that will receive the link to compute post ls; + ArrayList linkList = new ArrayList(); + + try { + // create the command + String command = "ls -l " + entry.getFullEscapedPath(); //$NON-NLS-1$ + if (entry.isDirectory()) { + // If we expect a file to behave like a directory, we should stick a "/" at the end. + // This is a good habit, and is mandatory for symlinks-to-directories, which will + // otherwise behave like symlinks. + command += FILE_SEPARATOR; + } + + // create the receiver object that will parse the result from ls + LsReceiver receiver = new LsReceiver(entry, entryList, linkList); + + // call ls. + mDevice.executeShellCommand(command, receiver); + + // finish the process of the receiver to handle links + receiver.finishLinks(mDevice, entryList); + } finally { + // at this point we need to refresh the viewer + entry.fetchTime = System.currentTimeMillis(); + + // sort the children and set them as the new children + Collections.sort(entryList, FileEntry.sEntryComparator); + entry.setChildren(entryList); + } + } + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleAppName.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleAppName.java new file mode 100644 index 0000000..e6b151e --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleAppName.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * Handle the "app name" chunk (APNM). + */ +final class HandleAppName extends ChunkHandler { + + public static final int CHUNK_APNM = ChunkHandler.type("APNM"); + + private static final HandleAppName mInst = new HandleAppName(); + + + private HandleAppName() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_APNM, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, + boolean isReply, int msgId) { + + Log.d("ddm-appname", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_APNM) { + assert !isReply; + handleAPNM(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our APNM message. + */ + private static void handleAPNM(Client client, ByteBuffer data) { + int appNameLen; + String appName; + + appNameLen = data.getInt(); + appName = ByteBufferUtil.getString(data, appNameLen); + + // Newer devices send user id in the APNM packet. + int userId = -1; + boolean validUserId = false; + if (data.hasRemaining()) { + try { + userId = data.getInt(); + validUserId = true; + } catch (BufferUnderflowException e) { + // two integers + utf-16 string + int expectedPacketLength = 8 + appNameLen * 2; + + Log.e("ddm-appname", "Insufficient data in APNM chunk to retrieve user id."); + Log.e("ddm-appname", "Actual chunk length: " + data.capacity()); + Log.e("ddm-appname", "Expected chunk length: " + expectedPacketLength); + } + } + + Log.d("ddm-appname", "APNM: app='" + appName + "'"); + + ClientData cd = client.getClientData(); + synchronized (cd) { + cd.setClientDescription(appName); + + if (validUserId) { + cd.setUserId(userId); + } + } + + client = checkDebuggerPortForAppName(client, appName); + + if (client != null) { + client.update(Client.CHANGE_NAME); + } + } + } + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleExit.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleExit.java new file mode 100644 index 0000000..adeedbb --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleExit.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Submit an exit request. + */ +final class HandleExit extends ChunkHandler { + + public static final int CHUNK_EXIT = type("EXIT"); + + private static final HandleExit mInst = new HandleExit(); + + + private HandleExit() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) {} + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + handleUnknownChunk(client, type, data, isReply, msgId); + } + + /** + * Send an EXIT request to the client. + */ + public static void sendEXIT(Client client, int status) + throws IOException + { + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(status); + + finishChunkPacket(packet, CHUNK_EXIT, buf.position()); + Log.d("ddm-exit", "Sending " + name(CHUNK_EXIT) + ": " + status); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHeap.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHeap.java new file mode 100644 index 0000000..97dd867 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHeap.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.ClientData.AllocationTrackingStatus; +import com.android.ddmlib.ClientData.IHprofDumpHandler; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * Handle heap status updates. + */ +final class HandleHeap extends ChunkHandler { + + public static final int CHUNK_HPIF = type("HPIF"); + public static final int CHUNK_HPST = type("HPST"); + public static final int CHUNK_HPEN = type("HPEN"); + public static final int CHUNK_HPSG = type("HPSG"); + public static final int CHUNK_HPGC = type("HPGC"); + public static final int CHUNK_HPDU = type("HPDU"); + public static final int CHUNK_HPDS = type("HPDS"); + public static final int CHUNK_REAE = type("REAE"); + public static final int CHUNK_REAQ = type("REAQ"); + public static final int CHUNK_REAL = type("REAL"); + + // args to sendHPSG + public static final int WHEN_DISABLE = 0; + public static final int WHEN_GC = 1; + public static final int WHAT_MERGE = 0; // merge adjacent objects + public static final int WHAT_OBJ = 1; // keep objects distinct + + // args to sendHPIF + public static final int HPIF_WHEN_NEVER = 0; + public static final int HPIF_WHEN_NOW = 1; + public static final int HPIF_WHEN_NEXT_GC = 2; + public static final int HPIF_WHEN_EVERY_GC = 3; + + private static final HandleHeap mInst = new HandleHeap(); + + private HandleHeap() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_HPIF, mInst); + mt.registerChunkHandler(CHUNK_HPST, mInst); + mt.registerChunkHandler(CHUNK_HPEN, mInst); + mt.registerChunkHandler(CHUNK_HPSG, mInst); + mt.registerChunkHandler(CHUNK_HPDS, mInst); + mt.registerChunkHandler(CHUNK_REAQ, mInst); + mt.registerChunkHandler(CHUNK_REAL, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + client.initializeHeapUpdateStatus(); + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + Log.d("ddm-heap", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_HPIF) { + handleHPIF(client, data); + } else if (type == CHUNK_HPST) { + handleHPST(client, data); + } else if (type == CHUNK_HPEN) { + handleHPEN(client, data); + } else if (type == CHUNK_HPSG) { + handleHPSG(client, data); + } else if (type == CHUNK_HPDU) { + handleHPDU(client, data); + } else if (type == CHUNK_HPDS) { + handleHPDS(client, data); + } else if (type == CHUNK_REAQ) { + handleREAQ(client, data); + } else if (type == CHUNK_REAL) { + handleREAL(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a heap info message. + */ + private void handleHPIF(Client client, ByteBuffer data) { + Log.d("ddm-heap", "HPIF!"); + try { + int numHeaps = data.getInt(); + + for (int i = 0; i < numHeaps; i++) { + int heapId = data.getInt(); + long timeStamp = data.getLong(); + byte reason = data.get(); + long maxHeapSize = (long)data.getInt() & 0x00ffffffff; + long heapSize = (long)data.getInt() & 0x00ffffffff; + long bytesAllocated = (long)data.getInt() & 0x00ffffffff; + long objectsAllocated = (long)data.getInt() & 0x00ffffffff; + + client.getClientData().setHeapInfo(heapId, maxHeapSize, + heapSize, bytesAllocated, objectsAllocated, timeStamp, reason); + client.update(Client.CHANGE_HEAP_DATA); + } + } catch (BufferUnderflowException ex) { + Log.w("ddm-heap", "malformed HPIF chunk from client"); + } + } + + /** + * Send an HPIF (HeaP InFo) request to the client. + */ + public static void sendHPIF(Client client, int when) throws IOException { + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte)when); + + finishChunkPacket(packet, CHUNK_HPIF, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPIF) + ": when=" + when); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle a heap segment series start message. + */ + private void handleHPST(Client client, ByteBuffer data) { + /* Clear out any data that's sitting around to + * get ready for the chunks that are about to come. + */ +//xxx todo: only clear data that belongs to the heap mentioned in . + client.getClientData().getVmHeapData().clearHeapData(); + } + + /* + * Handle a heap segment series end message. + */ + private void handleHPEN(Client client, ByteBuffer data) { + /* Let the UI know that we've received all of the + * data for this heap. + */ +//xxx todo: only seal data that belongs to the heap mentioned in . + client.getClientData().getVmHeapData().sealHeapData(); + client.update(Client.CHANGE_HEAP_DATA); + } + + /* + * Handle a heap segment message. + */ + private void handleHPSG(Client client, ByteBuffer data) { + byte dataCopy[] = new byte[data.limit()]; + data.rewind(); + data.get(dataCopy); + data = ByteBuffer.wrap(dataCopy); + client.getClientData().getVmHeapData().addHeapData(data); +//xxx todo: add to the heap mentioned in + } + + /** + * Sends an HPSG (HeaP SeGment) request to the client. + */ + public static void sendHPSG(Client client, int when, int what) + throws IOException { + + ByteBuffer rawBuf = allocBuffer(2); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte)when); + buf.put((byte)what); + + finishChunkPacket(packet, CHUNK_HPSG, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPSG) + ": when=" + + when + ", what=" + what); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends an HPGC request to the client. + */ + public static void sendHPGC(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_HPGC, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPGC)); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends an HPDU request to the client. + * + * We will get an HPDU response when the heap dump has completed. On + * failure we get a generic failure response. + * + * @param fileName name of output file (on device) + */ + public static void sendHPDU(Client client, String fileName) + throws IOException { + ByteBuffer rawBuf = allocBuffer(4 + fileName.length() * 2); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(fileName.length()); + ByteBufferUtil.putString(buf, fileName); + + finishChunkPacket(packet, CHUNK_HPDU, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPDU) + " '" + fileName +"'"); + client.sendAndConsume(packet, mInst); + client.getClientData().setPendingHprofDump(fileName); + } + + /** + * Sends an HPDS request to the client. + * + * We will get an HPDS response when the heap dump has completed. On + * failure we get a generic failure response. + * + * This is more expensive for the device than HPDU, because the entire + * heap dump is held in RAM instead of spooled out to a temp file. On + * the other hand, permission to write to /sdcard is not required. + * + * @param fileName name of output file (on device) + */ + public static void sendHPDS(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + finishChunkPacket(packet, CHUNK_HPDS, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPDS)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle notification of completion of a HeaP DUmp. + */ + private void handleHPDU(Client client, ByteBuffer data) { + byte result; + + // get the filename and make the client not have pending HPROF dump anymore. + String filename = client.getClientData().getPendingHprofDump(); + client.getClientData().setPendingHprofDump(null); + + // get the dump result + result = data.get(); + + // get the app-level handler for HPROF dump + IHprofDumpHandler handler = ClientData.getHprofDumpHandler(); + if (result == 0) { + if (handler != null) { + handler.onSuccess(filename, client); + } + client.getClientData().setHprofData(filename); + Log.d("ddm-heap", "Heap dump request has finished"); + } else { + if (handler != null) { + handler.onEndFailure(client, null); + } + client.getClientData().clearHprofData(); + Log.w("ddm-heap", "Heap dump request failed (check device log)"); + } + client.update(Client.CHANGE_HPROF); + client.getClientData().clearHprofData(); + } + + /* + * Handle HeaP Dump Streaming response. "data" contains the full + * hprof dump. + */ + private void handleHPDS(Client client, ByteBuffer data) { + byte[] stuff = new byte[data.capacity()]; + data.get(stuff, 0, stuff.length); + + Log.d("ddm-hprof", "got hprof file, size: " + data.capacity() + " bytes"); + client.getClientData().setHprofData(stuff); + IHprofDumpHandler handler = ClientData.getHprofDumpHandler(); + if (handler != null) { + handler.onSuccess(stuff, client); + } + client.update(Client.CHANGE_HPROF); + client.getClientData().clearHprofData(); + } + + /** + * Sends a REAE (REcent Allocation Enable) request to the client. + */ + public static void sendREAE(Client client, boolean enable) + throws IOException { + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte) (enable ? 1 : 0)); + + finishChunkPacket(packet, CHUNK_REAE, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAE) + ": " + enable); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends a REAQ (REcent Allocation Query) request to the client. + */ + public static void sendREAQ(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_REAQ, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAQ)); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends a REAL (REcent ALlocation) request to the client. + */ + public static void sendREAL(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_REAL, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAL)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle the response from our REcent Allocation Query message. + */ + private void handleREAQ(Client client, ByteBuffer data) { + boolean enabled; + + enabled = (data.get() != 0); + Log.d("ddm-heap", "REAQ says: enabled=" + enabled); + + client.getClientData().setAllocationStatus(enabled ? AllocationTrackingStatus.ON : AllocationTrackingStatus.OFF); + client.update(Client.CHANGE_HEAP_ALLOCATION_STATUS); + } + + /* + * Handle a REcent ALlocation response. + */ + private void handleREAL(Client client, ByteBuffer data) { + Log.e("ddm-heap", "*** Received " + name(CHUNK_REAL)); + ClientData.IAllocationTrackingHandler handler = ClientData.getAllocationTrackingHandler(); + + if (handler != null) { + byte[] stuff = new byte[data.capacity()]; + data.get(stuff, 0, stuff.length); + + Log.d("ddm-prof", "got allocations file, size: " + stuff.length + " bytes"); + handler.onSuccess(stuff, client); + } else { + // Allocation tracking did not start from Android Studio's device panel + client.getClientData().setAllocations(AllocationsParser.parse(data)); + client.update(Client.CHANGE_HEAP_ALLOCATIONS); + } + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHello.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHello.java new file mode 100644 index 0000000..6bf9150 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleHello.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * Handle the "hello" chunk (HELO) and feature discovery. + */ +final class HandleHello extends ChunkHandler { + + public static final int CHUNK_HELO = ChunkHandler.type("HELO"); + public static final int CHUNK_FEAT = ChunkHandler.type("FEAT"); + + private static final HandleHello mInst = new HandleHello(); + + private HandleHello() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_HELO, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + Log.d("ddm-hello", "Now ready: " + client); + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) { + Log.d("ddm-hello", "Now disconnected: " + client); + } + + /** + * Sends HELLO-type commands to the VM after a good handshake. + * @param client + * @param serverProtocolVersion + * @throws IOException + */ + public static void sendHelloCommands(Client client, int serverProtocolVersion) + throws IOException { + sendHELO(client, serverProtocolVersion); + sendFEAT(client); + HandleProfiling.sendMPRQ(client); + } + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-hello", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_HELO) { + assert isReply; + handleHELO(client, data); + } else if (type == CHUNK_FEAT) { + handleFEAT(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our HELO message. + */ + private static void handleHELO(Client client, ByteBuffer data) { + int version, pid, vmIdentLen, appNameLen; + String vmIdent, appName; + + version = data.getInt(); + pid = data.getInt(); + vmIdentLen = data.getInt(); + appNameLen = data.getInt(); + + vmIdent = ByteBufferUtil.getString(data, vmIdentLen); + appName = ByteBufferUtil.getString(data, appNameLen); + + // Newer devices send user id in the APNM packet. + int userId = -1; + boolean validUserId = false; + if (data.hasRemaining()) { + try { + userId = data.getInt(); + validUserId = true; + } catch (BufferUnderflowException e) { + // five integers + two utf-16 strings + int expectedPacketLength = 20 + appNameLen * 2 + vmIdentLen * 2; + + Log.e("ddm-hello", "Insufficient data in HELO chunk to retrieve user id."); + Log.e("ddm-hello", "Actual chunk length: " + data.capacity()); + Log.e("ddm-hello", "Expected chunk length: " + expectedPacketLength); + } + } + + // check if the VM has reported information about the ABI + boolean validAbi = false; + String abi = null; + if (data.hasRemaining()) { + try { + int abiLength = data.getInt(); + abi = ByteBufferUtil.getString(data, abiLength); + validAbi = true; + } catch (BufferUnderflowException e) { + Log.e("ddm-hello", "Insufficient data in HELO chunk to retrieve ABI."); + } + } + + boolean hasJvmFlags = false; + String jvmFlags = null; + if (data.hasRemaining()) { + try { + int jvmFlagsLength = data.getInt(); + jvmFlags = ByteBufferUtil.getString(data, jvmFlagsLength); + hasJvmFlags = true; + } catch (BufferUnderflowException e) { + Log.e("ddm-hello", "Insufficient data in HELO chunk to retrieve JVM flags"); + } + } + + Log.d("ddm-hello", "HELO: v=" + version + ", pid=" + pid + + ", vm='" + vmIdent + "', app='" + appName + "'"); + + ClientData cd = client.getClientData(); + + if (cd.getPid() == pid) { + cd.setVmIdentifier(vmIdent); + cd.setClientDescription(appName); + cd.isDdmAware(true); + + if (validUserId) { + cd.setUserId(userId); + } + + if (validAbi) { + cd.setAbi(abi); + } + + if (hasJvmFlags) { + cd.setJvmFlags(jvmFlags); + } + } else { + Log.e("ddm-hello", "Received pid (" + pid + ") does not match client pid (" + + cd.getPid() + ")"); + } + + client = checkDebuggerPortForAppName(client, appName); + + if (client != null) { + client.update(Client.CHANGE_NAME); + } + } + + + /** + * Send a HELO request to the client. + */ + public static void sendHELO(Client client, int serverProtocolVersion) + throws IOException + { + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(serverProtocolVersion); + + finishChunkPacket(packet, CHUNK_HELO, buf.position()); + Log.d("ddm-hello", "Sending " + name(CHUNK_HELO) + + " ID=0x" + Integer.toHexString(packet.getId())); + client.sendAndConsume(packet, mInst); + } + + /** + * Handle a reply to our FEAT request. + */ + private static void handleFEAT(Client client, ByteBuffer data) { + int featureCount; + int i; + + featureCount = data.getInt(); + for (i = 0; i < featureCount; i++) { + int len = data.getInt(); + String feature = ByteBufferUtil.getString(data, len); + client.getClientData().addFeature(feature); + + Log.d("ddm-hello", "Feature: " + feature); + } + } + + /** + * Send a FEAT request to the client. + */ + public static void sendFEAT(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_FEAT, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_FEAT)); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleNativeHeap.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleNativeHeap.java new file mode 100644 index 0000000..baf6db1 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleNativeHeap.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Handle thread status updates. + */ +final class HandleNativeHeap extends ChunkHandler { + + public static final int CHUNK_NHGT = type("NHGT"); //$NON-NLS-1$ + public static final int CHUNK_NHSG = type("NHSG"); //$NON-NLS-1$ + public static final int CHUNK_NHST = type("NHST"); //$NON-NLS-1$ + public static final int CHUNK_NHEN = type("NHEN"); //$NON-NLS-1$ + + private static final HandleNativeHeap mInst = new HandleNativeHeap(); + + /** + * Handle getting different sized size_t and pointer reads. + */ + abstract class NativeBuffer { + public NativeBuffer(ByteBuffer buffer) { + mBuffer = buffer; + } + + public abstract int getSizeT(); + public abstract long getPtr(); + + protected ByteBuffer mBuffer; + } + + /** + * This class treats size_t and pointer values as 32 bit. + */ + final class NativeBuffer32 extends NativeBuffer { + public NativeBuffer32(ByteBuffer buffer) { + super(buffer); + } + + @Override + public int getSizeT() { + return mBuffer.getInt(); + } + @Override + public long getPtr() { + return (long)mBuffer.getInt() & 0x00000000ffffffffL; + } + } + + /** + * This class treats size_t and pointer values as 64 bit. + */ + final class NativeBuffer64 extends NativeBuffer { + public NativeBuffer64(ByteBuffer buffer) { + super(buffer); + } + + @Override + public int getSizeT() { + return (int)mBuffer.getLong(); + } + @Override + public long getPtr() { + return mBuffer.getLong(); + } + } + + private HandleNativeHeap() { + } + + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_NHGT, mInst); + mt.registerChunkHandler(CHUNK_NHSG, mInst); + mt.registerChunkHandler(CHUNK_NHST, mInst); + mt.registerChunkHandler(CHUNK_NHEN, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-nativeheap", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_NHGT) { + handleNHGT(client, data); + } else if (type == CHUNK_NHST) { + // start chunk before any NHSG chunk(s) + client.getClientData().getNativeHeapData().clearHeapData(); + } else if (type == CHUNK_NHEN) { + // end chunk after NHSG chunk(s) + client.getClientData().getNativeHeapData().sealHeapData(); + } else if (type == CHUNK_NHSG) { + handleNHSG(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + + client.update(Client.CHANGE_NATIVE_HEAP_DATA); + } + + /** + * Send an NHGT (Native Thread GeT) request to the client. + */ + public static void sendNHGT(Client client) throws IOException { + + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data in request message + + finishChunkPacket(packet, CHUNK_NHGT, buf.position()); + Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHGT)); + client.sendAndConsume(packet, mInst); + + rawBuf = allocBuffer(2); + packet = new JdwpPacket(rawBuf); + buf = getChunkDataBuf(rawBuf); + + buf.put((byte)HandleHeap.WHEN_DISABLE); + buf.put((byte)HandleHeap.WHAT_OBJ); + + finishChunkPacket(packet, CHUNK_NHSG, buf.position()); + Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHSG)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle our native heap data. + */ + private void handleNHGT(Client client, ByteBuffer data) { + ClientData clientData = client.getClientData(); + + Log.d("ddm-nativeheap", "NHGT: " + data.limit() + " bytes"); + + data.order(ByteOrder.LITTLE_ENDIAN); + + // There are two supported header formats. + // + // The original version of the header for 32 bit processes: + // + // uint32_t mapSize; + // uint32_t mapSize; + // uint32_t allocSize; + // uint32_t allocInfoSize; + // uint32_t totalMemory; + // uint32_t backtrace_size; + // + // The new header which includes a signature and pointer size: + // + // uint32_t signature; (Which is always 0x812345dd) + // uint16_t version; (Only version 2 of the new format supported) + // uint16_t pointerSize; (Size in bytes of size_t/pointer values) + // size_t mapSize; + // size_t allocSize; + // size_t allocInfoSize; + // size_t totalMemory; + // size_t backtrace_size; + // + // If the signature doesn't match, then the code uses the original + // header format. If the signature matches, then use the new + // header format with variable sizes of size_t and pointers. + int signature = data.getInt(0); + short pointerSize = 4; + if (signature == 0x812345dd) { + // Consume signature value. + int ignore = data.getInt(); + short version = data.getShort(); + if (version != 2) { + Log.e("ddms", "Unknown header version: " + version); + return; + } + pointerSize = data.getShort(); + } + NativeBuffer buffer; + if (pointerSize == 4) { + buffer = new NativeBuffer32(data); + } else if (pointerSize == 8) { + buffer = new NativeBuffer64(data); + } else { + Log.e("ddms", "Unknown pointer size: " + pointerSize); + return; + } + + // clear the previous run + clientData.clearNativeAllocationInfo(); + + int mapSize = buffer.getSizeT(); + int allocSize = buffer.getSizeT(); + int allocInfoSize = buffer.getSizeT(); + int totalMemory = buffer.getSizeT(); + int backtraceSize = buffer.getSizeT(); + + Log.d("ddms", "mapSize: " + mapSize); + Log.d("ddms", "allocSize: " + allocSize); + Log.d("ddms", "allocInfoSize: " + allocInfoSize); + Log.d("ddms", "totalMemory: " + totalMemory); + + clientData.setTotalNativeMemory(totalMemory); + + // this means that updates aren't turned on. + if (allocInfoSize == 0) { + return; + } + + if (mapSize > 0) { + byte[] maps = new byte[mapSize]; + data.get(maps, 0, mapSize); + parseMaps(clientData, maps); + } + + int iterations = allocSize / allocInfoSize; + for (int i = 0 ; i < iterations ; i++) { + NativeAllocationInfo info = new NativeAllocationInfo( + buffer.getSizeT() /* size */, + buffer.getSizeT() /* allocations */); + + for (int j = 0 ; j < backtraceSize ; j++) { + long addr = buffer.getPtr(); + if (addr == 0x0) { + // skip past null addresses + continue; + } + + info.addStackCallAddress(addr); + } + clientData.addNativeAllocation(info); + } + } + + private void handleNHSG(Client client, ByteBuffer data) { + byte dataCopy[] = new byte[data.limit()]; + data.rewind(); + data.get(dataCopy); + data = ByteBuffer.wrap(dataCopy); + client.getClientData().getNativeHeapData().addHeapData(data); + + if (true) { + return; + } + + byte[] copy = new byte[data.limit()]; + data.get(copy); + + ByteBuffer buffer = ByteBuffer.wrap(copy); + buffer.order(ByteOrder.BIG_ENDIAN); + + int id = buffer.getInt(); + int unitsize = buffer.get(); + long startAddress = buffer.getInt() & 0x00000000ffffffffL; + int offset = buffer.getInt(); + int allocationUnitCount = buffer.getInt(); + + // read the usage + while (buffer.position() < buffer.limit()) { + int eState = buffer.get() & 0x000000ff; + int eLen = (buffer.get() & 0x000000ff) + 1; + } + } + + private void parseMaps(ClientData clientData, byte[] maps) { + InputStreamReader input = new InputStreamReader(new ByteArrayInputStream(maps)); + BufferedReader reader = new BufferedReader(input); + + String line; + + try { + while ((line = reader.readLine()) != null) { + Log.d("ddms", "line: " + line); + // Expected format: + // 7fe51f2000-7fe5213000 rw-p 00000000 00:00 0 [stack] + + int library_start = line.lastIndexOf(' '); + if (library_start == -1) { + continue; + } + + // Assume that any string that starts with a / is a + // shared library or executable that we will try to symbolize. + String library = line.substring(library_start+1); + if (!library.startsWith("/")) { + continue; + } + + // Parse the start and end address range. + int dashIndex = line.indexOf('-'); + int spaceIndex = line.indexOf(' ', dashIndex); + if (dashIndex == -1 || spaceIndex == -1) { + continue; + } + + long startAddr = 0; + long endAddr = 0; + try { + startAddr = Long.parseLong(line.substring(0, dashIndex), 16); + endAddr = Long.parseLong(line.substring(dashIndex+1, spaceIndex), 16); + } catch (NumberFormatException e) { + e.printStackTrace(); + continue; + } + + clientData.addNativeLibraryMapInfo(startAddr, endAddr, library); + Log.d("ddms", library + "(" + Long.toHexString(startAddr) + + " - " + Long.toHexString(endAddr) + ")"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleProfiling.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleProfiling.java new file mode 100644 index 0000000..ade7f27 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleProfiling.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.ClientData.IMethodProfilingHandler; +import com.android.ddmlib.ClientData.MethodProfilingStatus; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +/** + * Handle heap status updates. + */ +final class HandleProfiling extends ChunkHandler { + + public static final int CHUNK_MPRS = type("MPRS"); + public static final int CHUNK_MPRE = type("MPRE"); + public static final int CHUNK_MPSS = type("MPSS"); + public static final int CHUNK_MPSE = type("MPSE"); + public static final int CHUNK_SPSS = type("SPSS"); + public static final int CHUNK_SPSE = type("SPSE"); + public static final int CHUNK_MPRQ = type("MPRQ"); + public static final int CHUNK_FAIL = type("FAIL"); + + private static final HandleProfiling mInst = new HandleProfiling(); + + private HandleProfiling() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_MPRE, mInst); + mt.registerChunkHandler(CHUNK_MPSE, mInst); + mt.registerChunkHandler(CHUNK_MPRQ, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, + boolean isReply, int msgId) { + + Log.d("ddm-prof", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_MPRE) { + handleMPRE(client, data); + } else if (type == CHUNK_MPSE) { + handleMPSE(client, data); + } else if (type == CHUNK_MPRQ) { + handleMPRQ(client, data); + } else if (type == CHUNK_FAIL) { + handleFAIL(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /** + * Send a MPRS (Method PRofiling Start) request to the client. + * + * The arguments to this method will eventually be passed to + * android.os.Debug.startMethodTracing() on the device. + * + * @param fileName is the name of the file to which profiling data + * will be written (on the device); it will have {@link DdmConstants#DOT_TRACE} + * appended if necessary + * @param bufferSize is the desired buffer size in bytes (8MB is good) + * @param flags see startMethodTracing() docs; use 0 for default behavior + */ + public static void sendMPRS(Client client, String fileName, int bufferSize, + int flags) throws IOException { + + ByteBuffer rawBuf = allocBuffer(3*4 + fileName.length() * 2); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(bufferSize); + buf.putInt(flags); + buf.putInt(fileName.length()); + ByteBufferUtil.putString(buf, fileName); + + finishChunkPacket(packet, CHUNK_MPRS, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPRS) + " '" + fileName + + "', size=" + bufferSize + ", flags=" + flags); + client.sendAndConsume(packet, mInst); + + // record the filename we asked for. + client.getClientData().setPendingMethodProfiling(fileName); + + // send a status query. this ensure that the status is properly updated if for some + // reason starting the tracing failed. + sendMPRQ(client); + } + + /** + * Send a MPRE (Method PRofiling End) request to the client. + */ + public static void sendMPRE(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_MPRE, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPRE)); + client.sendAndConsume(packet, mInst); + } + + /** + * Handle notification that method profiling has finished writing + * data to disk. + */ + private void handleMPRE(Client client, ByteBuffer data) { + byte result; + + // get the filename and make the client not have pending HPROF dump anymore. + String filename = client.getClientData().getPendingMethodProfiling(); + client.getClientData().setPendingMethodProfiling(null); + + result = data.get(); + + // get the app-level handler for method tracing dump + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + if (result == 0) { + handler.onSuccess(filename, client); + + Log.d("ddm-prof", "Method profiling has finished"); + } else { + handler.onEndFailure(client, null /*message*/); + + Log.w("ddm-prof", "Method profiling has failed (check device log)"); + } + } + + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF); + client.update(Client.CHANGE_METHOD_PROFILING_STATUS); + } + + /** + * Send a MPSS (Method Profiling Streaming Start) request to the client. + * + * The arguments to this method will eventually be passed to + * android.os.Debug.startMethodTracing() on the device. + * + * @param bufferSize is the desired buffer size in bytes (8MB is good) + * @param flags see startMethodTracing() docs; use 0 for default behavior + */ + public static void sendMPSS(Client client, int bufferSize, + int flags) throws IOException { + + ByteBuffer rawBuf = allocBuffer(2*4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(bufferSize); + buf.putInt(flags); + + finishChunkPacket(packet, CHUNK_MPSS, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPSS) + + "', size=" + bufferSize + ", flags=" + flags); + client.sendAndConsume(packet, mInst); + + // send a status query. this ensure that the status is properly updated if for some + // reason starting the tracing failed. + sendMPRQ(client); + } + + /** + * Send a SPSS (Sampling Profiling Streaming Start) request to the client. + * + * @param bufferSize is the desired buffer size in bytes (8MB is good) + * @param samplingInterval sampling interval + * @param samplingIntervalTimeUnits units for sampling interval + */ + public static void sendSPSS(Client client, int bufferSize, int samplingInterval, + TimeUnit samplingIntervalTimeUnits) throws IOException { + int interval = (int) samplingIntervalTimeUnits.toMicros(samplingInterval); + + ByteBuffer rawBuf = allocBuffer(3*4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(bufferSize); + buf.putInt(0); // flags + buf.putInt(interval); + + finishChunkPacket(packet, CHUNK_SPSS, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_SPSS) + + "', size=" + bufferSize + ", flags=0, samplingInterval=" + interval); + client.sendAndConsume(packet, mInst); + + // send a status query. this ensure that the status is properly updated if for some + // reason starting the tracing failed. + sendMPRQ(client); + } + + /** + * Send a MPSE (Method Profiling Streaming End) request to the client. + */ + public static void sendMPSE(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_MPSE, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPSE)); + client.sendAndConsume(packet, mInst); + } + + /** + * Send a SPSE (Sampling Profiling Streaming End) request to the client. + */ + public static void sendSPSE(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_SPSE, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_SPSE)); + client.sendAndConsume(packet, mInst); + } + + /** + * Handle incoming profiling data. The MPSE packet includes the + * complete .trace file. + */ + private void handleMPSE(Client client, ByteBuffer data) { + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + byte[] stuff = new byte[data.capacity()]; + data.get(stuff, 0, stuff.length); + + Log.d("ddm-prof", "got trace file, size: " + stuff.length + " bytes"); + + handler.onSuccess(stuff, client); + } + + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF); + client.update(Client.CHANGE_METHOD_PROFILING_STATUS); + } + + /** + * Send a MPRQ (Method PRofiling Query) request to the client. + */ + public static void sendMPRQ(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_MPRQ, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPRQ)); + client.sendAndConsume(packet, mInst); + } + + /** + * Receive response to query. + */ + private void handleMPRQ(Client client, ByteBuffer data) { + byte result; + + result = data.get(); + + if (result == 0) { + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF); + Log.d("ddm-prof", "Method profiling is not running"); + } else if (result == 1) { + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.TRACER_ON); + Log.d("ddm-prof", "Method tracing is active"); + } else if (result == 2) { + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.SAMPLER_ON); + Log.d("ddm-prof", "Sampler based profiling is active"); + } + client.update(Client.CHANGE_METHOD_PROFILING_STATUS); + } + + private void handleFAIL(Client client, ByteBuffer data) { + /*int errorCode =*/ data.getInt(); + int length = data.getInt() * 2; + String message = null; + if (length > 0) { + byte[] messageBuffer = new byte[length]; + data.get(messageBuffer, 0, length); + message = new String(messageBuffer); + } + + // this can be sent if + // - MPRS failed (like wrong permission) + // - MPSE failed for whatever reason + + String filename = client.getClientData().getPendingMethodProfiling(); + if (filename != null) { + // reset the pending file. + client.getClientData().setPendingMethodProfiling(null); + + // and notify of failure + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + handler.onStartFailure(client, message); + } + } else { + // this is MPRE + // notify of failure + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + handler.onEndFailure(client, message); + } + } + + // send a query to know the current status + try { + sendMPRQ(client); + } catch (IOException e) { + Log.e("HandleProfiling", e); + } + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleTest.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleTest.java new file mode 100644 index 0000000..b9f3a74 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle thread status updates. + */ +final class HandleTest extends ChunkHandler { + + public static final int CHUNK_TEST = type("TEST"); + + private static final HandleTest mInst = new HandleTest(); + + + private HandleTest() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_TEST, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-test", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_TEST) { + handleTEST(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a thread creation message. + */ + private void handleTEST(Client client, ByteBuffer data) + { + /* + * Can't call data.array() on a read-only ByteBuffer, so we make + * a copy. + */ + byte[] copy = new byte[data.limit()]; + data.get(copy); + + Log.d("ddm-test", "Received:"); + Log.hexDump("ddm-test", LogLevel.DEBUG, copy, 0, copy.length); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleThread.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleThread.java new file mode 100644 index 0000000..f3874ce --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleThread.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle thread status updates. + */ +final class HandleThread extends ChunkHandler { + + public static final int CHUNK_THEN = type("THEN"); + public static final int CHUNK_THCR = type("THCR"); + public static final int CHUNK_THDE = type("THDE"); + public static final int CHUNK_THST = type("THST"); + public static final int CHUNK_THNM = type("THNM"); + public static final int CHUNK_STKL = type("STKL"); + + private static final HandleThread mInst = new HandleThread(); + + // only read/written by requestThreadUpdates() + private static volatile boolean sThreadStatusReqRunning = false; + private static volatile boolean sThreadStackTraceReqRunning = false; + + private HandleThread() {} + + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_THCR, mInst); + mt.registerChunkHandler(CHUNK_THDE, mInst); + mt.registerChunkHandler(CHUNK_THST, mInst); + mt.registerChunkHandler(CHUNK_THNM, mInst); + mt.registerChunkHandler(CHUNK_STKL, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + Log.d("ddm-thread", "Now ready: " + client); + if (client.isThreadUpdateEnabled()) + sendTHEN(client, true); + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-thread", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_THCR) { + handleTHCR(client, data); + } else if (type == CHUNK_THDE) { + handleTHDE(client, data); + } else if (type == CHUNK_THST) { + handleTHST(client, data); + } else if (type == CHUNK_THNM) { + handleTHNM(client, data); + } else if (type == CHUNK_STKL) { + handleSTKL(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a thread creation message. + * + * We should be tolerant of receiving a duplicate create message. (It + * shouldn't happen with the current implementation.) + */ + private void handleTHCR(Client client, ByteBuffer data) { + int threadId, nameLen; + String name; + + threadId = data.getInt(); + nameLen = data.getInt(); + name = ByteBufferUtil.getString(data, nameLen); + + Log.v("ddm-thread", "THCR: " + threadId + " '" + name + "'"); + + client.getClientData().addThread(threadId, name); + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a thread death message. + */ + private void handleTHDE(Client client, ByteBuffer data) { + int threadId; + + threadId = data.getInt(); + Log.v("ddm-thread", "THDE: " + threadId); + + client.getClientData().removeThread(threadId); + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a thread status update message. + * + * Response has: + * (1b) header len + * (1b) bytes per entry + * (2b) thread count + * Then, for each thread: + * (4b) threadId (matches value from THCR) + * (1b) thread status + * (4b) tid + * (4b) utime + * (4b) stime + */ + private void handleTHST(Client client, ByteBuffer data) { + int headerLen, bytesPerEntry, extraPerEntry; + int threadCount; + + headerLen = (data.get() & 0xff); + bytesPerEntry = (data.get() & 0xff); + threadCount = data.getShort(); + + headerLen -= 4; // we've read 4 bytes + while (headerLen-- > 0) + data.get(); + + extraPerEntry = bytesPerEntry - 18; // we want 18 bytes + + Log.v("ddm-thread", "THST: threadCount=" + threadCount); + + /* + * For each thread, extract the data, find the appropriate + * client, and add it to the ClientData. + */ + for (int i = 0; i < threadCount; i++) { + int threadId, status, tid, utime, stime; + boolean isDaemon = false; + + threadId = data.getInt(); + status = data.get(); + tid = data.getInt(); + utime = data.getInt(); + stime = data.getInt(); + if (bytesPerEntry >= 18) + isDaemon = (data.get() != 0); + + Log.v("ddm-thread", " id=" + threadId + + ", status=" + status + ", tid=" + tid + + ", utime=" + utime + ", stime=" + stime); + + ClientData cd = client.getClientData(); + ThreadInfo threadInfo = cd.getThread(threadId); + if (threadInfo != null) + threadInfo.updateThread(status, tid, utime, stime, isDaemon); + else + Log.d("ddms", "Thread with id=" + threadId + " not found"); + + // slurp up any extra + for (int slurp = extraPerEntry; slurp > 0; slurp--) + data.get(); + } + + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a THNM (THread NaMe) message. We get one of these after + * somebody calls Thread.setName() on a running thread. + */ + private void handleTHNM(Client client, ByteBuffer data) { + int threadId, nameLen; + String name; + + threadId = data.getInt(); + nameLen = data.getInt(); + name = ByteBufferUtil.getString(data, nameLen); + + Log.v("ddm-thread", "THNM: " + threadId + " '" + name + "'"); + + ThreadInfo threadInfo = client.getClientData().getThread(threadId); + if (threadInfo != null) { + threadInfo.setThreadName(name); + client.update(Client.CHANGE_THREAD_DATA); + } else { + Log.d("ddms", "Thread with id=" + threadId + " not found"); + } + } + + + /** + * Parse an incoming STKL. + */ + private void handleSTKL(Client client, ByteBuffer data) { + StackTraceElement[] trace; + int i, threadId, stackDepth; + @SuppressWarnings("unused") + int future; + + future = data.getInt(); + threadId = data.getInt(); + + Log.v("ddms", "STKL: " + threadId); + + /* un-serialize the StackTraceElement[] */ + stackDepth = data.getInt(); + trace = new StackTraceElement[stackDepth]; + for (i = 0; i < stackDepth; i++) { + String className, methodName, fileName; + int len, lineNumber; + + len = data.getInt(); + className = ByteBufferUtil.getString(data, len); + len = data.getInt(); + methodName = ByteBufferUtil.getString(data, len); + len = data.getInt(); + if (len == 0) { + fileName = null; + } else { + fileName = ByteBufferUtil.getString(data, len); + } + lineNumber = data.getInt(); + + trace[i] = new StackTraceElement(className, methodName, fileName, + lineNumber); + } + + ThreadInfo threadInfo = client.getClientData().getThread(threadId); + if (threadInfo != null) { + threadInfo.setStackCall(trace); + client.update(Client.CHANGE_THREAD_STACKTRACE); + } else { + Log.d("STKL", String.format( + "Got stackcall for thread %1$d, which does not exists (anymore?).", //$NON-NLS-1$ + threadId)); + } + } + + + /** + * Send a THEN (THread notification ENable) request to the client. + */ + public static void sendTHEN(Client client, boolean enable) + throws IOException { + + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + if (enable) + buf.put((byte)1); + else + buf.put((byte)0); + + finishChunkPacket(packet, CHUNK_THEN, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_THEN) + ": " + enable); + client.sendAndConsume(packet, mInst); + } + + + /** + * Send a STKL (STacK List) request to the client. The VM will suspend + * the target thread, obtain its stack, and return it. If the thread + * is no longer running, a failure result will be returned. + */ + public static void sendSTKL(Client client, int threadId) + throws IOException { + + if (false) { + Log.d("ddm-thread", "would send STKL " + threadId); + return; + } + + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(threadId); + + finishChunkPacket(packet, CHUNK_STKL, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_STKL) + ": " + threadId); + client.sendAndConsume(packet, mInst); + } + + + /** + * This is called periodically from the UI thread. To avoid locking + * the UI while we request the updates, we create a new thread. + * + */ + static void requestThreadUpdate(final Client client) { + if (client.isDdmAware() && client.isThreadUpdateEnabled()) { + if (sThreadStatusReqRunning) { + Log.w("ddms", "Waiting for previous thread update req to finish"); + return; + } + + new Thread("Thread Status Req") { + @Override + public void run() { + sThreadStatusReqRunning = true; + try { + sendTHST(client); + } catch (IOException ioe) { + Log.d("ddms", "Unable to request thread updates from " + + client + ": " + ioe.getMessage()); + } finally { + sThreadStatusReqRunning = false; + } + } + }.start(); + } + } + + static void requestThreadStackCallRefresh(final Client client, final int threadId) { + if (client.isDdmAware() && client.isThreadUpdateEnabled()) { + if (sThreadStackTraceReqRunning) { + Log.w("ddms", "Waiting for previous thread stack call req to finish"); + return; + } + + new Thread("Thread Status Req") { + @Override + public void run() { + sThreadStackTraceReqRunning = true; + try { + sendSTKL(client, threadId); + } catch (IOException ioe) { + Log.d("ddms", "Unable to request thread stack call updates from " + + client + ": " + ioe.getMessage()); + } finally { + sThreadStackTraceReqRunning = false; + } + } + }.start(); + } + + } + + /* + * Send a THST request to the specified client. + */ + private static void sendTHST(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // nothing much to say + + finishChunkPacket(packet, CHUNK_THST, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_THST)); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleViewDebug.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleViewDebug.java new file mode 100644 index 0000000..4378350 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleViewDebug.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public final class HandleViewDebug extends ChunkHandler { + /** Enable/Disable tracing of OpenGL calls. */ + public static final int CHUNK_VUGL = type("VUGL"); + + /** List {@link ViewRootImpl}'s of this process. */ + public static final int CHUNK_VULW = type("VULW"); + + /** Operation on view root, first parameter in packet should be one of VURT_* constants */ + public static final int CHUNK_VURT = type("VURT"); + + /** Dump view hierarchy. */ + private static final int VURT_DUMP_HIERARCHY = 1; + + /** Capture View Layers. */ + private static final int VURT_CAPTURE_LAYERS = 2; + + /** Dump View Theme. */ + private static final int VURT_DUMP_THEME = 3; + + /** + * Generic View Operation, first parameter in the packet should be one of the + * VUOP_* constants below. + */ + public static final int CHUNK_VUOP = type("VUOP"); + + /** Capture View. */ + private static final int VUOP_CAPTURE_VIEW = 1; + + /** Obtain the Display List corresponding to the view. */ + private static final int VUOP_DUMP_DISPLAYLIST = 2; + + /** Profile a view. */ + private static final int VUOP_PROFILE_VIEW = 3; + + /** Invoke a method on the view. */ + private static final int VUOP_INVOKE_VIEW_METHOD = 4; + + /** Set layout parameter. */ + private static final int VUOP_SET_LAYOUT_PARAMETER = 5; + + private static final String TAG = "ddmlib"; //$NON-NLS-1$ + + private static final HandleViewDebug sInstance = new HandleViewDebug(); + + private static final ViewDumpHandler sViewOpNullChunkHandler = + new NullChunkHandler(CHUNK_VUOP); + + private HandleViewDebug() {} + + public static void register(MonitorThread mt) { + // TODO: add chunk type for auto window updates + // and register here + mt.registerChunkHandler(CHUNK_VUGL, sInstance); + mt.registerChunkHandler(CHUNK_VULW, sInstance); + mt.registerChunkHandler(CHUNK_VUOP, sInstance); + mt.registerChunkHandler(CHUNK_VURT, sInstance); + } + + @Override + public void clientReady(Client client) throws IOException {} + + @Override + public void clientDisconnected(Client client) {} + + public abstract static class ViewDumpHandler extends ChunkHandler { + private final CountDownLatch mLatch = new CountDownLatch(1); + private final int mChunkType; + + public ViewDumpHandler(int chunkType) { + mChunkType = chunkType; + } + + @Override + void clientReady(Client client) throws IOException { + } + + @Override + void clientDisconnected(Client client) { + } + + @Override + void handleChunk(Client client, int type, ByteBuffer data, + boolean isReply, int msgId) { + if (type != mChunkType) { + handleUnknownChunk(client, type, data, isReply, msgId); + return; + } + + handleViewDebugResult(data); + mLatch.countDown(); + } + + protected abstract void handleViewDebugResult(ByteBuffer data); + + protected void waitForResult(long timeout, TimeUnit unit) { + try { + mLatch.await(timeout, unit); + } catch (InterruptedException e) { + // pass + } + } + } + + public static void listViewRoots(Client client, ViewDumpHandler replyHandler) + throws IOException { + ByteBuffer buf = allocBuffer(8); + JdwpPacket packet = new JdwpPacket(buf); + ByteBuffer chunkBuf = getChunkDataBuf(buf); + chunkBuf.putInt(1); + finishChunkPacket(packet, CHUNK_VULW, chunkBuf.position()); + client.sendAndConsume(packet, replyHandler); + } + + public static void dumpViewHierarchy(@NonNull Client client, @NonNull String viewRoot, + boolean skipChildren, boolean includeProperties, @NonNull ViewDumpHandler handler) + throws IOException { + ByteBuffer buf = allocBuffer(4 // opcode + + 4 // view root length + + viewRoot.length() * 2 // view root + + 4 // skip children + + 4); // include view properties + JdwpPacket packet = new JdwpPacket(buf); + ByteBuffer chunkBuf = getChunkDataBuf(buf); + + chunkBuf.putInt(VURT_DUMP_HIERARCHY); + chunkBuf.putInt(viewRoot.length()); + ByteBufferUtil.putString(chunkBuf, viewRoot); + chunkBuf.putInt(skipChildren ? 1 : 0); + chunkBuf.putInt(includeProperties ? 1 : 0); + + finishChunkPacket(packet, CHUNK_VURT, chunkBuf.position()); + client.sendAndConsume(packet, handler); + } + + public static void captureLayers(@NonNull Client client, @NonNull String viewRoot, + @NonNull ViewDumpHandler handler) throws IOException { + int bufLen = 8 + viewRoot.length() * 2; + + ByteBuffer buf = allocBuffer(bufLen); + JdwpPacket packet = new JdwpPacket(buf); + ByteBuffer chunkBuf = getChunkDataBuf(buf); + + chunkBuf.putInt(VURT_CAPTURE_LAYERS); + chunkBuf.putInt(viewRoot.length()); + ByteBufferUtil.putString(chunkBuf, viewRoot); + + finishChunkPacket(packet, CHUNK_VURT, chunkBuf.position()); + client.sendAndConsume(packet, handler); + } + + private static void sendViewOpPacket(@NonNull Client client, int op, @NonNull String viewRoot, + @NonNull String view, @Nullable byte[] extra, @Nullable ViewDumpHandler handler) + throws IOException { + int bufLen = 4 + // opcode + 4 + viewRoot.length() * 2 + // view root strlen + view root + 4 + view.length() * 2; // view strlen + view + + if (extra != null) { + bufLen += extra.length; + } + + ByteBuffer buf = allocBuffer(bufLen); + JdwpPacket packet = new JdwpPacket(buf); + ByteBuffer chunkBuf = getChunkDataBuf(buf); + + chunkBuf.putInt(op); + chunkBuf.putInt(viewRoot.length()); + ByteBufferUtil.putString(chunkBuf, viewRoot); + + chunkBuf.putInt(view.length()); + ByteBufferUtil.putString(chunkBuf, view); + + if (extra != null) { + chunkBuf.put(extra); + } + + finishChunkPacket(packet, CHUNK_VUOP, chunkBuf.position()); + if (handler != null) { + client.sendAndConsume(packet, handler); + } else { + client.sendAndConsume(packet); + } + } + + public static void profileView(@NonNull Client client, @NonNull String viewRoot, + @NonNull String view, @NonNull ViewDumpHandler handler) throws IOException { + sendViewOpPacket(client, VUOP_PROFILE_VIEW, viewRoot, view, null, handler); + } + + public static void captureView(@NonNull Client client, @NonNull String viewRoot, + @NonNull String view, @NonNull ViewDumpHandler handler) throws IOException { + sendViewOpPacket(client, VUOP_CAPTURE_VIEW, viewRoot, view, null, handler); + } + + public static void invalidateView(@NonNull Client client, @NonNull String viewRoot, + @NonNull String view) throws IOException { + invokeMethod(client, viewRoot, view, "invalidate"); + } + + public static void requestLayout(@NonNull Client client, @NonNull String viewRoot, + @NonNull String view) throws IOException { + invokeMethod(client, viewRoot, view, "requestLayout"); + } + + public static void dumpDisplayList(@NonNull Client client, @NonNull String viewRoot, + @NonNull String view) throws IOException { + sendViewOpPacket(client, VUOP_DUMP_DISPLAYLIST, viewRoot, view, null, + sViewOpNullChunkHandler); + } + + public static void dumpTheme(@NonNull Client client, @NonNull String viewRoot, + @NonNull ViewDumpHandler handler) + throws IOException { + ByteBuffer buf = allocBuffer(4 // opcode + + 4 // view root length + + viewRoot.length() * 2); // view root + JdwpPacket packet = new JdwpPacket(buf); + ByteBuffer chunkBuf = getChunkDataBuf(buf); + + chunkBuf.putInt(VURT_DUMP_THEME); + chunkBuf.putInt(viewRoot.length()); + ByteBufferUtil.putString(chunkBuf, viewRoot); + + finishChunkPacket(packet, CHUNK_VURT, chunkBuf.position()); + client.sendAndConsume(packet, handler); + } + + /** A {@link ViewDumpHandler} to use when no response is expected. */ + private static class NullChunkHandler extends ViewDumpHandler { + public NullChunkHandler(int chunkType) { + super(chunkType); + } + + @Override + protected void handleViewDebugResult(ByteBuffer data) { + } + } + + public static void invokeMethod(@NonNull Client client, @NonNull String viewRoot, + @NonNull String view, @NonNull String method, Object... args) throws IOException { + int len = 4 + method.length() * 2; + if (args != null) { + // # of args + len += 4; + + // for each argument, we send a char type specifier (2 bytes) and + // the arg value (max primitive size = sizeof(double) = 8 + len += 10 * args.length; + } + + byte[] extra = new byte[len]; + ByteBuffer b = ByteBuffer.wrap(extra); + + b.putInt(method.length()); + ByteBufferUtil.putString(b, method); + + if (args != null) { + b.putInt(args.length); + + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (arg instanceof Boolean) { + b.putChar('Z'); + b.put((byte) ((Boolean) arg ? 1 : 0)); + } else if (arg instanceof Byte) { + b.putChar('B'); + b.put((Byte) arg); + } else if (arg instanceof Character) { + b.putChar('C'); + b.putChar((Character) arg); + } else if (arg instanceof Short) { + b.putChar('S'); + b.putShort((Short) arg); + } else if (arg instanceof Integer) { + b.putChar('I'); + b.putInt((Integer) arg); + } else if (arg instanceof Long) { + b.putChar('J'); + b.putLong((Long) arg); + } else if (arg instanceof Float) { + b.putChar('F'); + b.putFloat((Float) arg); + } else if (arg instanceof Double) { + b.putChar('D'); + b.putDouble((Double) arg); + } else { + Log.e(TAG, "View method invocation only supports primitive arguments, supplied: " + arg); + return; + } + } + } + + sendViewOpPacket(client, VUOP_INVOKE_VIEW_METHOD, viewRoot, view, extra, + sViewOpNullChunkHandler ); + } + + public static void setLayoutParameter(@NonNull Client client, @NonNull String viewRoot, + @NonNull String view, @NonNull String parameter, int value) throws IOException { + int len = 4 + parameter.length() * 2 + 4; + byte[] extra = new byte[len]; + ByteBuffer b = ByteBuffer.wrap(extra); + + b.putInt(parameter.length()); + ByteBufferUtil.putString(b, parameter); + b.putInt(value); + sendViewOpPacket(client, VUOP_SET_LAYOUT_PARAMETER, viewRoot, view, extra, + sViewOpNullChunkHandler); + } + + @Override + public void handleChunk(Client client, int type, ByteBuffer data, + boolean isReply, int msgId) { + } + + public static void sendStartGlTracing(Client client) throws IOException { + ByteBuffer buf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(buf); + + ByteBuffer chunkBuf = getChunkDataBuf(buf); + chunkBuf.putInt(1); + finishChunkPacket(packet, CHUNK_VUGL, chunkBuf.position()); + + client.sendAndConsume(packet); + } + + public static void sendStopGlTracing(Client client) throws IOException { + ByteBuffer buf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(buf); + + ByteBuffer chunkBuf = getChunkDataBuf(buf); + chunkBuf.putInt(0); + finishChunkPacket(packet, CHUNK_VUGL, chunkBuf.position()); + + client.sendAndConsume(packet); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleWait.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleWait.java new file mode 100644 index 0000000..934cbea --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HandleWait.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.ClientData.DebuggerStatus; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle the "wait" chunk (WAIT). These are sent up when the client is + * waiting for something, e.g. for a debugger to attach. + */ +final class HandleWait extends ChunkHandler { + + public static final int CHUNK_WAIT = ChunkHandler.type("WAIT"); + + private static final HandleWait mInst = new HandleWait(); + + + private HandleWait() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_WAIT, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-wait", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_WAIT) { + assert !isReply; + handleWAIT(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our WAIT message. + */ + private static void handleWAIT(Client client, ByteBuffer data) { + byte reason; + + reason = data.get(); + + Log.d("ddm-wait", "WAIT: reason=" + reason); + + + ClientData cd = client.getClientData(); + synchronized (cd) { + cd.setDebuggerConnectionStatus(DebuggerStatus.WAITING); + } + + client.update(Client.CHANGE_DEBUGGER_STATUS); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HeapSegment.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HeapSegment.java new file mode 100644 index 0000000..b6acd65 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/HeapSegment.java @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.text.ParseException; + +/** + * Describes the types and locations of objects in a segment of a heap. + */ +public final class HeapSegment implements Comparable { + + /** + * Describes an object/region encoded in the HPSG data. + */ + public static class HeapSegmentElement implements Comparable { + + /* + * Solidity values, which must match the values in + * the HPSG data. + */ + + /** The element describes a free block. */ + public static final int SOLIDITY_FREE = 0; + + /** The element is strongly-reachable. */ + public static final int SOLIDITY_HARD = 1; + + /** The element is softly-reachable. */ + public static final int SOLIDITY_SOFT = 2; + + /** The element is weakly-reachable. */ + public static final int SOLIDITY_WEAK = 3; + + /** The element is phantom-reachable. */ + public static final int SOLIDITY_PHANTOM = 4; + + /** The element is pending finalization. */ + public static final int SOLIDITY_FINALIZABLE = 5; + + /** The element is not reachable, and is about to be swept/freed. */ + public static final int SOLIDITY_SWEEP = 6; + + /** The reachability of the object is unknown. */ + public static final int SOLIDITY_INVALID = -1; + + + /* + * Kind values, which must match the values in + * the HPSG data. + */ + + /** The element describes a data object. */ + public static final int KIND_OBJECT = 0; + + /** The element describes a class object. */ + public static final int KIND_CLASS_OBJECT = 1; + + /** The element describes an array of 1-byte elements. */ + public static final int KIND_ARRAY_1 = 2; + + /** The element describes an array of 2-byte elements. */ + public static final int KIND_ARRAY_2 = 3; + + /** The element describes an array of 4-byte elements. */ + public static final int KIND_ARRAY_4 = 4; + + /** The element describes an array of 8-byte elements. */ + public static final int KIND_ARRAY_8 = 5; + + /** The element describes an unknown type of object. */ + public static final int KIND_UNKNOWN = 6; + + /** The element describes a native object. */ + public static final int KIND_NATIVE = 7; + + /** The object kind is unknown or unspecified. */ + public static final int KIND_INVALID = -1; + + + /** + * A bit in the HPSG data that indicates that an element should + * be combined with the element that follows, typically because + * an element is too large to be described by a single element. + */ + private static final int PARTIAL_MASK = 1 << 7; + + + /** + * Describes the reachability/solidity of the element. Must + * be set to one of the SOLIDITY_* values. + */ + private int mSolidity; + + /** + * Describes the type/kind of the element. Must be set to one + * of the KIND_* values. + */ + private int mKind; + + /** + * Describes the length of the element, in bytes. + */ + private int mLength; + + + /** + * Creates an uninitialized element. + */ + public HeapSegmentElement() { + setSolidity(SOLIDITY_INVALID); + setKind(KIND_INVALID); + setLength(-1); + } + + /** + * Create an element describing the entry at the current + * position of hpsgData. + * + * @param hs The heap segment to pull the entry from. + * @throws BufferUnderflowException if there is not a whole entry + * following the current position + * of hpsgData. + * @throws ParseException if the provided data is malformed. + */ + public HeapSegmentElement(HeapSegment hs) + throws BufferUnderflowException, ParseException { + set(hs); + } + + /** + * Replace the element with the entry at the current position of + * hpsgData. + * + * @param hs The heap segment to pull the entry from. + * @return this object. + * @throws BufferUnderflowException if there is not a whole entry + * following the current position of + * hpsgData. + * @throws ParseException if the provided data is malformed. + */ + public HeapSegmentElement set(HeapSegment hs) + throws BufferUnderflowException, ParseException { + + /* TODO: Maybe keep track of the virtual address of each element + * so that they can be examined independently. + */ + ByteBuffer data = hs.mUsageData; + int eState = data.get() & 0x000000ff; + int eLen = (data.get() & 0x000000ff) + 1; + + while ((eState & PARTIAL_MASK) != 0) { + + /* If the partial bit was set, the next byte should describe + * the same object as the current one. + */ + int nextState = data.get() & 0x000000ff; + if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) { + throw new ParseException("State mismatch", data.position()); + } + eState = nextState; + eLen += (data.get() & 0x000000ff) + 1; + } + + setSolidity(eState & 0x7); + setKind((eState >> 3) & 0x7); + setLength(eLen * hs.mAllocationUnitSize); + + return this; + } + + public int getSolidity() { + return mSolidity; + } + + public void setSolidity(int solidity) { + this.mSolidity = solidity; + } + + public int getKind() { + return mKind; + } + + public void setKind(int kind) { + this.mKind = kind; + } + + public int getLength() { + return mLength; + } + + public void setLength(int length) { + this.mLength = length; + } + + @Override + public int compareTo(HeapSegmentElement other) { + if (mLength != other.mLength) { + return mLength < other.mLength ? -1 : 1; + } + return 0; + } + } + + //* The ID of the heap that this segment belongs to. + protected int mHeapId; + + //* The size of an allocation unit, in bytes. (e.g., 8 bytes) + protected int mAllocationUnitSize; + + //* The virtual address of the start of this segment. + protected long mStartAddress; + + //* The offset of this pices from mStartAddress, in bytes. + protected int mOffset; + + //* The number of allocation units described in this segment. + protected int mAllocationUnitCount; + + //* The raw data that describes the contents of this segment. + protected ByteBuffer mUsageData; + + //* mStartAddress is set to this value when the segment becomes invalid. + private static final long INVALID_START_ADDRESS = -1; + + /** + * Create a new HeapSegment based on the raw contents + * of an HPSG chunk. + * + * @param hpsgData The raw data from an HPSG chunk. + * @throws BufferUnderflowException if hpsgData is too small + * to hold the HPSG chunk header data. + */ + public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException { + /* Read the HPSG chunk header. + * These get*() calls may throw a BufferUnderflowException + * if the underlying data isn't big enough. + */ + hpsgData.order(ByteOrder.BIG_ENDIAN); + mHeapId = hpsgData.getInt(); + mAllocationUnitSize = hpsgData.get(); + mStartAddress = hpsgData.getInt() & 0x00000000ffffffffL; + mOffset = hpsgData.getInt(); + mAllocationUnitCount = hpsgData.getInt(); + + // Hold onto the remainder of the data. + mUsageData = hpsgData.slice(); + mUsageData.order(ByteOrder.BIG_ENDIAN); // doesn't actually matter + + // Validate the data. +//xxx do it +//xxx make sure the number of elements matches mAllocationUnitCount. +//xxx make sure the last element doesn't have P set + } + + /** + * See if this segment still contains data, and has not been + * appended to another segment. + * + * @return true if this segment has not been appended to + * another segment. + */ + public boolean isValid() { + return mStartAddress != INVALID_START_ADDRESS; + } + + /** + * See if other comes immediately after this segment. + * + * @param other The HeapSegment to check. + * @return true if other comes immediately after this + * segment. + */ + public boolean canAppend(HeapSegment other) { + return isValid() && other.isValid() && mHeapId == other.mHeapId && + mAllocationUnitSize == other.mAllocationUnitSize && + getEndAddress() == other.getStartAddress(); + } + + /** + * Append the contents of other to this segment + * if it describes the segment immediately after this one. + * + * @param other The segment to append to this segment, if possible. + * If appended, other will be invalid + * when this method returns. + * @return true if other was successfully appended to + * this segment. + */ + public boolean append(HeapSegment other) { + if (canAppend(other)) { + /* Preserve the position. The mark is not preserved, + * but we don't use it anyway. + */ + int pos = mUsageData.position(); + + // Guarantee that we have enough room for the new data. + if (mUsageData.capacity() - mUsageData.limit() < + other.mUsageData.limit()) { + /* Grow more than necessary in case another append() + * is about to happen. + */ + int newSize = mUsageData.limit() + other.mUsageData.limit(); + ByteBuffer newData = ByteBuffer.allocate(newSize * 2); + + mUsageData.rewind(); + newData.put(mUsageData); + mUsageData = newData; + } + + // Copy the data from the other segment and restore the position. + other.mUsageData.rewind(); + mUsageData.put(other.mUsageData); + mUsageData.position(pos); + + // Fix this segment's header to cover the new data. + mAllocationUnitCount += other.mAllocationUnitCount; + + // Mark the other segment as invalid. + other.mStartAddress = INVALID_START_ADDRESS; + other.mUsageData = null; + + return true; + } else { + return false; + } + } + + public long getStartAddress() { + return mStartAddress + mOffset; + } + + public int getLength() { + return mAllocationUnitSize * mAllocationUnitCount; + } + + public long getEndAddress() { + return getStartAddress() + getLength(); + } + + public void rewindElements() { + if (mUsageData != null) { + mUsageData.rewind(); + } + } + + public HeapSegmentElement getNextElement(HeapSegmentElement reuse) { + try { + if (reuse != null) { + return reuse.set(this); + } else { + return new HeapSegmentElement(this); + } + } catch (BufferUnderflowException ex) { + /* Normal "end of buffer" situation. + */ + } catch (ParseException ex) { + /* Malformed data. + */ +//TODO: we should catch this in the constructor + } + return null; + } + + /* + * Method overrides for Comparable + */ + @Override + public boolean equals(Object o) { + if (o instanceof HeapSegment) { + return compareTo((HeapSegment) o) == 0; + } + return false; + } + + @Override + public int hashCode() { + return mHeapId * 31 + + mAllocationUnitSize * 31 + + (int) mStartAddress * 31 + + mOffset * 31 + + mAllocationUnitCount * 31 + + mUsageData.hashCode(); + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + + str.append("HeapSegment { heap ").append(mHeapId) + .append(", start 0x") + .append(Integer.toHexString((int) getStartAddress())) + .append(", length ").append(getLength()) + .append(" }"); + + return str.toString(); + } + + @Override + public int compareTo(HeapSegment other) { + if (mHeapId != other.mHeapId) { + return mHeapId < other.mHeapId ? -1 : 1; + } + if (getStartAddress() != other.getStartAddress()) { + return getStartAddress() < other.getStartAddress() ? -1 : 1; + } + + /* If two segments have the same start address, the rest of + * the fields should be equal. Go through the motions, though. + * Note that we re-check the components of getStartAddress() + * (mStartAddress and mOffset) to make sure that all fields in + * an equal segment are equal. + */ + + if (mAllocationUnitSize != other.mAllocationUnitSize) { + return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1; + } + if (mStartAddress != other.mStartAddress) { + return mStartAddress < other.mStartAddress ? -1 : 1; + } + if (mOffset != other.mOffset) { + return mOffset < other.mOffset ? -1 : 1; + } + if (mAllocationUnitCount != other.mAllocationUnitCount) { + return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1; + } + if (mUsageData != other.mUsageData) { + return mUsageData.compareTo(other.mUsageData); + } + return 0; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IDevice.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IDevice.java new file mode 100644 index 0000000..be45449 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IDevice.java @@ -0,0 +1,639 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.log.LogReceiver; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * A Device. It can be a physical device or an emulator. + */ +public interface IDevice extends IShellEnabledDevice { + + String PROP_BUILD_VERSION = "ro.build.version.release"; + String PROP_BUILD_API_LEVEL = "ro.build.version.sdk"; + String PROP_BUILD_CODENAME = "ro.build.version.codename"; + String PROP_BUILD_TAGS = "ro.build.tags"; + String PROP_BUILD_TYPE = "ro.build.type"; + String PROP_DEVICE_MODEL = "ro.product.model"; + String PROP_DEVICE_MANUFACTURER = "ro.product.manufacturer"; + String PROP_DEVICE_CPU_ABI_LIST = "ro.product.cpu.abilist"; + String PROP_DEVICE_CPU_ABI = "ro.product.cpu.abi"; + String PROP_DEVICE_CPU_ABI2 = "ro.product.cpu.abi2"; + String PROP_BUILD_CHARACTERISTICS = "ro.build.characteristics"; + String PROP_DEVICE_DENSITY = "ro.sf.lcd_density"; + String PROP_DEVICE_LANGUAGE = "persist.sys.language"; + String PROP_DEVICE_REGION = "persist.sys.country"; + + String PROP_DEBUGGABLE = "ro.debuggable"; + + /** Serial number of the first connected emulator. */ + String FIRST_EMULATOR_SN = "emulator-5554"; //$NON-NLS-1$ + /** Device change bit mask: {@link DeviceState} change. */ + int CHANGE_STATE = 0x0001; + /** Device change bit mask: {@link Client} list change. */ + int CHANGE_CLIENT_LIST = 0x0002; + /** Device change bit mask: build info change. */ + int CHANGE_BUILD_INFO = 0x0004; + + /** Device level software features. */ + enum Feature { + SCREEN_RECORD, // screen recorder available? + PROCSTATS, // procstats service (dumpsys procstats) available + } + + /** Device level hardware features. */ + enum HardwareFeature { + WATCH("watch"), + TV("tv"); + + private final String mCharacteristic; + + HardwareFeature(String characteristic) { + mCharacteristic = characteristic; + } + + public String getCharacteristic() { + return mCharacteristic; + } + } + + /** @deprecated Use {@link #PROP_BUILD_API_LEVEL}. */ + @Deprecated + String PROP_BUILD_VERSION_NUMBER = PROP_BUILD_API_LEVEL; + + String MNT_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; //$NON-NLS-1$ + String MNT_ROOT = "ANDROID_ROOT"; //$NON-NLS-1$ + String MNT_DATA = "ANDROID_DATA"; //$NON-NLS-1$ + + /** + * The state of a device. + */ + enum DeviceState { + BOOTLOADER("bootloader"), //$NON-NLS-1$ + OFFLINE("offline"), //$NON-NLS-1$ + ONLINE("device"), //$NON-NLS-1$ + RECOVERY("recovery"), //$NON-NLS-1$ + UNAUTHORIZED("unauthorized"); //$NON-NLS-1$ + + private String mState; + + DeviceState(String state) { + mState = state; + } + + /** + * Returns a {@link DeviceState} from the string returned by adb devices. + * + * @param state the device state. + * @return a {@link DeviceState} object or null if the state is unknown. + */ + @Nullable + public static DeviceState getState(String state) { + for (DeviceState deviceState : values()) { + if (deviceState.mState.equals(state)) { + return deviceState; + } + } + return null; + } + } + + /** + * Namespace of a Unix Domain Socket created on the device. + */ + enum DeviceUnixSocketNamespace { + ABSTRACT("localabstract"), //$NON-NLS-1$ + FILESYSTEM("localfilesystem"), //$NON-NLS-1$ + RESERVED("localreserved"); //$NON-NLS-1$ + + private String mType; + + DeviceUnixSocketNamespace(String type) { + mType = type; + } + + String getType() { + return mType; + } + } + + /** Returns the serial number of the device. */ + @NonNull + String getSerialNumber(); + + /** + * Returns the name of the AVD the emulator is running. + *

This is only valid if {@link #isEmulator()} returns true. + *

If the emulator is not running any AVD (for instance it's running from an Android source + * tree build), this method will return "<build>". + * + * @return the name of the AVD or null if there isn't any. + */ + @Nullable + String getAvdName(); + + /** + * Returns the state of the device. + */ + DeviceState getState(); + + /** + * Returns the cached device properties. It contains the whole output of 'getprop' + * + * @deprecated use {@link #getSystemProperty(String)} instead + */ + @Deprecated + Map getProperties(); + + /** + * Returns the number of property for this device. + * + * @deprecated implementation detail + */ + @Deprecated + int getPropertyCount(); + + /** + * Convenience method that attempts to retrieve a property via + * {@link #getSystemProperty(String)} with minimal wait time, and swallows exceptions. + * + * @param name the name of the value to return. + * @return the value or null if the property value was not immediately available + */ + @Nullable + String getProperty(@NonNull String name); + + /** + * Returns true> if properties have been cached + */ + boolean arePropertiesSet(); + + /** + * A variant of {@link #getProperty(String)} that will attempt to retrieve the given + * property from device directly, without using cache. + * This method should (only) be used for any volatile properties. + * + * @param name the name of the value to return. + * @return the value or null if the property does not exist + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output for a + * given time. + * @throws IOException in case of I/O error on the connection. + * @deprecated use {@link #getSystemProperty(String)} + */ + @Deprecated + String getPropertySync(String name) throws TimeoutException, + AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException; + + /** + * A combination of {@link #getProperty(String)} and {@link #getPropertySync(String)} that + * will attempt to retrieve the property from cache. If not found, will synchronously + * attempt to query device directly and repopulate the cache if successful. + * + * @param name the name of the value to return. + * @return the value or null if the property does not exist + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output for a + * given time. + * @throws IOException in case of I/O error on the connection. + * @deprecated use {@link #getSystemProperty(String)} instead + */ + @Deprecated + String getPropertyCacheOrSync(String name) throws TimeoutException, + AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException; + + /** Returns whether this device supports the given software feature. */ + boolean supportsFeature(@NonNull Feature feature); + + /** Returns whether this device supports the given hardware feature. */ + boolean supportsFeature(@NonNull HardwareFeature feature); + + /** + * Returns a mount point. + * + * @param name the name of the mount point to return + * + * @see #MNT_EXTERNAL_STORAGE + * @see #MNT_ROOT + * @see #MNT_DATA + */ + @Nullable + String getMountPoint(@NonNull String name); + + /** + * Returns if the device is ready. + * + * @return true if {@link #getState()} returns {@link DeviceState#ONLINE}. + */ + boolean isOnline(); + + /** + * Returns true if the device is an emulator. + */ + boolean isEmulator(); + + /** + * Returns if the device is offline. + * + * @return true if {@link #getState()} returns {@link DeviceState#OFFLINE}. + */ + boolean isOffline(); + + /** + * Returns if the device is in bootloader mode. + * + * @return true if {@link #getState()} returns {@link DeviceState#BOOTLOADER}. + */ + boolean isBootLoader(); + + /** + * Returns whether the {@link Device} has {@link Client}s. + */ + boolean hasClients(); + + /** + * Returns the array of clients. + */ + Client[] getClients(); + + /** + * Returns a {@link Client} by its application name. + * + * @param applicationName the name of the application + * @return the Client object or null if no match was found. + */ + Client getClient(String applicationName); + + /** + * Returns a {@link SyncService} object to push / pull files to and from the device. + * + * @return null if the SyncService couldn't be created. This can happen if adb + * refuse to open the connection because the {@link IDevice} is invalid + * (or got disconnected). + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException if the connection with adb failed. + */ + SyncService getSyncService() + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Returns a {@link FileListingService} for this device. + */ + FileListingService getFileListingService(); + + /** + * Takes a screen shot of the device and returns it as a {@link RawImage}. + * + * @return the screenshot as a RawImage or null if something + * went wrong. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + RawImage getScreenshot() throws TimeoutException, AdbCommandRejectedException, IOException; + + RawImage getScreenshot(long timeout, TimeUnit unit) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Initiates screen recording on the device if the device supports {@link Feature#SCREEN_RECORD}. + */ + void startScreenRecorder(@NonNull String remoteFilePath, + @NonNull ScreenRecorderOptions options, @NonNull IShellOutputReceiver receiver) throws + TimeoutException, AdbCommandRejectedException, IOException, + ShellCommandUnresponsiveException; + + /** + * @deprecated Use {@link #executeShellCommand(String, IShellOutputReceiver, long, java.util.concurrent.TimeUnit)}. + */ + @Deprecated + void executeShellCommand(String command, IShellOutputReceiver receiver, + int maxTimeToOutputResponse) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; + + /** + * Executes a shell command on the device, and sends the result to a receiver + *

This is similar to calling + * executeShellCommand(command, receiver, DdmPreferences.getTimeOut()). + * + * @param command the shell command to execute + * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell + * command + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output + * for a given time. + * @throws IOException in case of I/O error on the connection. + * + * @see #executeShellCommand(String, IShellOutputReceiver, int) + * @see DdmPreferences#getTimeOut() + */ + void executeShellCommand(String command, IShellOutputReceiver receiver) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; + + /** + * Runs the event log service and outputs the event log to the {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * @param receiver the receiver to receive the event log entries. + * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the + * timeout happens during setup. Once logs start being received, no timeout will occur as it's + * not possible to detect a difference between no log and timeout. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + void runEventLogService(LogReceiver receiver) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Runs the log service for the given log and outputs the log to the {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * + * @param logname the logname of the log to read from. + * @param receiver the receiver to receive the event log entries. + * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the + * timeout happens during setup. Once logs start being received, no timeout will + * occur as it's not possible to detect a difference between no log and timeout. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + void runLogService(String logname, LogReceiver receiver) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Creates a port forwarding between a local and a remote port. + * + * @param localPort the local port to forward + * @param remotePort the remote port. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + void createForward(int localPort, int remotePort) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Creates a port forwarding between a local TCP port and a remote Unix Domain Socket. + * + * @param localPort the local port to forward + * @param remoteSocketName name of the unix domain socket created on the device + * @param namespace namespace in which the unix domain socket was created + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + void createForward(int localPort, String remoteSocketName, + DeviceUnixSocketNamespace namespace) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Removes a port forwarding between a local and a remote port. + * + * @param localPort the local port to forward + * @param remotePort the remote port. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + void removeForward(int localPort, int remotePort) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Removes an existing port forwarding between a local and a remote port. + * + * @param localPort the local port to forward + * @param remoteSocketName the remote unix domain socket name. + * @param namespace namespace in which the unix domain socket was created + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + void removeForward(int localPort, String remoteSocketName, + DeviceUnixSocketNamespace namespace) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Returns the name of the client by pid or null if pid is unknown + * @param pid the pid of the client. + */ + String getClientName(int pid); + + /** + * Push a single file. + * @param local the local filepath. + * @param remote The remote filepath. + * + * @throws IOException in case of I/O error on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws TimeoutException in case of a timeout reading responses from the device. + * @throws SyncException if file could not be pushed + */ + void pushFile(String local, String remote) + throws IOException, AdbCommandRejectedException, TimeoutException, SyncException; + + /** + * Pulls a single file. + * + * @param remote the full path to the remote file + * @param local The local destination. + * + * @throws IOException in case of an IO exception. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws TimeoutException in case of a timeout reading responses from the device. + * @throws SyncException in case of a sync exception. + */ + void pullFile(String remote, String local) + throws IOException, AdbCommandRejectedException, TimeoutException, SyncException; + + /** + * Installs an Android application on device. This is a helper method that combines the + * syncPackageToDevice, installRemotePackage, and removePackage steps + * + * @param packageFilePath the absolute file system path to file on local host to install + * @param reinstall set to true if re-install of app should be performed + * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for + * available options. + * @return a {@link String} with an error code, or null if success. + * @throws InstallException if the installation fails. + */ + String installPackage(String packageFilePath, boolean reinstall, String... extraArgs) + throws InstallException; + + /** + * Installs an Android application made of serveral APK files (one main and 0..n split packages) + * + * @param apkFilePaths list of absolute file system path to files on local host to install + * @param timeOutInMs + * @param reinstall set to true if re-install of app should be performed + * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for + * available options. + * @throws InstallException if the installation fails. + */ + + void installPackages(List apkFilePaths, int timeOutInMs, + boolean reinstall, String... extraArgs) throws InstallException; + /** + * Pushes a file to device + * + * @param localFilePath the absolute path to file on local host + * @return {@link String} destination path on device for file + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException in case of I/O error on the connection. + * @throws SyncException if an error happens during the push of the package on the device. + */ + String syncPackageToDevice(String localFilePath) + throws TimeoutException, AdbCommandRejectedException, IOException, SyncException; + + /** + * Installs the application package that was pushed to a temporary location on the device. + * + * @param remoteFilePath absolute file path to package file on device + * @param reinstall set to true if re-install of app should be performed + * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for + * available options. + * @throws InstallException if the installation fails. + */ + String installRemotePackage(String remoteFilePath, boolean reinstall, + String... extraArgs) throws InstallException; + + /** + * Removes a file from device. + * + * @param remoteFilePath path on device of file to remove + * @throws InstallException if the installation fails. + */ + void removeRemotePackage(String remoteFilePath) throws InstallException; + + /** + * Uninstalls an package from the device. + * + * @param packageName the Android application package name to uninstall + * @return a {@link String} with an error code, or null if success. + * @throws InstallException if the uninstallation fails. + */ + String uninstallPackage(String packageName) throws InstallException; + + /** + * Reboot the device. + * + * @param into the bootloader name to reboot into, or null to just reboot the device. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException + */ + void reboot(String into) + throws TimeoutException, AdbCommandRejectedException, IOException; + + /** + * Return the device's battery level, from 0 to 100 percent. + *

+ * The battery level may be cached. Only queries the device for its + * battery level if 5 minutes have expired since the last successful query. + * + * @return the battery level or null if it could not be retrieved + * @deprecated use {@link #getBattery()} + */ + @Deprecated + Integer getBatteryLevel() throws TimeoutException, + AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException; + + /** + * Return the device's battery level, from 0 to 100 percent. + *

+ * The battery level may be cached. Only queries the device for its + * battery level if freshnessMs ms have expired since the last successful query. + * + * @param freshnessMs + * @return the battery level or null if it could not be retrieved + * @throws ShellCommandUnresponsiveException + * @deprecated use {@link #getBattery(long, TimeUnit))} + */ + @Deprecated + Integer getBatteryLevel(long freshnessMs) throws TimeoutException, + AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException; + + /** + * Return the device's battery level, from 0 to 100 percent. + *

+ * The battery level may be cached. Only queries the device for its + * battery level if 5 minutes have expired since the last successful query. + * + * @return a {@link Future} that can be used to query the battery level. The Future will return + * a {@link ExecutionException} if battery level could not be retrieved. + */ + @NonNull + Future getBattery(); + + /** + * Return the device's battery level, from 0 to 100 percent. + *

+ * The battery level may be cached. Only queries the device for its + * battery level if freshnessTime has expired since the last successful query. + * + * @param freshnessTime the desired recency of battery level + * @param timeUnit the {@link TimeUnit} of freshnessTime + * @return a {@link Future} that can be used to query the battery level. The Future will return + * a {@link ExecutionException} if battery level could not be retrieved. + */ + @NonNull + Future getBattery(long freshnessTime, @NonNull TimeUnit timeUnit); + + + /** + * Returns the ABIs supported by this device. The ABIs are sorted in preferred order, with the + * first ABI being the most preferred. + * @return the list of ABIs. + */ + @NonNull + List getAbis(); + + /** + * Returns the density bucket of the device screen by reading the value for system property + * {@link #PROP_DEVICE_DENSITY}. + * + * @return the density, or -1 if it cannot be determined. + */ + int getDensity(); + + /** + * Returns the user's language. + * + * @return the user's language, or null if it's unknown + */ + String getLanguage(); + + /** + * Returns the user's region. + * + * @return the user's region, or null if it's unknown + */ + String getRegion(); +} \ No newline at end of file diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellEnabledDevice.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellEnabledDevice.java new file mode 100644 index 0000000..6d41c76 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellEnabledDevice.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; + +import java.io.IOException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * An abstract device that can receive shell commands. + */ +public interface IShellEnabledDevice { + + /** + * Returns a (humanized) name for this device. Typically this is the AVD name for AVD's, and + * a combination of the manufacturer name, model name & serial number for devices. + */ + String getName(); + + /** + * Executes a shell command on the device, and sends the result to a receiver. + *

maxTimeToOutputResponse is used as a maximum waiting time when expecting the + * command output from the device.
+ * At any time, if the shell command does not output anything for a period longer than + * maxTimeToOutputResponse, then the method will throw + * {@link ShellCommandUnresponsiveException}. + *

For commands like log output, a maxTimeToOutputResponse value of 0, meaning + * that the method will never throw and will block until the receiver's + * {@link IShellOutputReceiver#isCancelled()} returns true, should be + * used. + * + * @param command the shell command to execute + * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell + * command + * @param maxTimeToOutputResponse the maximum amount of time during which the command is allowed + * to not output any response. A value of 0 means the method will wait forever + * (until the receiver cancels the execution) for command output and + * never throw. + * @param maxTimeUnits Units for non-zero {@code maxTimeToOutputResponse} values. + * @throws TimeoutException in case of timeout on the connection when sending the command. + * @throws AdbCommandRejectedException if adb rejects the command. + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output + * for a period longer than maxTimeToOutputResponse. + * @throws IOException in case of I/O error on the connection. + * + * @see DdmPreferences#getTimeOut() + */ + void executeShellCommand(String command, IShellOutputReceiver receiver, + long maxTimeToOutputResponse, TimeUnit maxTimeUnits) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; + + /** + * Do a potential asynchronous query for a system property. + * + * @param name the name of the value to return. + * @return a {@link java.util.concurrent.Future} which can be used to retrieve value of property. Future#get() can + * return null if property can not be retrieved. + */ + @NonNull + Future getSystemProperty(@NonNull String name); +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellOutputReceiver.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellOutputReceiver.java new file mode 100644 index 0000000..cd11c86 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IShellOutputReceiver.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Classes which implement this interface provide methods that deal with out from a remote shell + * command on a device/emulator. + */ +public interface IShellOutputReceiver { + /** + * Called every time some new data is available. + * @param data The new data. + * @param offset The offset at which the new data starts. + * @param length The length of the new data. + */ + void addOutput(byte[] data, int offset, int length); + + /** + * Called at the end of the process execution (unless the process was + * canceled). This allows the receiver to terminate and flush whatever + * data was not yet processed. + */ + void flush(); + + /** + * Cancel method to stop the execution of the remote shell command. + * @return true to cancel the execution of the command. + */ + boolean isCancelled(); +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IStackTraceInfo.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IStackTraceInfo.java new file mode 100644 index 0000000..edea98d --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/IStackTraceInfo.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Classes which implement this interface provide a method that returns a stack trace. + */ +public interface IStackTraceInfo { + + /** + * Returns the stack trace. This can be null. + */ + StackTraceElement[] getStackTrace(); + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/InstallException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/InstallException.java new file mode 100644 index 0000000..9cf0468 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/InstallException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Thrown if installation or uninstallation of application fails. + */ +public class InstallException extends CanceledException { + private static final long serialVersionUID = 1L; + + public InstallException(Throwable cause) { + super(cause.getMessage(), cause); + } + + public InstallException(String message) { + super(message); + } + + public InstallException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Returns true if the installation was canceled by user input. This can typically only + * happen in the sync phase. + */ + @Override + public boolean wasCanceled() { + Throwable cause = getCause(); + return cause instanceof SyncException && ((SyncException)cause).wasCanceled(); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/JdwpPacket.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/JdwpPacket.java new file mode 100644 index 0000000..23b0249 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/JdwpPacket.java @@ -0,0 +1,371 @@ +/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; + +/** + * A JDWP packet, sitting at the start of a ByteBuffer somewhere. + * + * This allows us to wrap a "pointer" to the data with the results of + * decoding the packet. + * + * None of the operations here are synchronized. If multiple threads will + * be accessing the same ByteBuffers, external sync will be required. + * + * Use the constructor to create an empty packet, or "findPacket()" to + * wrap a JdwpPacket around existing data. + */ +final class JdwpPacket { + // header len + public static final int JDWP_HEADER_LEN = 11; + + // results from findHandshake + public static final int HANDSHAKE_GOOD = 1; + public static final int HANDSHAKE_NOTYET = 2; + public static final int HANDSHAKE_BAD = 3; + + // our cmdSet/cmd + private static final int DDMS_CMD_SET = 0xc7; // 'G' + 128 + private static final int DDMS_CMD = 0x01; + + // "flags" field + private static final int REPLY_PACKET = 0x80; + + // this is sent and expected at the start of a JDWP connection + private static final byte[] mHandshake = { + 'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e' + }; + + public static final int HANDSHAKE_LEN = mHandshake.length; + + private ByteBuffer mBuffer; + private int mLength, mId, mFlags, mCmdSet, mCmd, mErrCode; + private boolean mIsNew; + + private static int sSerialId = 0x40000000; + + + /** + * Create a new, empty packet, in "buf". + */ + JdwpPacket(ByteBuffer buf) { + mBuffer = buf; + mIsNew = true; + } + + /** + * Finish a packet created with newPacket(). + * + * This always creates a command packet, with the next serial number + * in sequence. + * + * We have to take "payloadLength" as an argument because we can't + * see the position in the "slice" returned by getPayload(). We could + * fish it out of the chunk header, but it's legal for there to be + * more than one chunk in a JDWP packet. + * + * On exit, "position" points to the end of the data. + */ + void finishPacket(int payloadLength) { + assert mIsNew; + + ByteOrder oldOrder = mBuffer.order(); + mBuffer.order(ChunkHandler.CHUNK_ORDER); + + mLength = JDWP_HEADER_LEN + payloadLength; + mId = getNextSerial(); + mFlags = 0; + mCmdSet = DDMS_CMD_SET; + mCmd = DDMS_CMD; + + mBuffer.putInt(0x00, mLength); + mBuffer.putInt(0x04, mId); + mBuffer.put(0x08, (byte) mFlags); + mBuffer.put(0x09, (byte) mCmdSet); + mBuffer.put(0x0a, (byte) mCmd); + + mBuffer.order(oldOrder); + mBuffer.position(mLength); + } + + /** + * Get the next serial number. This creates a unique serial number + * across all connections, not just for the current connection. This + * is a useful property when debugging, but isn't necessary. + * + * We can't synchronize on an int, so we use a sync method. + */ + private static synchronized int getNextSerial() { + return sSerialId++; + } + + /** + * Return a slice of the byte buffer, positioned past the JDWP header + * to the start of the chunk header. The buffer's limit will be set + * to the size of the payload if the size is known; if this is a + * packet under construction the limit will be set to the end of the + * buffer. + * + * Doesn't examine the packet at all -- works on empty buffers. + */ + ByteBuffer getPayload() { + ByteBuffer buf; + int oldPosn = mBuffer.position(); + + mBuffer.position(JDWP_HEADER_LEN); + buf = mBuffer.slice(); // goes from position to limit + mBuffer.position(oldPosn); + + if (mLength > 0) + buf.limit(mLength - JDWP_HEADER_LEN); + else + assert mIsNew; + buf.order(ChunkHandler.CHUNK_ORDER); + return buf; + } + + /** + * Returns "true" if this JDWP packet has a JDWP command type. + * + * This never returns "true" for reply packets. + */ + boolean isDdmPacket() { + return (mFlags & REPLY_PACKET) == 0 && + mCmdSet == DDMS_CMD_SET && + mCmd == DDMS_CMD; + } + + /** + * Returns "true" if this JDWP packet is tagged as a reply. + */ + boolean isReply() { + return (mFlags & REPLY_PACKET) != 0; + } + + /** + * Returns "true" if this JDWP packet is a reply with a nonzero + * error code. + */ + boolean isError() { + return isReply() && mErrCode != 0; + } + + /** + * Returns "true" if this JDWP packet has no data. + */ + boolean isEmpty() { + return (mLength == JDWP_HEADER_LEN); + } + + /** + * Return the packet's ID. For a reply packet, this allows us to + * match the reply with the original request. + */ + int getId() { + return mId; + } + + /** + * Return the length of a packet. This includes the header, so an + * empty packet is 11 bytes long. + */ + int getLength() { + return mLength; + } + + /** + * Write our packet to "chan". Consumes the packet as part of the + * write. + * + * The JDWP packet starts at offset 0 and ends at mBuffer.position(). + */ + void writeAndConsume(SocketChannel chan) throws IOException { + int oldLimit; + + //Log.i("ddms", "writeAndConsume: pos=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + + assert mLength > 0; + + mBuffer.flip(); // limit<-posn, posn<-0 + oldLimit = mBuffer.limit(); + mBuffer.limit(mLength); + while (mBuffer.position() != mBuffer.limit()) { + chan.write(mBuffer); + } + // position should now be at end of packet + assert mBuffer.position() == mLength; + + mBuffer.limit(oldLimit); + mBuffer.compact(); // shift posn...limit, posn<-pending data + + //Log.i("ddms", " : pos=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + } + + /** + * "Move" the packet data out of the buffer we're sitting on and into + * buf at the current position. + */ + void movePacket(ByteBuffer buf) { + Log.v("ddms", "moving " + mLength + " bytes"); + int oldPosn = mBuffer.position(); + + mBuffer.position(0); + mBuffer.limit(mLength); + buf.put(mBuffer); + mBuffer.position(mLength); + mBuffer.limit(oldPosn); + mBuffer.compact(); // shift posn...limit, posn<-pending data + } + + /** + * Consume the JDWP packet. + * + * On entry and exit, "position" is the #of bytes in the buffer. + */ + void consume() + { + //Log.d("ddms", "consuming " + mLength + " bytes"); + //Log.d("ddms", " posn=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + + /* + * The "flip" call sets "limit" equal to the position (usually the + * end of data) and "position" equal to zero. + * + * compact() copies everything from "position" and "limit" to the + * start of the buffer, sets "position" to the end of data, and + * sets "limit" to the capacity. + * + * On entry, "position" is set to the amount of data in the buffer + * and "limit" is set to the capacity. We want to call flip() + * so that position..limit spans our data, advance "position" past + * the current packet, then compact. + */ + mBuffer.flip(); // limit<-posn, posn<-0 + mBuffer.position(mLength); + mBuffer.compact(); // shift posn...limit, posn<-pending data + mLength = 0; + //Log.d("ddms", " after compact, posn=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + } + + /** + * Find the JDWP packet at the start of "buf". The start is known, + * but the length has to be parsed out. + * + * On entry, the packet data in "buf" must start at offset 0 and end + * at "position". "limit" should be set to the buffer capacity. This + * method does not alter "buf"s attributes. + * + * Returns a new JdwpPacket if a full one is found in the buffer. If + * not, returns null. Throws an exception if the data doesn't look like + * a valid JDWP packet. + */ + static JdwpPacket findPacket(ByteBuffer buf) { + int count = buf.position(); + int length, id, flags, cmdSet, cmd; + + if (count < JDWP_HEADER_LEN) + return null; + + ByteOrder oldOrder = buf.order(); + buf.order(ChunkHandler.CHUNK_ORDER); + + length = buf.getInt(0x00); + id = buf.getInt(0x04); + flags = buf.get(0x08) & 0xff; + cmdSet = buf.get(0x09) & 0xff; + cmd = buf.get(0x0a) & 0xff; + + buf.order(oldOrder); + + if (length < JDWP_HEADER_LEN) + throw new BadPacketException(); + if (count < length) + return null; + + JdwpPacket pkt = new JdwpPacket(buf); + //pkt.mBuffer = buf; + pkt.mLength = length; + pkt.mId = id; + pkt.mFlags = flags; + + if ((flags & REPLY_PACKET) == 0) { + pkt.mCmdSet = cmdSet; + pkt.mCmd = cmd; + pkt.mErrCode = -1; + } else { + pkt.mCmdSet = -1; + pkt.mCmd = -1; + pkt.mErrCode = cmdSet | (cmd << 8); + } + + return pkt; + } + + /** + * Like findPacket(), but when we're expecting the JDWP handshake. + * + * Returns one of: + * HANDSHAKE_GOOD - found handshake, looks good + * HANDSHAKE_BAD - found enough data, but it's wrong + * HANDSHAKE_NOTYET - not enough data has been read yet + */ + static int findHandshake(ByteBuffer buf) { + int count = buf.position(); + int i; + + if (count < mHandshake.length) + return HANDSHAKE_NOTYET; + + for (i = mHandshake.length -1; i >= 0; --i) { + if (buf.get(i) != mHandshake[i]) + return HANDSHAKE_BAD; + } + + return HANDSHAKE_GOOD; + } + + /** + * Remove the handshake string from the buffer. + * + * On entry and exit, "position" is the #of bytes in the buffer. + */ + static void consumeHandshake(ByteBuffer buf) { + // in theory, nothing else can have arrived, so this is overkill + buf.flip(); // limit<-posn, posn<-0 + buf.position(mHandshake.length); + buf.compact(); // shift posn...limit, posn<-pending data + } + + /** + * Copy the handshake string into the output buffer. + * + * On exit, "buf"s position will be advanced. + */ + static void putHandshake(ByteBuffer buf) { + buf.put(mHandshake); + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Log.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Log.java new file mode 100644 index 0000000..cc7328f --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/Log.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Log class that mirrors the API in main Android sources. + *

Default behavior outputs the log to {@link System#out}. Use + * {@link #setLogOutput(com.android.ddmlib.Log.ILogOutput)} to redirect the log somewhere else. + */ +public final class Log { + + /** + * Log Level enum. + */ + public enum LogLevel { + VERBOSE(2, "verbose", 'V'), //$NON-NLS-1$ + DEBUG(3, "debug", 'D'), //$NON-NLS-1$ + INFO(4, "info", 'I'), //$NON-NLS-1$ + WARN(5, "warn", 'W'), //$NON-NLS-1$ + ERROR(6, "error", 'E'), //$NON-NLS-1$ + ASSERT(7, "assert", 'A'); //$NON-NLS-1$ + + private int mPriorityLevel; + private String mStringValue; + private char mPriorityLetter; + + LogLevel(int intPriority, String stringValue, char priorityChar) { + mPriorityLevel = intPriority; + mStringValue = stringValue; + mPriorityLetter = priorityChar; + } + + public static LogLevel getByString(String value) { + for (LogLevel mode : values()) { + if (mode.mStringValue.equals(value)) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + * @param letter the letter matching a LogLevel enum + * @return a LogLevel object or null if no match were found. + */ + public static LogLevel getByLetter(char letter) { + for (LogLevel mode : values()) { + if (mode.mPriorityLetter == letter) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + *

+ * The letter is passed as a {@link String} argument, but only the first character + * is used. + * @param letter the letter matching a LogLevel enum + * @return a LogLevel object or null if no match were found. + */ + public static LogLevel getByLetterString(String letter) { + if (!letter.isEmpty()) { + return getByLetter(letter.charAt(0)); + } + + return null; + } + + /** + * Returns the letter identifying the priority of the {@link LogLevel}. + */ + public char getPriorityLetter() { + return mPriorityLetter; + } + + /** + * Returns the numerical value of the priority. + */ + public int getPriority() { + return mPriorityLevel; + } + + /** + * Returns a non translated string representing the LogLevel. + */ + public String getStringValue() { + return mStringValue; + } + } + + /** + * Classes which implement this interface provides methods that deal with outputting log + * messages. + */ + public interface ILogOutput { + /** + * Sent when a log message needs to be printed. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + void printLog(LogLevel logLevel, String tag, String message); + + /** + * Sent when a log message needs to be printed, and, if possible, displayed to the user + * in a dialog box. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + void printAndPromptLog(LogLevel logLevel, String tag, String message); + } + + private static LogLevel sLevel = DdmPreferences.getLogLevel(); + + private static ILogOutput sLogOutput; + + private static final char[] mSpaceLine = new char[72]; + private static final char[] mHexDigit = new char[] + { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; + static { + /* prep for hex dump */ + int i = mSpaceLine.length-1; + while (i >= 0) + mSpaceLine[i--] = ' '; + mSpaceLine[0] = mSpaceLine[1] = mSpaceLine[2] = mSpaceLine[3] = '0'; + mSpaceLine[4] = '-'; + } + + static final class Config { + static final boolean LOGV = true; + static final boolean LOGD = true; + } + + private Log() {} + + /** + * Outputs a {@link LogLevel#VERBOSE} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void v(String tag, String message) { + println(LogLevel.VERBOSE, tag, message); + } + + /** + * Outputs a {@link LogLevel#DEBUG} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void d(String tag, String message) { + println(LogLevel.DEBUG, tag, message); + } + + /** + * Outputs a {@link LogLevel#INFO} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void i(String tag, String message) { + println(LogLevel.INFO, tag, message); + } + + /** + * Outputs a {@link LogLevel#WARN} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void w(String tag, String message) { + println(LogLevel.WARN, tag, message); + } + + /** + * Outputs a {@link LogLevel#ERROR} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void e(String tag, String message) { + println(LogLevel.ERROR, tag, message); + } + + /** + * Outputs a log message and attempts to display it in a dialog. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void logAndDisplay(LogLevel logLevel, String tag, String message) { + if (sLogOutput != null) { + sLogOutput.printAndPromptLog(logLevel, tag, message); + } else { + println(logLevel, tag, message); + } + } + + /** + * Outputs a {@link LogLevel#ERROR} level {@link Throwable} information. + * @param tag The tag associated with the message. + * @param throwable The {@link Throwable} to output. + */ + public static void e(String tag, Throwable throwable) { + if (throwable != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + throwable.printStackTrace(pw); + println(LogLevel.ERROR, tag, throwable.getMessage() + '\n' + sw.toString()); + } + } + + static void setLevel(LogLevel logLevel) { + sLevel = logLevel; + } + + /** + * Sets the {@link ILogOutput} to use to print the logs. If not set, {@link System#out} + * will be used. + * @param logOutput The {@link ILogOutput} to use to print the log. + */ + public static void setLogOutput(ILogOutput logOutput) { + sLogOutput = logOutput; + } + + /** + * Show hex dump. + *

+ * Local addition. Output looks like: + * 1230- 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 0123456789abcdef + *

+ * Uses no string concatenation; creates one String object per line. + */ + static void hexDump(String tag, LogLevel level, byte[] data, int offset, int length) { + + int kHexOffset = 6; + int kAscOffset = 55; + char[] line = new char[mSpaceLine.length]; + int addr, baseAddr, count; + int i, ch; + boolean needErase = true; + + //Log.w(tag, "HEX DUMP: off=" + offset + ", length=" + length); + + baseAddr = 0; + while (length != 0) { + if (length > 16) { + // full line + count = 16; + } else { + // partial line; re-copy blanks to clear end + count = length; + needErase = true; + } + + if (needErase) { + System.arraycopy(mSpaceLine, 0, line, 0, mSpaceLine.length); + needErase = false; + } + + // output the address (currently limited to 4 hex digits) + addr = baseAddr; + addr &= 0xffff; + ch = 3; + while (addr != 0) { + line[ch] = mHexDigit[addr & 0x0f]; + ch--; + addr >>>= 4; + } + + // output hex digits and ASCII chars + ch = kHexOffset; + for (i = 0; i < count; i++) { + byte val = data[offset + i]; + + line[ch++] = mHexDigit[(val >>> 4) & 0x0f]; + line[ch++] = mHexDigit[val & 0x0f]; + ch++; + + if (val >= 0x20 && val < 0x7f) + line[kAscOffset + i] = (char) val; + else + line[kAscOffset + i] = '.'; + } + + println(level, tag, new String(line)); + + // advance to next chunk of data + length -= count; + offset += count; + baseAddr += count; + } + + } + + /** + * Dump the entire contents of a byte array with DEBUG priority. + */ + static void hexDump(byte[] data) { + hexDump("ddms", LogLevel.DEBUG, data, 0, data.length); + } + + /* currently prints to stdout; could write to a log window */ + private static void println(LogLevel logLevel, String tag, String message) { + if (logLevel.getPriority() >= sLevel.getPriority()) { + if (sLogOutput != null) { + sLogOutput.printLog(logLevel, tag, message); + } else { + printLog(logLevel, tag, message); + } + } + } + + /** + * Prints a log message. + * @param logLevel + * @param tag + * @param message + */ + public static void printLog(LogLevel logLevel, String tag, String message) { + System.out.print(getLogFormatString(logLevel, tag, message)); + } + + /** + * Formats a log message. + * @param logLevel + * @param tag + * @param message + */ + public static String getLogFormatString(LogLevel logLevel, String tag, String message) { + SimpleDateFormat formatter = new SimpleDateFormat("hh:mm:ss", Locale.getDefault()); + return String.format("%s %c/%s: %s\n", formatter.format(new Date()), + logLevel.getPriorityLetter(), tag, message); + } +} + + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MonitorThread.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MonitorThread.java new file mode 100644 index 0000000..a4ff115 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MonitorThread.java @@ -0,0 +1,790 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + + +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.Log.LogLevel; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.NotYetBoundException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Monitor open connections. + */ +final class MonitorThread extends Thread { + + // For broadcasts to message handlers + //private static final int CLIENT_CONNECTED = 1; + + private static final int CLIENT_READY = 2; + + private static final int CLIENT_DISCONNECTED = 3; + + private volatile boolean mQuit = false; + + // List of clients we're paying attention to + private ArrayList mClientList; + + // The almighty mux + private Selector mSelector; + + // Map chunk types to handlers + private HashMap mHandlerMap; + + // port for "debug selected" + private ServerSocketChannel mDebugSelectedChan; + + private int mNewDebugSelectedPort; + + private int mDebugSelectedPort = -1; + + /** + * "Selected" client setup to answer debugging connection to the mNewDebugSelectedPort port. + */ + private Client mSelectedClient = null; + + // singleton + private static MonitorThread sInstance; + + /** + * Generic constructor. + */ + private MonitorThread() { + super("Monitor"); + mClientList = new ArrayList(); + mHandlerMap = new HashMap(); + + mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort(); + } + + /** + * Creates and return the singleton instance of the client monitor thread. + */ + static MonitorThread createInstance() { + return sInstance = new MonitorThread(); + } + + /** + * Get singleton instance of the client monitor thread. + */ + static MonitorThread getInstance() { + return sInstance; + } + + + /** + * Sets or changes the port number for "debug selected". + */ + synchronized void setDebugSelectedPort(int port) throws IllegalStateException { + if (sInstance == null) { + return; + } + + if (!AndroidDebugBridge.getClientSupport()) { + return; + } + + if (mDebugSelectedChan != null) { + Log.d("ddms", "Changing debug-selected port to " + port); + mNewDebugSelectedPort = port; + wakeup(); + } else { + // we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically + // opened on the first run loop. + mNewDebugSelectedPort = port; + } + } + + /** + * Sets the client to accept debugger connection on the custom "Selected debug port". + * @param selectedClient the client. Can be null. + */ + synchronized void setSelectedClient(Client selectedClient) { + if (sInstance == null) { + return; + } + + if (mSelectedClient != selectedClient) { + Client oldClient = mSelectedClient; + mSelectedClient = selectedClient; + + if (oldClient != null) { + oldClient.update(Client.CHANGE_PORT); + } + + if (mSelectedClient != null) { + mSelectedClient.update(Client.CHANGE_PORT); + } + } + } + + /** + * Returns the client accepting debugger connection on the custom "Selected debug port". + */ + Client getSelectedClient() { + return mSelectedClient; + } + + + /** + * Returns "true" if we want to retry connections to clients if we get a bad + * JDWP handshake back, "false" if we want to just mark them as bad and + * leave them alone. + */ + boolean getRetryOnBadHandshake() { + return true; // TODO? make configurable + } + + /** + * Get an array of known clients. + */ + Client[] getClients() { + synchronized (mClientList) { + return mClientList.toArray(new Client[mClientList.size()]); + } + } + + /** + * Register "handler" as the handler for type "type". + */ + synchronized void registerChunkHandler(int type, ChunkHandler handler) { + if (sInstance == null) { + return; + } + + synchronized (mHandlerMap) { + if (mHandlerMap.get(type) == null) { + mHandlerMap.put(type, handler); + } + } + } + + /** + * Watch for activity from clients and debuggers. + */ + @Override + public void run() { + Log.d("ddms", "Monitor is up"); + + // create a selector + try { + mSelector = Selector.open(); + } catch (IOException ioe) { + Log.logAndDisplay(LogLevel.ERROR, "ddms", + "Failed to initialize Monitor Thread: " + ioe.getMessage()); + return; + } + + while (!mQuit) { + + try { + /* + * sync with new registrations: we wait until addClient is done before going through + * and doing mSelector.select() again. + * @see {@link #addClient(Client)} + */ + synchronized (mClientList) { + } + + // (re-)open the "debug selected" port, if it's not opened yet or + // if the port changed. + try { + if (AndroidDebugBridge.getClientSupport()) { + if ((mDebugSelectedChan == null || + mNewDebugSelectedPort != mDebugSelectedPort) && + mNewDebugSelectedPort != -1) { + if (reopenDebugSelectedPort()) { + mDebugSelectedPort = mNewDebugSelectedPort; + } + } + } + } catch (IOException ioe) { + Log.e("ddms", + "Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort); + Log.e("ddms", ioe); + mNewDebugSelectedPort = mDebugSelectedPort; // no retry + } + + int count; + try { + count = mSelector.select(); + } catch (IOException ioe) { + ioe.printStackTrace(); + continue; + } catch (CancelledKeyException cke) { + continue; + } + + if (count == 0) { + // somebody called wakeup() ? + // Log.i("ddms", "selector looping"); + continue; + } + + Set keys = mSelector.selectedKeys(); + Iterator iter = keys.iterator(); + + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + + try { + if (key.attachment() instanceof Client) { + processClientActivity(key); + } + else if (key.attachment() instanceof Debugger) { + processDebuggerActivity(key); + } + else if (key.attachment() instanceof MonitorThread) { + processDebugSelectedActivity(key); + } + else { + Log.e("ddms", "unknown activity key"); + } + } catch (Exception e) { + // we don't want to have our thread be killed because of any uncaught + // exception, so we intercept all here. + Log.e("ddms", "Exception during activity from Selector."); + Log.e("ddms", e); + } + } + } catch (Exception e) { + // we don't want to have our thread be killed because of any uncaught + // exception, so we intercept all here. + Log.e("ddms", "Exception MonitorThread.run()"); + Log.e("ddms", e); + } + } + } + + + /** + * Returns the port on which the selected client listen for debugger + */ + int getDebugSelectedPort() { + return mDebugSelectedPort; + } + + /* + * Something happened. Figure out what. + */ + private void processClientActivity(SelectionKey key) { + Client client = (Client)key.attachment(); + + try { + if (!key.isReadable() || !key.isValid()) { + Log.d("ddms", "Invalid key from " + client + ". Dropping client."); + dropClient(client, true /* notify */); + return; + } + + client.read(); + + /* + * See if we have a full packet in the buffer. It's possible we have + * more than one packet, so we have to loop. + */ + JdwpPacket packet = client.getJdwpPacket(); + while (packet != null) { + if (packet.isDdmPacket()) { + // unsolicited DDM request - hand it off + assert !packet.isReply(); + callHandler(client, packet, null); + packet.consume(); + } else if (packet.isReply() + && client.isResponseToUs(packet.getId()) != null) { + // reply to earlier DDM request + ChunkHandler handler = client + .isResponseToUs(packet.getId()); + if (packet.isError()) + client.packetFailed(packet); + else if (packet.isEmpty()) + Log.d("ddms", "Got empty reply for 0x" + + Integer.toHexString(packet.getId()) + + " from " + client); + else + callHandler(client, packet, handler); + packet.consume(); + client.removeRequestId(packet.getId()); + } else { + Log.v("ddms", "Forwarding client " + + (packet.isReply() ? "reply" : "event") + " 0x" + + Integer.toHexString(packet.getId()) + " to " + + client.getDebugger()); + client.forwardPacketToDebugger(packet); + } + + // find next + packet = client.getJdwpPacket(); + } + } catch (CancelledKeyException e) { + // key was canceled probably due to a disconnected client before we could + // read stuff coming from the client, so we drop it. + dropClient(client, true /* notify */); + } catch (IOException ex) { + // something closed down, no need to print anything. The client is simply dropped. + dropClient(client, true /* notify */); + } catch (Exception ex) { + Log.e("ddms", ex); + + /* close the client; automatically un-registers from selector */ + dropClient(client, true /* notify */); + + if (ex instanceof BufferOverflowException) { + Log.w("ddms", + "Client data packet exceeded maximum buffer size " + + client); + } else { + // don't know what this is, display it + Log.e("ddms", ex); + } + } + } + + /* + * Process an incoming DDM packet. If this is a reply to an earlier request, + * "handler" will be set to the handler responsible for the original + * request. The spec allows a JDWP message to include multiple DDM chunks. + */ + private void callHandler(Client client, JdwpPacket packet, + ChunkHandler handler) { + + // on first DDM packet received, broadcast a "ready" message + if (!client.ddmSeen()) + broadcast(CLIENT_READY, client); + + ByteBuffer buf = packet.getPayload(); + int type, length; + boolean reply = true; + + type = buf.getInt(); + length = buf.getInt(); + + if (handler == null) { + // not a reply, figure out who wants it + synchronized (mHandlerMap) { + handler = mHandlerMap.get(type); + reply = false; + } + } + + if (handler == null) { + Log.w("ddms", "Received unsupported chunk type " + + ChunkHandler.name(type) + " (len=" + length + ")"); + } else { + Log.d("ddms", "Calling handler for " + ChunkHandler.name(type) + + " [" + handler + "] (len=" + length + ")"); + ByteBuffer ibuf = buf.slice(); + ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O + roBuf.order(ChunkHandler.CHUNK_ORDER); + // do the handling of the chunk synchronized on the client list + // to be sure there's no concurrency issue when we look for HOME + // in hasApp() + synchronized (mClientList) { + handler.handleChunk(client, type, roBuf, reply, packet.getId()); + } + } + } + + /** + * Drops a client from the monitor. + *

This will lock the {@link Client} list of the {@link Device} running client. + * @param client + * @param notify + */ + synchronized void dropClient(Client client, boolean notify) { + if (sInstance == null) { + return; + } + + synchronized (mClientList) { + if (!mClientList.remove(client)) { + return; + } + } + client.close(notify); + broadcast(CLIENT_DISCONNECTED, client); + + /* + * http://forum.java.sun.com/thread.jspa?threadID=726715&start=0 + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504 + */ + wakeup(); + } + + /** + * Drops the provided list of clients from the monitor. This will lock the {@link Client} + * list of the {@link Device} running each of the clients. + */ + synchronized void dropClients(Collection clients, boolean notify) { + for (Client c : clients) { + dropClient(c, notify); + } + } + + /* + * Process activity from one of the debugger sockets. This could be a new + * connection or a data packet. + */ + private void processDebuggerActivity(SelectionKey key) { + Debugger dbg = (Debugger)key.attachment(); + + try { + if (key.isAcceptable()) { + try { + acceptNewDebugger(dbg, null); + } catch (IOException ioe) { + Log.w("ddms", "debugger accept() failed"); + ioe.printStackTrace(); + } + } else if (key.isReadable()) { + processDebuggerData(key); + } else { + Log.d("ddm-debugger", "key in unknown state"); + } + } catch (CancelledKeyException cke) { + // key has been cancelled we can ignore that. + } + } + + /* + * Accept a new connection from a debugger. If successful, register it with + * the Selector. + */ + private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan) + throws IOException { + + synchronized (mClientList) { + SocketChannel chan; + + if (acceptChan == null) + chan = dbg.accept(); + else + chan = dbg.accept(acceptChan); + + if (chan != null) { + chan.socket().setTcpNoDelay(true); + + wakeup(); + + try { + chan.register(mSelector, SelectionKey.OP_READ, dbg); + } catch (IOException ioe) { + // failed, drop the connection + dbg.closeData(); + throw ioe; + } catch (RuntimeException re) { + // failed, drop the connection + dbg.closeData(); + throw re; + } + } else { + Log.w("ddms", "ignoring duplicate debugger"); + // new connection already closed + } + } + } + + /* + * We have incoming data from the debugger. Forward it to the client. + */ + private void processDebuggerData(SelectionKey key) { + Debugger dbg = (Debugger)key.attachment(); + + try { + /* + * Read pending data. + */ + dbg.read(); + + /* + * See if we have a full packet in the buffer. It's possible we have + * more than one packet, so we have to loop. + */ + JdwpPacket packet = dbg.getJdwpPacket(); + while (packet != null) { + Log.v("ddms", "Forwarding dbg req 0x" + + Integer.toHexString(packet.getId()) + " to " + + dbg.getClient()); + + dbg.forwardPacketToClient(packet); + + packet = dbg.getJdwpPacket(); + } + } catch (IOException ioe) { + /* + * Close data connection; automatically un-registers dbg from + * selector. The failure could be caused by the debugger going away, + * or by the client going away and failing to accept our data. + * Either way, the debugger connection does not need to exist any + * longer. We also need to recycle the connection to the client, so + * that the VM sees the debugger disconnect. For a DDM-aware client + * this won't be necessary, and we can just send a "debugger + * disconnected" message. + */ + Log.d("ddms", "Closing connection to debugger " + dbg); + dbg.closeData(); + Client client = dbg.getClient(); + if (client.isDdmAware()) { + // TODO: soft-disconnect DDM-aware clients + Log.d("ddms", " (recycling client connection as well)"); + + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, + IDebugPortProvider.NO_STATIC_PORT); + } else { + Log.d("ddms", " (recycling client connection as well)"); + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, + IDebugPortProvider.NO_STATIC_PORT); + } + } + } + + /* + * Tell the thread that something has changed. + */ + private void wakeup() { + mSelector.wakeup(); + } + + /** + * Tell the thread to stop. Called from UI thread. + */ + synchronized void quit() { + mQuit = true; + wakeup(); + Log.d("ddms", "Waiting for Monitor thread"); + try { + this.join(); + // since we're quitting, lets drop all the client and disconnect + // the DebugSelectedPort + synchronized (mClientList) { + for (Client c : mClientList) { + c.close(false /* notify */); + broadcast(CLIENT_DISCONNECTED, c); + } + mClientList.clear(); + } + + if (mDebugSelectedChan != null) { + mDebugSelectedChan.close(); + mDebugSelectedChan.socket().close(); + mDebugSelectedChan = null; + } + mSelector.close(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + sInstance = null; + } + + /** + * Add a new Client to the list of things we monitor. Also adds the client's + * channel and the client's debugger listener to the selection list. This + * should only be called from one thread (the VMWatcherThread) to avoid a + * race between "alreadyOpen" and Client creation. + */ + synchronized void addClient(Client client) { + if (sInstance == null) { + return; + } + + Log.d("ddms", "Adding new client " + client); + + synchronized (mClientList) { + mClientList.add(client); + + /* + * Register the Client's socket channel with the selector. We attach + * the Client to the SelectionKey. If you try to register a new + * channel with the Selector while it is waiting for I/O, you will + * block. The solution is to call wakeup() and then hold a lock to + * ensure that the registration happens before the Selector goes + * back to sleep. + */ + try { + wakeup(); + + client.register(mSelector); + + Debugger dbg = client.getDebugger(); + if (dbg != null) { + dbg.registerListener(mSelector); + } + } catch (IOException ioe) { + // not really expecting this to happen + ioe.printStackTrace(); + } + } + } + + /* + * Broadcast an event to all message handlers. + */ + private void broadcast(int event, Client client) { + Log.d("ddms", "broadcast " + event + ": " + client); + + /* + * The handler objects appear once in mHandlerMap for each message they + * handle. We want to notify them once each, so we convert the HashMap + * to a HashSet before we iterate. + */ + HashSet set; + synchronized (mHandlerMap) { + Collection values = mHandlerMap.values(); + set = new HashSet(values); + } + + Iterator iter = set.iterator(); + while (iter.hasNext()) { + ChunkHandler handler = iter.next(); + switch (event) { + case CLIENT_READY: + try { + handler.clientReady(client); + } catch (IOException ioe) { + // Something failed with the client. It should + // fall out of the list the next time we try to + // do something with it, so we discard the + // exception here and assume cleanup will happen + // later. May need to propagate farther. The + // trouble is that not all values for "event" may + // actually throw an exception. + Log.w("ddms", + "Got exception while broadcasting 'ready'"); + return; + } + break; + case CLIENT_DISCONNECTED: + handler.clientDisconnected(client); + break; + default: + throw new UnsupportedOperationException(); + } + } + + } + + /** + * Opens (or reopens) the "debug selected" port and listen for connections. + * @return true if the port was opened successfully. + * @throws IOException + */ + private boolean reopenDebugSelectedPort() throws IOException { + + Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort); + if (mDebugSelectedChan != null) { + mDebugSelectedChan.close(); + } + + mDebugSelectedChan = ServerSocketChannel.open(); + mDebugSelectedChan.configureBlocking(false); // required for Selector + + InetSocketAddress addr = new InetSocketAddress( + InetAddress.getByName("localhost"), //$NON-NLS-1$ + mNewDebugSelectedPort); + mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR + + try { + mDebugSelectedChan.socket().bind(addr); + if (mSelectedClient != null) { + mSelectedClient.update(Client.CHANGE_PORT); + } + + mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this); + + return true; + } catch (java.net.BindException e) { + displayDebugSelectedBindError(mNewDebugSelectedPort); + + // do not attempt to reopen it. + mDebugSelectedChan = null; + mNewDebugSelectedPort = -1; + + return false; + } + } + + /* + * We have some activity on the "debug selected" port. Handle it. + */ + private void processDebugSelectedActivity(SelectionKey key) { + assert key.isAcceptable(); + + ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel(); + + /* + * Find the debugger associated with the currently-selected client. + */ + if (mSelectedClient != null) { + Debugger dbg = mSelectedClient.getDebugger(); + + if (dbg != null) { + Log.d("ddms", "Accepting connection on 'debug selected' port"); + try { + acceptNewDebugger(dbg, acceptChan); + } catch (IOException ioe) { + // client should be gone, keep going + } + + return; + } + } + + Log.w("ddms", + "Connection on 'debug selected' port, but none selected"); + try { + SocketChannel chan = acceptChan.accept(); + chan.close(); + } catch (IOException ioe) { + // not expected; client should be gone, keep going + } catch (NotYetBoundException e) { + displayDebugSelectedBindError(mDebugSelectedPort); + } + } + + private void displayDebugSelectedBindError(int port) { + String message = String.format( + "Could not open Selected VM debug port (%1$d). Make sure you do not have another instance of DDMS or of the eclipse plugin running. If it's being used by something else, choose a new port number in the preferences.", + port); + + Log.logAndDisplay(LogLevel.ERROR, "ddms", message); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MultiLineReceiver.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MultiLineReceiver.java new file mode 100644 index 0000000..79b3ede --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/MultiLineReceiver.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.google.common.base.Charsets; + +import java.util.ArrayList; + +/** + * Base implementation of {@link IShellOutputReceiver}, that takes the raw data coming from the + * socket, and convert it into {@link String} objects. + *

Additionally, it splits the string by lines. + *

Classes extending it must implement {@link #processNewLines(String[])} which receives + * new parsed lines as they become available. + */ +public abstract class MultiLineReceiver implements IShellOutputReceiver { + + private boolean mTrimLines = true; + + /** unfinished message line, stored for next packet */ + private String mUnfinishedLine = null; + + private final ArrayList mArray = new ArrayList(); + + /** + * Set the trim lines flag. + * @param trim whether the lines are trimmed, or not. + */ + public void setTrimLine(boolean trim) { + mTrimLines = trim; + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput( + * byte[], int, int) + */ + @Override + public final void addOutput(byte[] data, int offset, int length) { + if (!isCancelled()) { + String s = new String(data, offset, length, Charsets.UTF_8); + + // ok we've got a string + // if we had an unfinished line we add it. + if (mUnfinishedLine != null) { + s = mUnfinishedLine + s; + mUnfinishedLine = null; + } + + // now we split the lines + mArray.clear(); + int start = 0; + do { + int index = s.indexOf('\n', start); //$NON-NLS-1$ + + // if \n was not found, this is an unfinished line + // and we store it to be processed for the next packet + if (index == -1) { + mUnfinishedLine = s.substring(start); + break; + } + + // we found a \n, in older devices, this is preceded by a \r + int newlineLength = 1; + if (index > 0 && s.charAt(index - 1) == '\r') { + index--; + newlineLength = 2; + } + + // extract the line + String line = s.substring(start, index); + if (mTrimLines) { + line = line.trim(); + } + mArray.add(line); + + // move start to after the \r\n we found + start = index + newlineLength; + } while (true); + + if (!mArray.isEmpty()) { + // at this point we've split all the lines. + // make the array + String[] lines = mArray.toArray(new String[mArray.size()]); + + // send it for final processing + processNewLines(lines); + } + } + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#flush() + */ + @Override + public final void flush() { + if (mUnfinishedLine != null) { + processNewLines(new String[] { mUnfinishedLine }); + } + + done(); + } + + /** + * Terminates the process. This is called after the last lines have been through + * {@link #processNewLines(String[])}. + */ + public void done() { + // do nothing. + } + + /** + * Called when new lines are being received by the remote process. + *

It is guaranteed that the lines are complete when they are given to this method. + * @param lines The array containing the new lines. + */ + public abstract void processNewLines(String[] lines); +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeAllocationInfo.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeAllocationInfo.java new file mode 100644 index 0000000..65c14ba --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeAllocationInfo.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Stores native allocation information. + *

Contains number of allocations, their size and the stack trace. + *

Note: the ddmlib does not resolve the stack trace automatically. While this class provides + * storage for resolved stack trace, this is merely for convenience. + */ +public class NativeAllocationInfo { + /* Keywords used as delimiters in the string representation of a NativeAllocationInfo */ + public static final String END_STACKTRACE_KW = "EndStacktrace"; + public static final String BEGIN_STACKTRACE_KW = "BeginStacktrace:"; + public static final String TOTAL_SIZE_KW = "TotalSize:"; + public static final String SIZE_KW = "Size:"; + public static final String ALLOCATIONS_KW = "Allocations:"; + + /* constants for flag bits */ + private static final int FLAG_ZYGOTE_CHILD = (1<<31); + private static final int FLAG_MASK = (FLAG_ZYGOTE_CHILD); + + /** Libraries whose methods will be assumed to be not part of the user code. */ + private static final List FILTERED_LIBRARIES = Arrays.asList( + "libc.so", + "libc_malloc_debug_leak.so" + ); + + /** Method names that should be assumed to be not part of the user code. */ + private static final List FILTERED_METHOD_NAME_PATTERNS = Arrays.asList( + Pattern.compile("malloc", Pattern.CASE_INSENSITIVE), + Pattern.compile("calloc", Pattern.CASE_INSENSITIVE), + Pattern.compile("realloc", Pattern.CASE_INSENSITIVE), + Pattern.compile("operator new", Pattern.CASE_INSENSITIVE), + Pattern.compile("memalign", Pattern.CASE_INSENSITIVE) + ); + + private final int mSize; + + private final boolean mIsZygoteChild; + + private int mAllocations; + private final ArrayList mStackCallAddresses = new ArrayList(); + + private ArrayList mResolvedStackCall = null; + + private boolean mIsStackCallResolved = false; + + /** + * Constructs a new {@link NativeAllocationInfo}. + * @param size The size of the allocations. + * @param allocations the allocation count + */ + public NativeAllocationInfo(int size, int allocations) { + this.mSize = size & ~FLAG_MASK; + this.mIsZygoteChild = ((size & FLAG_ZYGOTE_CHILD) != 0); + this.mAllocations = allocations; + } + + /** + * Adds a stack call address for this allocation. + * @param address The address to add. + */ + public void addStackCallAddress(long address) { + mStackCallAddresses.add(address); + } + + /** + * Returns the size of this allocation. + */ + public int getSize() { + return mSize; + } + + /** + * Returns whether the allocation happened in a child of the zygote + * process. + */ + public boolean isZygoteChild() { + return mIsZygoteChild; + } + + /** + * Returns the allocation count. + */ + public int getAllocationCount() { + return mAllocations; + } + + /** + * Returns whether the stack call addresses have been resolved into + * {@link NativeStackCallInfo} objects. + */ + public boolean isStackCallResolved() { + return mIsStackCallResolved; + } + + /** + * Returns the stack call of this allocation as raw addresses. + * @return the list of addresses where the allocation happened. + */ + public List getStackCallAddresses() { + return mStackCallAddresses; + } + + /** + * Sets the resolved stack call for this allocation. + *

+ * If resolvedStackCall is non null then + * {@link #isStackCallResolved()} will return true after this call. + * @param resolvedStackCall The list of {@link NativeStackCallInfo}. + */ + public synchronized void setResolvedStackCall(List resolvedStackCall) { + if (mResolvedStackCall == null) { + mResolvedStackCall = new ArrayList(); + } else { + mResolvedStackCall.clear(); + } + mResolvedStackCall.addAll(resolvedStackCall); + mIsStackCallResolved = !mResolvedStackCall.isEmpty(); + } + + /** + * Returns the resolved stack call. + * @return An array of {@link NativeStackCallInfo} or null if the stack call + * was not resolved. + * @see #setResolvedStackCall(List) + * @see #isStackCallResolved() + */ + public synchronized List getResolvedStackCall() { + if (mIsStackCallResolved) { + return mResolvedStackCall; + } + + return null; + } + + /** + * Indicates whether some other object is "equal to" this one. + * @param obj the reference object with which to compare. + * @return true if this object is equal to the obj argument; + * false otherwise. + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof NativeAllocationInfo) { + NativeAllocationInfo mi = (NativeAllocationInfo)obj; + // compare of size and alloc + if (mSize != mi.mSize || mAllocations != mi.mAllocations) { + return false; + } + + // compare stacks + return stackEquals(mi); + } + return false; + } + + public boolean stackEquals(NativeAllocationInfo mi) { + if (mStackCallAddresses.size() != mi.mStackCallAddresses.size()) { + return false; + } + + int count = mStackCallAddresses.size(); + for (int i = 0 ; i < count ; i++) { + long a = mStackCallAddresses.get(i); + long b = mi.mStackCallAddresses.get(i); + if (a != b) { + return false; + } + } + + return true; + } + + + @Override + public int hashCode() { + // Follow Effective Java's recipe re hash codes. + // Includes all the fields looked at by equals(). + + int result = 17; // arbitrary starting point + + result = 31 * result + mSize; + result = 31 * result + mAllocations; + result = 31 * result + mStackCallAddresses.size(); + + for (long addr : mStackCallAddresses) { + result = 31 * result + (int) (addr ^ (addr >>> 32)); + } + + return result; + } + + /** + * Returns a string representation of the object. + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.append(ALLOCATIONS_KW); + buffer.append(' '); + buffer.append(mAllocations); + buffer.append('\n'); + + buffer.append(SIZE_KW); + buffer.append(' '); + buffer.append(mSize); + buffer.append('\n'); + + buffer.append(TOTAL_SIZE_KW); + buffer.append(' '); + buffer.append(mSize * mAllocations); + buffer.append('\n'); + + if (mResolvedStackCall != null) { + buffer.append(BEGIN_STACKTRACE_KW); + buffer.append('\n'); + for (NativeStackCallInfo source : mResolvedStackCall) { + long addr = source.getAddress(); + if (addr == 0) { + continue; + } + + if (source.getLineNumber() != -1) { + buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d\n", addr, + source.getLibraryName(), source.getMethodName(), + source.getSourceFile(), source.getLineNumber())); + } else { + buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s\n", addr, + source.getLibraryName(), source.getMethodName(), source.getSourceFile())); + } + } + buffer.append(END_STACKTRACE_KW); + buffer.append('\n'); + } + + return buffer.toString(); + } + + /** + * Returns the first {@link NativeStackCallInfo} that is relevant. + *

+ * A relevant NativeStackCallInfo is a stack call that is not deep in the + * lower level of the libc, but the actual method that performed the allocation. + * @return a NativeStackCallInfo or null if the stack call has not + * been processed from the raw addresses. + * @see #setResolvedStackCall(List) + * @see #isStackCallResolved() + */ + public synchronized NativeStackCallInfo getRelevantStackCallInfo() { + if (mIsStackCallResolved && mResolvedStackCall != null) { + for (NativeStackCallInfo info : mResolvedStackCall) { + if (isRelevantLibrary(info.getLibraryName()) + && isRelevantMethod(info.getMethodName())) { + return info; + } + } + + // couldn't find a relevant one, so we'll return the first one if it exists. + if (!mResolvedStackCall.isEmpty()) + return mResolvedStackCall.get(0); + } + + return null; + } + + private boolean isRelevantLibrary(String libPath) { + for (String l : FILTERED_LIBRARIES) { + if (libPath.endsWith(l)) { + return false; + } + } + + return true; + } + + private boolean isRelevantMethod(String methodName) { + for (Pattern p : FILTERED_METHOD_NAME_PATTERNS) { + Matcher m = p.matcher(methodName); + if (m.find()) { + return false; + } + } + + return true; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeLibraryMapInfo.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeLibraryMapInfo.java new file mode 100644 index 0000000..5a26317 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeLibraryMapInfo.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Memory address to library mapping for native libraries. + *

+ * Each instance represents a single native library and its start and end memory addresses. + */ +public final class NativeLibraryMapInfo { + private long mStartAddr; + private long mEndAddr; + + private String mLibrary; + + /** + * Constructs a new native library map info. + * @param startAddr The start address of the library. + * @param endAddr The end address of the library. + * @param library The name of the library. + */ + NativeLibraryMapInfo(long startAddr, long endAddr, String library) { + this.mStartAddr = startAddr; + this.mEndAddr = endAddr; + this.mLibrary = library; + } + + /** + * Returns the name of the library. + */ + public String getLibraryName() { + return mLibrary; + } + + /** + * Returns the start address of the library. + */ + public long getStartAddress() { + return mStartAddr; + } + + /** + * Returns the end address of the library. + */ + public long getEndAddress() { + return mEndAddr; + } + + /** + * Returns whether the specified address is inside the library. + * @param address The address to test. + * @return true if the address is between the start and end address of the library. + * @see #getStartAddress() + * @see #getEndAddress() + */ + public boolean isWithinLibrary(long address) { + return address >= mStartAddr && address <= mEndAddr; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java new file mode 100644 index 0000000..72d7c77 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a stack call. This is used to return all of the call + * information as one object. + */ +public final class NativeStackCallInfo { + private static final Pattern SOURCE_NAME_PATTERN = Pattern.compile("^(.+):(\\d+)(\\s+\\(discriminator\\s+\\d+\\))?$"); + + /** address of this stack frame */ + private long mAddress; + + /** name of the library */ + private String mLibrary; + + /** name of the method */ + private String mMethod; + + /** + * name of the source file + line number in the format
+ * <sourcefile>:<linenumber> + */ + private String mSourceFile; + + private int mLineNumber = -1; + + /** + * Basic constructor with library, method, and sourcefile information + * + * @param address address of this stack frame + * @param lib The name of the library + * @param method the name of the method + * @param sourceFile the name of the source file and the line number + * as "[sourcefile]:[fileNumber]" + */ + public NativeStackCallInfo(long address, String lib, String method, String sourceFile) { + mAddress = address; + mLibrary = lib; + mMethod = method; + + Matcher m = SOURCE_NAME_PATTERN.matcher(sourceFile); + if (m.matches()) { + mSourceFile = m.group(1); + try { + mLineNumber = Integer.parseInt(m.group(2)); + } catch (NumberFormatException e) { + // do nothing, the line number will stay at -1 + } + if (m.groupCount() == 3) { + // A discriminator was found, add that in the source file name. + mSourceFile += m.group(3); + } + } else { + mSourceFile = sourceFile; + } + } + + /** + * Returns the address of this stack frame. + */ + public long getAddress() { + return mAddress; + } + + /** + * Returns the name of the library name. + */ + public String getLibraryName() { + return mLibrary; + } + + /** + * Returns the name of the method. + */ + public String getMethodName() { + return mMethod; + } + + /** + * Returns the name of the source file. + */ + public String getSourceFile() { + return mSourceFile; + } + + /** + * Returns the line number, or -1 if unknown. + */ + public int getLineNumber() { + return mLineNumber; + } + + @Override + public String toString() { + return String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d", + getAddress(), getLibraryName(), getMethodName(), getSourceFile(), getLineNumber()); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NullOutputReceiver.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NullOutputReceiver.java new file mode 100644 index 0000000..a963a64 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/NullOutputReceiver.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Implementation of {@link IShellOutputReceiver} that does nothing. + *

This can be used to execute a remote shell command when the output is not needed. + */ +public final class NullOutputReceiver implements IShellOutputReceiver { + + private static NullOutputReceiver sReceiver = new NullOutputReceiver(); + + public static IShellOutputReceiver getReceiver() { + return sReceiver; + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(byte[], int, int) + */ + @Override + public void addOutput(byte[] data, int offset, int length) { + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#flush() + */ + @Override + public void flush() { + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#isCancelled() + */ + @Override + public boolean isCancelled() { + return false; + } + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/PropertyFetcher.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/PropertyFetcher.java new file mode 100644 index 0000000..dc83115 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/PropertyFetcher.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Fetches and caches 'getprop' values from device. + */ +class PropertyFetcher { + /** the amount of time to wait between unsuccessful prop fetch attempts */ + private static final String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$ + private static final Pattern GETPROP_PATTERN = Pattern.compile("^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"); //$NON-NLS-1$ + private static final int GETPROP_TIMEOUT_SEC = 2; + private static final int EXPECTED_PROP_COUNT = 150; + + private enum CacheState { + UNPOPULATED, FETCHING, POPULATED + } + + /** + * Shell output parser for a getprop command + */ + @VisibleForTesting + static class GetPropReceiver extends MultiLineReceiver { + + private final Map mCollectedProperties = + Maps.newHashMapWithExpectedSize(EXPECTED_PROP_COUNT); + + @Override + public void processNewLines(String[] lines) { + // We receive an array of lines. We're expecting + // to have the build info in the first line, and the build + // date in the 2nd line. There seems to be an empty line + // after all that. + + for (String line : lines) { + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + Matcher m = GETPROP_PATTERN.matcher(line); + if (m.matches()) { + String label = m.group(1); + String value = m.group(2); + + if (!label.isEmpty()) { + mCollectedProperties.put(label, value); + } + } + } + } + + @Override + public boolean isCancelled() { + return false; + } + + Map getCollectedProperties() { + return mCollectedProperties; + } + } + + private final Map mProperties = Maps.newHashMapWithExpectedSize( + EXPECTED_PROP_COUNT); + private final IDevice mDevice; + private CacheState mCacheState = CacheState.UNPOPULATED; + private final Map> mPendingRequests = + Maps.newHashMapWithExpectedSize(4); + + public PropertyFetcher(IDevice device) { + mDevice = device; + } + + /** + * Returns the full list of cached properties. + */ + public synchronized Map getProperties() { + return mProperties; + } + + /** + * Make a possibly asynchronous request for a system property value. + * + * @param name the property name to retrieve + * @return a {@link Future} that can be used to retrieve the prop value + */ + @NonNull + public synchronized Future getProperty(@NonNull String name) { + SettableFuture result; + if (mCacheState.equals(CacheState.FETCHING)) { + result = addPendingRequest(name); + } else if (mDevice.isOnline() && mCacheState.equals(CacheState.UNPOPULATED) || !isRoProp(name)) { + // cache is empty, or this is a volatile prop that requires a query + result = addPendingRequest(name); + mCacheState = CacheState.FETCHING; + initiatePropertiesQuery(); + } else { + result = SettableFuture.create(); + // cache is populated and this is a ro prop + result.set(mProperties.get(name)); + } + return result; + } + + private SettableFuture addPendingRequest(String name) { + SettableFuture future = mPendingRequests.get(name); + if (future == null) { + future = SettableFuture.create(); + mPendingRequests.put(name, future); + } + return future; + } + + private void initiatePropertiesQuery() { + String threadName = String.format("query-prop-%s", mDevice.getSerialNumber()); + Thread propThread = new Thread(threadName) { + @Override + public void run() { + try { + GetPropReceiver propReceiver = new GetPropReceiver(); + mDevice.executeShellCommand(GETPROP_COMMAND, propReceiver, GETPROP_TIMEOUT_SEC, + TimeUnit.SECONDS); + populateCache(propReceiver.getCollectedProperties()); + } catch (Exception e) { + handleException(e); + } + } + }; + propThread.setDaemon(true); + propThread.start(); + } + + private synchronized void populateCache(@NonNull Map props) { + mCacheState = props.isEmpty() ? CacheState.UNPOPULATED : CacheState.POPULATED; + if (!props.isEmpty()) { + mProperties.putAll(props); + } + for (Map.Entry> entry : mPendingRequests.entrySet()) { + entry.getValue().set(mProperties.get(entry.getKey())); + } + mPendingRequests.clear(); + } + + private synchronized void handleException(Exception e) { + mCacheState = CacheState.UNPOPULATED; + Log.w("PropertyFetcher", + String.format("%s getting properties for device %s: %s", + e.getClass().getSimpleName(), mDevice.getSerialNumber(), + e.getMessage())); + for (Map.Entry> entry : mPendingRequests.entrySet()) { + entry.getValue().setException(e); + } + mPendingRequests.clear(); + } + + /** + * Return true if cache is populated. + * + * @deprecated implementation detail + */ + @Deprecated + public synchronized boolean arePropertiesSet() { + return CacheState.POPULATED.equals(mCacheState); + } + + private static boolean isRoProp(@NonNull String propName) { + return propName.startsWith("ro."); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/RawImage.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/RawImage.java new file mode 100644 index 0000000..b164044 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/RawImage.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.nio.ByteBuffer; + +/** + * Data representing an image taken from a device frame buffer. + */ +public final class RawImage { + public int version; + public int bpp; + public int size; + public int width; + public int height; + public int red_offset; + public int red_length; + public int blue_offset; + public int blue_length; + public int green_offset; + public int green_length; + public int alpha_offset; + public int alpha_length; + + public byte[] data; + + /** + * Reads the header of a RawImage from a {@link ByteBuffer}. + *

The way the data is sent over adb is defined in system/core/adb/framebuffer_service.c + * @param version the version of the protocol. + * @param buf the buffer to read from. + * @return true if success + */ + public boolean readHeader(int version, ByteBuffer buf) { + this.version = version; + + if (version == 16) { + // compatibility mode with original protocol + this.bpp = 16; + + // read actual values. + this.size = buf.getInt(); + this.width = buf.getInt(); + this.height = buf.getInt(); + + // create default values for the rest. Format is 565 + this.red_offset = 11; + this.red_length = 5; + this.green_offset = 5; + this.green_length = 6; + this.blue_offset = 0; + this.blue_length = 5; + this.alpha_offset = 0; + this.alpha_length = 0; + } else if (version == 1) { + this.bpp = buf.getInt(); + this.size = buf.getInt(); + this.width = buf.getInt(); + this.height = buf.getInt(); + this.red_offset = buf.getInt(); + this.red_length = buf.getInt(); + this.blue_offset = buf.getInt(); + this.blue_length = buf.getInt(); + this.green_offset = buf.getInt(); + this.green_length = buf.getInt(); + this.alpha_offset = buf.getInt(); + this.alpha_length = buf.getInt(); + } else { + // unsupported protocol! + return false; + } + + return true; + } + + /** + * Returns the mask value for the red color. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + public int getRedMask() { + return getMask(red_length, red_offset); + } + + /** + * Returns the mask value for the green color. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + public int getGreenMask() { + return getMask(green_length, green_offset); + } + + /** + * Returns the mask value for the blue color. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + public int getBlueMask() { + return getMask(blue_length, blue_offset); + } + + /** + * Returns the size of the header for a specific version of the framebuffer adb protocol. + * @param version the version of the protocol + * @return the number of int that makes up the header. + */ + public static int getHeaderSize(int version) { + switch (version) { + case 16: // compatibility mode + return 3; // size, width, height + case 1: + return 12; // bpp, size, width, height, 4*(length, offset) + } + + return 0; + } + + /** + * Returns a rotated version of the image + * The image is rotated counter-clockwise. + */ + public RawImage getRotated() { + RawImage rotated = new RawImage(); + rotated.version = this.version; + rotated.bpp = this.bpp; + rotated.size = this.size; + rotated.red_offset = this.red_offset; + rotated.red_length = this.red_length; + rotated.blue_offset = this.blue_offset; + rotated.blue_length = this.blue_length; + rotated.green_offset = this.green_offset; + rotated.green_length = this.green_length; + rotated.alpha_offset = this.alpha_offset; + rotated.alpha_length = this.alpha_length; + + rotated.width = this.height; + rotated.height = this.width; + + int count = this.data.length; + rotated.data = new byte[count]; + + int byteCount = this.bpp >> 3; // bpp is in bits, we want bytes to match our array + final int w = this.width; + final int h = this.height; + for (int y = 0 ; y < h ; y++) { + for (int x = 0 ; x < w ; x++) { + System.arraycopy( + this.data, (y * w + x) * byteCount, + rotated.data, ((w-x-1) * h + y) * byteCount, + byteCount); + } + } + + return rotated; + } + + /** + * Returns an ARGB integer value for the pixel at index in {@link #data}. + */ + public int getARGB(int index) { + int value; + int r, g, b, a; + if (bpp == 16) { + value = data[index] & 0x00FF; + value |= (data[index+1] << 8) & 0x0FF00; + // RGB565 to RGB888 + // Multiply by 255/31 to convert from 5 bits (31 max) to 8 bits (255) + r = ((value >>> 11) & 0x1f) * 255/31; + g = ((value >>> 5) & 0x3f) * 255/63; + b = ((value) & 0x1f) * 255/31; + a = 0xFF; // force alpha to opaque if there's no alpha value in the framebuffer. + } else if (bpp == 32) { + value = data[index] & 0x00FF; + value |= (data[index+1] & 0x00FF) << 8; + value |= (data[index+2] & 0x00FF) << 16; + value |= (data[index+3] & 0x00FF) << 24; + r = ((value >>> red_offset) & getMask(red_length)) << (8 - red_length); + g = ((value >>> green_offset) & getMask(green_length)) << (8 - green_length); + b = ((value >>> blue_offset) & getMask(blue_length)) << (8 - blue_length); + a = ((value >>> alpha_offset) & getMask(alpha_length)) << (8 - alpha_length); + } else { + throw new UnsupportedOperationException("RawImage.getARGB(int) only works in 16 and 32 bit mode."); + } + + return a << 24 | r << 16 | g << 8 | b; + } + + /** + * creates a mask value based on a length and offset. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + private int getMask(int length, int offset) { + int res = getMask(length) << offset; + + // if the bpp is 32 bits then we need to invert it because the buffer is in little endian + if (bpp == 32) { + return Integer.reverseBytes(res); + } + + return res; + } + + /** + * Creates a mask value based on a length. + * @param length + * @return + */ + private static int getMask(int length) { + return (1 << length) - 1; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ScreenRecorderOptions.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ScreenRecorderOptions.java new file mode 100644 index 0000000..af8952a --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ScreenRecorderOptions.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.util.concurrent.TimeUnit; + +public class ScreenRecorderOptions { + // video size is given by width x height, defaults to device's main display resolution + // or 1280x720. + public final int width; + public final int height; + + // bit rate in Mbps. Defaults to 4Mbps + public final int bitrateMbps; + + // time limit, maximum of 3 seconds + public final long timeLimit; + public final TimeUnit timeLimitUnits; + + private ScreenRecorderOptions(Builder builder) { + width = builder.mWidth; + height = builder.mHeight; + + bitrateMbps = builder.mBitRate; + + timeLimit = builder.mTime; + timeLimitUnits = builder.mTimeUnits; + } + + public static class Builder { + private int mWidth; + private int mHeight; + private int mBitRate; + private long mTime; + private TimeUnit mTimeUnits; + + public Builder setSize(int w, int h) { + mWidth = w; + mHeight = h; + return this; + } + + public Builder setBitRate(int bitRateMbps) { + mBitRate = bitRateMbps; + return this; + } + + public Builder setTimeLimit(long time, TimeUnit units) { + mTime = time; + mTimeUnits = units; + return this; + } + + public ScreenRecorderOptions build() { + return new ScreenRecorderOptions(this); + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ShellCommandUnresponsiveException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ShellCommandUnresponsiveException.java new file mode 100644 index 0000000..09823c4 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ShellCommandUnresponsiveException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + + +/** + * Exception thrown when a shell command executed on a device takes too long to send its output. + *

The command may not actually be unresponsive, it just has spent too much time not outputting + * any thing to the console. + */ +public class ShellCommandUnresponsiveException extends Exception { + private static final long serialVersionUID = 1L; +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncException.java new file mode 100644 index 0000000..6a896a5 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncException.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; + +/** + * Exception thrown when a transfer using {@link SyncService} doesn't complete. + *

This is different from an {@link IOException} because it's not the underlying connection + * that triggered the error, but the adb transfer protocol that didn't work somehow, or that the + * targets (local and/or remote) were wrong. + */ +public class SyncException extends CanceledException { + private static final long serialVersionUID = 1L; + + public enum SyncError { + /** canceled transfer */ + CANCELED("Operation was canceled by the user."), + /** Transfer error */ + TRANSFER_PROTOCOL_ERROR("Adb Transfer Protocol Error."), + /** unknown remote object during a pull */ + NO_REMOTE_OBJECT("Remote object doesn't exist!"), + /** Result code when attempting to pull multiple files into a file */ + TARGET_IS_FILE("Target object is a file."), + /** Result code when attempting to pull multiple into a directory that does not exist. */ + NO_DIR_TARGET("Target directory doesn't exist."), + /** wrong encoding on the remote path. */ + REMOTE_PATH_ENCODING("Remote Path encoding is not supported."), + /** remote path that is too long. */ + REMOTE_PATH_LENGTH("Remote path is too long."), + /** error while reading local file. */ + FILE_READ_ERROR("Reading local file failed!"), + /** error while writing local file. */ + FILE_WRITE_ERROR("Writing local file failed!"), + /** attempting to push a directory. */ + LOCAL_IS_DIRECTORY("Local path is a directory."), + /** attempting to push a non-existent file. */ + NO_LOCAL_FILE("Local path doesn't exist."), + /** when the target path of a multi file push is a file. */ + REMOTE_IS_FILE("Remote path is a file."), + /** receiving too much data from the remove device at once */ + BUFFER_OVERRUN("Receiving too much data."); + + private final String mMessage; + + SyncError(String message) { + mMessage = message; + } + + public String getMessage() { + return mMessage; + } + } + + private final SyncError mError; + + public SyncException(SyncError error) { + super(error.getMessage()); + mError = error; + } + + public SyncException(SyncError error, String message) { + super(message); + mError = error; + } + + public SyncException(SyncError error, Throwable cause) { + super(error.getMessage(), cause); + mError = error; + } + + public SyncError getErrorCode() { + return mError; + } + + /** + * Returns true if the sync was canceled by user input. + */ + @Override + public boolean wasCanceled() { + return mError == SyncError.CANCELED; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncService.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncService.java new file mode 100644 index 0000000..19c3c14 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/SyncService.java @@ -0,0 +1,916 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.AdbHelper.AdbResponse; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.SyncException.SyncError; +import com.android.ddmlib.utils.ArrayHelper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Date; + +/** + * Sync service class to push/pull to/from devices/emulators, through the debug bridge. + *

+ * To get a {@link SyncService} object, use {@link Device#getSyncService()}. + */ +public class SyncService { + + private static final byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' }; + private static final byte[] ID_FAIL = { 'F', 'A', 'I', 'L' }; + private static final byte[] ID_STAT = { 'S', 'T', 'A', 'T' }; + private static final byte[] ID_RECV = { 'R', 'E', 'C', 'V' }; + private static final byte[] ID_DATA = { 'D', 'A', 'T', 'A' }; + private static final byte[] ID_DONE = { 'D', 'O', 'N', 'E' }; + private static final byte[] ID_SEND = { 'S', 'E', 'N', 'D' }; +// private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' }; +// private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' }; + + private static final NullSyncProgressMonitor sNullSyncProgressMonitor = + new NullSyncProgressMonitor(); + + private static final int S_ISOCK = 0xC000; // type: symbolic link + private static final int S_IFLNK = 0xA000; // type: symbolic link + private static final int S_IFREG = 0x8000; // type: regular file + private static final int S_IFBLK = 0x6000; // type: block device + private static final int S_IFDIR = 0x4000; // type: directory + private static final int S_IFCHR = 0x2000; // type: character device + private static final int S_IFIFO = 0x1000; // type: fifo +/* + private final static int S_ISUID = 0x0800; // set-uid bit + private final static int S_ISGID = 0x0400; // set-gid bit + private final static int S_ISVTX = 0x0200; // sticky bit + private final static int S_IRWXU = 0x01C0; // user permissions + private final static int S_IRUSR = 0x0100; // user: read + private final static int S_IWUSR = 0x0080; // user: write + private final static int S_IXUSR = 0x0040; // user: execute + private final static int S_IRWXG = 0x0038; // group permissions + private final static int S_IRGRP = 0x0020; // group: read + private final static int S_IWGRP = 0x0010; // group: write + private final static int S_IXGRP = 0x0008; // group: execute + private final static int S_IRWXO = 0x0007; // other permissions + private final static int S_IROTH = 0x0004; // other: read + private final static int S_IWOTH = 0x0002; // other: write + private final static int S_IXOTH = 0x0001; // other: execute +*/ + + private static final int SYNC_DATA_MAX = 64*1024; + private static final int REMOTE_PATH_MAX_LENGTH = 1024; + + /** + * Classes which implement this interface provide methods that deal + * with displaying transfer progress. + */ + public interface ISyncProgressMonitor { + /** + * Sent when the transfer starts + * @param totalWork the total amount of work. + */ + void start(int totalWork); + /** + * Sent when the transfer is finished or interrupted. + */ + void stop(); + /** + * Sent to query for possible cancellation. + * @return true if the transfer should be stopped. + */ + boolean isCanceled(); + /** + * Sent when a sub task is started. + * @param name the name of the sub task. + */ + void startSubTask(String name); + /** + * Sent when some progress have been made. + * @param work the amount of work done. + */ + void advance(int work); + } + + public static class FileStat { + private final int myMode; + private final int mySize; + private final Date myLastModified; + + public FileStat(int mode, int size, int lastModifiedSecs) { + myMode = mode; + mySize = size; + myLastModified = new Date((long)(lastModifiedSecs) * 1000); + } + + public int getMode() { + return myMode; + } + + public int getSize() { + return mySize; + } + + public Date getLastModified() { + return myLastModified; + } + } + + /** + * A Sync progress monitor that does nothing + */ + private static class NullSyncProgressMonitor implements ISyncProgressMonitor { + @Override + public void advance(int work) { + } + @Override + public boolean isCanceled() { + return false; + } + + @Override + public void start(int totalWork) { + } + @Override + public void startSubTask(String name) { + } + @Override + public void stop() { + } + } + + private InetSocketAddress mAddress; + private Device mDevice; + private SocketChannel mChannel; + + /** + * Buffer used to send data. Allocated when needed and reused afterward. + */ + private byte[] mBuffer; + + /** + * Creates a Sync service object. + * @param address The address to connect to + * @param device the {@link Device} that the service connects to. + */ + SyncService(InetSocketAddress address, Device device) { + mAddress = address; + mDevice = device; + } + + /** + * Opens the sync connection. This must be called before any calls to push[File] / pull[File]. + * @return true if the connection opened, false if adb refuse the connection. This can happen + * if the {@link Device} is invalid. + * @throws TimeoutException in case of timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws IOException If the connection to adb failed. + */ + boolean openSync() throws TimeoutException, AdbCommandRejectedException, IOException { + try { + mChannel = SocketChannel.open(mAddress); + mChannel.configureBlocking(false); + + // target a specific device + AdbHelper.setDevice(mChannel, mDevice); + + byte[] request = AdbHelper.formAdbRequest("sync:"); //$NON-NLS-1$ + AdbHelper.write(mChannel, request, -1, DdmPreferences.getTimeOut()); + + AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */); + + if (!resp.okay) { + Log.w("ddms", "Got unhappy response from ADB sync req: " + resp.message); + mChannel.close(); + mChannel = null; + return false; + } + } catch (TimeoutException e) { + if (mChannel != null) { + try { + mChannel.close(); + } catch (IOException e2) { + // we want to throw the original exception, so we ignore this one. + } + mChannel = null; + } + + throw e; + } catch (IOException e) { + if (mChannel != null) { + try { + mChannel.close(); + } catch (IOException e2) { + // we want to throw the original exception, so we ignore this one. + } + mChannel = null; + } + + throw e; + } + + return true; + } + + /** + * Closes the connection. + */ + public void close() { + if (mChannel != null) { + try { + mChannel.close(); + } catch (IOException e) { + // nothing to be done really... + } + mChannel = null; + } + } + + /** + * Returns a sync progress monitor that does nothing. This allows background tasks that don't + * want/need to display ui, to pass a valid {@link ISyncProgressMonitor}. + *

This object can be reused multiple times and can be used by concurrent threads. + */ + public static ISyncProgressMonitor getNullProgressMonitor() { + return sNullSyncProgressMonitor; + } + + /** + * Pulls file(s) or folder(s). + * @param entries the remote item(s) to pull + * @param localPath The local destination. If the entries count is > 1 or + * if the unique entry is a folder, this should be a folder. + * @param monitor The progress monitor. Cannot be null. + * @throws SyncException + * @throws IOException + * @throws TimeoutException + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public void pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + + // first we check the destination is a directory and exists + File f = new File(localPath); + if (!f.exists()) { + throw new SyncException(SyncError.NO_DIR_TARGET); + } + if (!f.isDirectory()) { + throw new SyncException(SyncError.TARGET_IS_FILE); + } + + // get a FileListingService object + FileListingService fls = new FileListingService(mDevice); + + // compute the number of file to move + int total = getTotalRemoteFileSize(entries, fls); + + // start the monitor + monitor.start(total); + + doPull(entries, localPath, fls, monitor); + + monitor.stop(); + } + + /** + * Pulls a single file. + * @param remote the remote file + * @param localFilename The local destination. + * @param monitor The progress monitor. Cannot be null. + * + * @throws IOException in case of an IO exception. + * @throws TimeoutException in case of a timeout reading responses from the device. + * @throws SyncException in case of a sync exception. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public void pullFile(FileEntry remote, String localFilename, ISyncProgressMonitor monitor) + throws IOException, SyncException, TimeoutException { + int total = remote.getSizeValue(); + monitor.start(total); + + doPullFile(remote.getFullPath(), localFilename, monitor); + + monitor.stop(); + } + + /** + * Pulls a single file. + *

Because this method just deals with a String for the remote file instead of a + * {@link FileEntry}, the size of the file being pulled is unknown and the + * {@link ISyncProgressMonitor} will not properly show the progress + * @param remoteFilepath the full path to the remote file + * @param localFilename The local destination. + * @param monitor The progress monitor. Cannot be null. + * + * @throws IOException in case of an IO exception. + * @throws TimeoutException in case of a timeout reading responses from the device. + * @throws SyncException in case of a sync exception. + * + * @see #getNullProgressMonitor() + */ + public void pullFile(String remoteFilepath, String localFilename, + ISyncProgressMonitor monitor) throws TimeoutException, IOException, SyncException { + FileStat fileStat = statFile(remoteFilepath); + if (fileStat == null) { + // attempts to download anyway + } else if (fileStat.getMode() == 0) { + throw new SyncException(SyncError.NO_REMOTE_OBJECT); + } + + monitor.start(0); + //TODO: use the {@link FileListingService} to get the file size. + + doPullFile(remoteFilepath, localFilename, monitor); + + monitor.stop(); + } + + /** + * Push several files. + * @param local An array of loca files to push + * @param remote the remote {@link FileEntry} representing a directory. + * @param monitor The progress monitor. Cannot be null. + * @throws SyncException if file could not be pushed + * @throws IOException in case of I/O error on the connection. + * @throws TimeoutException in case of a timeout reading responses from the device. + */ + public void push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + if (!remote.isDirectory()) { + throw new SyncException(SyncError.REMOTE_IS_FILE); + } + + // make a list of File from the list of String + ArrayList files = new ArrayList(); + for (String path : local) { + files.add(new File(path)); + } + + // get the total count of the bytes to transfer + File[] fileArray = files.toArray(new File[files.size()]); + int total = getTotalLocalFileSize(fileArray); + + monitor.start(total); + + doPush(fileArray, remote.getFullPath(), monitor); + + monitor.stop(); + } + + /** + * Push a single file. + * @param local the local filepath. + * @param remote The remote filepath. + * @param monitor The progress monitor. Cannot be null. + * + * @throws SyncException if file could not be pushed + * @throws IOException in case of I/O error on the connection. + * @throws TimeoutException in case of a timeout reading responses from the device. + */ + public void pushFile(String local, String remote, ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + File f = new File(local); + if (!f.exists()) { + throw new SyncException(SyncError.NO_LOCAL_FILE); + } + + if (f.isDirectory()) { + throw new SyncException(SyncError.LOCAL_IS_DIRECTORY); + } + + monitor.start((int)f.length()); + + doPushFile(local, remote, monitor); + + monitor.stop(); + } + + /** + * compute the recursive file size of all the files in the list. Folder + * have a weight of 1. + * @param entries + * @param fls + * @return + */ + private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) { + int count = 0; + for (FileEntry e : entries) { + int type = e.getType(); + if (type == FileListingService.TYPE_DIRECTORY) { + // get the children + FileEntry[] children = fls.getChildren(e, false, null); + count += getTotalRemoteFileSize(children, fls) + 1; + } else if (type == FileListingService.TYPE_FILE) { + count += e.getSizeValue(); + } + } + + return count; + } + + /** + * compute the recursive file size of all the files in the list. Folder + * have a weight of 1. + * This does not check for circular links. + * @param files + * @return + */ + private int getTotalLocalFileSize(File[] files) { + int count = 0; + + for (File f : files) { + if (f.exists()) { + if (f.isDirectory()) { + return getTotalLocalFileSize(f.listFiles()) + 1; + } else if (f.isFile()) { + count += f.length(); + } + } + } + + return count; + } + + /** + * Pulls multiple files/folders recursively. + * @param entries The list of entry to pull + * @param localPath the localpath to a directory + * @param fileListingService a FileListingService object to browse through remote directories. + * @param monitor the progress monitor. Must be started already. + * + * @throws SyncException if file could not be pushed + * @throws IOException in case of I/O error on the connection. + * @throws TimeoutException in case of a timeout reading responses from the device. + */ + private void doPull(FileEntry[] entries, String localPath, + FileListingService fileListingService, + ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { + + for (FileEntry e : entries) { + // check if we're cancelled + if (monitor.isCanceled()) { + throw new SyncException(SyncError.CANCELED); + } + + // get type (we only pull directory and files for now) + int type = e.getType(); + if (type == FileListingService.TYPE_DIRECTORY) { + monitor.startSubTask(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + + // make the directory + File d = new File(dest); + d.mkdir(); + + // then recursively call the content. Since we did a ls command + // to get the number of files, we can use the cache + FileEntry[] children = fileListingService.getChildren(e, true, null); + doPull(children, dest, fileListingService, monitor); + monitor.advance(1); + } else if (type == FileListingService.TYPE_FILE) { + monitor.startSubTask(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + doPullFile(e.getFullPath(), dest, monitor); + } + } + } + + /** + * Pulls a remote file + * @param remotePath the remote file (length max is 1024) + * @param localPath the local destination + * @param monitor the monitor. The monitor must be started already. + * @throws SyncException if file could not be pushed + * @throws IOException in case of I/O error on the connection. + * @throws TimeoutException in case of a timeout reading responses from the device. + */ + private void doPullFile(String remotePath, String localPath, + ISyncProgressMonitor monitor) throws IOException, SyncException, TimeoutException { + byte[] msg = null; + byte[] pullResult = new byte[8]; + + final int timeOut = DdmPreferences.getTimeOut(); + + try { + byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + throw new SyncException(SyncError.REMOTE_PATH_LENGTH); + } + + // create the full request message + msg = createFileReq(ID_RECV, remotePathContent); + + // and send it. + AdbHelper.write(mChannel, msg, -1, timeOut); + + // read the result, in a byte array containing 2 ints + // (id, size) + AdbHelper.read(mChannel, pullResult, -1, timeOut); + + // check we have the proper data back + if (!checkResult(pullResult, ID_DATA) && + !checkResult(pullResult, ID_DONE)) { + throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR, + readErrorMessage(pullResult, timeOut)); + } + } catch (UnsupportedEncodingException e) { + throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e); + } + + // access the destination file + File f = new File(localPath); + + // create the stream to write in the file. We use a new try/catch block to differentiate + // between file and network io exceptions. + FileOutputStream fos = null; + try { + fos = new FileOutputStream(f); + + // the buffer to read the data + byte[] data = new byte[SYNC_DATA_MAX]; + + // loop to get data until we're done. + while (true) { + // check if we're cancelled + if (monitor.isCanceled()) { + throw new SyncException(SyncError.CANCELED); + } + + // if we're done, we stop the loop + if (checkResult(pullResult, ID_DONE)) { + break; + } + if (!checkResult(pullResult, ID_DATA)) { + // hmm there's an error + throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR, + readErrorMessage(pullResult, timeOut)); + } + int length = ArrayHelper.swap32bitFromArray(pullResult, 4); + if (length > SYNC_DATA_MAX) { + // buffer overrun! + // error and exit + throw new SyncException(SyncError.BUFFER_OVERRUN); + } + + // now read the length we received + AdbHelper.read(mChannel, data, length, timeOut); + + // get the header for the next packet. + AdbHelper.read(mChannel, pullResult, -1, timeOut); + + // write the content in the file + fos.write(data, 0, length); + + monitor.advance(length); + } + + fos.flush(); + } catch (IOException e) { + Log.e("ddms", String.format("Failed to open local file %s for writing, Reason: %s", + f.getAbsolutePath(), e.toString())); + throw new SyncException(SyncError.FILE_WRITE_ERROR); + } finally { + if (fos != null) { + fos.close(); + } + } + } + + + /** + * Push multiple files + * @param fileArray + * @param remotePath + * @param monitor + * + * @throws SyncException if file could not be pushed + * @throws IOException in case of I/O error on the connection. + * @throws TimeoutException in case of a timeout reading responses from the device. + */ + private void doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + for (File f : fileArray) { + // check if we're canceled + if (monitor.isCanceled()) { + throw new SyncException(SyncError.CANCELED); + } + if (f.exists()) { + if (f.isDirectory()) { + // append the name of the directory to the remote path + String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S + monitor.startSubTask(dest); + doPush(f.listFiles(), dest, monitor); + + monitor.advance(1); + } else if (f.isFile()) { + // append the name of the file to the remote path + String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S + monitor.startSubTask(remoteFile); + doPushFile(f.getAbsolutePath(), remoteFile, monitor); + } + } + } + } + + /** + * Push a single file + * @param localPath the local file to push + * @param remotePath the remote file (length max is 1024) + * @param monitor the monitor. The monitor must be started already. + * + * @throws SyncException if file could not be pushed + * @throws IOException in case of I/O error on the connection. + * @throws TimeoutException in case of a timeout reading responses from the device. + */ + private void doPushFile(String localPath, String remotePath, + ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { + FileInputStream fis = null; + byte[] msg; + + final int timeOut = DdmPreferences.getTimeOut(); + File f = new File(localPath); + + try { + byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + throw new SyncException(SyncError.REMOTE_PATH_LENGTH); + } + + // create the stream to read the file + fis = new FileInputStream(f); + + // create the header for the action + msg = createSendFileReq(ID_SEND, remotePathContent, 0644); + + // and send it. We use a custom try/catch block to make the difference between + // file and network IO exceptions. + AdbHelper.write(mChannel, msg, -1, timeOut); + + System.arraycopy(ID_DATA, 0, getBuffer(), 0, ID_DATA.length); + + // look while there is something to read + while (true) { + // check if we're canceled + if (monitor.isCanceled()) { + throw new SyncException(SyncError.CANCELED); + } + + // read up to SYNC_DATA_MAX + int readCount = fis.read(getBuffer(), 8, SYNC_DATA_MAX); + + if (readCount == -1) { + // we reached the end of the file + break; + } + + // now send the data to the device + // first write the amount read + ArrayHelper.swap32bitsToArray(readCount, getBuffer(), 4); + + // now write it + AdbHelper.write(mChannel, getBuffer(), readCount+8, timeOut); + + // and advance the monitor + monitor.advance(readCount); + } + } catch (UnsupportedEncodingException e) { + throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e); + } finally { + // close the local file + if (fis != null) { + fis.close(); + } + } + + // create the DONE message + long time = f.lastModified() / 1000; + msg = createReq(ID_DONE, (int)time); + + // and send it. + AdbHelper.write(mChannel, msg, -1, timeOut); + + // read the result, in a byte array containing 2 ints + // (id, size) + byte[] result = new byte[8]; + AdbHelper.read(mChannel, result, -1 /* full length */, timeOut); + + if (!checkResult(result, ID_OKAY)) { + throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR, + readErrorMessage(result, timeOut)); + } + } + + /** + * Reads an error message from the opened {@link #mChannel}. + * @param result the current adb result. Must contain both FAIL and the length of the message. + * @param timeOut + * @return + * @throws TimeoutException in case of a timeout reading responses from the device. + * @throws IOException + */ + private String readErrorMessage(byte[] result, final int timeOut) throws TimeoutException, + IOException { + if (checkResult(result, ID_FAIL)) { + int len = ArrayHelper.swap32bitFromArray(result, 4); + + if (len > 0) { + AdbHelper.read(mChannel, getBuffer(), len, timeOut); + + String message = new String(getBuffer(), 0, len); + Log.e("ddms", "transfer error: " + message); + + return message; + } + } + + return null; + } + + /** + * Returns the stat info of the remote file. + * @param path the remote file + * @return an FileStat containing the mode, size and last modified info if all went well or null + * otherwise + * @throws IOException + * @throws TimeoutException in case of a timeout reading responses from the device. + */ + @Nullable + public FileStat statFile(@NonNull String path) throws TimeoutException, IOException { + // create the stat request message. + byte[] msg = createFileReq(ID_STAT, path); + + AdbHelper.write(mChannel, msg, -1 /* full length */, DdmPreferences.getTimeOut()); + + // read the result, in a byte array containing 4 ints + // (id, mode, size, time) + byte[] statResult = new byte[16]; + AdbHelper.read(mChannel, statResult, -1 /* full length */, DdmPreferences.getTimeOut()); + + // check we have the proper data back + if (!checkResult(statResult, ID_STAT)) { + return null; + } + + final int mode = ArrayHelper.swap32bitFromArray(statResult, 4); + final int size = ArrayHelper.swap32bitFromArray(statResult, 8); + final int lastModifiedSecs = ArrayHelper.swap32bitFromArray(statResult, 12); + return new FileStat(mode, size, lastModifiedSecs); + } + + /** + * Create a command with a code and an int values + * @param command + * @param value + * @return + */ + private static byte[] createReq(byte[] command, int value) { + byte[] array = new byte[8]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(value, array, 4); + + return array; + } + + /** + * Creates the data array for a stat request. + * @param command the 4 byte command (ID_STAT, ID_RECV, ...) + * @param path The path of the remote file on which to execute the command + * @return the byte[] to send to the device through adb + */ + private static byte[] createFileReq(byte[] command, String path) { + byte[] pathContent = null; + try { + pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + + return createFileReq(command, pathContent); + } + + /** + * Creates the data array for a file request. This creates an array with a 4 byte command + the + * remote file name. + * @param command the 4 byte command (ID_STAT, ID_RECV, ...). + * @param path The path, as a byte array, of the remote file on which to + * execute the command. + * @return the byte[] to send to the device through adb + */ + private static byte[] createFileReq(byte[] command, byte[] path) { + byte[] array = new byte[8 + path.length]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(path.length, array, 4); + System.arraycopy(path, 0, array, 8, path.length); + + return array; + } + + private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) { + // make the mode into a string + String modeStr = "," + (mode & 0777); // $NON-NLS-1S + byte[] modeContent = null; + try { + modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + + byte[] array = new byte[8 + path.length + modeContent.length]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4); + System.arraycopy(path, 0, array, 8, path.length); + System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length); + + return array; + + + } + + /** + * Checks the result array starts with the provided code + * @param result The result array to check + * @param code The 4 byte code. + * @return true if the code matches. + */ + private static boolean checkResult(byte[] result, byte[] code) { + return !(result[0] != code[0] || + result[1] != code[1] || + result[2] != code[2] || + result[3] != code[3]); + + } + + private static int getFileType(int mode) { + if ((mode & S_ISOCK) == S_ISOCK) { + return FileListingService.TYPE_SOCKET; + } + + if ((mode & S_IFLNK) == S_IFLNK) { + return FileListingService.TYPE_LINK; + } + + if ((mode & S_IFREG) == S_IFREG) { + return FileListingService.TYPE_FILE; + } + + if ((mode & S_IFBLK) == S_IFBLK) { + return FileListingService.TYPE_BLOCK; + } + + if ((mode & S_IFDIR) == S_IFDIR) { + return FileListingService.TYPE_DIRECTORY; + } + + if ((mode & S_IFCHR) == S_IFCHR) { + return FileListingService.TYPE_CHARACTER; + } + + if ((mode & S_IFIFO) == S_IFIFO) { + return FileListingService.TYPE_FIFO; + } + + return FileListingService.TYPE_OTHER; + } + + /** + * Retrieve the buffer, allocating if necessary + * @return + */ + private byte[] getBuffer() { + if (mBuffer == null) { + // create the buffer used to read. + // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning. + mBuffer = new byte[SYNC_DATA_MAX + 8]; + } + return mBuffer; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ThreadInfo.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ThreadInfo.java new file mode 100644 index 0000000..93db931 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/ThreadInfo.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Holds a thread information. + */ +public final class ThreadInfo implements IStackTraceInfo { + private int mThreadId; + private String mThreadName; + private int mStatus; + private int mTid; + private int mUtime; + private int mStime; + private boolean mIsDaemon; + private StackTraceElement[] mTrace; + private long mTraceTime; + + // priority? + // total CPU used? + // method at top of stack? + + /** + * Construct with basic identification. + */ + ThreadInfo(int threadId, String threadName) { + mThreadId = threadId; + mThreadName = threadName; + + mStatus = -1; + //mTid = mUtime = mStime = 0; + //mIsDaemon = false; + } + + /** + * Set with the values we get from a THST chunk. + */ + void updateThread(int status, int tid, int utime, int stime, boolean isDaemon) { + + mStatus = status; + mTid = tid; + mUtime = utime; + mStime = stime; + mIsDaemon = isDaemon; + } + + /** + * Sets the stack call of the thread. + * @param trace stackcall information. + */ + void setStackCall(StackTraceElement[] trace) { + mTrace = trace; + mTraceTime = System.currentTimeMillis(); + } + + /** + * Returns the thread's ID. + */ + public int getThreadId() { + return mThreadId; + } + + /** + * Returns the thread's name. + */ + public String getThreadName() { + return mThreadName; + } + + void setThreadName(String name) { + mThreadName = name; + } + + /** + * Returns the system tid. + */ + public int getTid() { + return mTid; + } + + /** + * Returns the VM thread status. + */ + public int getStatus() { + return mStatus; + } + + /** + * Returns the cumulative user time. + */ + public int getUtime() { + return mUtime; + } + + /** + * Returns the cumulative system time. + */ + public int getStime() { + return mStime; + } + + /** + * Returns whether this is a daemon thread. + */ + public boolean isDaemon() { + return mIsDaemon; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IStackTraceInfo#getStackTrace() + */ + @Override + public StackTraceElement[] getStackTrace() { + return mTrace; + } + + /** + * Returns the approximate time of the stacktrace data. + * @see #getStackTrace() + */ + public long getStackCallTime() { + return mTraceTime; + } +} + diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/TimeoutException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/TimeoutException.java new file mode 100644 index 0000000..cc083c1 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/TimeoutException.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + + +/** + * Exception thrown when a connection to Adb failed with a timeout. + * + */ +public class TimeoutException extends Exception { + private static final long serialVersionUID = 1L; + + public TimeoutException() { + } + + public TimeoutException(String s) { + super(s); + } + + public TimeoutException(String s, Throwable throwable) { + super(s, throwable); + } + + public TimeoutException(Throwable throwable) { + super(throwable); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventContainer.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventContainer.java new file mode 100644 index 0000000..3607e60 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventContainer.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.LogReceiver.LogEntry; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents an event and its data. + */ +public class EventContainer { + + /** + * Comparison method for {@link EventContainer#testValue(int, Object, com.android.ddmlib.log.EventContainer.CompareMethod)} + * + */ + public enum CompareMethod { + EQUAL_TO("equals", "=="), + LESSER_THAN("less than or equals to", "<="), + LESSER_THAN_STRICT("less than", "<"), + GREATER_THAN("greater than or equals to", ">="), + GREATER_THAN_STRICT("greater than", ">"), + BIT_CHECK("bit check", "&"); + + private final String mName; + private final String mTestString; + + CompareMethod(String name, String testString) { + mName = name; + mTestString = testString; + } + + /** + * Returns the display string. + */ + @Override + public String toString() { + return mName; + } + + /** + * Returns a short string representing the comparison. + */ + public String testString() { + return mTestString; + } + } + + + /** + * Type for event data. + */ + public enum EventValueType { + UNKNOWN(0), + INT(1), + LONG(2), + STRING(3), + LIST(4), + TREE(5); + + private static final Pattern STORAGE_PATTERN = Pattern.compile("^(\\d+)@(.*)$"); //$NON-NLS-1$ + + private int mValue; + + /** + * Returns a {@link EventValueType} from an integer value, or null if no match + * was found. + * @param value the integer value. + */ + static EventValueType getEventValueType(int value) { + for (EventValueType type : values()) { + if (type.mValue == value) { + return type; + } + } + + return null; + } + + /** + * Returns a storage string for an {@link Object} of type supported by + * {@link EventValueType}. + *

+ * Strings created by this method can be reloaded with + * {@link #getObjectFromStorageString(String)}. + *

+ * NOTE: for now, only {@link #STRING}, {@link #INT}, and {@link #LONG} are supported. + * @param object the object to "convert" into a storage string. + * @return a string storing the object and its type or null if the type was not recognized. + */ + public static String getStorageString(Object object) { + if (object instanceof String) { + return STRING.mValue + "@" + object; //$NON-NLS-1$ + } else if (object instanceof Integer) { + return INT.mValue + "@" + object.toString(); //$NON-NLS-1$ + } else if (object instanceof Long) { + return LONG.mValue + "@" + object.toString(); //$NON-NLS-1$ + } + + return null; + } + + /** + * Creates an {@link Object} from a storage string created with + * {@link #getStorageString(Object)}. + * @param value the storage string + * @return an {@link Object} or null if the string or type were not recognized. + */ + public static Object getObjectFromStorageString(String value) { + Matcher m = STORAGE_PATTERN.matcher(value); + if (m.matches()) { + try { + EventValueType type = getEventValueType(Integer.parseInt(m.group(1))); + + if (type == null) { + return null; + } + + switch (type) { + case STRING: + return m.group(2); + case INT: + return Integer.valueOf(m.group(2)); + case LONG: + return Long.valueOf(m.group(2)); + } + } catch (NumberFormatException nfe) { + return null; + } + } + + return null; + } + + + /** + * Returns the integer value of the enum. + */ + public int getValue() { + return mValue; + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.US); + } + + EventValueType(int value) { + mValue = value; + } + } + + public int mTag; + public int pid; /* generating process's pid */ + public int tid; /* generating process's tid */ + public int sec; /* seconds since Epoch */ + public int nsec; /* nanoseconds */ + + private Object mData; + + /** + * Creates an {@link EventContainer} from a {@link LogEntry}. + * @param entry the LogEntry from which pid, tid, and time info is copied. + * @param tag the event tag value + * @param data the data of the EventContainer. + */ + EventContainer(LogEntry entry, int tag, Object data) { + getType(data); + mTag = tag; + mData = data; + + pid = entry.pid; + tid = entry.tid; + sec = entry.sec; + nsec = entry.nsec; + } + + /** + * Creates an {@link EventContainer} with raw data + */ + EventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) { + getType(data); + mTag = tag; + mData = data; + + this.pid = pid; + this.tid = tid; + this.sec = sec; + this.nsec = nsec; + } + + /** + * Returns the data as an int. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}. + * @see #getType() + */ + public final Integer getInt() throws InvalidTypeException { + if (getType(mData) == EventValueType.INT) { + return (Integer)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns the data as a long. + * @throws InvalidTypeException if the data type is not {@link EventValueType#LONG}. + * @see #getType() + */ + public final Long getLong() throws InvalidTypeException { + if (getType(mData) == EventValueType.LONG) { + return (Long)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns the data as a String. + * @throws InvalidTypeException if the data type is not {@link EventValueType#STRING}. + * @see #getType() + */ + public final String getString() throws InvalidTypeException { + if (getType(mData) == EventValueType.STRING) { + return (String)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns a value by index. The return type is defined by its type. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + */ + public Object getValue(int valueIndex) { + return getValue(mData, valueIndex, true); + } + + /** + * Returns a value by index as a double. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}, + * {@link EventValueType#LONG}, {@link EventValueType#LIST}, or if the item in the + * list at index valueIndex is not of type {@link EventValueType#INT} or + * {@link EventValueType#LONG}. + * @see #getType() + */ + public double getValueAsDouble(int valueIndex) throws InvalidTypeException { + return getValueAsDouble(mData, valueIndex, true); + } + + /** + * Returns a value by index as a String. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}, + * {@link EventValueType#LONG}, {@link EventValueType#STRING}, {@link EventValueType#LIST}, + * or if the item in the list at index valueIndex is not of type + * {@link EventValueType#INT}, {@link EventValueType#LONG}, or {@link EventValueType#STRING} + * @see #getType() + */ + public String getValueAsString(int valueIndex) throws InvalidTypeException { + return getValueAsString(mData, valueIndex, true); + } + + /** + * Returns the type of the data. + */ + public EventValueType getType() { + return getType(mData); + } + + /** + * Returns the type of an object. + */ + public final EventValueType getType(Object data) { + if (data instanceof Integer) { + return EventValueType.INT; + } else if (data instanceof Long) { + return EventValueType.LONG; + } else if (data instanceof String) { + return EventValueType.STRING; + } else if (data instanceof Object[]) { + // loop through the list to see if we have another list + Object[] objects = (Object[])data; + for (Object obj : objects) { + EventValueType type = getType(obj); + if (type == EventValueType.LIST || type == EventValueType.TREE) { + return EventValueType.TREE; + } + } + return EventValueType.LIST; + } + + return EventValueType.UNKNOWN; + } + + /** + * Checks that the index-th value of this event against a provided value. + * @param index the index of the value to test + * @param value the value to test against + * @param compareMethod the method of testing + * @return true if the test passed. + * @throws InvalidTypeException in case of type mismatch between the value to test and the value + * to test against, or if the compare method is incompatible with the type of the values. + * @see CompareMethod + */ + public boolean testValue(int index, Object value, + CompareMethod compareMethod) throws InvalidTypeException { + EventValueType type = getType(mData); + if (index > 0 && type != EventValueType.LIST) { + throw new InvalidTypeException(); + } + + Object data = mData; + if (type == EventValueType.LIST) { + data = ((Object[])mData)[index]; + } + + if (!data.getClass().equals(data.getClass())) { + throw new InvalidTypeException(); + } + + switch (compareMethod) { + case EQUAL_TO: + return data.equals(value); + case LESSER_THAN: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) <= 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) <= 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case LESSER_THAN_STRICT: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) < 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) < 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case GREATER_THAN: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) >= 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) >= 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case GREATER_THAN_STRICT: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) > 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) > 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case BIT_CHECK: + if (data instanceof Integer) { + return ((Integer) data & (Integer) value) != 0; + } else if (data instanceof Long) { + return ((Long) data & (Long) value) != 0; + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + default : + throw new InvalidTypeException(); + } + } + + private Object getValue(Object data, int valueIndex, boolean recursive) { + EventValueType type = getType(data); + + switch (type) { + case INT: + case LONG: + case STRING: + return data; + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValue(list[valueIndex], valueIndex, false); + } + } + } + + return null; + } + + private double getValueAsDouble(Object data, int valueIndex, boolean recursive) + throws InvalidTypeException { + EventValueType type = getType(data); + + switch (type) { + case INT: + return ((Integer)data).doubleValue(); + case LONG: + return ((Long)data).doubleValue(); + case STRING: + throw new InvalidTypeException(); + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValueAsDouble(list[valueIndex], valueIndex, false); + } + } + } + + throw new InvalidTypeException(); + } + + private String getValueAsString(Object data, int valueIndex, boolean recursive) + throws InvalidTypeException { + EventValueType type = getType(data); + + switch (type) { + case INT: + return data.toString(); + case LONG: + return data.toString(); + case STRING: + return (String)data; + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValueAsString(list[valueIndex], valueIndex, false); + } + } else { + throw new InvalidTypeException( + "getValueAsString() doesn't support EventValueType.TREE"); + } + } + + throw new InvalidTypeException( + "getValueAsString() unsupported type:" + type); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventLogParser.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventLogParser.java new file mode 100644 index 0000000..c6fd882 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventLogParser.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.LogReceiver.LogEntry; +import com.android.ddmlib.utils.ArrayHelper; +import com.google.common.base.Charsets; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser for the "event" log. + */ +public final class EventLogParser { + + /** Location of the tag map file on the device */ + private static final String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$ + + /** + * Event log entry types. These must match up with the declarations in + * java/android/android/util/EventLog.java. + */ + private static final int EVENT_TYPE_INT = 0; + private static final int EVENT_TYPE_LONG = 1; + private static final int EVENT_TYPE_STRING = 2; + private static final int EVENT_TYPE_LIST = 3; + + private static final Pattern PATTERN_SIMPLE_TAG = Pattern.compile( + "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$ + private static final Pattern PATTERN_TAG_WITH_DESC = Pattern.compile( + "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$ + private static final Pattern PATTERN_DESCRIPTION = Pattern.compile( + "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$ + + private static final Pattern TEXT_LOG_LINE = Pattern.compile( + "(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$ + + private final TreeMap mTagMap = new TreeMap(); + + private final TreeMap mValueDescriptionMap = + new TreeMap(); + + public EventLogParser() { + } + + /** + * Inits the parser for a specific Device. + *

+ * This methods reads the event-log-tags located on the device to find out + * what tags are being written to the event log and what their format is. + * @param device The device. + * @return true if success, false if failure or cancellation. + */ + public boolean init(IDevice device) { + // read the event tag map file on the device. + try { + device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$ + new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + processTagLine(line); + } + } + @Override + public boolean isCancelled() { + return false; + } + }); + } catch (Exception e) { + // catch all possible exceptions and return false. + return false; + } + + return true; + } + + /** + * Inits the parser with the content of a tag file. + * @param tagFileContent the lines of a tag file. + * @return true if success, false if failure. + */ + public boolean init(String[] tagFileContent) { + for (String line : tagFileContent) { + processTagLine(line); + } + return true; + } + + /** + * Inits the parser with a specified event-log-tags file. + * @param filePath + * @return true if success, false if failure. + */ + public boolean init(String filePath) { + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(filePath)); + + String line = null; + do { + line = reader.readLine(); + if (line != null) { + processTagLine(line); + } + } while (line != null); + + return true; + } catch (IOException e) { + return false; + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + // ignore + } + } + } + + /** + * Processes a line from the event-log-tags file. + * @param line the line to process + */ + private void processTagLine(String line) { + // ignore empty lines and comment lines + if (!line.isEmpty() && line.charAt(0) != '#') { + Matcher m = PATTERN_TAG_WITH_DESC.matcher(line); + if (m.matches()) { + try { + int value = Integer.parseInt(m.group(1)); + String name = m.group(2); + if (name != null && mTagMap.get(value) == null) { + mTagMap.put(value, name); + } + + // special case for the GC tag. We ignore what is in the file, + // and take what the custom GcEventContainer class tells us. + // This is due to the event encoding several values on 2 longs. + // @see GcEventContainer + if (value == GcEventContainer.GC_EVENT_TAG) { + mValueDescriptionMap.put(value, + GcEventContainer.getValueDescriptions()); + } else { + + String description = m.group(3); + if (description != null && !description.isEmpty()) { + EventValueDescription[] desc = + processDescription(description); + + if (desc != null) { + mValueDescriptionMap.put(value, desc); + } + } + } + } catch (NumberFormatException e) { + // failed to convert the number into a string. just ignore it. + } + } else { + m = PATTERN_SIMPLE_TAG.matcher(line); + if (m.matches()) { + int value = Integer.parseInt(m.group(1)); + String name = m.group(2); + if (name != null && mTagMap.get(value) == null) { + mTagMap.put(value, name); + } + } + } + } + } + + private EventValueDescription[] processDescription(String description) { + String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$ + + ArrayList list = new ArrayList(); + + for (String desc : descriptions) { + Matcher m = PATTERN_DESCRIPTION.matcher(desc); + if (m.matches()) { + try { + String name = m.group(1); + + String typeString = m.group(2); + int typeValue = Integer.parseInt(typeString); + EventValueType eventValueType = EventValueType.getEventValueType(typeValue); + if (eventValueType == null) { + // just ignore this description if the value is not recognized. + // TODO: log the error. + } + + typeString = m.group(3); + if (typeString != null && !typeString.isEmpty()) { + //skip the | + typeString = typeString.substring(1); + + typeValue = Integer.parseInt(typeString); + ValueType valueType = ValueType.getValueType(typeValue); + + list.add(new EventValueDescription(name, eventValueType, valueType)); + } else { + list.add(new EventValueDescription(name, eventValueType)); + } + } catch (NumberFormatException nfe) { + // just ignore this description if one number is malformed. + // TODO: log the error. + } catch (InvalidValueTypeException e) { + // just ignore this description if data type and data unit don't match + // TODO: log the error. + } + } else { + Log.e("EventLogParser", //$NON-NLS-1$ + String.format("Can't parse %1$s", description)); //$NON-NLS-1$ + } + } + + if (list.isEmpty()) { + return null; + } + + return list.toArray(new EventValueDescription[list.size()]); + + } + + public EventContainer parse(LogEntry entry) { + if (entry.len < 4) { + return null; + } + + int inOffset = 0; + + int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset); + inOffset += 4; + + String tag = mTagMap.get(tagValue); + if (tag == null) { + Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue)); + } + + ArrayList list = new ArrayList(); + if (parseBinaryEvent(entry.data, inOffset, list) == -1) { + return null; + } + + Object data; + if (list.size() == 1) { + data = list.get(0); + } else{ + data = list.toArray(); + } + + EventContainer event = null; + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + event = new GcEventContainer(entry, tagValue, data); + } else { + event = new EventContainer(entry, tagValue, data); + } + + return event; + } + + public EventContainer parse(String textLogLine) { + // line will look like + // 04-29 23:16:16.691 I/dvm_gc_info( 427): + // where is either + // [value1,value2...] + // or + // value + if (textLogLine.isEmpty()) { + return null; + } + + // parse the header first + Matcher m = TEXT_LOG_LINE.matcher(textLogLine); + if (m.matches()) { + try { + int month = Integer.parseInt(m.group(1)); + int day = Integer.parseInt(m.group(2)); + int hours = Integer.parseInt(m.group(3)); + int minutes = Integer.parseInt(m.group(4)); + int seconds = Integer.parseInt(m.group(5)); + int milliseconds = Integer.parseInt(m.group(6)); + + // convert into seconds since epoch and nano-seconds. + Calendar cal = Calendar.getInstance(); + cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds); + int sec = (int)Math.floor(cal.getTimeInMillis()/1000); + int nsec = milliseconds * 1000000; + + String tag = m.group(7); + + // get the numerical tag value + int tagValue = -1; + Set> tagSet = mTagMap.entrySet(); + for (Entry entry : tagSet) { + if (tag.equals(entry.getValue())) { + tagValue = entry.getKey(); + break; + } + } + + if (tagValue == -1) { + return null; + } + + int pid = Integer.parseInt(m.group(8)); + + Object data = parseTextData(m.group(9), tagValue); + if (data == null) { + return null; + } + + // now we can allocate and return the EventContainer + EventContainer event = null; + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data); + } else { + event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data); + } + + return event; + } catch (NumberFormatException e) { + return null; + } + } + + return null; + } + + public Map getTagMap() { + return mTagMap; + } + + public Map getEventInfoMap() { + return mValueDescriptionMap; + } + + /** + * Recursively convert binary log data to printable form. + * + * This needs to be recursive because you can have lists of lists. + * + * If we run out of room, we stop processing immediately. It's important + * for us to check for space on every output element to avoid producing + * garbled output. + * + * Returns the amount read on success, -1 on failure. + */ + private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList list) { + + if (eventData.length - dataOffset < 1) + return -1; + + int offset = dataOffset; + + int type = eventData[offset++]; + + //fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen); + + switch (type) { + case EVENT_TYPE_INT: { /* 32-bit signed int */ + int ival; + + if (eventData.length - offset < 4) + return -1; + ival = ArrayHelper.swap32bitFromArray(eventData, offset); + offset += 4; + + list.add(ival); + } + break; + case EVENT_TYPE_LONG: { /* 64-bit signed long */ + long lval; + + if (eventData.length - offset < 8) + return -1; + lval = ArrayHelper.swap64bitFromArray(eventData, offset); + offset += 8; + + list.add(lval); + } + break; + case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */ + int strLen; + + if (eventData.length - offset < 4) + return -1; + strLen = ArrayHelper.swap32bitFromArray(eventData, offset); + offset += 4; + + if (eventData.length - offset < strLen) + return -1; + + // get the string + String str = new String(eventData, offset, strLen, Charsets.UTF_8); + list.add(str); + offset += strLen; + break; + } + case EVENT_TYPE_LIST: { /* N items, all different types */ + + if (eventData.length - offset < 1) + return -1; + + int count = eventData[offset++]; + + // make a new temp list + ArrayList subList = new ArrayList(); + for (int i = 0; i < count; i++) { + int result = parseBinaryEvent(eventData, offset, subList); + if (result == -1) { + return result; + } + + offset += result; + } + + list.add(subList.toArray()); + } + break; + default: + Log.e("EventLogParser", //$NON-NLS-1$ + String.format("Unknown binary event type %1$d", type)); //$NON-NLS-1$ + return -1; + } + + return offset - dataOffset; + } + + private Object parseTextData(String data, int tagValue) { + // first, get the description of what we're supposed to parse + EventValueDescription[] desc = mValueDescriptionMap.get(tagValue); + + if (desc == null) { + // TODO parse and create string values. + return null; + } + + if (desc.length == 1) { + return getObjectFromString(data, desc[0].getEventValueType()); + } else if (data.startsWith("[") && data.endsWith("]")) { + data = data.substring(1, data.length() - 1); + + // get each individual values as String + String[] values = data.split(","); + + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + // special case for the GC event! + Object[] objects = new Object[2]; + + objects[0] = getObjectFromString(values[0], EventValueType.LONG); + objects[1] = getObjectFromString(values[1], EventValueType.LONG); + + return objects; + } else { + // must be the same number as the number of descriptors. + if (values.length != desc.length) { + return null; + } + + Object[] objects = new Object[values.length]; + + for (int i = 0 ; i < desc.length ; i++) { + Object obj = getObjectFromString(values[i], desc[i].getEventValueType()); + if (obj == null) { + return null; + } + objects[i] = obj; + } + + return objects; + } + } + + return null; + } + + + private Object getObjectFromString(String value, EventValueType type) { + try { + switch (type) { + case INT: + return Integer.valueOf(value); + case LONG: + return Long.valueOf(value); + case STRING: + return value; + } + } catch (NumberFormatException e) { + // do nothing, we'll return null. + } + + return null; + } + + /** + * Recreates the event-log-tags at the specified file path. + * @param filePath the file path to write the file. + * @throws IOException + */ + public void saveTags(String filePath) throws IOException { + File destFile = new File(filePath); + destFile.createNewFile(); + FileOutputStream fos = null; + + try { + + fos = new FileOutputStream(destFile); + + for (Integer key : mTagMap.keySet()) { + // get the tag name + String tagName = mTagMap.get(key); + + // get the value descriptions + EventValueDescription[] descriptors = mValueDescriptionMap.get(key); + + String line = null; + if (descriptors != null) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$ + boolean first = true; + for (EventValueDescription evd : descriptors) { + if (first) { + sb.append(" ("); //$NON-NLS-1$ + first = false; + } else { + sb.append(",("); //$NON-NLS-1$ + } + sb.append(evd.getName()); + sb.append("|"); //$NON-NLS-1$ + sb.append(evd.getEventValueType().getValue()); + sb.append("|"); //$NON-NLS-1$ + sb.append(evd.getValueType().getValue()); + sb.append("|)"); //$NON-NLS-1$ + } + sb.append("\n"); //$NON-NLS-1$ + + line = sb.toString(); + } else { + line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$ + } + + byte[] buffer = line.getBytes(); + fos.write(buffer); + } + } finally { + if (fos != null) { + fos.close(); + } + } + } + + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventValueDescription.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventValueDescription.java new file mode 100644 index 0000000..a3f249c --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/EventValueDescription.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.EventContainer.EventValueType; + +import java.util.Locale; + + +/** + * Describes an {@link EventContainer} value. + *

+ * This is a stand-alone object, not linked to a particular Event. It describes the value, by + * name, type ({@link EventValueType}), and (if needed) value unit ({@link ValueType}). + *

+ * The index of the value is not contained within this class, and is instead dependent on the + * index of this particular object in the array of {@link EventValueDescription} returned by + * {@link EventLogParser#getEventInfoMap()} when queried for a particular event tag. + * + */ +public final class EventValueDescription { + + /** + * Represents the type of a numerical value. This is used to display values of vastly different + * type/range in graphs. + */ + public enum ValueType { + NOT_APPLICABLE(0), + OBJECTS(1), + BYTES(2), + MILLISECONDS(3), + ALLOCATIONS(4), + ID(5), + PERCENT(6); + + private int mValue; + + /** + * Checks that the {@link EventValueType} is compatible with the {@link ValueType}. + * @param type the {@link EventValueType} to check. + * @throws InvalidValueTypeException if the types are not compatible. + */ + public void checkType(EventValueType type) throws InvalidValueTypeException { + if ((type != EventValueType.INT && type != EventValueType.LONG) + && this != NOT_APPLICABLE) { + throw new InvalidValueTypeException( + String.format("%1$s doesn't support type %2$s", type, this)); + } + } + + /** + * Returns a {@link ValueType} from an integer value, or null if no match + * were found. + * @param value the integer value. + */ + public static ValueType getValueType(int value) { + for (ValueType type : values()) { + if (type.mValue == value) { + return type; + } + } + return null; + } + + /** + * Returns the integer value of the enum. + */ + public int getValue() { + return mValue; + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.US); + } + + ValueType(int value) { + mValue = value; + } + } + + private String mName; + private EventValueType mEventValueType; + private ValueType mValueType; + + /** + * Builds a {@link EventValueDescription} with a name and a type. + *

+ * If the type is {@link EventValueType#INT} or {@link EventValueType#LONG}, the + * {@link #mValueType} is set to {@link ValueType#BYTES} by default. It set to + * {@link ValueType#NOT_APPLICABLE} for all other {@link EventValueType} values. + * @param name + * @param type + */ + EventValueDescription(String name, EventValueType type) { + mName = name; + mEventValueType = type; + if (mEventValueType == EventValueType.INT || mEventValueType == EventValueType.LONG) { + mValueType = ValueType.BYTES; + } else { + mValueType = ValueType.NOT_APPLICABLE; + } + } + + /** + * Builds a {@link EventValueDescription} with a name and a type, and a {@link ValueType}. + *

+ * @param name + * @param type + * @param valueType + * @throws InvalidValueTypeException if type and valuetype are not compatible. + * + */ + EventValueDescription(String name, EventValueType type, ValueType valueType) + throws InvalidValueTypeException { + mName = name; + mEventValueType = type; + mValueType = valueType; + mValueType.checkType(mEventValueType); + } + + /** + * @return the Name. + */ + public String getName() { + return mName; + } + + /** + * @return the {@link EventValueType}. + */ + public EventValueType getEventValueType() { + return mEventValueType; + } + + /** + * @return the {@link ValueType}. + */ + public ValueType getValueType() { + return mValueType; + } + + @Override + public String toString() { + if (mValueType != ValueType.NOT_APPLICABLE) { + return String.format("%1$s (%2$s, %3$s)", mName, mEventValueType.toString(), + mValueType.toString()); + } + + return String.format("%1$s (%2$s)", mName, mEventValueType.toString()); + } + + /** + * Checks if the value is of the proper type for this receiver. + * @param value the value to check. + * @return true if the value is of the proper type for this receiver. + */ + public boolean checkForType(Object value) { + switch (mEventValueType) { + case INT: + return value instanceof Integer; + case LONG: + return value instanceof Long; + case STRING: + return value instanceof String; + case LIST: + return value instanceof Object[]; + } + + return false; + } + + /** + * Returns an object of a valid type (based on the value returned by + * {@link #getEventValueType()}) from a String value. + *

+ * IMPORTANT {@link EventValueType#LIST} and {@link EventValueType#TREE} are not + * supported. + * @param value the value of the object expressed as a string. + * @return an object or null if the conversion could not be done. + */ + public Object getObjectFromString(String value) { + switch (mEventValueType) { + case INT: + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + case LONG: + try { + return Long.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + case STRING: + return value; + } + + return null; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/GcEventContainer.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/GcEventContainer.java new file mode 100644 index 0000000..9fd8b59 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/GcEventContainer.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.LogReceiver.LogEntry; + +/** + * Custom Event Container for the Gc event since this event doesn't simply output data in + * int or long format, but encodes several values on 4 longs. + *

+ * The array of {@link EventValueDescription}s parsed from the "event-log-tags" file must + * be ignored, and instead, the array returned from {@link #getValueDescriptions()} must be used. + */ +final class GcEventContainer extends EventContainer { + + public static final int GC_EVENT_TAG = 20001; + + private String processId; + private long gcTime; + private long bytesFreed; + private long objectsFreed; + private long actualSize; + private long allowedSize; + private long softLimit; + private long objectsAllocated; + private long bytesAllocated; + private long zActualSize; + private long zAllowedSize; + private long zObjectsAllocated; + private long zBytesAllocated; + private long dlmallocFootprint; + private long mallinfoTotalAllocatedSpace; + private long externalLimit; + private long externalBytesAllocated; + + GcEventContainer(LogEntry entry, int tag, Object data) { + super(entry, tag, data); + init(data); + } + + GcEventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) { + super(tag, pid, tid, sec, nsec, data); + init(data); + } + + /** + * @param data + */ + private void init(Object data) { + if (data instanceof Object[]) { + Object[] values = (Object[])data; + for (int i = 0; i < values.length; i++) { + if (values[i] instanceof Long) { + parseDvmHeapInfo((Long)values[i], i); + } + } + } + } + + @Override + public EventValueType getType() { + return EventValueType.LIST; + } + + @Override + public boolean testValue(int index, Object value, CompareMethod compareMethod) + throws InvalidTypeException { + // do a quick easy check on the type. + if (index == 0) { + if (!(value instanceof String)) { + throw new InvalidTypeException(); + } + } else if (!(value instanceof Long)) { + throw new InvalidTypeException(); + } + + switch (compareMethod) { + case EQUAL_TO: + if (index == 0) { + return processId.equals(value); + } else { + return getValueAsLong(index) == (Long) value; + } + case LESSER_THAN: + return getValueAsLong(index) <= (Long) value; + case LESSER_THAN_STRICT: + return getValueAsLong(index) < (Long) value; + case GREATER_THAN: + return getValueAsLong(index) >= (Long) value; + case GREATER_THAN_STRICT: + return getValueAsLong(index) > (Long) value; + case BIT_CHECK: + return (getValueAsLong(index) & (Long) value) != 0; + } + + throw new ArrayIndexOutOfBoundsException(); + } + + @Override + public Object getValue(int valueIndex) { + if (valueIndex == 0) { + return processId; + } + + try { + return getValueAsLong(valueIndex); + } catch (InvalidTypeException e) { + // this would only happened if valueIndex was 0, which we test above. + } + + return null; + } + + @Override + public double getValueAsDouble(int valueIndex) throws InvalidTypeException { + return (double)getValueAsLong(valueIndex); + } + + @Override + public String getValueAsString(int valueIndex) { + switch (valueIndex) { + case 0: + return processId; + default: + try { + return Long.toString(getValueAsLong(valueIndex)); + } catch (InvalidTypeException e) { + // we shouldn't stop there since we test, in this method first. + } + } + + throw new ArrayIndexOutOfBoundsException(); + } + + /** + * Returns a custom array of {@link EventValueDescription} since the actual content of this + * event (list of (long, long) does not match the values encoded into those longs. + */ + static EventValueDescription[] getValueDescriptions() { + try { + return new EventValueDescription[] { + new EventValueDescription("Process Name", EventValueType.STRING), + new EventValueDescription("GC Time", EventValueType.LONG, + ValueType.MILLISECONDS), + new EventValueDescription("Freed Objects", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Freed Bytes", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Soft Limit", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Actual Size (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allowed Size (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allocated Objects (aggregate)", + EventValueType.LONG, ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Actual Size", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Allowed Size", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Allocated Objects", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Actual Size (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allowed Size (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allocated Objects (zygote)", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("External Allocation Limit", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("External Bytes Allocated", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("dlmalloc Footprint", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Malloc Info: Total Allocated Space", + EventValueType.LONG, ValueType.BYTES), + }; + } catch (InvalidValueTypeException e) { + // this shouldn't happen since we control manual the EventValueType and the ValueType + // values. For development purpose, we assert if this happens. + assert false; + } + + // this shouldn't happen, but the compiler complains otherwise. + return null; + } + + private void parseDvmHeapInfo(long data, int index) { + switch (index) { + case 0: + // [63 ] Must be zero + // [62-24] ASCII process identifier + // [23-12] GC time in ms + // [11- 0] Bytes freed + + gcTime = float12ToInt((int)((data >> 12) & 0xFFFL)); + bytesFreed = float12ToInt((int)(data & 0xFFFL)); + + // convert the long into an array, in the proper order so that we can convert the + // first 5 char into a string. + byte[] dataArray = new byte[8]; + put64bitsToArray(data, dataArray, 0); + + // get the name from the string + processId = new String(dataArray, 0, 5); + break; + case 1: + // [63-62] 10 + // [61-60] Reserved; must be zero + // [59-48] Objects freed + // [47-36] Actual size (current footprint) + // [35-24] Allowed size (current hard max) + // [23-12] Objects allocated + // [11- 0] Bytes allocated + objectsFreed = float12ToInt((int)((data >> 48) & 0xFFFL)); + actualSize = float12ToInt((int)((data >> 36) & 0xFFFL)); + allowedSize = float12ToInt((int)((data >> 24) & 0xFFFL)); + objectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL)); + bytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + case 2: + // [63-62] 11 + // [61-60] Reserved; must be zero + // [59-48] Soft limit (current soft max) + // [47-36] Actual size (current footprint) + // [35-24] Allowed size (current hard max) + // [23-12] Objects allocated + // [11- 0] Bytes allocated + softLimit = float12ToInt((int)((data >> 48) & 0xFFFL)); + zActualSize = float12ToInt((int)((data >> 36) & 0xFFFL)); + zAllowedSize = float12ToInt((int)((data >> 24) & 0xFFFL)); + zObjectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL)); + zBytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + case 3: + // [63-48] Reserved; must be zero + // [47-36] dlmallocFootprint + // [35-24] mallinfo: total allocated space + // [23-12] External byte limit + // [11- 0] External bytes allocated + dlmallocFootprint = float12ToInt((int)((data >> 36) & 0xFFFL)); + mallinfoTotalAllocatedSpace = float12ToInt((int)((data >> 24) & 0xFFFL)); + externalLimit = float12ToInt((int)((data >> 12) & 0xFFFL)); + externalBytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + default: + break; + } + } + + /** + * Converts a 12 bit float representation into an unsigned int (returned as a long) + * @param f12 + */ + private static long float12ToInt(int f12) { + return (f12 & 0x1FF) << ((f12 >>> 9) * 4); + } + + /** + * puts an unsigned value in an array. + * @param value The value to put. + * @param dest the destination array + * @param offset the offset in the array where to put the value. + * Array length must be at least offset + 8 + */ + private static void put64bitsToArray(long value, byte[] dest, int offset) { + dest[offset + 7] = (byte)(value & 0x00000000000000FFL); + dest[offset + 6] = (byte)((value & 0x000000000000FF00L) >> 8); + dest[offset + 5] = (byte)((value & 0x0000000000FF0000L) >> 16); + dest[offset + 4] = (byte)((value & 0x00000000FF000000L) >> 24); + dest[offset + 3] = (byte)((value & 0x000000FF00000000L) >> 32); + dest[offset + 2] = (byte)((value & 0x0000FF0000000000L) >> 40); + dest[offset + 1] = (byte)((value & 0x00FF000000000000L) >> 48); + dest[offset + 0] = (byte)((value & 0xFF00000000000000L) >> 56); + } + + /** + * Returns the long value of the valueIndex-th value. + * @param valueIndex the index of the value. + * @throws InvalidTypeException if index is 0 as it is a string value. + */ + private long getValueAsLong(int valueIndex) throws InvalidTypeException { + switch (valueIndex) { + case 0: + throw new InvalidTypeException(); + case 1: + return gcTime; + case 2: + return objectsFreed; + case 3: + return bytesFreed; + case 4: + return softLimit; + case 5: + return actualSize; + case 6: + return allowedSize; + case 7: + return objectsAllocated; + case 8: + return bytesAllocated; + case 9: + return actualSize - zActualSize; + case 10: + return allowedSize - zAllowedSize; + case 11: + return objectsAllocated - zObjectsAllocated; + case 12: + return bytesAllocated - zBytesAllocated; + case 13: + return zActualSize; + case 14: + return zAllowedSize; + case 15: + return zObjectsAllocated; + case 16: + return zBytesAllocated; + case 17: + return externalLimit; + case 18: + return externalBytesAllocated; + case 19: + return dlmallocFootprint; + case 20: + return mallinfoTotalAllocatedSpace; + } + + throw new ArrayIndexOutOfBoundsException(); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidTypeException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidTypeException.java new file mode 100644 index 0000000..016f8aa --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidTypeException.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import java.io.Serializable; + +/** + * Exception thrown when accessing an {@link EventContainer} value with the wrong type. + */ +public final class InvalidTypeException extends Exception { + + /** + * Needed by {@link Serializable}. + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the default detail message. + * @see java.lang.Exception + */ + public InvalidTypeException() { + super("Invalid Type"); + } + + /** + * Constructs a new exception with the specified detail message. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @see java.lang.Exception + */ + public InvalidTypeException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * (cause==null ? null : cause.toString()) (which typically contains + * the class and detail message of cause). + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidTypeException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidValueTypeException.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidValueTypeException.java new file mode 100644 index 0000000..a3050c8 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/InvalidValueTypeException.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventValueDescription.ValueType; + +import java.io.Serializable; + +/** + * Exception thrown when associating an {@link EventValueType} with an incompatible + * {@link ValueType}. + */ +public final class InvalidValueTypeException extends Exception { + + /** + * Needed by {@link Serializable}. + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the default detail message. + * @see java.lang.Exception + */ + public InvalidValueTypeException() { + super("Invalid Type"); + } + + /** + * Constructs a new exception with the specified detail message. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @see java.lang.Exception + */ + public InvalidValueTypeException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * (cause==null ? null : cause.toString()) (which typically contains + * the class and detail message of cause). + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidValueTypeException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidValueTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/LogReceiver.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/LogReceiver.java new file mode 100644 index 0000000..4ef5c62 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/log/LogReceiver.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + + +import com.android.ddmlib.utils.ArrayHelper; + +import java.security.InvalidParameterException; + +/** + * Receiver able to provide low level parsing for device-side log services. + */ +public final class LogReceiver { + + private static final int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry. + + /** + * Represents a log entry and its raw data. + */ + public static final class LogEntry { + /* + * See //device/include/utils/logger.h + */ + /** 16bit unsigned: length of the payload. */ + public int len; /* This is normally followed by a 16 bit padding */ + /** pid of the process that generated this {@link LogEntry} */ + public int pid; + /** tid of the process that generated this {@link LogEntry} */ + public int tid; + /** Seconds since epoch. */ + public int sec; + /** nanoseconds. */ + public int nsec; + /** The entry's raw data. */ + public byte[] data; + } + + /** + * Classes which implement this interface provide a method that deals + * with {@link LogEntry} objects coming from log service through a {@link LogReceiver}. + *

This interface provides two methods. + *

    + *
  • {@link #newEntry(com.android.ddmlib.log.LogReceiver.LogEntry)} provides a + * first level of parsing, extracting {@link LogEntry} objects out of the log service output.
  • + *
  • {@link #newData(byte[], int, int)} provides a way to receive the raw information + * coming directly from the log service.
  • + *
+ */ + public interface ILogListener { + /** + * Sent when a new {@link LogEntry} has been parsed by the {@link LogReceiver}. + * @param entry the new log entry. + */ + void newEntry(LogEntry entry); + + /** + * Sent when new raw data is coming from the log service. + * @param data the raw data buffer. + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + void newData(byte[] data, int offset, int length); + } + + /** Current {@link LogEntry} being read, before sending it to the listener. */ + private LogEntry mCurrentEntry; + + /** Temp buffer to store partial entry headers. */ + private byte[] mEntryHeaderBuffer = new byte[ENTRY_HEADER_SIZE]; + /** Offset in the partial header buffer */ + private int mEntryHeaderOffset = 0; + /** Offset in the partial entry data */ + private int mEntryDataOffset = 0; + + /** Listener waiting for receive fully read {@link LogEntry} objects */ + private ILogListener mListener; + + private boolean mIsCancelled = false; + + /** + * Creates a {@link LogReceiver} with an {@link ILogListener}. + *

+ * The {@link ILogListener} will receive new log entries as they are parsed, in the form + * of {@link LogEntry} objects. + * @param listener the listener to receive new log entries. + */ + public LogReceiver(ILogListener listener) { + mListener = listener; + } + + + /** + * Parses new data coming from the log service. + * @param data the data buffer + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + public void parseNewData(byte[] data, int offset, int length) { + // notify the listener of new raw data + if (mListener != null) { + mListener.newData(data, offset, length); + } + + // loop while there is still data to be read and the receiver has not be cancelled. + while (length > 0 && !mIsCancelled) { + // first check if we have no current entry. + if (mCurrentEntry == null) { + if (mEntryHeaderOffset + length < ENTRY_HEADER_SIZE) { + // if we don't have enough data to finish the header, save + // the data we have and return + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, length); + mEntryHeaderOffset += length; + return; + } else { + // we have enough to fill the header, let's do it. + // did we store some part at the beginning of the header? + if (mEntryHeaderOffset != 0) { + // copy the rest of the entry header into the header buffer + int size = ENTRY_HEADER_SIZE - mEntryHeaderOffset; + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, + size); + + // create the entry from the header buffer + mCurrentEntry = createEntry(mEntryHeaderBuffer, 0); + + // since we used the whole entry header buffer, we reset the offset + mEntryHeaderOffset = 0; + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += size; + length -= size; + } else { + // create the entry directly from the data array + mCurrentEntry = createEntry(data, offset); + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += ENTRY_HEADER_SIZE; + length -= ENTRY_HEADER_SIZE; + } + } + } + + // at this point, we have an entry, and offset/length have been updated to skip + // the entry header. + + // if we have enough data for this entry or more, we'll need to end this entry + if (length >= mCurrentEntry.len - mEntryDataOffset) { + // compute and save the size of the data that we have to read for this entry, + // based on how much we may already have read. + int dataSize = mCurrentEntry.len - mEntryDataOffset; + + // we only read what we need, and put it in the entry buffer. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, dataSize); + + // notify the listener of a new entry + if (mListener != null) { + mListener.newEntry(mCurrentEntry); + } + + // reset some flags: we have read 0 data of the current entry. + // and we have no current entry being read. + mEntryDataOffset = 0; + mCurrentEntry = null; + + // and update the data buffer info to the end of the current entry / start + // of the next one. + offset += dataSize; + length -= dataSize; + } else { + // we don't have enough data to fill this entry, so we store what we have + // in the entry itself. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, length); + + // save the amount read for the data. + mEntryDataOffset += length; + return; + } + } + } + + /** + * Returns whether this receiver is canceling the remote service. + */ + public boolean isCancelled() { + return mIsCancelled; + } + + /** + * Cancels the current remote service. + */ + public void cancel() { + mIsCancelled = true; + } + + /** + * Creates a {@link LogEntry} from the array of bytes. This expects the data buffer size + * to be at least offset + {@link #ENTRY_HEADER_SIZE}. + * @param data the data buffer the entry is read from. + * @param offset the offset of the first byte from the buffer representing the entry. + * @return a new {@link LogEntry} or null if some error happened. + */ + private LogEntry createEntry(byte[] data, int offset) { + if (data.length < offset + ENTRY_HEADER_SIZE) { + throw new InvalidParameterException( + "Buffer not big enough to hold full LoggerEntry header"); + } + + // create the new entry and fill it. + LogEntry entry = new LogEntry(); + entry.len = ArrayHelper.swapU16bitFromArray(data, offset); + + // we've read only 16 bits, but since there's also a 16 bit padding, + // we can skip right over both. + offset += 4; + + entry.pid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.tid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.sec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.nsec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + + // allocate the data + entry.data = new byte[entry.len]; + + return entry; + } + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatFilter.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatFilter.java new file mode 100644 index 0000000..34fdc38 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatFilter.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.logcat; + +import com.android.annotations.NonNull; +import com.android.ddmlib.Log.LogLevel; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A Filter for logcat messages. A filter can be constructed to match + * different fields of a logcat message. It can then be queried to see if + * a message matches the filter's settings. + */ +public final class LogCatFilter { + private static final String PID_KEYWORD = "pid:"; //$NON-NLS-1$ + private static final String APP_KEYWORD = "app:"; //$NON-NLS-1$ + private static final String TAG_KEYWORD = "tag:"; //$NON-NLS-1$ + private static final String TEXT_KEYWORD = "text:"; //$NON-NLS-1$ + + private final String mName; + private final String mTag; + private final String mText; + private final String mPid; + private final String mAppName; + private final LogLevel mLogLevel; + + private boolean mCheckPid; + private boolean mCheckAppName; + private boolean mCheckTag; + private boolean mCheckText; + + private Pattern mAppNamePattern; + private Pattern mTagPattern; + private Pattern mTextPattern; + + /** + * Construct a filter with the provided restrictions for the logcat message. All the text + * fields accept Java regexes as input, but ignore invalid regexes. + * @param name name for the filter + * @param tag value for the logcat message's tag field. + * @param text value for the logcat message's text field. + * @param pid value for the logcat message's pid field. + * @param appName value for the logcat message's app name field. + * @param logLevel value for the logcat message's log level. Only messages of + * higher priority will be accepted by the filter. + */ + public LogCatFilter(@NonNull String name, @NonNull String tag, @NonNull String text, + @NonNull String pid, @NonNull String appName, @NonNull LogLevel logLevel) { + mName = name.trim(); + mTag = tag.trim(); + mText = text.trim(); + mPid = pid.trim(); + mAppName = appName.trim(); + mLogLevel = logLevel; + + mCheckPid = !mPid.isEmpty(); + + if (!mAppName.isEmpty()) { + try { + mAppNamePattern = Pattern.compile(mAppName, getPatternCompileFlags(mAppName)); + mCheckAppName = true; + } catch (PatternSyntaxException e) { + mCheckAppName = false; + } + } + + if (!mTag.isEmpty()) { + try { + mTagPattern = Pattern.compile(mTag, getPatternCompileFlags(mTag)); + mCheckTag = true; + } catch (PatternSyntaxException e) { + mCheckTag = false; + } + } + + if (!mText.isEmpty()) { + try { + mTextPattern = Pattern.compile(mText, getPatternCompileFlags(mText)); + mCheckText = true; + } catch (PatternSyntaxException e) { + mCheckText = false; + } + } + } + + /** + * Obtain the flags to pass to {@link Pattern#compile(String, int)}. This method + * tries to figure out whether case sensitive matching should be used. It is based on + * the following heuristic: if the regex has an upper case character, then the match + * will be case sensitive. Otherwise it will be case insensitive. + */ + private int getPatternCompileFlags(String regex) { + for (char c : regex.toCharArray()) { + if (Character.isUpperCase(c)) { + return 0; + } + } + + return Pattern.CASE_INSENSITIVE; + } + + /** + * Construct a list of {@link LogCatFilter} objects by decoding the query. + * @param query encoded search string. The query is simply a list of words (can be regexes) + * a user would type in a search bar. These words are searched for in the text field of + * each collected logcat message. To search in a different field, the word could be prefixed + * with a keyword corresponding to the field name. Currently, the following keywords are + * supported: "pid:", "tag:" and "text:". Invalid regexes are ignored. + * @param minLevel minimum log level to match + * @return list of filter settings that fully match the given query + */ + public static List fromString(String query, LogLevel minLevel) { + List filterSettings = new ArrayList(); + + for (String s : query.trim().split(" ")) { + String tag = ""; + String text = ""; + String pid = ""; + String app = ""; + + if (s.startsWith(PID_KEYWORD)) { + pid = s.substring(PID_KEYWORD.length()); + } else if (s.startsWith(APP_KEYWORD)) { + app = s.substring(APP_KEYWORD.length()); + } else if (s.startsWith(TAG_KEYWORD)) { + tag = s.substring(TAG_KEYWORD.length()); + } else { + if (s.startsWith(TEXT_KEYWORD)) { + text = s.substring(TEXT_KEYWORD.length()); + } else { + text = s; + } + } + filterSettings.add(new LogCatFilter("livefilter-" + s, + tag, text, pid, app, minLevel)); + } + + return filterSettings; + } + + @NonNull + public String getName() { + return mName; + } + + @NonNull + public String getTag() { + return mTag; + } + + @NonNull + public String getText() { + return mText; + } + + @NonNull + public String getPid() { + return mPid; + } + + @NonNull + public String getAppName() { + return mAppName; + } + + @NonNull + public LogLevel getLogLevel() { + return mLogLevel; + } + + /** + * Check whether a given message will make it through this filter. + * @param m message to check + * @return true if the message matches the filter's conditions. + */ + public boolean matches(LogCatMessage m) { + /* filter out messages of a lower priority */ + if (m.getLogLevel().getPriority() < mLogLevel.getPriority()) { + return false; + } + + /* if pid filter is enabled, filter out messages whose pid does not match + * the filter's pid */ + if (mCheckPid && !m.getPid().equals(mPid)) { + return false; + } + + /* if app name filter is enabled, filter out messages not matching the app name */ + if (mCheckAppName) { + Matcher matcher = mAppNamePattern.matcher(m.getAppName()); + if (!matcher.find()) { + return false; + } + } + + /* if tag filter is enabled, filter out messages not matching the tag */ + if (mCheckTag) { + Matcher matcher = mTagPattern.matcher(m.getTag()); + if (!matcher.find()) { + return false; + } + } + + if (mCheckText) { + Matcher matcher = mTextPattern.matcher(m.getMessage()); + if (!matcher.find()) { + return false; + } + } + + return true; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatListener.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatListener.java new file mode 100644 index 0000000..2050402 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatListener.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.logcat; + +import java.util.List; + +public interface LogCatListener { + void log(List msgList); +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessage.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessage.java new file mode 100644 index 0000000..bca1df0 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessage.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.logcat; + +import com.android.annotations.NonNull; +import com.android.ddmlib.Log.LogLevel; + +/** + * Model a single log message output from {@code logcat -v long}. + * A logcat message has a {@link LogLevel}, the pid (process id) of the process + * generating the message, the time at which the message was generated, and + * the tag and message itself. + */ +public final class LogCatMessage { + private final LogLevel mLogLevel; + private final String mPid; + private final String mTid; + private final String mAppName; + private final String mTag; + private final String mTime; + private final String mMessage; + + /** + * Construct an immutable log message object. + */ + public LogCatMessage(@NonNull LogLevel logLevel, @NonNull String pid, @NonNull String tid, + @NonNull String appName, @NonNull String tag, + @NonNull String time, @NonNull String msg) { + mLogLevel = logLevel; + mPid = pid; + mAppName = appName; + mTag = tag; + mTime = time; + mMessage = msg; + + long tidValue; + try { + // Thread id's may be in hex on some platforms. + // Decode and store them in radix 10. + tidValue = Long.decode(tid.trim()); + } catch (NumberFormatException e) { + tidValue = -1; + } + + mTid = Long.toString(tidValue); + } + + @NonNull + public LogLevel getLogLevel() { + return mLogLevel; + } + + @NonNull + public String getPid() { + return mPid; + } + + @NonNull + public String getTid() { + return mTid; + } + + @NonNull + public String getAppName() { + return mAppName; + } + + @NonNull + public String getTag() { + return mTag; + } + + @NonNull + public String getTime() { + return mTime; + } + + @NonNull + public String getMessage() { + return mMessage; + } + + @Override + public String toString() { + return mTime + ": " + + mLogLevel.getPriorityLetter() + "/" + + mTag + "(" + + mPid + "): " + + mMessage; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessageParser.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessageParser.java new file mode 100644 index 0000000..0e8b03c --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessageParser.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.logcat; + +import com.android.annotations.NonNull; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log.LogLevel; +import com.google.common.primitives.Ints; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class to parse raw output of {@code adb logcat -v long} to {@link LogCatMessage} objects. + */ +public final class LogCatMessageParser { + private LogLevel mCurLogLevel = LogLevel.WARN; + private String mCurPid = "?"; + private String mCurTid = "?"; + private String mCurTag = "?"; + private String mCurTime = "?:??"; + + /** + * This pattern is meant to parse the first line of a log message with the option + * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the + * following lines are the message (can be several lines).
+ * This first line looks something like:
+ * {@code "[ 00-00 00:00:00.000 :0x /]"} + *
+ * Note: severity is one of V, D, I, W, E, A? or F. However, there doesn't seem to be + * a way to actually generate an A (assert) message. Log.wtf is supposed to generate + * a message with severity A, however it generates the undocumented F level. In + * such a case, the parser will change the level from F to A.
+ * Note: the fraction of second value can have any number of digit.
+ * Note: the tag should be trimmed as it may have spaces at the end. + */ + private static final Pattern sLogHeaderPattern = Pattern.compile( + "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + + "\\s+(\\d*):\\s*(\\S+)\\s([VDIWEAF])/(.*)\\]$"); + + /** + * Parse a list of strings into {@link LogCatMessage} objects. This method + * maintains state from previous calls regarding the last seen header of + * logcat messages. + * @param lines list of raw strings obtained from logcat -v long + * @param device device from which these log messages have been received + * @return list of LogMessage objects parsed from the input + */ + @NonNull + public List processLogLines(String[] lines, IDevice device) { + List messages = new ArrayList(lines.length); + + for (String line : lines) { + if (line.isEmpty()) { + continue; + } + + Matcher matcher = sLogHeaderPattern.matcher(line); + if (matcher.matches()) { + mCurTime = matcher.group(1); + mCurPid = matcher.group(2); + mCurTid = matcher.group(3); + mCurLogLevel = LogLevel.getByLetterString(matcher.group(4)); + mCurTag = matcher.group(5).trim(); + + /* LogLevel doesn't support messages with severity "F". Log.wtf() is supposed + * to generate "A", but generates "F". */ + if (mCurLogLevel == null && matcher.group(4).equals("F")) { + mCurLogLevel = LogLevel.ASSERT; + } + } else { + String pkgName = ""; //$NON-NLS-1$ + Integer pid = Ints.tryParse(mCurPid); + if (pid != null && device != null) { + pkgName = device.getClientName(pid); + } + LogCatMessage m = new LogCatMessage(mCurLogLevel, mCurPid, mCurTid, + pkgName, mCurTag, mCurTime, line); + messages.add(m); + } + } + + return messages; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatReceiverTask.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatReceiverTask.java new file mode 100644 index 0000000..b5fd36e --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatReceiverTask.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.logcat; + +import com.android.annotations.NonNull; +import com.android.annotations.concurrency.GuardedBy; +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class LogCatReceiverTask implements Runnable { + private static final String LOGCAT_COMMAND = "logcat -v long"; //$NON-NLS-1$ + private static final int DEVICE_POLL_INTERVAL_MSEC = 1000; + + private static final LogCatMessage sDeviceDisconnectedMsg = + errorMessage("Device disconnected: 1"); + private static final LogCatMessage sConnectionTimeoutMsg = + errorMessage("LogCat Connection timed out"); + private static final LogCatMessage sConnectionErrorMsg = + errorMessage("LogCat Connection error"); + + private final IDevice mDevice; + private final LogCatOutputReceiver mReceiver; + private final LogCatMessageParser mParser; + private final AtomicBoolean mCancelled; + + @GuardedBy("this") + private final Set mListeners = new HashSet(); + + public LogCatReceiverTask(@NonNull IDevice device) { + mDevice = device; + + mReceiver = new LogCatOutputReceiver(); + mParser = new LogCatMessageParser(); + mCancelled = new AtomicBoolean(); + } + + @Override + public void run() { + // wait while device comes online + while (!mDevice.isOnline()) { + try { + Thread.sleep(DEVICE_POLL_INTERVAL_MSEC); + } catch (InterruptedException e) { + return; + } + } + + try { + mDevice.executeShellCommand(LOGCAT_COMMAND, mReceiver, 0); + } catch (TimeoutException e) { + notifyListeners(Collections.singletonList(sConnectionTimeoutMsg)); + } catch (AdbCommandRejectedException ignored) { + // will not be thrown as long as the shell supports logcat + } catch (ShellCommandUnresponsiveException ignored) { + // this will not be thrown since the last argument is 0 + } catch (IOException e) { + notifyListeners(Collections.singletonList(sConnectionErrorMsg)); + } + + notifyListeners(Collections.singletonList(sDeviceDisconnectedMsg)); + } + + public void stop() { + mCancelled.set(true); + } + + private class LogCatOutputReceiver extends MultiLineReceiver { + public LogCatOutputReceiver() { + setTrimLine(false); + } + + /** Implements {@link IShellOutputReceiver#isCancelled() }. */ + @Override + public boolean isCancelled() { + return mCancelled.get(); + } + + @Override + public void processNewLines(String[] lines) { + if (!mCancelled.get()) { + processLogLines(lines); + } + } + + private void processLogLines(String[] lines) { + List newMessages = mParser.processLogLines(lines, mDevice); + if (!newMessages.isEmpty()) { + notifyListeners(newMessages); + } + } + } + + public synchronized void addLogCatListener(LogCatListener l) { + mListeners.add(l); + } + + public synchronized void removeLogCatListener(LogCatListener l) { + mListeners.remove(l); + } + + private synchronized void notifyListeners(List messages) { + for (LogCatListener l: mListeners) { + l.log(messages); + } + } + + private static LogCatMessage errorMessage(String msg) { + return new LogCatMessage(LogLevel.ERROR, "", "", "", "", "", msg); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/IRemoteAndroidTestRunner.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/IRemoteAndroidTestRunner.java new file mode 100644 index 0000000..b401c12 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/IRemoteAndroidTestRunner.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; + +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +/** + * Interface for running a Android test command remotely and reporting result to a listener. + */ +public interface IRemoteAndroidTestRunner { + + enum TestSize { + /** Run tests annotated with SmallTest */ + SMALL("small"), + /** Run tests annotated with MediumTest */ + MEDIUM("medium"), + /** Run tests annotated with LargeTest */ + LARGE("large"); + + private String mRunnerValue; + + /** + * Create a {@link TestSize}. + * + * @param runnerValue the {@link String} value that represents the size that is passed to + * device. Defined on device in android.test.InstrumentationTestRunner. + */ + TestSize(String runnerValue) { + mRunnerValue = runnerValue; + } + + String getRunnerValue() { + return mRunnerValue; + } + + /** + * Return the {@link TestSize} corresponding to the given Android platform defined value. + * + * @throws IllegalArgumentException if {@link TestSize} cannot be found. + */ + public static TestSize getTestSize(String value) { + // build the error message in the success case too, to avoid two for loops + StringBuilder msgBuilder = new StringBuilder("Unknown TestSize "); + msgBuilder.append(value); + msgBuilder.append(", Must be one of "); + for (TestSize size : values()) { + if (size.getRunnerValue().equals(value)) { + return size; + } + msgBuilder.append(size.getRunnerValue()); + msgBuilder.append(", "); + } + throw new IllegalArgumentException(msgBuilder.toString()); + } + } + + /** + * Returns the application package name. + */ + String getPackageName(); + + /** + * Returns the runnerName. + */ + String getRunnerName(); + + /** + * Sets to run only tests in this class + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) + */ + void setClassName(String className); + + /** + * Sets to run only tests in the provided classes + * Must be called before 'run'. + *

+ * If providing more than one class, requires a InstrumentationTestRunner that supports + * the multiple class argument syntax. + * + * @param classNames array of fully qualified class names (eg x.y.z) + */ + void setClassNames(String[] classNames); + + /** + * Sets to run only specified test method + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) + * @param testName method name + */ + void setMethodName(String className, String testName); + + /** + * Sets to run all tests in specified package + * Must be called before 'run'. + * + * @param packageName fully qualified package name (eg x.y.z) + */ + void setTestPackageName(String packageName); + + /** + * Sets to run only tests of given size. + * Must be called before 'run'. + * + * @param size the {@link TestSize} to run. + */ + void setTestSize(TestSize size); + + /** + * Adds a argument to include in instrumentation command. + *

+ * Must be called before 'run'. If an argument with given name has already been provided, it's + * value will be overridden. + * + * @param name the name of the instrumentation bundle argument + * @param value the value of the argument + */ + void addInstrumentationArg(String name, String value); + + /** + * Removes a previously added argument. + * + * @param name the name of the instrumentation bundle argument to remove + */ + void removeInstrumentationArg(String name); + + /** + * Adds a boolean argument to include in instrumentation command. + *

+ * @see RemoteAndroidTestRunner#addInstrumentationArg + * + * @param name the name of the instrumentation bundle argument + * @param value the value of the argument + */ + void addBooleanArg(String name, boolean value); + + /** + * Sets this test run to log only mode - skips test execution. + */ + void setLogOnly(boolean logOnly); + + /** + * Sets this debug mode of this test run. If true, the Android test runner will wait for a + * debugger to attach before proceeding with test execution. + */ + void setDebug(boolean debug); + + /** + * Sets this code coverage mode of this test run. + */ + void setCoverage(boolean coverage); + + /** + * Sets this test run to test collection mode. If true, will skip test execution and will set + * all appropriate runner arguments required for a successful test collection. + */ + void setTestCollection(boolean collection); + + /** + * @deprecated Use {@link #setMaxTimeToOutputResponse(long, java.util.concurrent.TimeUnit)}. + */ + @Deprecated + void setMaxtimeToOutputResponse(int maxTimeToOutputResponse); + + /** + * Sets the maximum time allowed between output of the shell command running the tests on + * the devices. + *

+ * This allows setting a timeout in case the tests can become stuck and never finish. This is + * different from the normal timeout on the connection. + *

+ * By default no timeout will be specified. + * + * @param maxTimeToOutputResponse the maximum amount of time during which the command is allowed + * to not output any response. A value of 0 means the method will wait forever + * (until the receiver cancels the execution) for command output and + * never throw. + * @param maxTimeUnits Units for non-zero {@code maxTimeToOutputResponse} values. + * + * @see IDevice#executeShellCommand(String, com.android.ddmlib.IShellOutputReceiver, int) + */ + void setMaxTimeToOutputResponse(long maxTimeToOutputResponse, TimeUnit maxTimeUnits); + + /** + * Set a custom run name to be reported to the {@link ITestRunListener} on {@link #run} + *

+ * If unspecified, will use package name + * + * @param runName + */ + void setRunName(String runName); + + /** + * Execute this test run. + *

+ * Convenience method for {@link #run(Collection)}. + * + * @param listeners listens for test results + * @throws TimeoutException in case of a timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws ShellCommandUnresponsiveException if the device did not output any test result for + * a period longer than the max time to output. + * @throws IOException if connection to device was lost. + * + * @see #setMaxtimeToOutputResponse(int) + */ + void run(ITestRunListener... listeners) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; + + /** + * Execute this test run. + * + * @param listeners collection of listeners for test results + * @throws TimeoutException in case of a timeout on the connection. + * @throws AdbCommandRejectedException if adb rejects the command + * @throws ShellCommandUnresponsiveException if the device did not output any test result for + * a period longer than the max time to output. + * @throws IOException if connection to device was lost. + * + * @see #setMaxtimeToOutputResponse(int) + */ + void run(Collection listeners) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; + + /** + * Requests cancellation of this test run. + */ + void cancel(); + +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/ITestRunListener.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/ITestRunListener.java new file mode 100644 index 0000000..9c620e6 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/ITestRunListener.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import java.util.Map; + +/** + * Receives event notifications during instrumentation test runs. + *

+ * Patterned after org.junit.runner.notification.RunListener + *

+ * The sequence of calls will be: + *

    + *
  • testRunStarted + *
  • testStarted + *
  • [testFailed] + *
  • [testAssumptionFailure] + *
  • [testIgnored] + *
  • testEnded + *
  • .... + *
  • [testRunFailed] + *
  • testRunEnded + *
+ */ +public interface ITestRunListener { + + /** + * Reports the start of a test run. + * + * @param runName the test run name + * @param testCount total number of tests in test run + */ + void testRunStarted(String runName, int testCount); + + /** + * Reports the start of an individual test case. + * + * @param test identifies the test + */ + void testStarted(TestIdentifier test); + + /** + * Reports the failure of a individual test case. + *

+ * Will be called between testStarted and testEnded. + * + * @param test identifies the test + * @param trace stack trace of failure + */ + void testFailed(TestIdentifier test, String trace); + + /** + * Called when an atomic test flags that it assumes a condition that is + * false + * + * @param test identifies the test + * @param trace stack trace of failure + */ + void testAssumptionFailure(TestIdentifier test, String trace); + + /** + * Called when a test will not be run, generally because a test method is annotated + * with org.junit.Ignore. + * + * @param test identifies the test + */ + void testIgnored(TestIdentifier test); + + /** + * Reports the execution end of an individual test case. + *

+ * If {@link #testFailed} was not invoked, this test passed. Also returns any key/value + * metrics which may have been emitted during the test case's execution. + * + * @param test identifies the test + * @param testMetrics a {@link Map} of the metrics emitted + */ + void testEnded(TestIdentifier test, Map testMetrics); + + /** + * Reports test run failed to complete due to a fatal error. + * + * @param errorMessage {@link String} describing reason for run failure. + */ + void testRunFailed(String errorMessage); + + /** + * Reports test run stopped before completion due to a user request. + *

+ * TODO: currently unused, consider removing + * + * @param elapsedTime device reported elapsed time, in milliseconds + */ + void testRunStopped(long elapsedTime); + + /** + * Reports end of test run. + * + * @param elapsedTime device reported elapsed time, in milliseconds + * @param runMetrics key-value pairs reported at the end of a test run + */ + void testRunEnded(long elapsedTime, Map runMetrics); +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/InstrumentationResultParser.java new file mode 100644 index 0000000..40c9971 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/InstrumentationResultParser.java @@ -0,0 +1,629 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a + * ITestRunListener of the results. + * + *

Expects the following output: + * + *

If fatal error occurred when attempted to run the tests: + *

+ * INSTRUMENTATION_STATUS: Error=error Message
+ * INSTRUMENTATION_FAILED:
+ * 
+ *

or + *

+ * INSTRUMENTATION_RESULT: shortMsg=error Message
+ * 
+ * + *

Otherwise, expect a series of test results, each one containing a set of status key/value + * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test + * run, expects that the elapsed test time in seconds will be displayed + * + *

For example: + *

+ * INSTRUMENTATION_STATUS_CODE: 1
+ * INSTRUMENTATION_STATUS: class=com.foo.FooTest
+ * INSTRUMENTATION_STATUS: test=testFoo
+ * INSTRUMENTATION_STATUS: numtests=2
+ * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
+ *    com.foo.X
+ * INSTRUMENTATION_STATUS_CODE: -2
+ * ...
+ *
+ * Time: X
+ * 
+ *

Note that the "value" portion of the key-value pair may wrap over several text lines + */ +public class InstrumentationResultParser extends MultiLineReceiver { + + /** Relevant test status keys. */ + private static class StatusKeys { + private static final String TEST = "test"; + private static final String CLASS = "class"; + private static final String STACK = "stack"; + private static final String NUMTESTS = "numtests"; + private static final String ERROR = "Error"; + private static final String SHORTMSG = "shortMsg"; + } + + /** The set of expected status keys. Used to filter which keys should be stored as metrics */ + private static final Set KNOWN_KEYS = new HashSet(); + static { + KNOWN_KEYS.add(StatusKeys.TEST); + KNOWN_KEYS.add(StatusKeys.CLASS); + KNOWN_KEYS.add(StatusKeys.STACK); + KNOWN_KEYS.add(StatusKeys.NUMTESTS); + KNOWN_KEYS.add(StatusKeys.ERROR); + KNOWN_KEYS.add(StatusKeys.SHORTMSG); + // unused, but regularly occurring status keys. + KNOWN_KEYS.add("stream"); + KNOWN_KEYS.add("id"); + KNOWN_KEYS.add("current"); + } + + /** Test result status codes. */ + private static class StatusCodes { + private static final int START = 1; + private static final int IN_PROGRESS = 2; + + // codes used for test completed + private static final int ASSUMPTION_FAILURE = -4; + private static final int IGNORED = -3; + private static final int FAILURE = -2; + private static final int ERROR = -1; + private static final int OK = 0; + } + + /** Prefixes used to identify output. */ + private static class Prefixes { + private static final String STATUS = "INSTRUMENTATION_STATUS: "; + private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: "; + private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: "; + private static final String CODE = "INSTRUMENTATION_CODE: "; + private static final String RESULT = "INSTRUMENTATION_RESULT: "; + private static final String TIME_REPORT = "Time: "; + } + + private final Collection mTestListeners; + + /** + * Test result data + */ + private static class TestResult { + private Integer mCode = null; + private String mTestName = null; + private String mTestClass = null; + private String mStackTrace = null; + private Integer mNumTests = null; + + /** Returns true if all expected values have been parsed */ + boolean isComplete() { + return mCode != null && mTestName != null && mTestClass != null; + } + + /** Provides a more user readable string for TestResult, if possible */ + @Override + public String toString() { + StringBuilder output = new StringBuilder(); + if (mTestClass != null ) { + output.append(mTestClass); + output.append('#'); + } + if (mTestName != null) { + output.append(mTestName); + } + if (output.length() > 0) { + return output.toString(); + } + return "unknown result"; + } + } + + /** the name to provide to {@link ITestRunListener#testRunStarted(String, int)} */ + private final String mTestRunName; + + /** Stores the status values for the test result currently being parsed */ + private TestResult mCurrentTestResult = null; + + /** Stores the status values for the test result last parsed */ + private TestResult mLastTestResult = null; + + /** Stores the current "key" portion of the status key-value being parsed. */ + private String mCurrentKey = null; + + /** Stores the current "value" portion of the status key-value being parsed. */ + private StringBuilder mCurrentValue = null; + + /** True if start of test has already been reported to listener. */ + private boolean mTestStartReported = false; + + /** True if the completion of the test run has been detected. */ + private boolean mTestRunFinished = false; + + /** True if test run failure has already been reported to listener. */ + private boolean mTestRunFailReported = false; + + /** The elapsed time of the test run, in milliseconds. */ + private long mTestTime = 0; + + /** True if current test run has been canceled by user. */ + private boolean mIsCancelled = false; + + /** The number of tests currently run */ + private int mNumTestsRun = 0; + + /** The number of tests expected to run */ + private int mNumTestsExpected = 0; + + /** True if the parser is parsing a line beginning with "INSTRUMENTATION_RESULT" */ + private boolean mInInstrumentationResultKey = false; + + /** + * Stores key-value pairs under INSTRUMENTATION_RESULT header, these are printed at the + * end of a test run, if applicable + */ + private Map mInstrumentationResultBundle = new HashMap(); + + /** + * Stores key-value pairs of metrics emitted during the execution of each test case. Note that + * standard keys that are stored in the TestResults class are filtered out of this Map. + */ + private Map mTestMetrics = new HashMap(); + + private static final String LOG_TAG = "InstrumentationResultParser"; + + /** Error message supplied when no parseable test results are received from test run. */ + static final String NO_TEST_RESULTS_MSG = "No test results"; + + /** Error message supplied when a test start bundle is parsed, but not the test end bundle. */ + static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run to completion"; + static final String INCOMPLETE_TEST_ERR_MSG_POSTFIX = "Check device logcat for details"; + + /** Error message supplied when the test run is incomplete. */ + static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete"; + + /** + * Creates the InstrumentationResultParser. + * + * @param runName the test run name to provide to + * {@link ITestRunListener#testRunStarted(String, int)} + * @param listeners informed of test results as the tests are executing + */ + public InstrumentationResultParser(String runName, Collection listeners) { + mTestRunName = runName; + mTestListeners = new ArrayList(listeners); + } + + /** + * Creates the InstrumentationResultParser for a single listener. + * + * @param runName the test run name to provide to + * {@link ITestRunListener#testRunStarted(String, int)} + * @param listener informed of test results as the tests are executing + */ + public InstrumentationResultParser(String runName, ITestRunListener listener) { + this(runName, Collections.singletonList(listener)); + } + + /** + * Processes the instrumentation test output from shell. + * + * @see MultiLineReceiver#processNewLines + */ + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + parse(line); + // in verbose mode, dump all adb output to log + Log.v(LOG_TAG, line); + } + } + + /** + * Parse an individual output line. Expects a line that is one of: + *

    + *
  • + * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE), + * and thus there is a new key=value pair to parse, and the previous key-value pair is + * finished. + *
  • + *
  • + * A continuation of the previous status (the "value" portion of the key has wrapped + * to the next line). + *
  • + *
  • A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED)
  • + *
  • A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT)
  • + *
+ * + * @param line Text output line + */ + private void parse(String line) { + if (line.startsWith(Prefixes.STATUS_CODE)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + mInInstrumentationResultKey = false; + parseStatusCode(line); + } else if (line.startsWith(Prefixes.STATUS)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + mInInstrumentationResultKey = false; + parseKey(line, Prefixes.STATUS.length()); + } else if (line.startsWith(Prefixes.RESULT)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + mInInstrumentationResultKey = true; + parseKey(line, Prefixes.RESULT.length()); + } else if (line.startsWith(Prefixes.STATUS_FAILED) || + line.startsWith(Prefixes.CODE)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + mInInstrumentationResultKey = false; + // these codes signal the end of the instrumentation run + mTestRunFinished = true; + // just ignore the remaining data on this line + } else if (line.startsWith(Prefixes.TIME_REPORT)) { + parseTime(line); + } else { + if (mCurrentValue != null) { + // this is a value that has wrapped to next line. + mCurrentValue.append("\r\n"); + mCurrentValue.append(line); + } else if (!line.trim().isEmpty()) { + Log.d(LOG_TAG, "unrecognized line " + line); + } + } + } + + /** + * Stores the currently parsed key-value pair in the appropriate place. + */ + private void submitCurrentKeyValue() { + if (mCurrentKey != null && mCurrentValue != null) { + String statusValue = mCurrentValue.toString(); + if (mInInstrumentationResultKey) { + if (!KNOWN_KEYS.contains(mCurrentKey)) { + mInstrumentationResultBundle.put(mCurrentKey, statusValue); + } else if (mCurrentKey.equals(StatusKeys.SHORTMSG)) { + // test run must have failed + handleTestRunFailed(String.format("Instrumentation run failed due to '%1$s'", + statusValue)); + } + } else { + TestResult testInfo = getCurrentTestInfo(); + + if (mCurrentKey.equals(StatusKeys.CLASS)) { + testInfo.mTestClass = statusValue.trim(); + } else if (mCurrentKey.equals(StatusKeys.TEST)) { + testInfo.mTestName = statusValue.trim(); + } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) { + try { + testInfo.mNumTests = Integer.parseInt(statusValue); + } catch (NumberFormatException e) { + Log.w(LOG_TAG, "Unexpected integer number of tests, received " + + statusValue); + } + } else if (mCurrentKey.equals(StatusKeys.ERROR)) { + // test run must have failed + handleTestRunFailed(statusValue); + } else if (mCurrentKey.equals(StatusKeys.STACK)) { + testInfo.mStackTrace = statusValue; + } else if (!KNOWN_KEYS.contains(mCurrentKey)) { + // Not one of the recognized key/value pairs, so dump it in mTestMetrics + mTestMetrics.put(mCurrentKey, statusValue); + } + } + + mCurrentKey = null; + mCurrentValue = null; + } + } + + /** + * A utility method to return the test metrics from the current test case execution and get + * ready for the next one. + */ + private Map getAndResetTestMetrics() { + Map retVal = mTestMetrics; + mTestMetrics = new HashMap(); + return retVal; + } + + private TestResult getCurrentTestInfo() { + if (mCurrentTestResult == null) { + mCurrentTestResult = new TestResult(); + } + return mCurrentTestResult; + } + + private void clearCurrentTestInfo() { + mLastTestResult = mCurrentTestResult; + mCurrentTestResult = null; + } + + /** + * Parses the key from the current line. + * Expects format of "key=value". + * + * @param line full line of text to parse + * @param keyStartPos the starting position of the key in the given line + */ + private void parseKey(String line, int keyStartPos) { + int endKeyPos = line.indexOf('=', keyStartPos); + if (endKeyPos != -1) { + mCurrentKey = line.substring(keyStartPos, endKeyPos).trim(); + parseValue(line, endKeyPos + 1); + } + } + + /** + * Parses the start of a key=value pair. + * + * @param line - full line of text to parse + * @param valueStartPos - the starting position of the value in the given line + */ + private void parseValue(String line, int valueStartPos) { + mCurrentValue = new StringBuilder(); + mCurrentValue.append(line.substring(valueStartPos)); + } + + /** + * Parses out a status code result. + */ + private void parseStatusCode(String line) { + String value = line.substring(Prefixes.STATUS_CODE.length()).trim(); + TestResult testInfo = getCurrentTestInfo(); + testInfo.mCode = StatusCodes.ERROR; + try { + testInfo.mCode = Integer.parseInt(value); + } catch (NumberFormatException e) { + Log.w(LOG_TAG, "Expected integer status code, received: " + value); + testInfo.mCode = StatusCodes.ERROR; + } + if (testInfo.mCode != StatusCodes.IN_PROGRESS) { + // this means we're done with current test result bundle + reportResult(testInfo); + clearCurrentTestInfo(); + } + } + + /** + * Returns true if test run canceled. + * + * @see IShellOutputReceiver#isCancelled() + */ + @Override + public boolean isCancelled() { + return mIsCancelled; + } + + /** + * Requests cancellation of test run. + */ + public void cancel() { + mIsCancelled = true; + } + + /** + * Reports a test result to the test run listener. Must be called when a individual test + * result has been fully parsed. + * + * @param statusMap key-value status pairs of test result + */ + private void reportResult(TestResult testInfo) { + if (!testInfo.isComplete()) { + Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString()); + return; + } + reportTestRunStarted(testInfo); + TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName); + Map metrics; + + switch (testInfo.mCode) { + case StatusCodes.START: + for (ITestRunListener listener : mTestListeners) { + listener.testStarted(testId); + } + break; + case StatusCodes.FAILURE: + metrics = getAndResetTestMetrics(); + for (ITestRunListener listener : mTestListeners) { + listener.testFailed(testId, getTrace(testInfo)); + listener.testEnded(testId, metrics); + } + mNumTestsRun++; + break; + case StatusCodes.ERROR: + // we're dealing with a legacy JUnit3 runner that still reports errors. + // just report this as a failure, since thats what upstream JUnit4 does + metrics = getAndResetTestMetrics(); + for (ITestRunListener listener : mTestListeners) { + listener.testFailed(testId, getTrace(testInfo)); + listener.testEnded(testId, metrics); + } + mNumTestsRun++; + break; + case StatusCodes.IGNORED: + metrics = getAndResetTestMetrics(); + for (ITestRunListener listener : mTestListeners) { + listener.testStarted(testId); + listener.testIgnored(testId); + listener.testEnded(testId, metrics); + } + mNumTestsRun++; + break; + case StatusCodes.ASSUMPTION_FAILURE: + metrics = getAndResetTestMetrics(); + for (ITestRunListener listener : mTestListeners) { + listener.testAssumptionFailure(testId, getTrace(testInfo)); + listener.testEnded(testId, metrics); + } + mNumTestsRun++; + break; + case StatusCodes.OK: + metrics = getAndResetTestMetrics(); + for (ITestRunListener listener : mTestListeners) { + listener.testEnded(testId, metrics); + } + mNumTestsRun++; + break; + default: + metrics = getAndResetTestMetrics(); + Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode); + for (ITestRunListener listener : mTestListeners) { + listener.testEnded(testId, metrics); + } + mNumTestsRun++; + break; + } + + } + + /** + * Reports the start of a test run, and the total test count, if it has not been previously + * reported. + * + * @param testInfo current test status values + */ + private void reportTestRunStarted(TestResult testInfo) { + // if start test run not reported yet + if (!mTestStartReported && testInfo.mNumTests != null) { + for (ITestRunListener listener : mTestListeners) { + listener.testRunStarted(mTestRunName, testInfo.mNumTests); + } + mNumTestsExpected = testInfo.mNumTests; + mTestStartReported = true; + } + } + + /** + * Returns the stack trace of the current failed test, from the provided testInfo. + */ + private String getTrace(TestResult testInfo) { + if (testInfo.mStackTrace != null) { + return testInfo.mStackTrace; + } else { + Log.e(LOG_TAG, "Could not find stack trace for failed test "); + return new Throwable("Unknown failure").toString(); + } + } + + /** + * Parses out and store the elapsed time. + */ + private void parseTime(String line) { + final Pattern timePattern = Pattern.compile(String.format("%s\\s*([\\d\\.]+)", + Prefixes.TIME_REPORT)); + Matcher timeMatcher = timePattern.matcher(line); + if (timeMatcher.find()) { + String timeString = timeMatcher.group(1); + try { + float timeSeconds = Float.parseFloat(timeString); + mTestTime = (long) (timeSeconds * 1000); + } catch (NumberFormatException e) { + Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line)); + } + } else { + Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line)); + } + } + + /** + * Inform the parser of a instrumentation run failure. Should be called when the adb command + * used to run the test fails. + */ + public void handleTestRunFailed(String errorMsg) { + errorMsg = (errorMsg == null ? "Unknown error" : errorMsg); + Log.i(LOG_TAG, String.format("test run failed: '%1$s'", errorMsg)); + if (mLastTestResult != null && + mLastTestResult.isComplete() && + StatusCodes.START == mLastTestResult.mCode) { + + // received test start msg, but not test complete + // assume test caused this, report as test failure + TestIdentifier testId = new TestIdentifier(mLastTestResult.mTestClass, + mLastTestResult.mTestName); + for (ITestRunListener listener : mTestListeners) { + listener.testFailed(testId, + String.format("%1$s. Reason: '%2$s'. %3$s", INCOMPLETE_TEST_ERR_MSG_PREFIX, + errorMsg, INCOMPLETE_TEST_ERR_MSG_POSTFIX)); + listener.testEnded(testId, getAndResetTestMetrics()); + } + } + for (ITestRunListener listener : mTestListeners) { + if (!mTestStartReported) { + // test run wasn't started - must have crashed before it started + listener.testRunStarted(mTestRunName, 0); + } + listener.testRunFailed(errorMsg); + listener.testRunEnded(mTestTime, mInstrumentationResultBundle); + } + mTestStartReported = true; + mTestRunFailReported = true; + } + + /** + * Called by parent when adb session is complete. + */ + @Override + public void done() { + super.done(); + if (!mTestRunFailReported) { + handleOutputDone(); + } + } + + /** + * Handles the end of the adb session when a test run failure has not been reported yet + */ + private void handleOutputDone() { + if (!mTestStartReported && !mTestRunFinished) { + // no results + handleTestRunFailed(NO_TEST_RESULTS_MSG); + } else if (mNumTestsExpected > mNumTestsRun) { + final String message = + String.format("%1$s. Expected %2$d tests, received %3$d", + INCOMPLETE_RUN_ERR_MSG_PREFIX, mNumTestsExpected, mNumTestsRun); + handleTestRunFailed(message); + } else { + for (ITestRunListener listener : mTestListeners) { + if (!mTestStartReported) { + // test run wasn't started, but it finished successfully. Must be a run with + // no tests + listener.testRunStarted(mTestRunName, 0); + } + listener.testRunEnded(mTestTime, mInstrumentationResultBundle); + } + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java new file mode 100644 index 0000000..a8f1491 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import com.android.annotations.NonNull; +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellEnabledDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Hashtable; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +/** + * Runs a Android test command remotely and reports results. + */ +public class RemoteAndroidTestRunner implements IRemoteAndroidTestRunner { + + private final String mPackageName; + private final String mRunnerName; + private IShellEnabledDevice mRemoteDevice; + // default to no timeout + private long mMaxTimeToOutputResponse = 0; + private TimeUnit mMaxTimeUnits = TimeUnit.MILLISECONDS; + private String mRunName = null; + + /** map of name-value instrumentation argument pairs */ + private Map mArgMap; + private InstrumentationResultParser mParser; + + private static final String LOG_TAG = "RemoteAndroidTest"; + private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner"; + + private static final char CLASS_SEPARATOR = ','; + private static final char METHOD_SEPARATOR = '#'; + private static final char RUNNER_SEPARATOR = '/'; + + // defined instrumentation argument names + private static final String CLASS_ARG_NAME = "class"; + private static final String LOG_ARG_NAME = "log"; + private static final String DEBUG_ARG_NAME = "debug"; + private static final String COVERAGE_ARG_NAME = "coverage"; + private static final String PACKAGE_ARG_NAME = "package"; + private static final String SIZE_ARG_NAME = "size"; + private static final String DELAY_MSEC_ARG_NAME = "delay_msec"; + private String mRunOptions = ""; + + private static final int TEST_COLLECTION_TIMEOUT = 2 * 60 * 1000; //2 min + + /** + * Creates a remote Android test runner. + * + * @param packageName the Android application package that contains the tests to run + * @param runnerName the instrumentation test runner to execute. If null, will use default + * runner + * @param remoteDevice the Android device to execute tests on + */ + public RemoteAndroidTestRunner(String packageName, + String runnerName, + IShellEnabledDevice remoteDevice) { + + mPackageName = packageName; + mRunnerName = runnerName; + mRemoteDevice = remoteDevice; + mArgMap = new Hashtable(); + } + + /** + * Alternate constructor. Uses default instrumentation runner. + * + * @param packageName the Android application package that contains the tests to run + * @param remoteDevice the Android device to execute tests on + */ + public RemoteAndroidTestRunner(String packageName, + IShellEnabledDevice remoteDevice) { + this(packageName, null, remoteDevice); + } + + @Override + public String getPackageName() { + return mPackageName; + } + + @Override + public String getRunnerName() { + if (mRunnerName == null) { + return DEFAULT_RUNNER_NAME; + } + return mRunnerName; + } + + /** + * Returns the complete instrumentation component path. + */ + private String getRunnerPath() { + return getPackageName() + RUNNER_SEPARATOR + getRunnerName(); + } + + @Override + public void setClassName(String className) { + addInstrumentationArg(CLASS_ARG_NAME, className); + } + + @Override + public void setClassNames(String[] classNames) { + StringBuilder classArgBuilder = new StringBuilder(); + + for (int i = 0; i < classNames.length; i++) { + if (i != 0) { + classArgBuilder.append(CLASS_SEPARATOR); + } + classArgBuilder.append(classNames[i]); + } + setClassName(classArgBuilder.toString()); + } + + @Override + public void setMethodName(String className, String testName) { + setClassName(className + METHOD_SEPARATOR + testName); + } + + @Override + public void setTestPackageName(String packageName) { + addInstrumentationArg(PACKAGE_ARG_NAME, packageName); + } + + @Override + public void addInstrumentationArg(String name, String value) { + if (name == null || value == null) { + throw new IllegalArgumentException("name or value arguments cannot be null"); + } + mArgMap.put(name, value); + } + + @Override + public void removeInstrumentationArg(String name) { + if (name == null) { + throw new IllegalArgumentException("name argument cannot be null"); + } + mArgMap.remove(name); + } + + @Override + public void addBooleanArg(String name, boolean value) { + addInstrumentationArg(name, Boolean.toString(value)); + } + + @Override + public void setLogOnly(boolean logOnly) { + addBooleanArg(LOG_ARG_NAME, logOnly); + } + + @Override + public void setDebug(boolean debug) { + addBooleanArg(DEBUG_ARG_NAME, debug); + } + + @Override + public void setCoverage(boolean coverage) { + addBooleanArg(COVERAGE_ARG_NAME, coverage); + } + + @Override + public void setTestSize(TestSize size) { + addInstrumentationArg(SIZE_ARG_NAME, size.getRunnerValue()); + } + + @Override + public void setTestCollection(boolean collect) { + if (collect) { + // skip test execution + setLogOnly(true); + // force a timeout for test collection + setMaxTimeToOutputResponse(TEST_COLLECTION_TIMEOUT, TimeUnit.MILLISECONDS); + if (getApiLevel() < 16 ) { + // On older platforms, collecting tests can fail for large volume of tests. + // Insert a small delay between each test to prevent this + addInstrumentationArg(DELAY_MSEC_ARG_NAME, "15" /* ms */); + } + } else { + setLogOnly(false); + // restore timeout to its original set value + setMaxTimeToOutputResponse(mMaxTimeToOutputResponse, mMaxTimeUnits); + if (getApiLevel() < 16 ) { + // remove delay + removeInstrumentationArg(DELAY_MSEC_ARG_NAME); + } + } + } + + /** + * Attempts to retrieve the Api level of the Android device + * @return the api level or -1 if the communication with the device wasn't successful + */ + private int getApiLevel() { + try { + return Integer.parseInt(mRemoteDevice.getSystemProperty( + IDevice.PROP_BUILD_API_LEVEL).get()); + } catch (Exception e) { + return -1; + } + } + + @Override + public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) { + setMaxTimeToOutputResponse(maxTimeToOutputResponse, TimeUnit.MILLISECONDS); + } + + @Override + public void setMaxTimeToOutputResponse(long maxTimeToOutputResponse, TimeUnit maxTimeUnits) { + mMaxTimeToOutputResponse = maxTimeToOutputResponse; + mMaxTimeUnits = maxTimeUnits; + } + + @Override + public void setRunName(String runName) { + mRunName = runName; + } + + @Override + public void run(ITestRunListener... listeners) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + run(Arrays.asList(listeners)); + } + + @Override + public void run(Collection listeners) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + final String runCaseCommandStr = String.format("am instrument -w -r %1$s %2$s %3$s", + getRunOptions(), getArgsCommand(), getRunnerPath()); + Log.i(LOG_TAG, String.format("Running %1$s on %2$s", runCaseCommandStr, + mRemoteDevice.getName())); + String runName = mRunName == null ? mPackageName : mRunName; + mParser = new InstrumentationResultParser(runName, listeners); + + try { + mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser, mMaxTimeToOutputResponse, + mMaxTimeUnits); + } catch (IOException e) { + Log.w(LOG_TAG, String.format("IOException %1$s when running tests %2$s on %3$s", + e.toString(), getPackageName(), mRemoteDevice.getName())); + // rely on parser to communicate results to listeners + mParser.handleTestRunFailed(e.toString()); + throw e; + } catch (ShellCommandUnresponsiveException e) { + Log.w(LOG_TAG, String.format( + "ShellCommandUnresponsiveException %1$s when running tests %2$s on %3$s", + e.toString(), getPackageName(), mRemoteDevice.getName())); + mParser.handleTestRunFailed(String.format( + "Failed to receive adb shell test output within %1$d ms. " + + "Test may have timed out, or adb connection to device became unresponsive", + mMaxTimeToOutputResponse)); + throw e; + } catch (TimeoutException e) { + Log.w(LOG_TAG, String.format( + "TimeoutException when running tests %1$s on %2$s", getPackageName(), + mRemoteDevice.getName())); + mParser.handleTestRunFailed(e.toString()); + throw e; + } catch (AdbCommandRejectedException e) { + Log.w(LOG_TAG, String.format( + "AdbCommandRejectedException %1$s when running tests %2$s on %3$s", + e.toString(), getPackageName(), mRemoteDevice.getName())); + mParser.handleTestRunFailed(e.toString()); + throw e; + } + } + + /** + * Returns options for the am instrument command. + */ + @NonNull public String getRunOptions() { + return mRunOptions; + } + + /** + * Sets options for the am instrument command. + * See com/android/commands/am/Am.java for full list of options. + */ + public void setRunOptions(@NonNull String options) { + mRunOptions = options; + } + + @Override + public void cancel() { + if (mParser != null) { + mParser.cancel(); + } + } + + /** + * Returns the full instrumentation command line syntax for the provided instrumentation + * arguments. + * Returns an empty string if no arguments were specified. + */ + private String getArgsCommand() { + StringBuilder commandBuilder = new StringBuilder(); + for (Entry argPair : mArgMap.entrySet()) { + final String argCmd = String.format(" -e %1$s %2$s", argPair.getKey(), + argPair.getValue()); + commandBuilder.append(argCmd); + } + return commandBuilder.toString(); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestIdentifier.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestIdentifier.java new file mode 100644 index 0000000..7de5736 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestIdentifier.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +/** + * Identifies a parsed instrumentation test. + */ +public class TestIdentifier { + + private final String mClassName; + private final String mTestName; + + /** + * Creates a test identifier. + * + * @param className fully qualified class name of the test. Cannot be null. + * @param testName name of the test. Cannot be null. + */ + public TestIdentifier(String className, String testName) { + if (className == null || testName == null) { + throw new IllegalArgumentException("className and testName must " + + "be non-null"); + } + mClassName = className; + mTestName = testName; + } + + /** + * Returns the fully qualified class name of the test. + */ + public String getClassName() { + return mClassName; + } + + /** + * Returns the name of the test. + */ + public String getTestName() { + return mTestName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mClassName == null) ? 0 : mClassName.hashCode()); + result = prime * result + ((mTestName == null) ? 0 : mTestName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestIdentifier other = (TestIdentifier) obj; + if (mClassName == null) { + if (other.mClassName != null) + return false; + } else if (!mClassName.equals(other.mClassName)) + return false; + if (mTestName == null) { + if (other.mTestName != null) + return false; + } else if (!mTestName.equals(other.mTestName)) + return false; + return true; + } + + @Override + public String toString() { + return String.format("%s#%s", getClassName(), getTestName()); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestResult.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestResult.java new file mode 100644 index 0000000..75986cb --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestResult.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.testrunner.TestResult.TestStatus; + +import java.util.Arrays; +import java.util.Map; + +/** + * Container for a result of a single test. + */ +public class TestResult { + + public enum TestStatus { + /** Test failed. */ + FAILURE, + /** Test passed */ + PASSED, + /** Test started but not ended */ + INCOMPLETE, + /** Test assumption failure */ + ASSUMPTION_FAILURE, + /** Test ignored */ + IGNORED, + } + + private TestStatus mStatus; + private String mStackTrace; + private Map mMetrics; + // the start and end time of the test, measured via {@link System#currentTimeMillis()} + private long mStartTime = 0; + private long mEndTime = 0; + + public TestResult() { + mStatus = TestStatus.INCOMPLETE; + mStartTime = System.currentTimeMillis(); + } + + /** + * Get the {@link TestStatus} result of the test. + */ + public TestStatus getStatus() { + return mStatus; + } + + /** + * Get the associated {@link String} stack trace. Should be null if + * {@link #getStatus()} is {@link TestStatus.PASSED}. + */ + public String getStackTrace() { + return mStackTrace; + } + + /** + * Get the associated test metrics. + */ + public Map getMetrics() { + return mMetrics; + } + + /** + * Set the test metrics, overriding any previous values. + */ + public void setMetrics(Map metrics) { + mMetrics = metrics; + } + + /** + * Return the {@link System#currentTimeMillis()} time that the + * {@link ITestInvocationListener#testStarted(TestIdentifier)} event was received. + */ + public long getStartTime() { + return mStartTime; + } + + /** + * Return the {@link System#currentTimeMillis()} time that the + * {@link ITestInvocationListener#testEnded(TestIdentifier)} event was received. + */ + public long getEndTime() { + return mEndTime; + } + + /** + * Set the {@link TestStatus}. + */ + public TestResult setStatus(TestStatus status) { + mStatus = status; + return this; + } + + /** + * Set the stack trace. + */ + public void setStackTrace(String trace) { + mStackTrace = trace; + } + + /** + * Sets the end time + */ + public void setEndTime(long currentTimeMillis) { + mEndTime = currentTimeMillis; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {mMetrics, mStackTrace, mStatus}); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TestResult other = (TestResult) obj; + return equal(mMetrics, other.mMetrics) && + equal(mStackTrace, other.mStackTrace) && + equal(mStatus, other.mStatus); + } + + private static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestRunResult.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestRunResult.java new file mode 100644 index 0000000..4707f1a --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestRunResult.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.Log; +import com.android.ddmlib.testrunner.TestResult.TestStatus; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Holds results from a single test run. + *

+ * Maintains an accurate count of tests, and tracks incomplete tests. + *

+ * Not thread safe! The test* callbacks must be called in order + */ +public class TestRunResult implements ITestRunListener { + private static final String LOG_TAG = TestRunResult.class.getSimpleName(); + private String mTestRunName; + // Uses a LinkedHashMap to have predictable iteration order + private Map mTestResults = + new LinkedHashMap(); + private Map mRunMetrics = new HashMap(); + private boolean mIsRunComplete = false; + private long mElapsedTime = 0; + + /** represents sums of tests in each TestStatus state. Indexed by TestStatus.ordinal() */ + private int[] mStatusCounts = new int[TestStatus.values().length]; + /** tracks if mStatusCounts is accurate, or if it needs to be recalculated */ + private boolean mIsCountDirty = true; + + private String mRunFailureError = null; + + private boolean mAggregateMetrics = false; + + /** + * Create an empty{@link TestRunResult}. + */ + public TestRunResult() { + mTestRunName = "not started"; + } + + public void setAggregateMetrics(boolean metricAggregation) { + mAggregateMetrics = metricAggregation; + } + + /** + * @return the test run name + */ + public String getName() { + return mTestRunName; + } + + /** + * Gets a map of the test results. + * @return + */ + public Map getTestResults() { + return mTestResults; + } + + /** + * @return a {@link Map} of the test test run metrics. + */ + public Map getRunMetrics() { + return mRunMetrics; + } + + /** + * Gets the set of completed tests. + */ + public Set getCompletedTests() { + Set completedTests = new LinkedHashSet(); + for (Map.Entry testEntry : getTestResults().entrySet()) { + if (!testEntry.getValue().getStatus().equals(TestStatus.INCOMPLETE)) { + completedTests.add(testEntry.getKey()); + } + } + return completedTests; + } + + /** + * @return true if test run failed. + */ + public boolean isRunFailure() { + return mRunFailureError != null; + } + + /** + * @return true if test run finished. + */ + public boolean isRunComplete() { + return mIsRunComplete; + } + + public void setRunComplete(boolean runComplete) { + mIsRunComplete = runComplete; + } + + /** + * Gets the number of tests in given state for this run. + */ + public int getNumTestsInState(TestStatus status) { + if (mIsCountDirty) { + // clear counts + for (int i=0; i < mStatusCounts.length; i++) { + mStatusCounts[i] = 0; + } + // now recalculate + for (TestResult r : mTestResults.values()) { + mStatusCounts[r.getStatus().ordinal()]++; + } + mIsCountDirty = false; + } + return mStatusCounts[status.ordinal()]; + } + + /** + * Gets the number of tests in this run. + */ + public int getNumTests() { + return mTestResults.size(); + } + + /** + * Gets the number of complete tests in this run ie with status != incomplete. + */ + public int getNumCompleteTests() { + return getNumTests() - getNumTestsInState(TestStatus.INCOMPLETE); + } + + /** + * @return true if test run had any failed or error tests. + */ + public boolean hasFailedTests() { + return getNumAllFailedTests() > 0; + } + + /** + * Return total number of tests in a failure state (failed, assumption failure) + */ + public int getNumAllFailedTests() { + return getNumTestsInState(TestStatus.FAILURE); + } + + /** + * @return + */ + public long getElapsedTime() { + return mElapsedTime; + } + + /** + * Return the run failure error message, null if run did not fail. + */ + public String getRunFailureMessage() { + return mRunFailureError; + } + + + @Override + public void testRunStarted(String runName, int testCount) { + mTestRunName = runName; + mIsRunComplete = false; + mRunFailureError = null; + } + + @Override + public void testStarted(TestIdentifier test) { + addTestResult(test, new TestResult()); + } + + private void addTestResult(TestIdentifier test, TestResult testResult) { + mIsCountDirty = true; + mTestResults.put(test, testResult); + } + + private void updateTestResult(TestIdentifier test, TestStatus status, String trace) { + TestResult r = mTestResults.get(test); + if (r == null) { + Log.d(LOG_TAG, String.format("received test event without test start for %s", test)); + r = new TestResult(); + } + r.setStatus(status); + r.setStackTrace(trace); + addTestResult(test, r); + } + + @Override + public void testFailed(TestIdentifier test, String trace) { + updateTestResult(test, TestStatus.FAILURE, trace); + } + + @Override + public void testAssumptionFailure(TestIdentifier test, String trace) { + updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, trace); + } + + @Override + public void testIgnored(TestIdentifier test) { + updateTestResult(test, TestStatus.IGNORED, null); + } + + @Override + public void testEnded(TestIdentifier test, Map testMetrics) { + TestResult result = mTestResults.get(test); + if (result == null) { + result = new TestResult(); + } + if (result.getStatus().equals(TestStatus.INCOMPLETE)) { + result.setStatus(TestStatus.PASSED); + } + result.setEndTime(System.currentTimeMillis()); + result.setMetrics(testMetrics); + addTestResult(test, result); + } + + @Override + public void testRunFailed(String errorMessage) { + mRunFailureError = errorMessage; + } + + @Override + public void testRunStopped(long elapsedTime) { + mElapsedTime+= elapsedTime; + mIsRunComplete = true; + } + + @Override + public void testRunEnded(long elapsedTime, Map runMetrics) { + if (mAggregateMetrics) { + for (Map.Entry entry : runMetrics.entrySet()) { + String existingValue = mRunMetrics.get(entry.getKey()); + String combinedValue = combineValues(existingValue, entry.getValue()); + mRunMetrics.put(entry.getKey(), combinedValue); + } + } else { + mRunMetrics.putAll(runMetrics); + } + mElapsedTime+= elapsedTime; + mIsRunComplete = true; + } + + /** + * Combine old and new metrics value + * + * @param existingValue + * @param value + * @return + */ + private String combineValues(String existingValue, String newValue) { + if (existingValue != null) { + try { + Long existingLong = Long.parseLong(existingValue); + Long newLong = Long.parseLong(newValue); + return Long.toString(existingLong + newLong); + } catch (NumberFormatException e) { + // not a long, skip to next + } + try { + Double existingDouble = Double.parseDouble(existingValue); + Double newDouble = Double.parseDouble(newValue); + return Double.toString(existingDouble + newDouble); + } catch (NumberFormatException e) { + // not a double either, fall through + } + } + // default to overriding existingValue + return newValue; + } + + /** + * Return a user friendly string describing results. + * + * @return + */ + public String getTextSummary() { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("Total tests %d, ", getNumTests())); + for (TestStatus status : TestStatus.values()) { + int count = getNumTestsInState(status); + // only add descriptive state for states that have non zero values, to avoid cluttering + // the response + if (count > 0) { + builder.append(String.format("%s %d, ", status.toString().toLowerCase(), count)); + } + } + return builder.toString(); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/XmlTestRunListener.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/XmlTestRunListener.java new file mode 100644 index 0000000..36cb4d4 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/testrunner/XmlTestRunListener.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.testrunner.TestResult.TestStatus; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import org.kxml2.io.KXmlSerializer; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +/** + * Writes JUnit results to an XML files in a format consistent with + * Ant's XMLJUnitResultFormatter. + *

+ * Creates a separate XML file per test run. + *

+ * @see https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd + */ +public class XmlTestRunListener implements ITestRunListener { + + private static final String LOG_TAG = "XmlResultReporter"; + + private static final String TEST_RESULT_FILE_SUFFIX = ".xml"; + private static final String TEST_RESULT_FILE_PREFIX = "test_result_"; + + private static final String TESTSUITE = "testsuite"; + private static final String TESTCASE = "testcase"; + private static final String ERROR = "error"; + private static final String FAILURE = "failure"; + private static final String SKIPPED_TAG = "skipped"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_TIME = "time"; + private static final String ATTR_ERRORS = "errors"; + private static final String ATTR_FAILURES = "failures"; + private static final String ATTR_SKIPPED = "skipped"; + private static final String ATTR_ASSERTIOMS = "assertions"; + private static final String ATTR_TESTS = "tests"; + //private static final String ATTR_TYPE = "type"; + //private static final String ATTR_MESSAGE = "message"; + private static final String PROPERTIES = "properties"; + private static final String PROPERTY = "property"; + private static final String ATTR_CLASSNAME = "classname"; + private static final String TIMESTAMP = "timestamp"; + private static final String HOSTNAME = "hostname"; + + /** the XML namespace */ + private static final String ns = null; + + private String mHostName = "localhost"; + + private File mReportDir = new File(System.getProperty("java.io.tmpdir")); + + private String mReportPath = ""; + + private TestRunResult mRunResult = new TestRunResult(); + + /** + * Sets the report file to use. + */ + public void setReportDir(File file) { + mReportDir = file; + } + + public void setHostName(String hostName) { + mHostName = hostName; + } + + /** + * Returns the {@link TestRunResult} + * @return the test run results. + */ + public TestRunResult getRunResult() { + return mRunResult; + } + + @Override + public void testRunStarted(String runName, int numTests) { + mRunResult = new TestRunResult(); + mRunResult.testRunStarted(runName, numTests); + } + + @Override + public void testStarted(TestIdentifier test) { + mRunResult.testStarted(test); + } + + @Override + public void testFailed(TestIdentifier test, String trace) { + mRunResult.testFailed(test, trace); + } + + @Override + public void testAssumptionFailure(TestIdentifier test, String trace) { + mRunResult.testAssumptionFailure(test, trace); + } + + @Override + public void testIgnored(TestIdentifier test) { + mRunResult.testIgnored(test); + } + + @Override + public void testEnded(TestIdentifier test, Map testMetrics) { + mRunResult.testEnded(test, testMetrics); + } + + @Override + public void testRunFailed(String errorMessage) { + mRunResult.testRunFailed(errorMessage); + } + + @Override + public void testRunStopped(long elapsedTime) { + mRunResult.testRunStopped(elapsedTime); + } + + @Override + public void testRunEnded(long elapsedTime, Map runMetrics) { + mRunResult.testRunEnded(elapsedTime, runMetrics); + generateDocument(mReportDir, elapsedTime); + } + + /** + * Creates a report file and populates it with the report data from the completed tests. + */ + private void generateDocument(File reportDir, long elapsedTime) { + String timestamp = getTimestamp(); + + OutputStream stream = null; + try { + stream = createOutputResultStream(reportDir); + KXmlSerializer serializer = new KXmlSerializer(); + serializer.setOutput(stream, SdkConstants.UTF_8); + serializer.startDocument(SdkConstants.UTF_8, null); + serializer.setFeature( + "http://xmlpull.org/v1/doc/features.html#indent-output", true); + // TODO: insert build info + printTestResults(serializer, timestamp, elapsedTime); + serializer.endDocument(); + String msg = String.format("XML test result file generated at %s. %s" , + getAbsoluteReportPath(), mRunResult.getTextSummary()); + Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg); + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to generate report data"); + // TODO: consider throwing exception + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException ignored) { + } + } + } + } + + private String getAbsoluteReportPath() { + return mReportPath ; + } + + /** + * Return the current timestamp as a {@link String}. + */ + String getTimestamp() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", + Locale.getDefault()); + TimeZone gmt = TimeZone.getTimeZone("UTC"); + dateFormat.setTimeZone(gmt); + dateFormat.setLenient(true); + String timestamp = dateFormat.format(new Date()); + return timestamp; + } + + /** + * Creates a {@link File} where the report will be created. + * @param reportDir the root directory of the report. + * @return a file + * @throws IOException + */ + protected File getResultFile(File reportDir) throws IOException { + File reportFile = File.createTempFile(TEST_RESULT_FILE_PREFIX, TEST_RESULT_FILE_SUFFIX, + reportDir); + Log.i(LOG_TAG, String.format("Created xml report file at %s", + reportFile.getAbsolutePath())); + + return reportFile; + } + + /** + * Creates the output stream to use for test results. Exposed for mocking. + */ + OutputStream createOutputResultStream(File reportDir) throws IOException { + File reportFile = getResultFile(reportDir); + mReportPath = reportFile.getAbsolutePath(); + return new BufferedOutputStream(new FileOutputStream(reportFile)); + } + + protected String getTestSuiteName() { + return mRunResult.getName(); + } + + void printTestResults(KXmlSerializer serializer, String timestamp, long elapsedTime) + throws IOException { + serializer.startTag(ns, TESTSUITE); + String name = getTestSuiteName(); + if (name != null) { + serializer.attribute(ns, ATTR_NAME, name); + } + serializer.attribute(ns, ATTR_TESTS, Integer.toString(mRunResult.getNumTests())); + serializer.attribute(ns, ATTR_FAILURES, Integer.toString( + mRunResult.getNumAllFailedTests())); + // legacy - there are no errors in JUnit4 + serializer.attribute(ns, ATTR_ERRORS, "0"); + serializer.attribute(ns, ATTR_SKIPPED, Integer.toString(mRunResult.getNumTestsInState( + TestStatus.IGNORED))); + + serializer.attribute(ns, ATTR_TIME, Double.toString((double) elapsedTime / 1000.f)); + serializer.attribute(ns, TIMESTAMP, timestamp); + serializer.attribute(ns, HOSTNAME, mHostName); + + serializer.startTag(ns, PROPERTIES); + for (Map.Entry entry: getPropertiesAttributes().entrySet()) { + serializer.startTag(ns, PROPERTY); + serializer.attribute(ns, "name", entry.getKey()); + serializer.attribute(ns, "value", entry.getValue()); + serializer.endTag(ns, PROPERTY); + } + serializer.endTag(ns, PROPERTIES); + + Map testResults = mRunResult.getTestResults(); + for (Map.Entry testEntry : testResults.entrySet()) { + print(serializer, testEntry.getKey(), testEntry.getValue()); + } + + serializer.endTag(ns, TESTSUITE); + } + + /** + * Get the properties attributes as key value pairs to be included in the test report. + */ + @NonNull + protected Map getPropertiesAttributes() { + return ImmutableMap.of(); + } + + protected String getTestName(TestIdentifier testId) { + return testId.getTestName(); + } + + void print(KXmlSerializer serializer, TestIdentifier testId, TestResult testResult) + throws IOException { + + serializer.startTag(ns, TESTCASE); + serializer.attribute(ns, ATTR_NAME, getTestName(testId)); + serializer.attribute(ns, ATTR_CLASSNAME, testId.getClassName()); + long elapsedTimeMs = testResult.getEndTime() - testResult.getStartTime(); + serializer.attribute(ns, ATTR_TIME, Double.toString((double)elapsedTimeMs / 1000.f)); + + switch (testResult.getStatus()) { + case FAILURE: + printFailedTest(serializer, FAILURE, testResult.getStackTrace()); + break; + case ASSUMPTION_FAILURE: + printFailedTest(serializer, SKIPPED_TAG, testResult.getStackTrace()); + break; + case IGNORED: + serializer.startTag(ns, SKIPPED_TAG); + serializer.endTag(ns, SKIPPED_TAG); + break; + } + + serializer.endTag(ns, TESTCASE); + } + + private void printFailedTest(KXmlSerializer serializer, String tag, String stack) + throws IOException { + serializer.startTag(ns, tag); + // TODO: get message of stack trace ? + // String msg = testResult.getStackTrace(); + // if (msg != null && msg.length() > 0) { + // serializer.attribute(ns, ATTR_MESSAGE, msg); + // } + // TODO: get class name of stackTrace exception + // serializer.attribute(ns, ATTR_TYPE, testId.getClassName()); + serializer.text(sanitize(stack)); + serializer.endTag(ns, tag); + } + + /** + * Returns the text in a format that is safe for use in an XML document. + */ + private String sanitize(String text) { + return text.replace("\0", "<\\0>"); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/ArrayHelper.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/ArrayHelper.java new file mode 100644 index 0000000..8167e5d --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/ArrayHelper.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.utils; + +/** + * Utility class providing array to int/long conversion for data received from devices through adb. + */ +public final class ArrayHelper { + + /** + * Swaps an unsigned value around, and puts the result in an array that can be sent to a device. + * @param value The value to swap. + * @param dest the destination array + * @param offset the offset in the array where to put the swapped value. + * Array length must be at least offset + 4 + */ + public static void swap32bitsToArray(int value, byte[] dest, int offset) { + dest[offset] = (byte)(value & 0x000000FF); + dest[offset + 1] = (byte)((value & 0x0000FF00) >> 8); + dest[offset + 2] = (byte)((value & 0x00FF0000) >> 16); + dest[offset + 3] = (byte)((value & 0xFF000000) >> 24); + } + + /** + * Reads a signed 32 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * @return the integer read from the array + */ + public static int swap32bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + v |= (((int)value[offset + 2]) & 0x000000FF) << 16; + v |= (((int)value[offset + 3]) & 0x000000FF) << 24; + + return v; + } + + /** + * Reads an unsigned 16 bit integer from an array coming from a device, + * and returns it as an 'int' + * @param value the array containing the 16 bit int (2 byte). + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 2 + * @return the integer read from the array. + */ + public static int swapU16bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + + return v; + } + + /** + * Reads a signed 64 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 8 + * @return the integer read from the array + */ + public static long swap64bitFromArray(byte[] value, int offset) { + long v = 0; + v |= ((long)value[offset]) & 0x00000000000000FFL; + v |= (((long)value[offset + 1]) & 0x00000000000000FFL) << 8; + v |= (((long)value[offset + 2]) & 0x00000000000000FFL) << 16; + v |= (((long)value[offset + 3]) & 0x00000000000000FFL) << 24; + v |= (((long)value[offset + 4]) & 0x00000000000000FFL) << 32; + v |= (((long)value[offset + 5]) & 0x00000000000000FFL) << 40; + v |= (((long)value[offset + 6]) & 0x00000000000000FFL) << 48; + v |= (((long)value[offset + 7]) & 0x00000000000000FFL) << 56; + + return v; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/DebuggerPorts.java b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/DebuggerPorts.java new file mode 100644 index 0000000..ee2eca3 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/main/java/com/android/ddmlib/utils/DebuggerPorts.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.utils; + +import com.android.annotations.concurrency.GuardedBy; + +import java.util.ArrayList; +import java.util.List; + +public class DebuggerPorts { + @GuardedBy("mDebuggerPorts") + private final List mDebuggerPorts = new ArrayList(); + + public DebuggerPorts(int basePort) { + mDebuggerPorts.add(basePort); + } + + public int next() { + // get the first port and remove it + synchronized (mDebuggerPorts) { + if (!mDebuggerPorts.isEmpty()) { + int port = mDebuggerPorts.get(0); + + // remove it. + mDebuggerPorts.remove(0); + + // if there's nothing left, add the next port to the list + if (mDebuggerPorts.isEmpty()) { + mDebuggerPorts.add(port+1); + } + + return port; + } + } + + return -1; + } + + public void free(int port) { + if (port <= 0) { + return; + } + + synchronized (mDebuggerPorts) { + // because there could be case where clients are closed twice, we have to make + // sure the port number is not already in the list. + if (mDebuggerPorts.indexOf(port) == -1) { + // add the port to the list while keeping it sorted. It's not like there's + // going to be tons of objects so we do it linearly. + int count = mDebuggerPorts.size(); + for (int i = 0; i < count; i++) { + if (port < mDebuggerPorts.get(i)) { + mDebuggerPorts.add(i, port); + break; + } + } + // TODO: check if we can compact the end of the list. + } + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/.classpath b/src/main/resources/external/src/ddmlib/src/test/.classpath new file mode 100644 index 0000000..498af80 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/external/src/ddmlib/src/test/.project b/src/main/resources/external/src/ddmlib/src/test/.project new file mode 100644 index 0000000..81a7f69 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/.project @@ -0,0 +1,17 @@ + + + ddmlib-tests + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AdbVersionTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AdbVersionTest.java new file mode 100644 index 0000000..6f1c51f --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AdbVersionTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import junit.framework.TestCase; + +public class AdbVersionTest extends TestCase { + public void testParser() { + AdbVersion version = AdbVersion.parseFrom("Android Debug Bridge version 1.0.32"); + assertEquals("1.0.32", version.toString()); + + version = AdbVersion.parseFrom("Android Debug Bridge version 1.0.32 8b22c293a0e5-android"); + assertEquals(AdbVersion.parseFrom("1.0.32"), version); + + version = AdbVersion.parseFrom("1.0.unknown"); + assertEquals(AdbVersion.UNKNOWN, version); + } + + public void testComponents() { + AdbVersion version = AdbVersion + .parseFrom("Android Debug Bridge version 1.23.32 8b22c293a0e5-android"); + assertEquals(1, version.major); + assertEquals(23, version.minor); + assertEquals(32, version.micro); + } + + public void testComparison() { + AdbVersion min = AdbVersion.parseFrom("1.0.20"); + AdbVersion now = AdbVersion.parseFrom("1.0.32"); + assertTrue(now.compareTo(min) > 0); + assertTrue(min.compareTo(now) < 0); + assertTrue(now.compareTo(now) == 0); + + AdbVersion f = AdbVersion.parseFrom("2.0.32"); + assertTrue(f.compareTo(now) > 0); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AndroidDebugBridgeTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AndroidDebugBridgeTest.java new file mode 100644 index 0000000..85a8f53 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/AndroidDebugBridgeTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import junit.framework.TestCase; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class AndroidDebugBridgeTest extends TestCase { + private String mAndroidHome; + private File mAdbPath; + + @Override + protected void setUp() throws Exception { + mAndroidHome = System.getenv("ANDROID_HOME"); + assertNotNull( + "This test requires ANDROID_HOME environment variable to point to a valid SDK", + mAndroidHome); + + mAdbPath = new File(mAndroidHome, "platform-tools" + File.separator + "adb"); + + AndroidDebugBridge.initIfNeeded(false); + } + + // https://code.google.com/p/android/issues/detail?id=63170 + public void testCanRecreateAdb() throws IOException { + AndroidDebugBridge adb = AndroidDebugBridge.createBridge(mAdbPath.getCanonicalPath(), true); + assertNotNull(adb); + AndroidDebugBridge.terminate(); + + adb = AndroidDebugBridge.createBridge(mAdbPath.getCanonicalPath(), true); + assertNotNull(adb); + AndroidDebugBridge.terminate(); + } + + // Some consumers of ddmlib rely on adb being on the path, and hence being + // able to create a bridge by simply passing in "adb" as the path to adb. + // We should be able to create a bridge in such a case as well. + // This test will fail if adb is not currently on the path. It is disabled since we + // can't enforce that condition (adb on $PATH) very well from a test.. + public void disabled_testEmptyAdbPath() throws Exception { + AdbVersion version = AndroidDebugBridge.getAdbVersion( + new File("adb")).get(5, TimeUnit.SECONDS); + assertTrue(version.compareTo(AdbVersion.parseFrom("1.0.20")) > 0); + } + + public void testAdbVersion() throws Exception { + AdbVersion version = AndroidDebugBridge + .getAdbVersion(mAdbPath).get(5, TimeUnit.SECONDS); + assertNotSame(version, AdbVersion.UNKNOWN); + assertTrue(version.compareTo(AdbVersion.parseFrom("1.0.20")) > 0); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/BatteryFetcherTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/BatteryFetcherTest.java new file mode 100644 index 0000000..173d96b --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/BatteryFetcherTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class BatteryFetcherTest extends TestCase { + + /** + * Test that getBattery works as expected when queries made in different states. + */ + public void testGetBattery() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellResponse(mockDevice, "20\r\n"); + EasyMock.replay(mockDevice); + + BatteryFetcher fetcher = new BatteryFetcher(mockDevice); + // do query in unpopulated state + Future uncachedFuture = fetcher.getBattery(0, TimeUnit.MILLISECONDS); + // do query in fetching state + Future fetchingFuture = fetcher.getBattery(0, TimeUnit.MILLISECONDS); + // do query in fetching state + + assertEquals(20, uncachedFuture.get().intValue()); + // do queries with short timeout to ensure battery already available + assertEquals(20, fetchingFuture.get(1, TimeUnit.MILLISECONDS).intValue()); + assertEquals(20, + fetcher.getBattery(1, TimeUnit.SECONDS).get(1, TimeUnit.MILLISECONDS).intValue()); + } + + /** + * Test that getBattery returns exception when battery checks return invalid data. + */ + public void testGetBattery_badResponse() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellResponse(mockDevice, "blargh"); + DeviceTest.injectShellResponse(mockDevice, "blargh"); + EasyMock.replay(mockDevice); + + BatteryFetcher fetcher = new BatteryFetcher(mockDevice); + try { + fetcher.getBattery(0, TimeUnit.MILLISECONDS).get(); + fail("ExecutionException not thrown"); + } catch (ExecutionException e) { + // expected + assertTrue(e.getCause() instanceof IOException); + } + } + + /** + * Test that getBattery propagates executeShell exceptions. + */ + public void testGetBattery_shellException() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + mockDevice.executeShellCommand(EasyMock.anyObject(), + EasyMock.anyObject(), + EasyMock.anyLong(), EasyMock.anyObject()); + EasyMock.expectLastCall().andThrow(new ShellCommandUnresponsiveException()); + EasyMock.replay(mockDevice); + + BatteryFetcher fetcher = new BatteryFetcher(mockDevice); + try { + fetcher.getBattery(0, TimeUnit.MILLISECONDS).get(); + fail("ExecutionException not thrown"); + } catch (ExecutionException e) { + // expected + assertTrue(e.getCause() instanceof ShellCommandUnresponsiveException); + } + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceMonitorTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceMonitorTest.java new file mode 100644 index 0000000..0182e69 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceMonitorTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import com.android.annotations.NonNull; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class DeviceMonitorTest extends TestCase { + public void testDeviceListMonitor() { + Map map = DeviceMonitor.DeviceListMonitorTask + .parseDeviceListResponse("R32C801BL5K\tdevice\n0079864fd1d150fd\tunauthorized\n"); + + assertEquals(IDevice.DeviceState.ONLINE, map.get("R32C801BL5K")); + assertEquals(IDevice.DeviceState.UNAUTHORIZED, map.get("0079864fd1d150fd")); + } + + public void testDeviceListComparator() { + List previous = Arrays.asList( + mockDevice("1", IDevice.DeviceState.ONLINE), + mockDevice("2", IDevice.DeviceState.BOOTLOADER) + ); + List current = Arrays.asList( + mockDevice("2", IDevice.DeviceState.ONLINE), + mockDevice("3", IDevice.DeviceState.OFFLINE) + ); + + DeviceMonitor.DeviceListComparisonResult result = DeviceMonitor.DeviceListComparisonResult + .compare(previous, current); + + assertEquals(1, result.updated.size()); + assertEquals(IDevice.DeviceState.ONLINE, result.updated.get(previous.get(1))); + + assertEquals(1, result.removed.size()); + assertEquals("1", result.removed.get(0).getSerialNumber()); + + assertEquals(1, result.added.size()); + assertEquals("3", result.added.get(0).getSerialNumber()); + } + + private IDevice mockDevice(@NonNull String serial, @NonNull IDevice.DeviceState state) { + IDevice device = EasyMock.createMock(IDevice.class); + EasyMock.expect(device.getSerialNumber()).andStubReturn(serial); + EasyMock.expect(device.getState()).andStubReturn(state); + EasyMock.replay(device); + return device; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceTest.java new file mode 100644 index 0000000..5de9feb --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/DeviceTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.annotations.NonNull; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; +import org.easymock.IAnswer; + +import java.util.concurrent.TimeUnit; + +public class DeviceTest extends TestCase { + public void testScreenRecorderOptions() { + ScreenRecorderOptions options = + new ScreenRecorderOptions.Builder() + .setBitRate(6) + .setSize(600,400) + .build(); + assertEquals("screenrecord --size 600x400 --bit-rate 6000000 /sdcard/1.mp4", + Device.getScreenRecorderCommand("/sdcard/1.mp4", options)); + + options = new ScreenRecorderOptions.Builder().setTimeLimit(100, TimeUnit.SECONDS).build(); + assertEquals("screenrecord --time-limit 100 /sdcard/1.mp4", + Device.getScreenRecorderCommand("/sdcard/1.mp4", options)); + + options = new ScreenRecorderOptions.Builder().setTimeLimit(4, TimeUnit.MINUTES).build(); + assertEquals("screenrecord --time-limit 180 /sdcard/1.mp4", + Device.getScreenRecorderCommand("/sdcard/1.mp4", options)); + } + + /** Helper method that sets the mock device to return the given response on a shell command */ + @SuppressWarnings("unchecked") + static void injectShellResponse(IDevice mockDevice, final String response) throws Exception { + IAnswer shellAnswer = new IAnswer() { + @Override + public Object answer() throws Throwable { + // insert small delay to simulate latency + Thread.sleep(50); + IShellOutputReceiver receiver = + (IShellOutputReceiver)EasyMock.getCurrentArguments()[1]; + byte[] inputData = response.getBytes(); + receiver.addOutput(inputData, 0, inputData.length); + return null; + } + }; + mockDevice.executeShellCommand(EasyMock.anyObject(), + EasyMock.anyObject(), + EasyMock.anyLong(), EasyMock.anyObject()); + EasyMock.expectLastCall().andAnswer(shellAnswer); + } + + /** Helper method that sets the mock device to throw the given exception on a shell command */ + static void injectShellExceptionResponse(@NonNull IDevice mockDevice, @NonNull Exception e) + throws Exception { + mockDevice.executeShellCommand(EasyMock.anyObject(), + EasyMock.anyObject(), + EasyMock.anyLong(), EasyMock.anyObject()); + EasyMock.expectLastCall().andThrow(e); + } + + /** Helper method that creates a mock device. */ + static IDevice createMockDevice() { + IDevice mockDevice = EasyMock.createMock(IDevice.class); + EasyMock.expect(mockDevice.getSerialNumber()).andStubReturn("serial"); + EasyMock.expect(mockDevice.isOnline()).andStubReturn(Boolean.TRUE); + return mockDevice; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/EmulatorConsoleTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/EmulatorConsoleTest.java new file mode 100644 index 0000000..1697acd --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/EmulatorConsoleTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link EmulatorConsole}. + */ +public class EmulatorConsoleTest extends TestCase { + + /** + * Test success case for {@link EmulatorConsole#getEmulatorPort(String)}. + */ + public void testGetEmulatorPort() { + assertEquals(Integer.valueOf(5554), EmulatorConsole.getEmulatorPort("emulator-5554")); + } + + /** + * Test {@link EmulatorConsole#getEmulatorPort(String)} when input serial has invalid format. + */ + public void testGetEmulatorPort_invalid() { + assertNull(EmulatorConsole.getEmulatorPort("invalidserial")); + } + + /** + * Test {@link EmulatorConsole#getEmulatorPort(String)} when port is not a number. + */ + public void testGetEmulatorPort_nan() { + assertNull(EmulatorConsole.getEmulatorPort("emulator-NaN")); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/PropertyFetcherTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/PropertyFetcherTest.java new file mode 100644 index 0000000..e715a6b --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/PropertyFetcherTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib; + +import com.android.ddmlib.PropertyFetcher.GetPropReceiver; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class PropertyFetcherTest extends TestCase { + static final String GETPROP_RESPONSE = + "\n[ro.sf.lcd_density]: [480]\n" + + "[ro.secure]: [1]\r\n"; + + /** + * Simple test to ensure parsing result of 'shell getprop' works as expected + */ + public void testGetPropReceiver() { + GetPropReceiver receiver = new GetPropReceiver(); + byte[] byteData = GETPROP_RESPONSE.getBytes(); + receiver.addOutput(byteData, 0, byteData.length); + assertEquals("480", receiver.getCollectedProperties().get("ro.sf.lcd_density")); + } + + /** + * Test that getProperty works as expected when queries made in different states + */ + public void testGetProperty() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellResponse(mockDevice, GETPROP_RESPONSE); + EasyMock.replay(mockDevice); + + PropertyFetcher fetcher = new PropertyFetcher(mockDevice); + // do query in unpopulated state + Future unpopulatedFuture = fetcher.getProperty("ro.sf.lcd_density"); + // do query in fetching state + Future fetchingFuture = fetcher.getProperty("ro.secure"); + + assertEquals("480", unpopulatedFuture.get()); + // do queries with short timeout to ensure props already available + assertEquals("1", fetchingFuture.get(1, TimeUnit.MILLISECONDS)); + assertEquals("480", fetcher.getProperty("ro.sf.lcd_density").get(1, + TimeUnit.MILLISECONDS)); + } + + /** + * Test that getProperty always does a getprop query when requested prop is not + * read only aka volatile + */ + public void testGetProperty_volatile() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellResponse(mockDevice, "[dev.bootcomplete]: [0]\r\n"); + DeviceTest.injectShellResponse(mockDevice, "[dev.bootcomplete]: [1]\r\n"); + EasyMock.replay(mockDevice); + + PropertyFetcher fetcher = new PropertyFetcher(mockDevice); + assertEquals("0", fetcher.getProperty("dev.bootcomplete").get()); + assertEquals("1", fetcher.getProperty("dev.bootcomplete").get()); + } + + /** + * Test that getProperty returns when the 'shell getprop' command response is invalid + */ + public void testGetProperty_badResponse() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellResponse(mockDevice, "blargh"); + EasyMock.replay(mockDevice); + + PropertyFetcher fetcher = new PropertyFetcher(mockDevice); + assertNull(fetcher.getProperty("dev.bootcomplete").get()); + } + + /** + * Test that null is returned when querying an unknown property + */ + public void testGetProperty_unknown() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellResponse(mockDevice, GETPROP_RESPONSE); + EasyMock.replay(mockDevice); + + PropertyFetcher fetcher = new PropertyFetcher(mockDevice); + assertNull(fetcher.getProperty("unknown").get()); + } + + /** + * Test that getProperty propagates exception thrown by 'shell getprop' + */ + public void testGetProperty_shellException() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellExceptionResponse(mockDevice, new ShellCommandUnresponsiveException()); + EasyMock.replay(mockDevice); + + PropertyFetcher fetcher = new PropertyFetcher(mockDevice); + try { + fetcher.getProperty("dev.bootcomplete").get(); + fail("ExecutionException not thrown"); + } catch (ExecutionException e) { + // expected + assertTrue(e.getCause() instanceof ShellCommandUnresponsiveException); + } + } + + /** + * Tests that property fetcher works under the following scenario: + *
    + *
  1. first fetch fails due to a shell exception
  2. + *
  3. subsequent fetches should work fine
  4. + *
+ */ + public void testGetProperty_FetchAfterException() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellExceptionResponse(mockDevice, new ShellCommandUnresponsiveException()); + DeviceTest.injectShellResponse(mockDevice, GETPROP_RESPONSE); + EasyMock.replay(mockDevice); + + PropertyFetcher fetcher = new PropertyFetcher(mockDevice); + try { + fetcher.getProperty("dev.bootcomplete").get(2, TimeUnit.SECONDS); + fail("ExecutionException not thrown"); + } catch (ExecutionException e) { + // expected + assertTrue(e.getCause() instanceof ShellCommandUnresponsiveException); + } + + assertEquals("480", fetcher.getProperty("ro.sf.lcd_density").get(2, TimeUnit.SECONDS)); + } + + /** + * Tests that property fetcher works under the following scenario: + *
    + *
  1. first fetch succeeds, but receives an empty response
  2. + *
  3. subsequent fetches should work fine
  4. + *
+ */ + public void testGetProperty_FetchAfterEmptyResponse() throws Exception { + IDevice mockDevice = DeviceTest.createMockDevice(); + DeviceTest.injectShellResponse(mockDevice, ""); + DeviceTest.injectShellResponse(mockDevice, GETPROP_RESPONSE); + EasyMock.replay(mockDevice); + + PropertyFetcher fetcher = new PropertyFetcher(mockDevice); + assertNull(fetcher.getProperty("ro.sf.lcd_density").get(2, TimeUnit.SECONDS)); + assertEquals("480", fetcher.getProperty("ro.sf.lcd_density").get(2, TimeUnit.SECONDS)); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/SysFsBatteryLevelReceiverTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/SysFsBatteryLevelReceiverTest.java new file mode 100644 index 0000000..3b45068 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/SysFsBatteryLevelReceiverTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.BatteryFetcher.SysFsBatteryLevelReceiver; + +import junit.framework.TestCase; + +import java.util.Random; + +public class SysFsBatteryLevelReceiverTest extends TestCase { + + private SysFsBatteryLevelReceiver mReceiver; + private Integer mExpected1, mExpected2; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mReceiver = new SysFsBatteryLevelReceiver(); + Random r = new Random(System.currentTimeMillis()); + mExpected1 = r.nextInt(101); + mExpected2 = r.nextInt(101); + } + + public void testSingleLine() { + String[] lines = {mExpected1.toString()}; + mReceiver.processNewLines(lines); + assertEquals(mExpected1, mReceiver.getBatteryLevel()); + } + + public void testWithTrailingWhitespace1() { + String[] lines = {mExpected1 + " "}; + mReceiver.processNewLines(lines); + assertEquals(mExpected1, mReceiver.getBatteryLevel()); + } + + public void testWithTrailingWhitespace2() { + String[] lines = {mExpected1 + "\n"}; + mReceiver.processNewLines(lines); + assertEquals(mExpected1, mReceiver.getBatteryLevel()); + } + + public void testWithTrailingWhitespace3() { + String[] lines = {mExpected1 + "\r"}; + mReceiver.processNewLines(lines); + assertEquals(mExpected1, mReceiver.getBatteryLevel()); + } + + public void testWithTrailingWhitespace4() { + String[] lines = {mExpected1 + "\r\n"}; + mReceiver.processNewLines(lines); + assertEquals(mExpected1, mReceiver.getBatteryLevel()); + } + + public void testMultipleLinesSame() { + String[] lines = {mExpected1 + "\n", mExpected2.toString()}; + mReceiver.processNewLines(lines); + assertEquals(mExpected1, mReceiver.getBatteryLevel()); + } + + public void testMultipleLinesDifferent() { + String[] lines = {mExpected1 + "\n", mExpected2.toString()}; + mReceiver.processNewLines(lines); + assertEquals(mExpected1, mReceiver.getBatteryLevel()); + } + + public void testInvalid() { + String[] lines = {"foo\n", "bar", "yadda"}; + mReceiver.processNewLines(lines); + assertNull(mReceiver.getBatteryLevel()); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/allocations/AllocationsParserTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/allocations/AllocationsParserTest.java new file mode 100644 index 0000000..f0a2076 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/allocations/AllocationsParserTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.allocations; + +import com.android.ddmlib.AllocationInfo; +import com.android.ddmlib.AllocationsParser; +import com.google.common.base.Charsets; +import junit.framework.TestCase; + +import java.io.IOException; +import java.net.URL; +import java.nio.ByteBuffer; + +public class AllocationsParserTest extends TestCase { + + public void testParsingOnNoAllocations() throws IOException { + ByteBuffer data = putAllocationInfo(new String[0], new String[0], new String[0], new int[0][], new short[0][][]); + assertEquals(0, AllocationsParser.parse(data).length); + } + + public void testParsingOnOneAllocationWithoutStackTrace() throws IOException { + ByteBuffer data = + putAllocationInfo(new String[]{"path.Foo"}, new String[0], new String[0], new int[][]{{32, 4, 0, 0}}, new short[][][]{{}}); + AllocationInfo[] info = AllocationsParser.parse(data); + assertEquals(1, info.length); + + AllocationInfo alloc = info[0]; + checkEntry(1, "path.Foo", 32, 4, alloc); + checkFirstTrace(null, null, alloc); + assertEquals(0, alloc.getStackTrace().length); + } + + public void testParsingOnOneAllocationWithStackTrace() throws IOException { + ByteBuffer data = putAllocationInfo(new String[]{"path.Foo", "path.Bar", "path.Baz"}, new String[]{"foo", "bar", "baz"}, + new String[]{"Foo.java", "Bar.java"}, new int[][]{{64, 0, 1, 3}}, + new short[][][]{{{1, 1, 1, -1}, {2, 0, 1, 2000}, {0, 2, 0, 10}}}); + AllocationInfo[] info = AllocationsParser.parse(data); + assertEquals(1, info.length); + + AllocationInfo alloc = info[0]; + checkEntry(1, "path.Bar", 64, 0, alloc); + checkFirstTrace("path.Bar", "bar", alloc); + + StackTraceElement[] elems = alloc.getStackTrace(); + assertEquals(3, elems.length); + + checkStackFrame("path.Bar", "bar", "Bar.java", -1, elems[0]); + checkStackFrame("path.Baz", "foo", "Bar.java", 2000, elems[1]); + checkStackFrame("path.Foo", "baz", "Foo.java", 10, elems[2]); + } + + public void testParsing() throws IOException { + ByteBuffer data = putAllocationInfo(new String[]{"path.Red", "path.Green", "path.Blue", "path.LightCanaryishGrey"}, + new String[]{"eatTiramisu", "failUnitTest", "watchCatVideos", "passGo", "collectFakeMoney", "findWaldo"}, + new String[]{"Red.java", "SomewhatBlue.java", "LightCanaryishGrey.java"}, + new int[][]{{128, 8, 0, 2}, {16, 8, 2, 1}, {42, 2, 1, 3}}, + new short[][][]{{{1, 0, 1, 100}, {2, 5, 1, -2}}, {{0, 1, 0, -1}}, {{3, 4, 2, 10001}, {0, 3, 0, 0}, {2, 2, 1, 16}}}); + AllocationInfo[] info = AllocationsParser.parse(data); + assertEquals(3, info.length); + + AllocationInfo alloc1 = info[0]; + checkEntry(3, "path.Red", 128, 8, alloc1); + checkFirstTrace("path.Green", "eatTiramisu", alloc1); + + StackTraceElement[] elems1 = alloc1.getStackTrace(); + assertEquals(2, elems1.length); + + checkStackFrame("path.Green", "eatTiramisu", "SomewhatBlue.java", 100, elems1[0]); + checkStackFrame("path.Blue", "findWaldo", "SomewhatBlue.java", -2, elems1[1]); + + AllocationInfo alloc2 = info[1]; + checkEntry(2, "path.Blue", 16, 8, alloc2); + checkFirstTrace("path.Red", "failUnitTest", alloc2); + + StackTraceElement[] elems2 = alloc2.getStackTrace(); + assertEquals(1, elems2.length); + + checkStackFrame("path.Red", "failUnitTest", "Red.java", -1, elems2[0]); + + AllocationInfo alloc3 = info[2]; + checkEntry(1, "path.Green", 42, 2, alloc3); + checkFirstTrace("path.LightCanaryishGrey", "collectFakeMoney", alloc3); + + StackTraceElement[] elems3 = alloc3.getStackTrace(); + assertEquals(3, elems3.length); + + checkStackFrame("path.LightCanaryishGrey", "collectFakeMoney", "LightCanaryishGrey.java", 10001, elems3[0]); + checkStackFrame("path.Red", "passGo", "Red.java", 0, elems3[1]); + checkStackFrame("path.Blue", "watchCatVideos", "SomewhatBlue.java", 16, elems3[2]); + } + + private static void checkEntry(int order, String className, int size, int thread, AllocationInfo alloc) { + assertEquals(order, alloc.getAllocNumber()); + assertEquals(className, alloc.getAllocatedClass()); + assertEquals(size, alloc.getSize()); + assertEquals(thread, alloc.getThreadId()); + } + + private static void checkFirstTrace(String className, String methodName, AllocationInfo alloc) { + assertEquals(className, alloc.getFirstTraceClassName()); + assertEquals(methodName, alloc.getFirstTraceMethodName()); + } + + private static void checkStackFrame(String className, String methodName, String fileName, int lineNumber, StackTraceElement elem) { + assertEquals(className, elem.getClassName()); + assertEquals(methodName, elem.getMethodName()); + assertEquals(fileName, elem.getFileName()); + assertEquals(lineNumber, elem.getLineNumber()); + } + + public static ByteBuffer putAllocationInfo(String[] classNames, String[] methodNames, String[] fileNames, int[][] entries, + short[][][] stackFrames) throws IOException { + byte msgHdrLen = 15, entryHdrLen = 9, stackFrameLen = 8; + + // Number of bytes from start of message to string tables + int offset = msgHdrLen; + for (int[] entry : entries) { + offset += entryHdrLen + (stackFrameLen * entry[3]); + } + + // Number of bytes in string tables + int strNamesLen = 0; + for (String name : classNames) { strNamesLen += 4 + (2 * name.length()); } + for (String name : methodNames) { strNamesLen += 4 + (2 * name.length()); } + for (String name : fileNames) { strNamesLen += 4 + (2 * name.length()); } + + ByteBuffer data = ByteBuffer.allocate(offset + strNamesLen); + + data.put(new byte[]{msgHdrLen, entryHdrLen, stackFrameLen}); + data.putShort((short) entries.length); + data.putInt(offset); + data.putShort((short) classNames.length); + data.putShort((short) methodNames.length); + data.putShort((short) fileNames.length); + + for (short i = 0; i < entries.length; ++i) { + data.putInt(entries[i][0]); // total alloc size + data.putShort((short) entries[i][1]); // thread id + data.putShort((short) entries[i][2]); // allocated class index + data.put((byte) entries[i][3]); // stack depth + + short[][] frames = stackFrames[i]; + for (short[] frame : frames) { + data.putShort(frame[0]); // class name + data.putShort(frame[1]); // method name + data.putShort(frame[2]); // source file + data.putShort(frame[3]); // line number + } + } + + for (String name : classNames) { + data.putInt(name.length()); + data.put(strToBytes(name)); + } + for (String name : methodNames) { + data.putInt(name.length()); + data.put(strToBytes(name)); + } + for (String name : fileNames) { + data.putInt(name.length()); + data.put(strToBytes(name)); + } + data.rewind(); + return data; + } + + private static byte[] strToBytes(String str) { + return str.getBytes(Charsets.UTF_16BE); + } +} \ No newline at end of file diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatFilterTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatFilterTest.java new file mode 100644 index 0000000..9940a11 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatFilterTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.logcat; + +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.logcat.LogCatFilter; +import com.android.ddmlib.logcat.LogCatMessage; + +import java.util.List; + +import junit.framework.TestCase; + +public class LogCatFilterTest extends TestCase { + public void testFilterByLogLevel() { + LogCatFilter filter = new LogCatFilter("", + "", "", "", "", LogLevel.DEBUG); + + /* filter message below filter's log level */ + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "", "", ""); + assertEquals(false, filter.matches(msg)); + + /* do not filter message above filter's log level */ + msg = new LogCatMessage(LogLevel.ERROR, + "", "", "", "", "", ""); + assertEquals(true, filter.matches(msg)); + } + + public void testFilterByPid() { + LogCatFilter filter = new LogCatFilter("", + "", "", "123", "", LogLevel.VERBOSE); + + /* show message with pid matching filter */ + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "123", "", "", "", "", ""); + assertEquals(true, filter.matches(msg)); + + /* don't show message with pid not matching filter */ + msg = new LogCatMessage(LogLevel.VERBOSE, + "12", "", "", "", "", ""); + assertEquals(false, filter.matches(msg)); + } + + public void testFilterByAppNameRegex() { + LogCatFilter filter = new LogCatFilter("", + "", "", "", "dalvik.*", LogLevel.VERBOSE); + + /* show message with pid matching filter */ + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "dalvikvm1", "", "", ""); + assertEquals(true, filter.matches(msg)); + + /* don't show message with pid not matching filter */ + msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "system", "", "", ""); + assertEquals(false, filter.matches(msg)); + } + + public void testFilterByTagRegex() { + LogCatFilter filter = new LogCatFilter("", + "tag.*", "", "", "", LogLevel.VERBOSE); + + /* show message with tag matching filter */ + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "tag123", "", ""); + assertEquals(true, filter.matches(msg)); + + msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "ta123", "", ""); + assertEquals(false, filter.matches(msg)); + } + + public void testFilterByTextRegex() { + LogCatFilter filter = new LogCatFilter("", + "", "text.*", "", "", LogLevel.VERBOSE); + + /* show message with text matching filter */ + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "", "", "text123"); + assertEquals(true, filter.matches(msg)); + + msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "", "", "te123"); + assertEquals(false, filter.matches(msg)); + } + + public void testMatchingText() { + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "message with word1 and word2"); //$NON-NLS-1$ + assertEquals(true, search("word1 with", msg)); //$NON-NLS-1$ + assertEquals(true, search("text:w.* ", msg)); //$NON-NLS-1$ + assertEquals(false, search("absent", msg)); //$NON-NLS-1$ + } + + public void testTagKeyword() { + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "tag", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "sample message"); //$NON-NLS-1$ + assertEquals(false, search("t.*", msg)); //$NON-NLS-1$ + assertEquals(true, search("tag:t.*", msg)); //$NON-NLS-1$ + } + + public void testPidKeyword() { + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "123", "", "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "sample message"); //$NON-NLS-1$ + assertEquals(false, search("123", msg)); //$NON-NLS-1$ + assertEquals(true, search("pid:123", msg)); //$NON-NLS-1$ + } + + public void testAppNameKeyword() { + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "dalvik", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "sample message"); //$NON-NLS-1$ + assertEquals(false, search("dalv.*", msg)); //$NON-NLS-1$ + assertEquals(true, search("app:dal.*k", msg)); //$NON-NLS-1$ + } + + public void testCaseSensitivity() { + LogCatMessage msg = new LogCatMessage(LogLevel.VERBOSE, + "", "", "", "", "", + "Sample message"); + + // if regex has an upper case character, it should be + // treated as a case sensitive search + assertEquals(false, search("Message", msg)); + + // if regex is all lower case, then it should be a + // case insensitive search + assertEquals(true, search("sample", msg)); + } + + /** + * Helper method: search if the query string matches the message. + * @param query words to search for + * @param message text to search in + * @return true if the encoded query is present in message + */ + private boolean search(String query, LogCatMessage message) { + List filters = LogCatFilter.fromString(query, + LogLevel.VERBOSE); + + /* all filters have to match for the query to match */ + for (LogCatFilter f : filters) { + if (!f.matches(message)) { + return false; + } + } + return true; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatMessageParserTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatMessageParserTest.java new file mode 100644 index 0000000..cdd55c5 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/logcat/LogCatMessageParserTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.logcat; + +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.logcat.LogCatMessage; +import com.android.ddmlib.logcat.LogCatMessageParser; + +import java.util.List; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link LogCatMessageParser}. + */ +public final class LogCatMessageParserTest extends TestCase { + private List mParsedMessages; + + /** A list of messages generated with the following code: + *
+     * {@code
+     * Log.d("dtag", "debug message");
+     * Log.e("etag", "error message");
+     * Log.i("itag", "info message");
+     * Log.v("vtag", "verbose message");
+     * Log.w("wtag", "warning message");
+     * Log.wtf("wtftag", "wtf message");
+     * Log.d("dtag", "debug message");
+     * }
+     *  
+ * Note: On Android 2.3, Log.wtf doesn't really generate the message. + * It only produces the message header, but swallows the message tag. + * This string has been modified to include the message. + */ + private static final String[] MESSAGES = new String[] { + "[ 08-11 19:11:07.132 495:0x1ef D/dtag ]", //$NON-NLS-1$ + "debug message", //$NON-NLS-1$ + "[ 08-11 19:11:07.132 495: 234 E/etag ]", //$NON-NLS-1$ + "error message", //$NON-NLS-1$ + "[ 08-11 19:11:07.132 495:0x1ef I/itag ]", //$NON-NLS-1$ + "info message", //$NON-NLS-1$ + "[ 08-11 19:11:07.132 495:0x1ef V/vtag ]", //$NON-NLS-1$ + "verbose message", //$NON-NLS-1$ + "[ 08-11 19:11:07.132 495:0x1ef W/wtag ]", //$NON-NLS-1$ + "warning message", //$NON-NLS-1$ + "[ 08-11 19:11:07.132 495:0x1ef F/wtftag ]", //$NON-NLS-1$ + "wtf message", //$NON-NLS-1$ + "[ 08-11 21:15:35.7524 540:0x21c D/dtag ]", //$NON-NLS-1$ + "debug message", //$NON-NLS-1$ + }; + + @Override + protected void setUp() throws Exception { + LogCatMessageParser parser = new LogCatMessageParser(); + mParsedMessages = parser.processLogLines(MESSAGES, null); + } + + /** Check that the correct number of messages are received. */ + public void testMessageCount() { + assertEquals(7, mParsedMessages.size()); + } + + /** Check the log level in a few of the parsed messages. */ + public void testLogLevel() { + assertEquals(mParsedMessages.get(0).getLogLevel(), LogLevel.DEBUG); + assertEquals(mParsedMessages.get(5).getLogLevel(), LogLevel.ASSERT); + } + + /** Check the parsed tag. */ + public void testTag() { + assertEquals(mParsedMessages.get(1).getTag(), "etag"); //$NON-NLS-1$ + } + + /** Check the time field. */ + public void testTime() { + assertEquals(mParsedMessages.get(6).getTime(), "08-11 21:15:35.7524"); //$NON-NLS-1$ + } + + /** Check the message field. */ + public void testMessage() { + assertEquals(mParsedMessages.get(2).getMessage(), MESSAGES[5]); + } + + public void testTid() { + assertEquals(mParsedMessages.get(0).getTid(), Integer.toString(0x1ef)); + assertEquals(mParsedMessages.get(1).getTid(), "234"); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java new file mode 100644 index 0000000..409e2c1 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java @@ -0,0 +1,533 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import junit.framework.TestCase; + +import org.easymock.Capture; +import org.easymock.EasyMock; + +import java.util.Collections; +import java.util.Map; + +/** + * Unit tests for {@link @InstrumentationResultParser}. + */ +@SuppressWarnings("unchecked") +public class InstrumentationResultParserTest extends TestCase { + + private InstrumentationResultParser mParser; + private ITestRunListener mMockListener; + + // static dummy test names to use for validation + private static final String RUN_NAME = "foo"; + private static final String CLASS_NAME = "com.test.FooTest"; + private static final String TEST_NAME = "testFoo"; + private static final String STACK_TRACE = "java.lang.AssertionFailedException"; + private static final TestIdentifier TEST_ID = new TestIdentifier(CLASS_NAME, TEST_NAME); + + /** + * @param name - test name + */ + public InstrumentationResultParserTest(String name) { + super(name); + } + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + // use a strict mock to verify order of method calls + mMockListener = EasyMock.createStrictMock(ITestRunListener.class); + mParser = new InstrumentationResultParser(RUN_NAME, mMockListener); + } + + /** + * Tests parsing empty output. + */ + public void testParse_empty() { + mMockListener.testRunStarted(RUN_NAME, 0); + mMockListener.testRunFailed(InstrumentationResultParser.NO_TEST_RESULTS_MSG); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(""); + } + + /** + * Tests parsing output for a successful test run with no tests. + */ + public void testParse_noTests() { + StringBuilder output = new StringBuilder(); + addLine(output, "INSTRUMENTATION_RESULT: stream="); + addLine(output, "Test results for InstrumentationTestRunner="); + addLine(output, "Time: 0.001"); + addLine(output, "OK (0 tests)"); + addLine(output, "INSTRUMENTATION_CODE: -1"); + + mMockListener.testRunStarted(RUN_NAME, 0); + mMockListener.testRunEnded(1, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Tests parsing output for a single successful test execution. + */ + public void testParse_singleTest() { + StringBuilder output = createSuccessTest(); + + mMockListener.testRunStarted(RUN_NAME, 1); + mMockListener.testStarted(TEST_ID); + mMockListener.testEnded(TEST_ID, Collections.EMPTY_MAP); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Tests parsing output for a successful test execution with metrics. + */ + public void testParse_testMetrics() { + StringBuilder output = buildCommonResult(); + + addStatusKey(output, "randomKey", "randomValue"); + addSuccessCode(output); + + final Capture> captureMetrics = new Capture>(); + mMockListener.testRunStarted(RUN_NAME, 1); + mMockListener.testStarted(TEST_ID); + mMockListener.testEnded(EasyMock.eq(TEST_ID), EasyMock.capture(captureMetrics)); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + + assertEquals("randomValue", captureMetrics.getValue().get("randomKey")); + } + + /** + * Test parsing output for a test that produces repeated metrics values + *

+ * This mimics launch performance test output. + */ + public void testParse_repeatedTestMetrics() { + StringBuilder output = new StringBuilder(); + // add test start output + addCommonStatus(output); + addStartCode(output); + + addStatusKey(output, "currentiterations", "1"); + addStatusCode(output, "2"); + addStatusKey(output, "currentiterations", "2"); + addStatusCode(output, "2"); + addStatusKey(output, "currentiterations", "3"); + addStatusCode(output, "2"); + + // add test end + addCommonStatus(output); + addStatusKey(output, "numiterations", "3"); + addSuccessCode(output); + + final Capture> captureMetrics = new Capture>(); + mMockListener.testRunStarted(RUN_NAME, 1); + mMockListener.testStarted(TEST_ID); + mMockListener.testEnded(EasyMock.eq(TEST_ID), EasyMock.capture(captureMetrics)); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + + assertEquals("3", captureMetrics.getValue().get("currentiterations")); + assertEquals("3", captureMetrics.getValue().get("numiterations")); + } + + /** + * Test parsing output for a test failure. + */ + public void testParse_testFailed() { + StringBuilder output = buildCommonResult(); + addStackTrace(output); + addFailureCode(output); + + mMockListener.testRunStarted(RUN_NAME, 1); + mMockListener.testStarted(TEST_ID); + mMockListener.testFailed(TEST_ID, STACK_TRACE); + mMockListener.testEnded(TEST_ID, Collections.EMPTY_MAP); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Test parsing and conversion of time output that contains extra chars. + */ + public void testParse_timeBracket() { + StringBuilder output = createSuccessTest(); + output.append("Time: 0.001)"); + + mMockListener.testRunStarted(RUN_NAME, 1); + mMockListener.testStarted(TEST_ID); + mMockListener.testEnded(TEST_ID, Collections.EMPTY_MAP); + mMockListener.testRunEnded(1, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Test parsing output for a test run failure. + */ + public void testParse_runFailed() { + StringBuilder output = new StringBuilder(); + final String errorMessage = "Unable to find instrumentation info"; + addStatusKey(output, "Error", errorMessage); + addStatusCode(output, "-1"); + output.append("INSTRUMENTATION_FAILED: com.dummy/android.test.InstrumentationTestRunner"); + addLineBreak(output); + + mMockListener.testRunStarted(RUN_NAME, 0); + mMockListener.testRunFailed(errorMessage); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Test parsing output when a status code cannot be parsed + */ + public void testParse_invalidCode() { + StringBuilder output = new StringBuilder(); + addLine(output, "android.util.AndroidException: INSTRUMENTATION_FAILED: foo/foo"); + addLine(output, "INSTRUMENTATION_STATUS: id=ActivityManagerService"); + addLine(output, "INSTRUMENTATION_STATUS: Error=Unable to find instrumentation target package: foo"); + addLine(output, "INSTRUMENTATION_STATUS_CODE: -1at com.android.commands.am.Am.runInstrument(Am.java:532)"); + addLine(output, ""); + addLine(output, " at com.android.commands.am.Am.run(Am.java:111)"); + addLineBreak(output); + + mMockListener.testRunStarted(RUN_NAME, 0); + mMockListener.testRunFailed((String)EasyMock.anyObject()); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Test parsing output for a test run failure, where an instrumentation component failed to + * load. + *

+ * Parsing input takes the from of INSTRUMENTATION_RESULT: fff + */ + public void testParse_failedResult() { + StringBuilder output = new StringBuilder(); + final String errorMessage = "Unable to instantiate instrumentation"; + output.append("INSTRUMENTATION_RESULT: shortMsg="); + output.append(errorMessage); + addLineBreak(output); + output.append("INSTRUMENTATION_CODE: 0"); + addLineBreak(output); + + mMockListener.testRunStarted(RUN_NAME, 0); + mMockListener.testRunFailed(EasyMock.contains(errorMessage)); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Test parsing output for a test run that did not complete. + *

+ * This can occur if device spontaneously reboots, or if test method could not be found. + */ + public void testParse_incomplete() { + StringBuilder output = new StringBuilder(); + // add a start test sequence, but without an end test sequence + addCommonStatus(output); + addStartCode(output); + + mMockListener.testRunStarted(RUN_NAME, 1); + mMockListener.testStarted(TEST_ID); + mMockListener.testFailed(EasyMock.eq(TEST_ID), + EasyMock.startsWith(InstrumentationResultParser.INCOMPLETE_TEST_ERR_MSG_PREFIX)); + mMockListener.testEnded(TEST_ID, Collections.EMPTY_MAP); + mMockListener.testRunFailed(EasyMock.startsWith( + InstrumentationResultParser.INCOMPLETE_RUN_ERR_MSG_PREFIX)); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Test parsing output for a test run that did not start due to incorrect syntax supplied to am. + */ + public void testParse_amFailed() { + StringBuilder output = new StringBuilder(); + addLine(output, "usage: am [subcommand] [options]"); + addLine(output, "start an Activity: am start [-D] [-W] "); + addLine(output, "-D: enable debugging"); + addLine(output, "-W: wait for launch to complete"); + addLine(output, "start a Service: am startservice "); + addLine(output, "Error: Bad component name: wfsdafddfasasdf"); + + mMockListener.testRunStarted(RUN_NAME, 0); + mMockListener.testRunFailed(InstrumentationResultParser.NO_TEST_RESULTS_MSG); + mMockListener.testRunEnded(0, Collections.EMPTY_MAP); + + injectAndVerifyTestString(output.toString()); + } + + /** + * Test parsing output for a test run that produces INSTRUMENTATION_RESULT output. + *

+ * This mimics launch performance test output. + */ + public void testParse_instrumentationResults() { + StringBuilder output = new StringBuilder(); + addResultKey(output, "other_pss", "2390"); + addResultKey(output, "java_allocated", "2539"); + addResultKey(output, "foo", "bar"); + addResultKey(output, "stream", "should not be captured"); + addLine(output, "INSTRUMENTATION_CODE: -1"); + + Capture> captureMetrics = new Capture>(); + mMockListener.testRunStarted(RUN_NAME, 0); + mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.capture(captureMetrics)); + + injectAndVerifyTestString(output.toString()); + + assertEquals("2390", captureMetrics.getValue().get("other_pss")); + assertEquals("2539", captureMetrics.getValue().get("java_allocated")); + assertEquals("bar", captureMetrics.getValue().get("foo")); + assertEquals(3, captureMetrics.getValue().size()); + } + + public void testParse_AssumptionFailuresIgnored() { + StringBuilder output = new StringBuilder(); + addLine(output, "INSTRUMENTATION_STATUS: numtests=3"); + addLine(output, "INSTRUMENTATION_STATUS: stream="); + addLine(output, "com.example.helloworld.FailureAssumptionTest:"); + addLine(output, "INSTRUMENTATION_STATUS: id=AndroidJUnitRunner"); + addLine(output, "INSTRUMENTATION_STATUS: test=checkIgnoreTestsArePossible"); + addLine(output, "INSTRUMENTATION_STATUS: class=com.example.helloworld.FailureAssumptionTest"); + addLine(output, "INSTRUMENTATION_STATUS: current=1"); + addLine(output, "INSTRUMENTATION_STATUS_CODE: -3"); + addLine(output, "INSTRUMENTATION_STATUS: numtests=3"); + addLine(output, "INSTRUMENTATION_STATUS: stream="); + addLine(output, "INSTRUMENTATION_STATUS: id=AndroidJUnitRunner"); + addLine(output, "INSTRUMENTATION_STATUS: test=checkAssumptionIsSkipped"); + addLine(output, "INSTRUMENTATION_STATUS: class=com.example.helloworld.FailureAssumptionTest"); + addLine(output, "INSTRUMENTATION_STATUS: current=2"); + addLine(output, "INSTRUMENTATION_STATUS_CODE: 1"); + addLine(output, "INSTRUMENTATION_STATUS: numtests=3"); + addLine(output, "INSTRUMENTATION_STATUS: stream="); + addLine(output, "INSTRUMENTATION_STATUS: id=AndroidJUnitRunner"); + addLine(output, "INSTRUMENTATION_STATUS: test=checkAssumptionIsSkipped"); + addLine(output, "INSTRUMENTATION_STATUS: class=com.example.helloworld.FailureAssumptionTest"); + addLine(output, "INSTRUMENTATION_STATUS: stack=org.junit.AssumptionViolatedException: got: , expected: is "); + addLine(output, "at org.junit.Assume.assumeThat(Assume.java:95)"); + addLine(output, "at org.junit.Assume.assumeTrue(Assume.java:41)"); + addLine(output, "at com.example.helloworld.FailureAssumptionTest.checkAssumptionIsSkipped(FailureAssumptionTest.java:19)"); + addLine(output, "at java.lang.reflect.Method.invoke(Native Method)"); + addLine(output, "at java.lang.reflect.Method.invoke(Method.java:372)"); + addLine(output, "at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)"); + addLine(output, "at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)"); + addLine(output, "at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)"); + addLine(output, "at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)"); + addLine(output, "at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)"); + addLine(output, "at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)"); + addLine(output, "at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)"); + addLine(output, "at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)"); + addLine(output, "at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)"); + addLine(output, "at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)"); + addLine(output, "at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)"); + addLine(output, "at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)"); + addLine(output, "at org.junit.runners.ParentRunner.run(ParentRunner.java:363)"); + addLine(output, "at org.junit.runners.Suite.runChild(Suite.java:128)"); + addLine(output, "at org.junit.runners.Suite.runChild(Suite.java:27)"); + addLine(output, "at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)"); + addLine(output, "at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)"); + addLine(output, "at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)"); + addLine(output, "at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)"); + addLine(output, "at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)"); + addLine(output, "at org.junit.runners.ParentRunner.run(ParentRunner.java:363)"); + addLine(output, "at org.junit.runner.JUnitCore.run(JUnitCore.java:137)"); + addLine(output, "at org.junit.runner.JUnitCore.run(JUnitCore.java:115)"); + addLine(output, "at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:54)"); + addLine(output, "at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:228)"); + addLine(output, "at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1853)"); + addLine(output, ""); + addLine(output, "INSTRUMENTATION_STATUS: current=2"); + addLine(output, "INSTRUMENTATION_STATUS_CODE: -4"); + addLine(output, "INSTRUMENTATION_STATUS: numtests=3"); + addLine(output, "INSTRUMENTATION_STATUS: stream="); + addLine(output, "com.example.helloworld.HelloWorldTest:"); + addLine(output, "INSTRUMENTATION_STATUS: id=AndroidJUnitRunner"); + addLine(output, "INSTRUMENTATION_STATUS: test=testPreconditions"); + addLine(output, "INSTRUMENTATION_STATUS: class=com.example.helloworld.HelloWorldTest"); + addLine(output, "INSTRUMENTATION_STATUS: current=3"); + addLine(output, "INSTRUMENTATION_STATUS_CODE: 1"); + addLine(output, "INSTRUMENTATION_STATUS: numtests=3"); + addLine(output, "INSTRUMENTATION_STATUS: stream=."); + addLine(output, "INSTRUMENTATION_STATUS: id=AndroidJUnitRunner"); + addLine(output, "INSTRUMENTATION_STATUS: test=testPreconditions"); + addLine(output, "INSTRUMENTATION_STATUS: class=com.example.helloworld.HelloWorldTest"); + addLine(output, "INSTRUMENTATION_STATUS: current=3"); + addLine(output, "INSTRUMENTATION_STATUS_CODE: 0"); + addLine(output, "INSTRUMENTATION_RESULT: stream="); + addLine(output, ""); + addLine(output, "Time: 0.676"); + addLine(output, ""); + addLine(output, "OK (2 tests)"); + addLine(output, ""); + addLine(output, ""); + addLine(output, "INSTRUMENTATION_CODE: -1"); + + + mMockListener.testRunStarted(RUN_NAME, 3); + final TestIdentifier IGNORED_ANNOTATION = new TestIdentifier( + "com.example.helloworld.FailureAssumptionTest", + "checkIgnoreTestsArePossible"); + mMockListener.testStarted(IGNORED_ANNOTATION); + mMockListener.testIgnored(IGNORED_ANNOTATION); + mMockListener.testEnded(IGNORED_ANNOTATION, Collections.EMPTY_MAP); + final TestIdentifier ASSUME_FALSE = new TestIdentifier( + "com.example.helloworld.FailureAssumptionTest", + "checkAssumptionIsSkipped"); + mMockListener.testStarted(ASSUME_FALSE); + mMockListener.testAssumptionFailure(EasyMock.eq(ASSUME_FALSE), + EasyMock.startsWith( + "org.junit.AssumptionViolatedException: got: , expected: is ")); + mMockListener.testEnded(ASSUME_FALSE, Collections.EMPTY_MAP); + final TestIdentifier HELLO_WORLD = new TestIdentifier("com.example.helloworld.HelloWorldTest", + "testPreconditions"); + mMockListener.testStarted(HELLO_WORLD); + mMockListener.testEnded(HELLO_WORLD, Collections.EMPTY_MAP); + mMockListener.testRunEnded(EasyMock.eq(676L), EasyMock.anyObject(Map.class)); + + injectAndVerifyTestString(output.toString()); +} + + /** + * Builds a common test result using TEST_NAME and TEST_CLASS. + */ + private StringBuilder buildCommonResult() { + StringBuilder output = new StringBuilder(); + // add test start bundle + addCommonStatus(output); + addStartCode(output); + // add end test bundle, without status + addCommonStatus(output); + return output; + } + + /** + * Create instrumentation output for a successful single test case execution. + */ + private StringBuilder createSuccessTest() { + StringBuilder output = buildCommonResult(); + addSuccessCode(output); + return output; + } + + /** + * Adds common status results to the provided output. + */ + private void addCommonStatus(StringBuilder output) { + addStatusKey(output, "stream", "\r\n" + CLASS_NAME); + addStatusKey(output, "test", TEST_NAME); + addStatusKey(output, "class", CLASS_NAME); + addStatusKey(output, "current", "1"); + addStatusKey(output, "numtests", "1"); + addStatusKey(output, "id", "InstrumentationTestRunner"); + } + + /** + * Adds a stack trace status bundle to output. + */ + private void addStackTrace(StringBuilder output) { + addStatusKey(output, "stack", STACK_TRACE); + } + + /** + * Helper method to add a status key-value bundle. + */ + private void addStatusKey(StringBuilder outputBuilder, String key, + String value) { + outputBuilder.append("INSTRUMENTATION_STATUS: "); + outputBuilder.append(key); + outputBuilder.append('='); + outputBuilder.append(value); + addLineBreak(outputBuilder); + } + + /** + * Helper method to add a result key value bundle. + */ + private void addResultKey(StringBuilder outputBuilder, String key, + String value) { + outputBuilder.append("INSTRUMENTATION_RESULT: "); + outputBuilder.append(key); + outputBuilder.append('='); + outputBuilder.append(value); + addLineBreak(outputBuilder); + } + + /** + * Append a line to output. + */ + private void addLine(StringBuilder outputBuilder, String lineContent) { + outputBuilder.append(lineContent); + addLineBreak(outputBuilder); + } + + /** + * Append line break characters to output + */ + private void addLineBreak(StringBuilder outputBuilder) { + outputBuilder.append("\r\n"); + } + + private void addStartCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "1"); + } + + private void addSuccessCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "0"); + } + + private void addFailureCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "-2"); + } + + private void addStatusCode(StringBuilder outputBuilder, String value) { + outputBuilder.append("INSTRUMENTATION_STATUS_CODE: "); + outputBuilder.append(value); + addLineBreak(outputBuilder); + } + + /** + * Inject a test string into the result parser, and verify the mock listener. + * + * @param result the string to inject into parser under test. + */ + private void injectAndVerifyTestString(String result) { + EasyMock.replay(mMockListener); + byte[] data = result.getBytes(); + mParser.addOutput(data, 0, data.length); + mParser.flush(); + EasyMock.verify(mMockListener); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java new file mode 100644 index 0000000..523e599 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.IShellEnabledDevice; +import com.android.ddmlib.IShellOutputReceiver; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; + +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for {@link RemoteAndroidTestRunner}. + */ +public class RemoteAndroidTestRunnerTest extends TestCase { + + private RemoteAndroidTestRunner mRunner; + private IShellEnabledDevice mMockDevice; + private ITestRunListener mMockListener; + + private static final String TEST_PACKAGE = "com.test"; + private static final String TEST_RUNNER = "com.test.InstrumentationTestRunner"; + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + mMockDevice = EasyMock.createMock(IShellEnabledDevice.class); + EasyMock.expect(mMockDevice.getName()).andStubReturn("serial"); + mMockListener = EasyMock.createNiceMock(ITestRunListener.class); + mRunner = new RemoteAndroidTestRunner(TEST_PACKAGE, TEST_RUNNER, mMockDevice); + } + + /** + * Test the basic case building of the instrumentation runner command with no arguments. + */ + public void testRun() throws Exception { + String expectedCmd = EasyMock.eq(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, + TEST_RUNNER)); + runAndVerify(expectedCmd); + } + + /** + * Test the building of the instrumentation runner command with log set. + */ + public void testRun_withLog() throws Exception { + mRunner.setLogOnly(true); + String expectedCmd = EasyMock.contains("-e log true"); + runAndVerify(expectedCmd); + } + + /** + * Test the building of the instrumentation runner command with method set. + */ + public void testRun_withMethod() throws Exception { + final String className = "FooTest"; + final String testName = "fooTest"; + mRunner.setMethodName(className, testName); + String expectedCmd = EasyMock.contains(String.format("-e class %s#%s", className, + testName)); + runAndVerify(expectedCmd); + } + + /** + * Test the building of the instrumentation runner command with test package set. + */ + public void testRun_withPackage() throws Exception { + final String packageName = "foo.test"; + mRunner.setTestPackageName(packageName); + String expectedCmd = EasyMock.contains(String.format("-e package %s", packageName)); + runAndVerify(expectedCmd); + } + + /** + * Test the building of the instrumentation runner command with extra argument added. + */ + public void testRun_withAddInstrumentationArg() throws Exception { + final String extraArgName = "blah"; + final String extraArgValue = "blahValue"; + mRunner.addInstrumentationArg(extraArgName, extraArgValue); + String expectedCmd = EasyMock.contains(String.format("-e %s %s", extraArgName, + extraArgValue)); + runAndVerify(expectedCmd); + } + + /** + * Test additional run options. + */ + public void testRun_runOptions() throws Exception { + mRunner.setRunOptions("--no-window-animation"); + String expectedCmd = + EasyMock.eq( + String.format( + "am instrument -w -r --no-window-animation %s/%s", + TEST_PACKAGE, + TEST_RUNNER)); + runAndVerify(expectedCmd); + } + + /** + * Test run when the device throws a IOException + */ + @SuppressWarnings("unchecked") + public void testRun_ioException() throws Exception { + mMockDevice.executeShellCommand((String)EasyMock.anyObject(), (IShellOutputReceiver) + EasyMock.anyObject(), EasyMock.eq(0L), EasyMock.eq(TimeUnit.MILLISECONDS)); + EasyMock.expectLastCall().andThrow(new IOException()); + // verify that the listeners run started, run failure, and run ended methods are called + mMockListener.testRunStarted(TEST_PACKAGE, 0); + mMockListener.testRunFailed((String)EasyMock.anyObject()); + mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.eq(Collections.EMPTY_MAP)); + + EasyMock.replay(mMockDevice, mMockListener); + try { + mRunner.run(mMockListener); + fail("IOException not thrown"); + } catch (IOException e) { + // expected + } + EasyMock.verify(mMockDevice, mMockListener); + } + + /** + * Calls {@link RemoteAndroidTestRunner#run(ITestRunListener...)} and verifies the given + * expectedCmd pattern was received by the mock device. + */ + private void runAndVerify(String expectedCmd) throws Exception { + mMockDevice.executeShellCommand(expectedCmd, (IShellOutputReceiver) + EasyMock.anyObject(), EasyMock.eq(0L), EasyMock.eq(TimeUnit.MILLISECONDS)); + EasyMock.replay(mMockDevice); + mRunner.run(mMockListener); + EasyMock.verify(mMockDevice); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/TestRunResultTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/TestRunResultTest.java new file mode 100644 index 0000000..d697cc4 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/TestRunResultTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.testrunner.TestResult.TestStatus; + +import junit.framework.TestCase; + +import java.util.Collections; + +/** + * Unit tests for {@link TestRunResult} + */ +public class TestRunResultTest extends TestCase { + + public void testGetNumTestsInState() { + TestIdentifier test = new TestIdentifier("FooTest", "testBar"); + TestRunResult result = new TestRunResult(); + assertEquals(0, result.getNumTestsInState(TestStatus.PASSED)); + result.testStarted(test); + assertEquals(0, result.getNumTestsInState(TestStatus.PASSED)); + assertEquals(1, result.getNumTestsInState(TestStatus.INCOMPLETE)); + result.testEnded(test, Collections.EMPTY_MAP); + assertEquals(1, result.getNumTestsInState(TestStatus.PASSED)); + assertEquals(0, result.getNumTestsInState(TestStatus.INCOMPLETE)); + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java new file mode 100644 index 0000000..04f1856 --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.testrunner; + +import junit.framework.TestCase; + +import org.xml.sax.InputSource; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringReader; +import java.util.Collections; +import java.util.Map; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +/** + * Unit tests for {@link XmlTestRunListener}. + */ +public class XmlTestRunListenerTest extends TestCase { + + private XmlTestRunListener mResultReporter; + private ByteArrayOutputStream mOutputStream; + private File mReportDir; + + /** + * {@inheritDoc} + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + + mOutputStream = new ByteArrayOutputStream(); + mResultReporter = new XmlTestRunListener() { + @Override + OutputStream createOutputResultStream(File reportDir) throws IOException { + return mOutputStream; + } + + @Override + String getTimestamp() { + return "ignore"; + } + }; + // TODO: use mock file dir instead + mReportDir = createTmpDir(); + mResultReporter.setReportDir(mReportDir); + } + + private File createTmpDir() throws IOException { + // create a temp file with unique name, then make it a directory + File tmpDir = File.createTempFile("foo", "dir"); + tmpDir.delete(); + if (!tmpDir.mkdirs()) { + throw new IOException("unable to create directory"); + } + return tmpDir; + } + + /** + * Recursively delete given file and all its contents + */ + private static void recursiveDelete(File rootDir) { + if (rootDir.isDirectory()) { + File[] childFiles = rootDir.listFiles(); + if (childFiles != null) { + for (File child : childFiles) { + recursiveDelete(child); + } + } + } + rootDir.delete(); + } + + @Override + protected void tearDown() throws Exception { + if (mReportDir != null) { + recursiveDelete(mReportDir); + } + super.tearDown(); + } + + /** + * A simple test to ensure expected output is generated for test run with no tests. + */ + public void testEmptyGeneration() { + final String expectedOutput = "" + + " " + + "" + + ""; + mResultReporter.testRunStarted("test", 1); + mResultReporter.testRunEnded(1, Collections. emptyMap()); + + // because the timestamp is impossible to hardcode, look for the actual timestamp and + // replace it in the expected string. + String output = getOutput(); + String time = getTime(output); + assertNotNull(time); + + String expectedTimedOutput = expectedOutput.replaceFirst("#TIMEVALUE#", time); + assertEquals(expectedTimedOutput, output); + } + + /** + * A simple test to ensure expected output is generated for test run with a single passed test. + */ + public void testSinglePass() { + Map emptyMap = Collections.emptyMap(); + final TestIdentifier testId = new TestIdentifier("FooTest", "testFoo"); + mResultReporter.testRunStarted("run", 1); + mResultReporter.testStarted(testId); + mResultReporter.testEnded(testId, emptyMap); + mResultReporter.testRunEnded(3, emptyMap); + String output = getOutput(); + // TODO: consider doing xml based compare + assertTrue(output.contains("tests=\"1\" failures=\"0\" errors=\"0\"")); + final String testCaseTag = String.format(" emptyMap = Collections.emptyMap(); + final TestIdentifier testId = new TestIdentifier("FooTest", "testFoo"); + final String trace = "this is a trace"; + mResultReporter.testRunStarted("run", 1); + mResultReporter.testStarted(testId); + mResultReporter.testFailed(testId, trace); + mResultReporter.testEnded(testId, emptyMap); + mResultReporter.testRunEnded(3, emptyMap); + String output = getOutput(); + // TODO: consider doing xml based compare + assertTrue(output.contains("tests=\"1\" failures=\"1\" errors=\"0\"")); + final String testCaseTag = String.format("%s", trace); + assertTrue(output.contains(failureTag)); + } + + /** + * Gets the output produced, stripping it of extraneous whitespace characters. + */ + private String getOutput() { + String output = mOutputStream.toString(); + // ignore newlines and tabs whitespace + output = output.replaceAll("[\\r\\n\\t]", ""); + // replace two ws chars with one + return output.replaceAll(" ", " "); + } + + /** + * Returns the value if the time attribute from the given XML content + * + * Actual XPATH: /testsuite/@time + * + * @param xml XML content. + * @return + */ + private String getTime(String xml) { + XPath xpath = XPathFactory.newInstance().newXPath(); + + try { + return xpath.evaluate("/testsuite/@time", new InputSource(new StringReader(xml))); + } catch (XPathExpressionException e) { + // won't happen. + } + + return null; + } +} diff --git a/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/utils/DebuggerPortsTest.java b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/utils/DebuggerPortsTest.java new file mode 100644 index 0000000..ac54c4b --- /dev/null +++ b/src/main/resources/external/src/ddmlib/src/test/java/com/android/ddmlib/utils/DebuggerPortsTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ddmlib.utils; + +import junit.framework.TestCase; + +public class DebuggerPortsTest extends TestCase { + public void testNextFreePort() { + DebuggerPorts freePorts = new DebuggerPorts(9000); + assertEquals(9000, freePorts.next()); + assertEquals(9001, freePorts.next()); + } + + public void testReleasePort() { + DebuggerPorts freePorts = new DebuggerPorts(9000); + int first = freePorts.next(); + int second = freePorts.next(); + + freePorts.free(first); + assertEquals(first, freePorts.next()); + assertEquals(second + 1, freePorts.next()); + + freePorts.free(second); + assertEquals(second, freePorts.next()); + } +} diff --git a/src/main/scala/org/ftd/Master/Config.scala b/src/main/scala/org/ftd/Master/Config.scala new file mode 100644 index 0000000..a1cbf4b --- /dev/null +++ b/src/main/scala/org/ftd/Master/Config.scala @@ -0,0 +1,25 @@ +package org.ftd.Master + +import java.io.File +import java.nio.file.Path + +import scala.concurrent.duration.Duration + +case class Config( + apkPath: Seq[Path] = null, + apkInstallOpts: Seq[String] = Seq(), + testRunnerClsPath: String = null, + testClassPath: String = null, + testMethodPath: String = null, + adbPath: Path = null, + deviceName: String = null, + appName: String = null, + testPackage: String = null, + debug: Boolean = false, + max_runs: Int = -1, // -1 means forever + from_files: File = null, + disable_ddm_log: Boolean = false, + testCaseHangTimeout: Duration = Duration.Inf, + strategy: String = "XiaoScheduling", + givenPassed: Boolean = true, + ) \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/FTDDebugger.scala b/src/main/scala/org/ftd/Master/FTDDebugger.scala new file mode 100644 index 0000000..d7054af --- /dev/null +++ b/src/main/scala/org/ftd/Master/FTDDebugger.scala @@ -0,0 +1,461 @@ +package org.ftd.Master + +import java.net.ConnectException +import java.util + +import com.sun.jdi.{Bootstrap, ClassType, Location, Method, ObjectReference, ThreadReference, VMDisconnectedException, VirtualMachine} +import com.sun.jdi.connect.Connector.Argument +import com.sun.jdi.event.{BreakpointEvent, ClassPrepareEvent, Event, EventSet, LocatableEvent, MethodEntryEvent, MonitorWaitEvent, MonitorWaitedEvent, StepEvent, VMDisconnectEvent} +import com.sun.jdi.request.{BreakpointRequest, EventRequest, MonitorWaitRequest, MonitorWaitedRequest, StepRequest} +import com.sun.jdi.request.EventRequest.{SUSPEND_ALL, SUSPEND_EVENT_THREAD} +import org.apache.logging.log4j.LogManager +import org.ftd.Master.SuspendTiming.SuspendTiming +import org.ftd.Master.TestRunnerThreadState.TestRunnerThreadState +import org.ftd.Master.utils.Retry + +import scala.util.control.Breaks._ +//import collection.JavaConverters._ +import scala.jdk.CollectionConverters._ +import scala.collection.mutable.ListBuffer +import scala.language.postfixOps + +object SuspendTiming extends Enumeration { + type SuspendTiming = Value + val SUSPEND_DISABLE, SUSPEND_ASAP, SUSPEND_TestStart = Value +} + +trait QueueIdleListener { + def whenIdle(): Unit +} + +trait MessageListener { + def processMessage(msg: Message): Unit +} + +trait TestStepListener { + def testStep(testStepEvent: BreakpointEvent, eventSet: EventSet): Unit +} + +object TestRunnerThreadState extends Enumeration { + type TestRunnerThreadState = Value + val Sleep, Wait, Normal = Value +} + +trait TestRunnerThreadStatusListener { + def stall(nowState: TestRunnerThreadState): Unit + + def recover(from: TestRunnerThreadState): Unit +} + +/** + * + * @param port local port being forwarded to JVM debugger port + * @param suspend whether and when suspend on attach + */ +class FTDDebugger(val port: Int, val testClassPath: String, val testMethodPath: String, val suspend: SuspendTiming = SuspendTiming.SUSPEND_DISABLE) { + + private val logger = LogManager.getLogger() + + private var mainEnqueueMsgReq: BreakpointRequest = _ + private var messageListener: MessageListener = _ + var testRunnerThread: ThreadReference = _ + + private def setConnectionArg(cArgs: util.Map[String, Argument], argName: String, value: String): Unit = { + cArgs.get(argName) match { + case null => throw new RuntimeException(s"Setting invalid argument ${argName}") + case arg => arg setValue value + } + } + + private val vmm = Bootstrap.virtualMachineManager() + private val attachingConnectors = vmm.attachingConnectors() + + // HACK: always use the first attaching connector having hostname argument + private val connector = attachingConnectors.asScala.find(c => c.defaultArguments().containsKey("hostname")).head + + private val cArgs = connector.defaultArguments() + setConnectionArg(cArgs, "hostname", "localhost") + setConnectionArg(cArgs, "port", port toString) + + val vm: VirtualMachine = Retry.retry(3, {connector attach cArgs}, classOf[ConnectException]) + + require(vm canRequestMonitorEvents) + require(vm canUseInstanceFilters) + + private val eventRequestManager = vm eventRequestManager + + private val eventQueue = vm.eventQueue() + + private var suspendEvtSet: EventSet = _ + private var suspendEvt: Event = _ + + private var vmSuspended: Boolean = false + + var testClsRef: ClassType = _ + var testMethod: Method = _ + + try { + suspend match { + case SuspendTiming.SUSPEND_DISABLE => + case _ => + + logger.debug("Suspending the vm ASAP") + + vm.suspend() + vmSuspended = true + + suspend match { + case SuspendTiming.SUSPEND_TestStart => + stepToClassLoad(testClassPath) + + val testClsRefs = vm classesByName testClassPath + assert(testClsRefs.size() == 1) + testClsRef = (testClsRefs get 0).asInstanceOf[ClassType] + testMethod = testClsRef concreteMethodByName(testMethodPath, "()V") + assert(testMethod != null, s"Test method name ${testMethod} is not found! (maybe typo?)") + + stepToTestClassInit() + + testRunnerThread = suspendEvt match { + case evt: LocatableEvent => + evt.thread() + case evt: ClassPrepareEvent => + evt.thread() + } + + case _ => + } + } + } + catch { + case ex: VMDisconnectedException => + logger.error("Didn't get VM suspended while VM disconnected. Most likely to be a bug.") + throw ex + } + + val mainThread: ThreadReference = vm.allThreads().asScala.find(t => t.name().equals("main")).head + + def steppingByReq(reqs: EventRequest*): Unit = { + + reqs.filter(!_.isEnabled).foreach(req => { + req addCountFilter (1) + req.enable() + }) + + if (suspendEvtSet != null) { + logger.debug(s"Resuming from suspendEventSet ${suspendEvtSet}") + suspendEvtSet.resume() + // Clean values for careless mistake + suspendEvtSet = null + suspendEvt = null + } + if(vmSuspended) { + vm.resume() + vmSuspended = false + } + + logger.debug(s"Start waiting for req to be fulfilled ${reqs}") + + while (suspendEvtSet == null) { + val eventSet: EventSet = eventQueue.remove() + breakable { + for (evt <- eventSet.asScala) { + if (reqs.contains(evt.request())) { + + reqs.foreach(req=>req.disable()) + + suspendEvtSet = eventSet + suspendEvt = evt + break + } else { + logger.debug(s"Dropping event: ${evt}") + } + } + } + + } + } + + def stepToClassLoad(classPath: String): Unit = { + logger.debug("stepToClassLoad - start") + vm.classesByName(classPath).size match { + case 1 => + case 0 => + val clsPrepareReq = eventRequestManager.createClassPrepareRequest() + clsPrepareReq addClassFilter (classPath) + clsPrepareReq setSuspendPolicy (SUSPEND_ALL) + steppingByReq(clsPrepareReq) + } + } + + def stepToTestClassInit(): Unit = { + //throw new RuntimeException("Unsupported!") + logger.debug("stepToTestClassInit - start") + // + //val breakpointReq = eventRequestManager createBreakpointRequest (testClsRef.allLineLocations().get(0)) + //breakpointReq setSuspendPolicy (SUSPEND_ALL) + // + //val breakpointReq2 = eventRequestManager createBreakpointRequest (testMethod.location()) + //breakpointReq2 setSuspendPolicy (SUSPEND_ALL) + // + //steppingByReq(breakpointReq, breakpointReq2) + steppingTestExecution(SUSPEND_ALL, (_: BreakpointEvent, _: EventSet) => {}) + steppingByReq(testStepReqs: _*) + disableTestStepping() + logger.debug("stepToTestClassInit - done") + } + + private var interceptMessage_asyncThreadOnly: Boolean = false + + private var testStepListener: TestStepListener = _ + private var testStepReqs: Seq[BreakpointRequest] = Seq() + + def steppingTestExecution(suspendPolicy: Int, testStepListener: TestStepListener): Unit = { + require(suspend == SuspendTiming.SUSPEND_TestStart) + this.testStepListener = testStepListener + + //testStepReq = eventRequestManager createStepRequest(testRunnerThread, StepRequest.STEP_LINE, StepRequest.STEP_OVER) + //testStepReq setSuspendPolicy (SUSPEND_EVENT_THREAD) + //testStepReq.enable() + + //val breakpointReq = (testClsRef.allLineLocations().get(0)) + //breakpointReq setSuspendPolicy (SUSPEND_ALL) + testStepReqs = testClsRef.allLineLocations().asScala.map(eventRequestManager createBreakpointRequest(_)).toSeq + testStepReqs.foreach(req => { + req setSuspendPolicy (suspendPolicy) + req.enable() + }) + + } + + def disableTestStepping(): Unit = { + require(testStepReqs != null, "test execution hasn't been stepped!") + testStepReqs.foreach(_.disable()) + testStepReqs = Seq() + } + + private var msgSuspend: Boolean = _ + + private var queueIdleListener: QueueIdleListener = _ + private var idleReq: BreakpointRequest = _ + + def handleQueueIdle(handler: QueueIdleListener): Unit = { + + // TODO: Do we really need the check below? + require(suspend == SuspendTiming.SUSPEND_TestStart) + + queueIdleListener = handler + val aThread = testRunnerThread//suspendEvt.asInstanceOf[LocatableEvent].thread() + + val mainMsgQueueRef = getMainMsgQueueRef(aThread) + + val mIdleHandlersRef = mainMsgQueueRef.getValue(mainMsgQueueRef.referenceType().fieldByName("mIdleHandlers")).asInstanceOf[ObjectReference] + assert(mIdleHandlersRef != null) + val mIdleHandlersClsRef = mIdleHandlersRef.referenceType().asInstanceOf[ClassType] + assert(mIdleHandlersClsRef != null) + val interestedToArrayMethodRef = mIdleHandlersClsRef.concreteMethodByName("toArray", "([Ljava/lang/Object;)[Ljava/lang/Object;") + assert(interestedToArrayMethodRef != null) + + idleReq = eventRequestManager createBreakpointRequest (interestedToArrayMethodRef.location()) + idleReq addInstanceFilter mIdleHandlersRef + idleReq setSuspendPolicy SUSPEND_EVENT_THREAD + idleReq.enable() + } + + private var testRunnerThreadStatusListener: TestRunnerThreadStatusListener = _ + private var testRunnerThreadWaitReq: MonitorWaitRequest = _ + private var testRunnerThreadWaitedReq: MonitorWaitedRequest = _ + //private var testRunnerThreadSleepReqs: Seq[BreakpointRequest] = _ + private var testRunnerThreadSleepDoneReqs: ListBuffer[StepRequest] = ListBuffer() + + def handleTestRunnerThreadStall(listener: TestRunnerThreadStatusListener): Unit = { + require(suspend == SuspendTiming.SUSPEND_TestStart) + + testRunnerThreadStatusListener = listener + + testRunnerThreadWaitReq = eventRequestManager.createMonitorWaitRequest() + testRunnerThreadWaitReq addThreadFilter (testRunnerThread) + testRunnerThreadWaitReq setSuspendPolicy SUSPEND_EVENT_THREAD + testRunnerThreadWaitReq.enable() + + testRunnerThreadWaitedReq = eventRequestManager.createMonitorWaitedRequest() + testRunnerThreadWaitedReq addThreadFilter (testRunnerThread) + testRunnerThreadWaitedReq setSuspendPolicy SUSPEND_EVENT_THREAD + testRunnerThreadWaitedReq.enable() + + // TODO: clean unused code, not doing it now since we might need later + // The following doens't work since can't set breakpoint on native method + //testRunnerThreadSleepReqs = Seq() + //val threadClsRefs = vm.classesByName("java.lang.Thread") + //assert(threadClsRefs.size() == 1) + //val threadClsRef = threadClsRefs get(0) + //val sleepMethodRefs = threadClsRef.asInstanceOf[ClassType].methodsByName("sleep") + //assert(sleepMethodRefs.size() > 0) + //testRunnerThreadSleepReqs = sleepMethodRefs.asScala.map(sleepMethodRef => { + // val req = eventRequestManager createBreakpointRequest (sleepMethodRef.location()) + // req addThreadFilter (testRunnerThread) + // req enable() + // req + //}) + + } + + /** + * + * @note Must call after calling stepToTestRunStart + * @param asyncThreadOnly + * @param msgSuspend whether suspend the message posting thread + * @param messageListener + */ + def enableInterceptMessage(asyncThreadOnly: Boolean = false, msgSuspend: Boolean, messageListener: MessageListener): Unit = { + + //if (suspend == SuspendTiming.SUSPEND_ASAP) logger.warn("Requested intercepting messages while the current suspend mode might make the app crash for doing this.") + require(suspend == SuspendTiming.SUSPEND_TestStart) + + interceptMessage_asyncThreadOnly = asyncThreadOnly + this.msgSuspend = msgSuspend + this.messageListener = messageListener + + //// Look for test runner thread + + val aThread = testRunnerThread//suspendEvt.asInstanceOf[LocatableEvent].thread() + + assert(aThread.isSuspended) + + val msgQueueClsRefs = vm classesByName "android.os.MessageQueue" + assert(msgQueueClsRefs.size() == 1) + val msgQueueClsRef = (msgQueueClsRefs get 0).asInstanceOf[ClassType] + + val method_enqueueMsg = msgQueueClsRef concreteMethodByName("enqueueMessage", "(Landroid/os/Message;J)Z") + assert(method_enqueueMsg != null) + + val enqueueMsgFstLoc: Location = method_enqueueMsg.location() + + mainEnqueueMsgReq = eventRequestManager createBreakpointRequest (enqueueMsgFstLoc) + + val mainMsgQueueRef = getMainMsgQueueRef(aThread) + + logger.debug(s"mainMsgQueueRef: ${mainMsgQueueRef}") + + mainEnqueueMsgReq addInstanceFilter mainMsgQueueRef + mainEnqueueMsgReq setSuspendPolicy SUSPEND_EVENT_THREAD + mainEnqueueMsgReq.enable() + + logger.debug("Finish message intercept setup") + } + + private var mainMsgQueueRef_ : ObjectReference = _ + private def getMainMsgQueueRef(fromThread: ThreadReference): ObjectReference = { + if (this.mainMsgQueueRef_ == null) { + val mainLooperRef = getMainLooperRef(fromThread) + + val method_getQueue = mainLooperRef.referenceType().asInstanceOf[ClassType].concreteMethodByName("getQueue", "()Landroid/os/MessageQueue;") + assert(method_getQueue != null) + logger.debug("Getting main queue") + val invocationRst2 = mainLooperRef invokeMethod(fromThread, method_getQueue, new util.ArrayList(), 0) + mainMsgQueueRef_ = invocationRst2.asInstanceOf[ObjectReference] + } + + mainMsgQueueRef_ + } + + private def getMainLooperRef(fromThread: ThreadReference) = { + val looperClsRefs = vm classesByName "android.os.Looper" + assert(looperClsRefs.size() == 1) + val looperClsRef = (looperClsRefs get 0).asInstanceOf[ClassType] + + val method_getMainLooper = looperClsRef concreteMethodByName("getMainLooper", "()Landroid/os/Looper;") + assert(method_getMainLooper != null) + + // Require vm to be suspended by event + logger.debug("Getting main looper") + val invocationRst = looperClsRef invokeMethod(fromThread, method_getMainLooper, new util.ArrayList(), 0) + invocationRst.asInstanceOf[ObjectReference] + } + + def isMainMsgQueueEmpty(fromThread: ThreadReference): Boolean = { + val mainMsgQueueRef = getMainMsgQueueRef(fromThread) + val value = mainMsgQueueRef.getValue(mainMsgQueueRef.referenceType().fieldByName("mMessages")) + value == null + } + + /** + * WARNING: This is blocking + * + */ + def process(): Unit = { + + if (suspendEvtSet != null) { + suspendEvtSet.resume() + suspendEvtSet = null + suspendEvt = null + } + + try { + while (true) { + val eventSet = eventQueue remove + + logger.debug(s"EventSet retrieved: ${eventSet}") + + eventSet.forEach(evt => { + evt.request() match { + case breakpointRequest: BreakpointRequest if (breakpointRequest == mainEnqueueMsgReq && mainEnqueueMsgReq != null) => + logger.debug(s"Processing mainEnqueueMsgReq: ${breakpointRequest}") + val breakpointEvent = evt.asInstanceOf[BreakpointEvent] + val appThread = breakpointEvent.thread + if (!msgSuspend) appThread.resume() + // TODO: design better API for processMessage + messageListener processMessage (Message(MessageID(StackTrace.fromThread(appThread), MessageGroup(null)), new Message_RuntimeInfo(appThread, false))) + case testStepReq: BreakpointRequest if (testStepReqs.contains(testStepReq)) => + logger.debug(s"Processing testStepReq: ${testStepReq}") + testStepListener.testStep(evt.asInstanceOf[BreakpointEvent], eventSet) + case breakpointRequest: BreakpointRequest if (breakpointRequest == idleReq && idleReq != null) => + logger.debug(s"Processing idleReq: ${idleReq}") + queueIdleListener.whenIdle() + evt.asInstanceOf[BreakpointEvent].thread().resume() + logger.debug(s"Resuming from ${evt}") + case monitorWaitRequest: MonitorWaitRequest if (monitorWaitRequest == testRunnerThreadWaitReq && testRunnerThreadWaitReq != null) => + logger.debug(s"Processing testRunnerThreadWaitReq: ${testRunnerThreadWaitReq}") + testRunnerThreadStatusListener.stall(TestRunnerThreadState.Wait) + evt.asInstanceOf[MonitorWaitEvent].thread().resume() + logger.debug(s"Resuming from ${evt}") + case monitorWaitedRequest: MonitorWaitedRequest if (monitorWaitedRequest == testRunnerThreadWaitedReq && testRunnerThreadWaitedReq != null) => + logger.debug(s"Processing monitorWaitedRequest: ${monitorWaitedRequest}") + testRunnerThreadStatusListener.recover(TestRunnerThreadState.Wait) + evt.asInstanceOf[MonitorWaitedEvent].thread().resume() + logger.debug(s"Resuming from ${evt}") + //case testRunnerSleepReq: BreakpointRequest if (testRunnerThreadSleepReqs != null && testRunnerThreadSleepReqs.contains(testRunnerSleepReq)) => + // val newSleepDoneReq = eventRequestManager createStepRequest(testRunnerThread, StepRequest.STEP_LINE, StepRequest.STEP_OVER) + // newSleepDoneReq addCountFilter 1 + // newSleepDoneReq setSuspendPolicy SUSPEND_EVENT_THREAD + // newSleepDoneReq enable() + // testRunnerThreadSleepDoneReqs.append(newSleepDoneReq) + // testRunnerThreadStatusListener.stall(TestRunnerThreadState.Sleep) + // evt.asInstanceOf[BreakpointEvent].thread().resume() + // logger.debug(s"Resuming from ${evt}") + //case testRunnerSleepDoneReq: StepRequest if (testRunnerThreadSleepDoneReqs.contains(testRunnerSleepDoneReq)) => + // testRunnerSleepDoneReq.thread().resume() + // testRunnerThreadSleepDoneReqs -= testRunnerSleepDoneReq + // testRunnerThreadStatusListener.recover(TestRunnerThreadState.Sleep) + // evt.asInstanceOf[StepEvent].thread().resume() + // logger.debug(s"Resuming from ${evt}") + case req => + if (req != null) { + throw new RuntimeException(s"Event ${evt} from request ${req} is unexpected!") + } else { + evt match { + case _: VMDisconnectEvent => + logger.info("Received VMDisconnectEvent") + case _ => + logger.warn(s"Received unexpected event ${evt}") + } + } + } + }) + } + } catch { + case _: VMDisconnectedException => + } + } + +} \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/Main.scala b/src/main/scala/org/ftd/Master/Main.scala new file mode 100644 index 0000000..ba1104d --- /dev/null +++ b/src/main/scala/org/ftd/Master/Main.scala @@ -0,0 +1,183 @@ +package org.ftd.Master + +import java.util +import java.util.concurrent.TimeUnit + +import scala.concurrent._ +import scala.concurrent.duration.Duration +import com.android.ddmlib.{AndroidDebugBridge, Client, DdmPreferences, IDevice, InstallException, TimeoutException} +import com.android.ddmlib.AndroidDebugBridge.{IClientChangeListener, IDeviceChangeListener} +import com.android.ddmlib.ClientData.DebuggerStatus +import com.android.ddmlib.IDevice.DeviceState +import com.android.ddmlib.testrunner.{ITestRunListener, RemoteAndroidTestRunner, TestIdentifier} +import com.android.ddmlib.Log.LogLevel +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.LoggerContext +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.core.appender.FileAppender +import org.apache.logging.log4j.core.config.Configurator +import org.ftd.Master.Scheduler.{DescendingDelayScheduler, MaxDelayScheduler, SchedulingInfo} +import org.ftd.Master.Strategy.{NaturalProfilingStrategy, RerunStrategy, Strategy, XiaoScheduling} +import org.ftd.Master.utils.Retry + +import scala.util.Try +import scala.sys.process._ +import ExecutionContext.Implicits.global +import scala.collection.immutable.{HashSet, ListMap} +import scala.collection.mutable +import scala.collection.mutable.ListBuffer +import scala.language.postfixOps + +class DeviceChangeListener(val connectPromise: Promise[IDevice], forDevice: String = null) extends IDeviceChangeListener { + + private var waitForOnline: Boolean = false + + def deviceChanged(device: IDevice, changeMask: Int): Unit = { + if (!connectPromise.isCompleted) { + if (waitForOnline && (changeMask & IDevice.CHANGE_STATE) != 0 && device.getState == DeviceState.ONLINE) { + connectPromise success(device) + } + } + } + + def deviceConnected(device: IDevice): Unit = { + if (!connectPromise.isCompleted) { + if (forDevice == null || device.getName == forDevice) { + if (device.getState != DeviceState.ONLINE) { + waitForOnline = true + } else { + connectPromise success(device) + } + } + } + } + + def deviceDisconnected(device: IDevice): Unit = { + } +} + +object Main { + + private val logger = LogManager.getLogger() + + def main(config: Config): Unit = { + + try { + + if (config.debug) { + + if(!config.disable_ddm_log) DdmPreferences.setLogLevel(LogLevel.VERBOSE.getStringValue) + + Configurator.setRootLevel(Level.TRACE) + val log4jCtx = LogManager.getContext(false).asInstanceOf[LoggerContext] + val log4jConfig = log4jCtx.getConfiguration + + val rootConfig = log4jConfig.getLoggers get "" + + log4jConfig.getAppenders.forEach((name, appender) => { + appender match { + case fileAppender: FileAppender => + val newFileAppender: FileAppender = FileAppender + .newBuilder().asInstanceOf[FileAppender.Builder[Nothing]] + .setConfiguration(log4jConfig).asInstanceOf[FileAppender.Builder[Nothing]] + .withFileName(fileAppender.getFileName).asInstanceOf[FileAppender.Builder[Nothing]] + .setName(fileAppender.getName).asInstanceOf[FileAppender.Builder[Nothing]] + .withImmediateFlush(true).asInstanceOf[FileAppender.Builder[Nothing]] + .setIgnoreExceptions(fileAppender.ignoreExceptions()).asInstanceOf[FileAppender.Builder[Nothing]] + .withBufferedIo(false).asInstanceOf[FileAppender.Builder[Nothing]] + .setLayout(fileAppender.getLayout).asInstanceOf[FileAppender.Builder[Nothing]] + .setFilter(fileAppender.getFilter).asInstanceOf[FileAppender.Builder[Nothing]] + .build() + rootConfig removeAppender name + log4jConfig addAppender (newFileAppender) + case _ => + } + }) + + log4jCtx.updateLoggers() + } + + val deviceConnectPromise = Promise[IDevice]() + val deviceConnectPromise_future = deviceConnectPromise.future + + val deviceChangeListener = new DeviceChangeListener(deviceConnectPromise, config.deviceName) + + AndroidDebugBridge addDeviceChangeListener deviceChangeListener + + AndroidDebugBridge.init(true) + + AndroidDebugBridge.createBridge(config.adbPath toString, true) + + // Simply execute without caring whether successful and wait for a connected device + if (config.deviceName != null) { + val adbConnectCMD = s"${config.adbPath} connect ${config.deviceName}" + logger.info("Making device to connect") + val adbConnectCMD_msg = Process(adbConnectCMD).!! + logger.info(adbConnectCMD_msg) + } + + logger.info("Waiting device to connect") + // Wait for device to connect + val device: IDevice = Await.result(deviceConnectPromise_future, Duration.Inf) + + AndroidDebugBridge removeDeviceChangeListener deviceChangeListener + + logger.info(s"Device ${device} connected") + + val packageInstallOpts = config.apkInstallOpts :+ "-t" + config.apkPath.foreach(p => Retry.retry(10, device.installPackage(p toString, false, packageInstallOpts: _*), (e)=>e.isInstanceOf[InstallException] && e.getCause.isInstanceOf[TimeoutException]) ) + + logger.info("Package installed") + + val testRunner = new RemoteAndroidTestRunner(config.testPackage, config.testRunnerClsPath, device) + testRunner setMethodName(config.testClassPath, config.testMethodPath) + testRunner setRunOptions( testRunner.getRunOptions + " --no-window-animation" ) + + val strategy: Strategy = config.strategy match { + case s if (s.equals(new XiaoScheduling().name)) => new XiaoScheduling() + case s if (s.equals(new NaturalProfilingStrategy().name)) => new NaturalProfilingStrategy() + case s if (s.equals(new RerunStrategy().name)) => new RerunStrategy() + } + + val startDetectionTime = System.currentTimeMillis() + val rst: Boolean = strategy.start(device, testRunner, config.appName, config.testClassPath, config.testMethodPath, config.testCaseHangTimeout, config.max_runs, config.givenPassed) + val endDetectionTime = System.currentTimeMillis() + val detectionDuration = Duration(endDetectionTime - startDetectionTime, TimeUnit.MILLISECONDS) + logger.info(s"Detection duration: ${detectionDuration.toSeconds} seconds") + + if(rst) { + exitFlaky() + } else { + exitNormal() + } + + } finally { + logger.trace("Cleanup start") + System.out.flush() + try { + AndroidDebugBridge.terminate() + } catch { + case _: InterruptedException => + // This can be safely ignored + case ex: Throwable => + logger.error(ex) + // Just print it out without affecting exit status + // Not really care if it successfully cleans up or not + } + + logger.trace("Cleanup end") + } + + } + + def exitNormal(): Unit = { + println("Not detected flaky") + System.exit(0) + } + + def exitFlaky(): Unit = { + logger.info("Detected Flaky!!!") + println("Flaky") + System.exit(0) + } +} \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/Message.scala b/src/main/scala/org/ftd/Master/Message.scala new file mode 100644 index 0000000..98596d9 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Message.scala @@ -0,0 +1,29 @@ +package org.ftd.Master + +import com.sun.jdi.ThreadReference + +// group uses stacktrace location info + +case class MessageGroup(testRunnerSF: StackTrace) + +case class MessageID(appSF: StackTrace, group: MessageGroup) + +class Message_RuntimeInfo(val fromThread: ThreadReference, var usSuspended: Boolean, var passThrough: Boolean = false, var releaseTime: MessageGroup = null) { + case class Message_RuntimeInfo_Store(threadName: String, usSuspended: Boolean, releaseTime: MessageGroup) + + val threadName: String = fromThread.name() + + override def toString: String = Message_RuntimeInfo_Store(threadName, usSuspended, releaseTime).toString +} + +case class Message(ID: MessageID, runtimeInfo: Message_RuntimeInfo, mustExecBeforeLocation: SourceLine = null, occurrence: Int = 0) { + def isSuspended: Boolean = runtimeInfo.usSuspended + + def unSuspend(timing: MessageGroup): Unit = { + require(runtimeInfo.usSuspended) + assert(runtimeInfo.fromThread.isSuspended, "Message leaking?!") + runtimeInfo.usSuspended = false + runtimeInfo.releaseTime = timing + runtimeInfo.fromThread.resume() + } +} diff --git a/src/main/scala/org/ftd/Master/MessageStore.scala b/src/main/scala/org/ftd/Master/MessageStore.scala new file mode 100644 index 0000000..267f3a8 --- /dev/null +++ b/src/main/scala/org/ftd/Master/MessageStore.scala @@ -0,0 +1,51 @@ +package org.ftd.Master + +import com.sun.jdi.ThreadReference +import org.apache.logging.log4j.LogManager + +import scala.collection.mutable + +class MessageStore { + + private val logger = LogManager.getLogger() + + val map = new mutable.LinkedHashMap[MessageGroup, mutable.LinkedHashMap[MessageID, mutable.LinkedHashSet[Message]]]() + + def createMessage(msg: Message): Message = { + createMessage(msg.ID, msg.runtimeInfo) + } + + def createMessage(ID: MessageID, runtimeInfo: Message_RuntimeInfo): Message = { + val msgSet = map.getOrElseUpdate(ID.group, new mutable.LinkedHashMap()).getOrElseUpdate(ID, new mutable.LinkedHashSet[Message]()) + val occurrence = msgSet.size + val msg = Message(ID, runtimeInfo, null, occurrence) + assert(msgSet.add(msg), "Repeated message detected!") + + logger.debug(s"Adding message: ${msg}") + + msg + } + + def getSuspendedMessages: Iterable[Message] = { + //noinspection SpellCheckingInspection + map.values.flatMap(IDmap=>IDmap.values.flatMap(msgSet => msgSet.filter(msg => msg.isSuspended))) + } + + override def toString: String = { + var str = "" + + for ((group, groupMap) <- map) { + str += s"Group: ${group}\n" + for ((id, msgSet) <- groupMap) { + str += s"ID: ${id}\n" + for (msg <- msgSet) { + str += s"msg: ${msg}\n" + } + str += '\n' + } + str += '\n' + } + + str + } +} diff --git a/src/main/scala/org/ftd/Master/Profiler.scala b/src/main/scala/org/ftd/Master/Profiler.scala new file mode 100644 index 0000000..2597825 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Profiler.scala @@ -0,0 +1,237 @@ +package org.ftd.Master + +import java.util.{Timer, TimerTask} +import java.util.concurrent.TimeUnit + +import com.sun.jdi.ThreadReference +import com.sun.jdi.request.EventRequest.{SUSPEND_ALL, SUSPEND_EVENT_THREAD} +import com.sun.jdi.event.{BreakpointEvent, EventSet, StepEvent} +import org.apache.logging.log4j.LogManager +import org.ftd.Master.TestRunnerThreadState.TestRunnerThreadState + +import scala.util.control.Breaks._ +import scala.collection.mutable +import scala.collection.mutable.ListBuffer +import scala.concurrent.duration.Duration +import scala.language.postfixOps + +class Profiler(val testClassPath: String, val testMethodPath: String, val debuggerListenPort: Int, val releaseMap: collection.Map[MessageGroup, mutable.LinkedHashSet[MessageID]] = Map(), val strictRelease: Boolean, val ignoreUnknonMsgs: Boolean, val uselessMsgIDs: Set[MessageID] = Set(), val testCaseHangTimeout: Duration = Duration.Inf) { + + private val logger = LogManager.getLogger() + + val debugger: FTDDebugger = new FTDDebugger(debuggerListenPort, testClassPath, testMethodPath, SuspendTiming.SUSPEND_TestStart) + + val msgStore = new MessageStore + + val thisReleaseRec: mutable.LinkedHashMap[MessageGroup, mutable.LinkedHashSet[MessageID]] = mutable.LinkedHashMap() + val thisUselessMsgIDs: mutable.Set[MessageID] = mutable.HashSet[MessageID](uselessMsgIDs.toSeq: _*) + + private var messageQIdle: Boolean = _ + val msgGroups: ListBuffer[MessageGroup] = ListBuffer() + + private var lastSeenTestRunnerST: StackTrace = StackTrace.fromThread(debugger.testRunnerThread) + + // NOTE: Must execute the following before steppingTestExecution, else locking will happen + debugger enableInterceptMessage(false, true, (msg: Message) => { + + logger.trace(s"Handling message ${msg}") + + val newMsg_ = Message(MessageID(msg.ID.appSF, MessageGroup(lastSeenTestRunnerST)), new Message_RuntimeInfo(msg.runtimeInfo.fromThread, usSuspended = true), msg.mustExecBeforeLocation, msg.occurrence) + + val newMsg = msgStore createMessage (newMsg_) + if (Array( + debugger.testRunnerThread, // If we suspend, the test runner thread will be stall + debugger.mainThread // If we suspend, the whole thing will be stall + ).contains(newMsg.runtimeInfo.fromThread)) { + newMsg.runtimeInfo.passThrough = true + logger.trace(s"Pass through msg: ${newMsg}") + unsuspendMsg(newMsg) + } else { + if(ignoreUnknonMsgs && !releaseMap.values.exists(_.contains(newMsg.ID))) { + logger.trace(s"Pass through UNKNOWN msg: ${newMsg}") + unsuspendMsg(newMsg) + } else { + releaseOneMsgIfStall(newMsg.runtimeInfo.fromThread) + } + } + }) + + var lastTimeStmtTimer: Timer = _ + + var enteredTestMethodBefore: Boolean = false + debugger steppingTestExecution(SUSPEND_EVENT_THREAD, (stepEvt: BreakpointEvent, eventSet: EventSet) => { + + if (lastTimeStmtTimer != null) lastTimeStmtTimer.cancel() + + assert(debugger.testMethod != null) + //if (stepEvt.location().method() == debugger.testMethod) { + enteredTestMethodBefore = true + + lastSeenTestRunnerST = StackTrace.fromThread(stepEvt.thread()) + msgGroups += MessageGroup(lastSeenTestRunnerST) + logger.trace(s"lastSeenTestRunnerST: ${lastSeenTestRunnerST}") + + if (testCaseHangTimeout.isFinite) { + lastTimeStmtTimer = new Timer() + lastTimeStmtTimer.schedule(new TimerTask { + override def run(): Unit = { + //// Start releasing messages + //msgStore.getSuspendedMessages.headOption match { + // case Some(msg) => + // logger.trace(s"Release msg: ${msg}") + // msg.runtimeInfo.fromThread.resume() + // case _ => throw new RuntimeException("Test Execution hanged while no message suspended") + //} + //throw new RuntimeException(s"Test case unexpectedly hanged after waiting for ${testCaseHangTimeout}") + logger.error(s"Test case unexpectedly hanged after waiting for ${testCaseHangTimeout}") + System.exit(81) + } + }, testCaseHangTimeout.toMillis) + } + + //} + //else { + // lastSeenTestRunnerST = null + // + // if (enteredTestMethodBefore) { + // debugger.disableTestStepping() + // } + //} + + if (strictRelease) { + // Release all messages indicated by release map + while (releaseOneByReleaseMap()) {} + } + + stepEvt.thread().resume() + }) + + debugger handleQueueIdle (() => { + logger.debug("Message queue idle!") + messageQIdle = true + releaseOneMsgIfStall(debugger.mainThread) + }) + + var testRunnerThreadWaitTimer: Timer = _ + debugger handleTestRunnerThreadStall (new TestRunnerThreadStatusListener { + override def stall(nowState: TestRunnerThreadState): Unit = { + logger.debug(s"Test runner thread stall! now state is ${nowState}") + if (messageQIdle) { + // Only the test runner thread has waited for a while, then release message + testRunnerThreadWaitTimer = new Timer() + testRunnerThreadWaitTimer.schedule(new TimerTask { + override def run(): Unit = { + releaseOneMsgIfStall(debugger.testRunnerThread) + } + }, Duration(5, TimeUnit.SECONDS).toMillis) + } else { + // Just wait for the message queue to be idle to release message + } + } + + override def recover(from: TestRunnerThreadState): Unit = { + logger.debug(s"Test runner thread recovered from ${from}!") + if (testRunnerThreadWaitTimer != null) testRunnerThreadWaitTimer.cancel() + } + }) + + def releaseOneMsgIfStall(suspendedThread: ThreadReference): Unit = { + logger.debug(s"messageQIdle=${messageQIdle}") + if (messageQIdle) { + val testRunnerThreadStatus = debugger.testRunnerThread.status() + logger.debug(s"debugger.testRunnerThread.status()=${testRunnerThreadStatus}") + if (testRunnerThreadStatus == ThreadReference.THREAD_STATUS_WAIT) { + val mainMsgQueueEmpty = debugger.isMainMsgQueueEmpty(suspendedThread) + logger.debug(s"mainMsgQueueEmpty=${mainMsgQueueEmpty}") + if (mainMsgQueueEmpty) { + releaseOneMsg() + } + } + } + } + + /** + * + * @return whether any message released + */ + def releaseOneByReleaseMap(suspendedMsgs: Iterable[Message] = msgStore.getSuspendedMessages): Boolean = { + + val targetMsgGrp = MessageGroup(lastSeenTestRunnerST) + val reversedReleaseMapKeys = releaseMap.keys.toList.reverse + val targetMsgGrpIdx = reversedReleaseMapKeys.indexOf(targetMsgGrp) + + if (targetMsgGrpIdx != -1) { + for (k <- reversedReleaseMapKeys.drop(targetMsgGrpIdx)) { + releaseMap.getOrElse(k, null) match { + case x if (x == null || x.isEmpty) => + case seq => + + for (id <- seq) { + suspendedMsgs.find(msg => msg.ID == id) match { + case Some(msg) => + logger.trace(s"Hitting releaseMap for ${id}!") + unsuspendMsg(msg) + return true + case _ => + + } + } + } + } + + } + + false + } + + def releaseOneMsg(): Unit = { + + val suspendedMsgs = msgStore.getSuspendedMessages + + val releasedOne = releaseOneByReleaseMap(suspendedMsgs) + + if (!releasedOne) releaseOneMsg_1st(suspendedMsgs) + + } + + /** + * release one message that is suspended and sent the first + * + * @param suspendedMsgs + */ + private def releaseOneMsg_1st(suspendedMsgs: Iterable[Message]): Unit = { + logger.trace("Randomly selecting suspended message for releasing.") + // Start releasing messages + if (suspendedMsgs.isEmpty) { + logger.info("Test Execution hanged while no message suspended") + } else { + suspendedMsgs.find(msg => !uselessMsgIDs.contains(msg.ID)) match { + case Some(msg) => + unsuspendMsg(msg) + case _ => logger.info("Test Execution hanged while no *interesting* message suspended") + + } + } + + } + + /** + * Start profiling. This is a sync blocking method. + */ + def start(): Unit = { + messageQIdle = debugger.isMainMsgQueueEmpty(debugger.mainThread) + debugger.process() + thisUselessMsgIDs ++= msgStore.getSuspendedMessages.map(msg => msg.ID) + if (lastTimeStmtTimer != null) lastTimeStmtTimer.cancel() + if (testRunnerThreadWaitTimer != null) testRunnerThreadWaitTimer.cancel() + } + + def unsuspendMsg(msg: Message): Unit = { + logger.trace(s"Release msg: ${msg}") + messageQIdle = false + val currentGrp = MessageGroup(lastSeenTestRunnerST) + msg.unSuspend(currentGrp) + if (!msg.runtimeInfo.passThrough) thisReleaseRec.getOrElseUpdate(currentGrp, mutable.LinkedHashSet()) += msg.ID + } + +} diff --git a/src/main/scala/org/ftd/Master/ProfilerRunner.scala b/src/main/scala/org/ftd/Master/ProfilerRunner.scala new file mode 100644 index 0000000..9f7cc8f --- /dev/null +++ b/src/main/scala/org/ftd/Master/ProfilerRunner.scala @@ -0,0 +1,104 @@ +package org.ftd.Master + +import com.android.ddmlib.testrunner.{ITestRunListener, RemoteAndroidTestRunner, TestIdentifier} +import com.android.ddmlib.{AndroidDebugBridge, Client, IDevice} +import org.apache.logging.log4j.LogManager +import org.ftd.Master.ProfilerRunner.{ClientChangeListener, RunStatus} + +import scala.collection.mutable +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future, Promise} +import scala.language.postfixOps +import scala.concurrent.ExecutionContext.Implicits.global + +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener +import com.android.ddmlib.ClientData.DebuggerStatus +import org.ftd.Master.utils.{Retry, RetryException} + +object ProfilerRunner { + + class ClientChangeListener(val appName: String, val device: IDevice, val changeDebugStatusPromise: Promise[Client]) extends IClientChangeListener { + + private val logger = LogManager.getLogger() + private var waitForNameChange = false + + def clientChanged(client: Client, changeMask: Int): Unit = { + if (!changeDebugStatusPromise.isCompleted) { + logger.debug(s"appName: ${client.getClientData.getClientDescription}, pid: ${client.getClientData.getPid}, changeMask: ${changeMask}, isDebuggerStatus: ${(changeMask & Client.CHANGE_DEBUGGER_STATUS) != 0}") + if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) != 0) { + client.getClientData.getClientDescription match { + case null => + waitForNameChange = true + case x: String if x == appName => + logger.debug(s"Target app has debuggerConnectionStatus: ${client.getClientData.getDebuggerConnectionStatus}") + if (client.getClientData.getDebuggerConnectionStatus == DebuggerStatus.WAITING) { + changeDebugStatusPromise success client + } + case _ => + } + } else if (waitForNameChange && (changeMask & Client.CHANGE_NAME) != 0) { + if (client.getClientData.getClientDescription == appName && + client.getClientData.getDebuggerConnectionStatus == DebuggerStatus.WAITING) { + changeDebugStatusPromise success client + } + } + } + } + } + + case class RunStatus(testResult: Boolean, profiler: Profiler) + +} + +class ProfilerRunner(device: IDevice, testRunner: RemoteAndroidTestRunner, appName: String, testClassPath: String, testMethodPath: String) { + + private val logger = LogManager.getLogger() + + // NOTE: This is blocking + def run(releaseMap: collection.Map[MessageGroup, mutable.LinkedHashSet[MessageID]] = Map(), strictRelease: Boolean, ignoreUnknonMsgs: Boolean, uselessMsgIDs: Set[MessageID] = Set(), testCaseHangTimeout: Duration = Duration.Inf): RunStatus = { + + Retry.retry(3, { + + val clientChangePromise = Promise[Client]() + val clientChangePromise_future = clientChangePromise.future + + val clientChangeListener = new ClientChangeListener(appName, device, clientChangePromise) + AndroidDebugBridge addClientChangeListener clientChangeListener + // Wait for debug status change + + val testEndPromise = Promise[Boolean]() + val testEndPromise_future = testEndPromise.future + + val testRunEndPromise = Promise[Unit]() + val testRunEndPromise_future = testRunEndPromise.future + logger.info("Starting test run") + val testRunFuture = + Future { + TestRunner.run(testRunner, testEndPromise, testRunEndPromise) + logger.info("Test run returned") + } + + logger.debug("Waiting client change to have debugger status WAITING") + + val client = Await.result(clientChangePromise_future, Duration.Inf) + assert(client != null) + logger.info(s"Client for ${appName} retrieved: ${client}") + + // logger.info(s"Debugger attached: ${client.isDebuggerAttached}") + + val profiler = new Profiler(testClassPath, testMethodPath, client getDebuggerListenPort, releaseMap, strictRelease, ignoreUnknonMsgs, uselessMsgIDs, testCaseHangTimeout) + + profiler.start() + + logger.trace("Waiting for test to terminate") + val testRst = Await.result(testEndPromise_future, Duration.Inf) + logger.trace("Waiting for test run to terminate") + Await.result(testRunEndPromise_future, Duration.Inf) + + //Await.ready(testRunFuture, Duration.Inf) + client.kill() + RunStatus(testRst, profiler) + }, classOf[RetryException]) + + } +} diff --git a/src/main/scala/org/ftd/Master/Scheduler/AdaptiveScheduler.scala b/src/main/scala/org/ftd/Master/Scheduler/AdaptiveScheduler.scala new file mode 100644 index 0000000..965dcc6 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Scheduler/AdaptiveScheduler.scala @@ -0,0 +1,5 @@ +package org.ftd.Master.Scheduler + +abstract class AdaptiveScheduler() extends Scheduler { + def update(lastRunResult: SchedulingUpdateInfo): Unit +} \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/Scheduler/DescendingDelayScheduler.scala b/src/main/scala/org/ftd/Master/Scheduler/DescendingDelayScheduler.scala new file mode 100644 index 0000000..b74479f --- /dev/null +++ b/src/main/scala/org/ftd/Master/Scheduler/DescendingDelayScheduler.scala @@ -0,0 +1,44 @@ +package org.ftd.Master.Scheduler + +import org.apache.logging.log4j.LogManager +import org.ftd.Master.{MessageGroup, MessageID} + +import scala.collection.immutable.{HashSet, ListMap} +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +class DescendingDelayScheduler(val maxDelayReleaseMap: ListMap[MessageGroup, mutable.LinkedHashSet[MessageID]], val msgGroupOrder: ListBuffer[MessageGroup], var uselessMsgIDs: Set[MessageID] = HashSet(), var unknownMsgIDs: Set[MessageID] = HashSet(), passedBefore: Boolean, failedBefore: Boolean) extends AdaptiveScheduler { + + private val logger = LogManager.getLogger() + + val baseReleaseMap: ListMap[MessageGroup, mutable.LinkedHashSet[MessageID]] = (if (!failedBefore) maxDelayReleaseMap else { + msgGroupOrder.iterator.foldLeft(ListMap[MessageGroup, mutable.LinkedHashSet[MessageID]]())( + (map, thisGrp) => map.updated(thisGrp, maxDelayReleaseMap.getOrElse(thisGrp, mutable.LinkedHashSet[MessageID]())) + ) + }).updated(null, (uselessMsgIDs | unknownMsgIDs).to(mutable.LinkedHashSet)) + + private val attempts: Iterator[ListMap[MessageGroup, mutable.LinkedHashSet[MessageID]]] = baseReleaseMap.toList.zipWithIndex.drop(1).iterator.flatMap { + case ((group, msgsIDInGrp), i) => + msgsIDInGrp.iterator.flatMap(msgID => { + logger.debug(s"xx: ${msgID}") + val orderIdx = msgGroupOrder.indexOf(msgID.group) + ( (i - 1).until(orderIdx).by(-1) ).iterator.map(targetMsgGroupIdx=>{ + val targetMsgGroup = msgGroupOrder(targetMsgGroupIdx) + maxDelayReleaseMap.updated(group, baseReleaseMap.get(group).head.clone.subtractOne(msgID)) + .updated(targetMsgGroup, mutable.LinkedHashSet[MessageID](baseReleaseMap.get(targetMsgGroup).head.toList.appended(msgID): _*)) + }) + }) + + } + + def next(): SchedulingInfo = { + SchedulingInfo(attempts.next(), uselessMsgIDs, strictRelease = true, ignoreUnknonMsgs = true) + } + + def hasNext: Boolean = attempts.hasNext + + override def update(lastRunResult: SchedulingUpdateInfo): Unit = { + uselessMsgIDs = uselessMsgIDs ++ lastRunResult.ignoreMsgs + } + +} diff --git a/src/main/scala/org/ftd/Master/Scheduler/MaxDelayScheduler.scala b/src/main/scala/org/ftd/Master/Scheduler/MaxDelayScheduler.scala new file mode 100644 index 0000000..00f66f9 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Scheduler/MaxDelayScheduler.scala @@ -0,0 +1,32 @@ +package org.ftd.Master.Scheduler + +import org.ftd.Master.{MessageGroup, MessageID} + +import scala.collection.immutable.{HashSet, ListMap} +import scala.collection.mutable + +class MaxDelayScheduler(var uselessMsgIDs: Set[MessageID] = HashSet()) extends AdaptiveScheduler { + + var lastReleaseMap: ListMap[MessageGroup, mutable.LinkedHashSet[MessageID]] = ListMap() + var maxDelayAchieved: Boolean = false + var initUpdate: Boolean = false + + def update(lastRunResult: SchedulingUpdateInfo): Unit = { + require(!initUpdate || lastRunResult.releaseMap.keys == lastReleaseMap.keys) + if (initUpdate && lastRunResult.releaseMap.forall { case (k, v) => lastReleaseMap.get(k).head == v }) { + maxDelayAchieved = true + } + initUpdate = true; + lastReleaseMap = lastRunResult.releaseMap + uselessMsgIDs = uselessMsgIDs ++ lastRunResult.ignoreMsgs + } + + def next(): SchedulingInfo = { + //require(lastReleaseMap != null, "Must call update method for at least once") + require(hasNext) + val newReleaseMap = lastReleaseMap.mapValues(s => (mutable.LinkedHashSet.newBuilder ++= s.toList.reverse).result()).to(ListMap) + SchedulingInfo(newReleaseMap, uselessMsgIDs, false, false) + } + + def hasNext: Boolean = !maxDelayAchieved +} diff --git a/src/main/scala/org/ftd/Master/Scheduler/Scheduler.scala b/src/main/scala/org/ftd/Master/Scheduler/Scheduler.scala new file mode 100644 index 0000000..8e5b560 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Scheduler/Scheduler.scala @@ -0,0 +1,16 @@ +package org.ftd.Master.Scheduler + +import org.ftd.Master.{MessageGroup, MessageID} + +import scala.collection.immutable.ListMap +import scala.collection.mutable + +case class SchedulingUpdateInfo(testStatus: Boolean, releaseMap: ListMap[MessageGroup, mutable.LinkedHashSet[MessageID]], ignoreMsgs: Set[MessageID]) +case class SchedulingInfo(releaseMap: ListMap[MessageGroup, mutable.LinkedHashSet[MessageID]], ignoreMsgs: Set[MessageID], strictRelease: Boolean, ignoreUnknonMsgs: Boolean) + +abstract class Scheduler() extends Iterator[SchedulingInfo] { + /** + * Whether the scheduler has attempted all possibilities + */ + def hasNext: Boolean +} \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/Scheduler/SchedulerRunner.scala b/src/main/scala/org/ftd/Master/Scheduler/SchedulerRunner.scala new file mode 100644 index 0000000..17375f6 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Scheduler/SchedulerRunner.scala @@ -0,0 +1,61 @@ +package org.ftd.Master.Scheduler + +import com.android.ddmlib.IDevice +import com.android.ddmlib.testrunner.RemoteAndroidTestRunner +import org.apache.logging.log4j.LogManager +import org.ftd.Master.Scheduler.AdaptiveSchedulerRunner.Status +import org.ftd.Master.{Profiler, ProfilerRunner} + +import scala.collection.immutable.ListMap +import scala.concurrent.duration.Duration + +object AdaptiveSchedulerRunner { + case class Status(detectedFlaky: Boolean, runs: Int, passedBefore: Boolean, failedBefore: Boolean) +} + +class AdaptiveSchedulerRunner[S <: AdaptiveScheduler](val scheduler: S, val device: IDevice, val testRunner: RemoteAndroidTestRunner, val appName: String, val testClassPath: String, val testMethodPath: String, val testCaseHangTimeout: Duration = Duration.Inf) { + + private val logger = LogManager.getLogger() + + var lastProfiler: Profiler = _ + /** + * + * @param max_runs + * @return `true` if detected flaky, `false` otherwise + */ + def run(max_runs: Int, init_passedBefore: Boolean, init_failedBefore: Boolean): Status = { + + var passedBefore = init_passedBefore + var failedBefore = init_failedBefore + + var runningCnt: Int = 0 + + while (scheduler.hasNext && (max_runs == -1 || runningCnt < max_runs)) { + runningCnt += 1; + logger.info(s"${scheduler}: Running ${runningCnt}th time") + + val SchedulingInfo(newReleaseMap, uselessMsgIDs, strictRelease, ignoreUnknonMsgs) = scheduler.next() + + val profilerRunner = new ProfilerRunner(device, testRunner, appName, testClassPath, testMethodPath) + val ProfilerRunner.RunStatus(testRst, profiler) = profilerRunner.run(newReleaseMap, strictRelease, ignoreUnknonMsgs, uselessMsgIDs, testCaseHangTimeout) + + logger.trace(s"All profiled msg: \n${profiler.msgStore}") + logger.trace(s"Useless msg: \n${profiler.thisUselessMsgIDs}") + + lastProfiler = profiler + + scheduler.update(SchedulingUpdateInfo(testRst, profiler.thisReleaseRec.to(ListMap), if(testRst) profiler.thisUselessMsgIDs.toSet else Set())) + + if (!testRst) { + failedBefore = true + if (passedBefore) return Status(detectedFlaky = true, runningCnt, passedBefore, failedBefore) + } else { + passedBefore = true + if (failedBefore) return Status(detectedFlaky = true, runningCnt, passedBefore, failedBefore) + } + + } + + Status(detectedFlaky = false, runningCnt, passedBefore, failedBefore) + } +} diff --git a/src/main/scala/org/ftd/Master/SourceLine.scala b/src/main/scala/org/ftd/Master/SourceLine.scala new file mode 100644 index 0000000..5086613 --- /dev/null +++ b/src/main/scala/org/ftd/Master/SourceLine.scala @@ -0,0 +1,22 @@ +package org.ftd.Master + +import com.sun.jdi.Location +import org.apache.logging.log4j.LogManager + +case class SourceLine(sourcePath: String, line: Int) + +object SourceLine { + + private val logger = LogManager.getLogger() + + def fromLocation(location: Location): SourceLine = { + try { + SourceLine(location.sourcePath(), location.lineNumber()) + } catch { + case ex: IllegalArgumentException => { + logger.debug(s"Encountered IllegalArgumentException when getting location data: ${ex}") + null + } + } + } +} diff --git a/src/main/scala/org/ftd/Master/StackTrace.scala b/src/main/scala/org/ftd/Master/StackTrace.scala new file mode 100644 index 0000000..172e292 --- /dev/null +++ b/src/main/scala/org/ftd/Master/StackTrace.scala @@ -0,0 +1,31 @@ +package org.ftd.Master + +import com.sun.jdi.{AbsentInformationException, Location, StackFrame, ThreadReference} +import org.apache.logging.log4j.LogManager + +import collection.JavaConverters._ + +case class StackTrace(locations: List[SourceLine]) + +object StackTrace { + + def fromThread(thread: ThreadReference): StackTrace = { + var resumeThread: Boolean = false + if (!thread.isSuspended) { + thread.suspend() + resumeThread = true + } + val allFrameLocations = thread.frames().asScala.map { x => + try { + SourceLine.fromLocation(x.location) + } catch { + case _: AbsentInformationException => + null + } + } + if(resumeThread) { + thread.resume() + } + StackTrace(allFrameLocations.toList) + } +} \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/Strategy/NaturalProfilingStrategy.scala b/src/main/scala/org/ftd/Master/Strategy/NaturalProfilingStrategy.scala new file mode 100644 index 0000000..9a13d58 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Strategy/NaturalProfilingStrategy.scala @@ -0,0 +1,41 @@ +package org.ftd.Master.Strategy + +import com.android.ddmlib.IDevice +import com.android.ddmlib.testrunner.RemoteAndroidTestRunner +import org.apache.logging.log4j.LogManager +import org.ftd.Master.{ProfilerRunner, TestRunner} + +import scala.concurrent.{Await, Promise} +import scala.concurrent.duration.Duration + +class NaturalProfilingStrategy extends Strategy { + private val logger = LogManager.getLogger() + + val name: String = this.getClass.getSimpleName + + def start(device: IDevice, testRunner: RemoteAndroidTestRunner, appName: String, testClassPath: String, testMethodPath: String, testCaseHangTimeout: Duration = Duration.Inf, max_runs: Int, givenPassed: Boolean): Boolean = { + + // Note: handle when givenPassed == false + + val iterationStream_ = LazyList.from(1) + val iterationStream = if (max_runs != -1 ) iterationStream_.take(max_runs) else iterationStream_ + + if (max_runs != -1 ) logger.warn("max runs is infinity") + + for(i <- iterationStream) { + + logger.info(s"Running ${i}th time") + + testRunner setDebug(true) + + val profilerRunner = new ProfilerRunner(device, testRunner, appName, testClassPath, testMethodPath) + val ProfilerRunner.RunStatus(testRst, _) = profilerRunner.run(Map(), strictRelease = false, ignoreUnknonMsgs = true, uselessMsgIDs = Set(), testCaseHangTimeout) + + if (!testRst) { + return true + } + } + + false + } +} diff --git a/src/main/scala/org/ftd/Master/Strategy/RerunStrategy.scala b/src/main/scala/org/ftd/Master/Strategy/RerunStrategy.scala new file mode 100644 index 0000000..7c87219 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Strategy/RerunStrategy.scala @@ -0,0 +1,44 @@ +package org.ftd.Master.Strategy + +import com.android.ddmlib.IDevice +import com.android.ddmlib.testrunner.RemoteAndroidTestRunner +import org.apache.logging.log4j.LogManager +import org.ftd.Master.TestRunner + +import scala.concurrent.{Await, Promise} +import scala.concurrent.duration.Duration + +class RerunStrategy extends Strategy { + private val logger = LogManager.getLogger() + + val name: String = this.getClass.getSimpleName + + def start(device: IDevice, testRunner: RemoteAndroidTestRunner, appName: String, testClassPath: String, testMethodPath: String, testCaseHangTimeout: Duration = Duration.Inf, max_runs: Int, givenPassed: Boolean): Boolean = { + + // Note: handle when givenPassed == false + + val iterationStream_ = LazyList.from(1) + val iterationStream = if (max_runs != -1 ) iterationStream_.take(max_runs) else iterationStream_ + + if (max_runs != -1 ) logger.warn("max runs is infinity") + + for(i <- iterationStream) { + + logger.info(s"Running ${i}th time") + + val testEndPromise = Promise[Boolean]() + val testEndPromise_future = testEndPromise.future + + val testRunEndPromise = Promise[Unit]() + + TestRunner.run(testRunner, testEndPromise, testRunEndPromise) + + val rst = Await.result(testEndPromise_future, Duration.Inf) + if (!rst) { + return true + } + } + + false + } +} \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/Strategy/Strategy.scala b/src/main/scala/org/ftd/Master/Strategy/Strategy.scala new file mode 100644 index 0000000..2fec2e5 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Strategy/Strategy.scala @@ -0,0 +1,12 @@ +package org.ftd.Master.Strategy + +import com.android.ddmlib.IDevice +import com.android.ddmlib.testrunner.RemoteAndroidTestRunner + +import scala.concurrent.duration.Duration + +trait Strategy { + val name: String + + def start(device: IDevice, testRunner: RemoteAndroidTestRunner, appName: String, testClassPath: String, testMethodPath: String, testCaseHangTimeout: Duration = Duration.Inf, max_runs: Int, givenPassed: Boolean): Boolean +} diff --git a/src/main/scala/org/ftd/Master/Strategy/XiaoScheduling.scala b/src/main/scala/org/ftd/Master/Strategy/XiaoScheduling.scala new file mode 100644 index 0000000..d16fb60 --- /dev/null +++ b/src/main/scala/org/ftd/Master/Strategy/XiaoScheduling.scala @@ -0,0 +1,46 @@ +package org.ftd.Master.Strategy + +import com.android.ddmlib.IDevice +import com.android.ddmlib.testrunner.RemoteAndroidTestRunner +import org.apache.logging.log4j.LogManager +import org.ftd.Master.Scheduler.{AdaptiveSchedulerRunner, DescendingDelayScheduler, MaxDelayScheduler} + +import scala.concurrent.duration.Duration + +class XiaoScheduling extends Strategy { + + private val logger = LogManager.getLogger() + + val name: String = this.getClass.getSimpleName + + /** + * + * @return indicates whether detected flaky + */ + def start(device: IDevice, testRunner: RemoteAndroidTestRunner, appName: String, testClassPath: String, testMethodPath: String, testCaseHangTimeout: Duration = Duration.Inf, max_runs: Int, givenPassed: Boolean): Boolean = { + + testRunner setDebug true + + val init_passedBefore = givenPassed + val init_failedBefore = !givenPassed + + val maxDelaySchedulerRunner = new AdaptiveSchedulerRunner(new MaxDelayScheduler(), device, testRunner, appName, testClassPath, testMethodPath, testCaseHangTimeout) + val AdaptiveSchedulerRunner.Status(detectedFlaky, runs, passedBefore, failedBefore) = maxDelaySchedulerRunner.run(max_runs, init_passedBefore, init_failedBefore) + + if(detectedFlaky) { + return true + } + + logger.info("Now using DescendingDelayScheduler") + val descendingDelayScheduler = new DescendingDelayScheduler(maxDelaySchedulerRunner.scheduler.lastReleaseMap, maxDelaySchedulerRunner.lastProfiler.msgGroups, maxDelaySchedulerRunner.scheduler.uselessMsgIDs, maxDelaySchedulerRunner.lastProfiler.thisUselessMsgIDs.toSet, passedBefore, failedBefore) + val descendingDelaySchedulerRunner = new AdaptiveSchedulerRunner(descendingDelayScheduler, device, testRunner, appName, testClassPath, testMethodPath, testCaseHangTimeout) + val AdaptiveSchedulerRunner.Status(detectedFlaky2, _, _, _) = descendingDelaySchedulerRunner.run(if (max_runs == -1) -1 else max_runs - runs, passedBefore, failedBefore) + + if(detectedFlaky2) { + return true + } + + logger.info("All possibilities tried, test case not detected flaky.") + false + } +} diff --git a/src/main/scala/org/ftd/Master/TestRunner.scala b/src/main/scala/org/ftd/Master/TestRunner.scala new file mode 100644 index 0000000..85eb528 --- /dev/null +++ b/src/main/scala/org/ftd/Master/TestRunner.scala @@ -0,0 +1,83 @@ +package org.ftd.Master + +import com.android.ddmlib.testrunner.{ITestRunListener, RemoteAndroidTestRunner, TestIdentifier} +import org.apache.logging.log4j.LogManager +import org.ftd.Master.utils.RetryException +import java.util + +import scala.concurrent.Promise + +object TestRunner { + class TestRunListener(testEndPromise: Promise[Boolean], testRunEndPromise: Promise[Unit]) extends ITestRunListener { + + private val logger = LogManager.getLogger() + private var runFailed: Boolean = false + private var shouldRetry: Boolean = false + + def testRunStarted(runName: String, testCount: Int): Unit = { + logger.debug(s"Test run started, runName: ${runName}") + } + + def testStarted(test: TestIdentifier): Unit = { + logger.debug(s"Test started, test: ${test}") + } + + def testFailed(test: TestIdentifier, trace: String): Unit = { + logger.debug("Test failed") + logger.debug(trace) + assert(!testEndPromise.isCompleted) + testEndPromise success (false) + } + + def testAssumptionFailure(test: TestIdentifier, trace: String): Unit = { + throw new RuntimeException("Test assumption failed") + } + + def testIgnored(test: TestIdentifier): Unit = { + throw new RuntimeException("Test ignored") + } + + def testEnded(test: TestIdentifier, testMetrics: util.Map[String, String]): Unit = { + logger.debug("Test ended") + if (!testEndPromise.isCompleted) { + testEndPromise success (true) + } + } + + def testRunFailed(errMsg: String): Unit = { + logger.debug("Test run failed.") + logger.debug(errMsg) + runFailed = true + if (errMsg.startsWith("com.android.ddmlib.TimeoutException")) { + shouldRetry = true + } + } + + def testRunStopped(elapsedTime: Long): Unit = { + + } + + def testRunEnded(elapsedTime: Long, runMetrics: util.Map[String, String]): Unit = { + assert(testEndPromise.isCompleted, "No test executed!") + logger.debug(s"Test run ended in ${elapsedTime} milliseconds.") + assert(!testRunEndPromise.isCompleted) + + if (!runFailed) { + testRunEndPromise success() + } else { + if (shouldRetry) { + testRunEndPromise failure (RetryException()) + } else { + testRunEndPromise failure (new RuntimeException("Test run failed!")) + } + + } + + } + } + + def run(testRunner: RemoteAndroidTestRunner, testEndPromise: Promise[Boolean], testRunEndPromise: Promise[Unit]): Unit = { + val testRunListener = new TestRunListener(testEndPromise, testRunEndPromise) + testRunner run testRunListener + } +} diff --git a/src/main/scala/org/ftd/Master/utils/CLI.scala b/src/main/scala/org/ftd/Master/utils/CLI.scala new file mode 100644 index 0000000..cacdcc7 --- /dev/null +++ b/src/main/scala/org/ftd/Master/utils/CLI.scala @@ -0,0 +1,106 @@ +package org.ftd.Master.utils + +import java.io.File +import java.lang.Thread.UncaughtExceptionHandler +import java.nio.file.{Files, Paths} + +import org.ftd.Master.{Config, Main} + +import scala.concurrent.duration.Duration + +object CLI { + def main(args: Array[String]): Unit = { + + val prevUncaughtExHandler = Thread.getDefaultUncaughtExceptionHandler + + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler { + override def uncaughtException(thread: Thread, throwable: Throwable): Unit = { + if (prevUncaughtExHandler != null) prevUncaughtExHandler.uncaughtException(thread, throwable) + System.err.println(throwable) + System.err.println(s"Happened in thread ${thread}") + throwable.printStackTrace(System.err) + System.exit(1) + } + }) + + import scopt.OParser + + val builder = OParser.builder[Config] + val parser = { + import builder._ + OParser.sequence( + programName("FTD"), + + opt[String]("adbPath") + .required() + // .withFallback(() => System.getenv("PATH").split(File.pathSeparator).map(p => Paths.get(p).resolve("adb")).find( p => Files.exists(p) ).head ) + .action((x, c) => c.copy(adbPath = Paths get x)), + + opt[Unit]("debug") + .action((_, c) => c.copy(debug = true)), + + opt[Unit]("disable-ddm-log") + .action((_, c) => c.copy(disable_ddm_log = true)), + + opt[Seq[String]]("apkInstallOpts") + .optional() + .action((x, c) => c.copy(apkInstallOpts = x)), + + opt[String]("deviceName") + .optional() + .action((x, c) => c.copy(deviceName = x)) + .text("Use default device if not supplied"), + + opt[Int]("max-runs") + .optional() + .action((x, c) => c.copy(max_runs = x)), + + opt[File]("config-from-file") + .optional() + .action((x, c)=>c.copy(from_files = x)), + + opt[Duration]("test-hang-timeout") + .optional() + .action((x, c)=>c.copy(testCaseHangTimeout = x)), + + opt[Boolean]("given-passed") + .optional() + .action((x, c)=>c.copy(givenPassed = x)), + + opt[String]("strategy") + .optional() + .action((x, c)=>c.copy(strategy = x)), + + arg[String]("appName") + .required() + .action((x, c) => c.copy(appName = x)), + + arg[String]("testPackage") + .required() + .action((x, c) => c.copy(testPackage = x)), + + arg[Seq[String]]("apkPath") + .required() + .action((x, c) => c.copy(apkPath = x.map(p => Paths.get(p)))), + + arg[String]("testRunnerClsPath") + .required() + .action((x, c) => c.copy(testRunnerClsPath = x)), + + arg[String]("testClassPath") + .required() + .action((x, c) => c.copy(testClassPath = x)), + + arg[String]("testMethodPath") + .required() + .action((x, c) => c.copy(testMethodPath = x)), + + ) + } + + OParser.parse(parser, args, Config()) match { + case Some(config) => Main.main(config) + case None => // Do nothing + } + } +} \ No newline at end of file diff --git a/src/main/scala/org/ftd/Master/utils/Retry.scala b/src/main/scala/org/ftd/Master/utils/Retry.scala new file mode 100644 index 0000000..4986e66 --- /dev/null +++ b/src/main/scala/org/ftd/Master/utils/Retry.scala @@ -0,0 +1,18 @@ +package org.ftd.Master.utils + +object Retry { + + def retry[T, E <: Throwable](n: Int, fn: => T, onlyException: Class[E]): T = { + retry(n, fn, (e: Throwable) => onlyException.isInstance(e) ) + } + + @annotation.tailrec + def retry[T](n: Int, fn: => T, retryCond: (Throwable) => Boolean): T = { + util.Try { fn } match { + case util.Success(x) => x + case util.Failure(e) => + if (n > 1 && retryCond(e)) retry(n-1, fn, retryCond) else throw e + } + } + +} diff --git a/src/main/scala/org/ftd/Master/utils/RetryException.scala b/src/main/scala/org/ftd/Master/utils/RetryException.scala new file mode 100644 index 0000000..c25193a --- /dev/null +++ b/src/main/scala/org/ftd/Master/utils/RetryException.scala @@ -0,0 +1,3 @@ +package org.ftd.Master.utils + +case class RetryException() extends Exception diff --git a/src/scala/org/yxliang01/ftd/MessageTracer.scala b/src/scala/org/yxliang01/ftd/MessageTracer.scala deleted file mode 100644 index d1c65dc..0000000 --- a/src/scala/org/yxliang01/ftd/MessageTracer.scala +++ /dev/null @@ -1,3 +0,0 @@ -class MessageTracer { - -} \ No newline at end of file diff --git a/template.dockerignore b/template.dockerignore new file mode 100644 index 0000000..ef4c6f4 --- /dev/null +++ b/template.dockerignore @@ -0,0 +1,6 @@ +.git +.gitignore +scripts/build-docker.bash +scripts/run-docker.bash +Dockerfile +template.dockerignore \ No newline at end of file