├── .document ├── .gitignore ├── .rvmrc ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── VERSION ├── bin ├── hit_tracer.rb └── print_variables.rb ├── lib ├── android_debug.rb └── android_debug │ ├── adb.rb │ ├── adb_native.rb │ ├── debugger.rb │ ├── jpda │ ├── event.rb │ ├── frame.rb │ ├── jpda_helper.rb │ ├── local_variable.rb │ └── object_in_frame.rb │ └── mixin │ ├── invokable.rb │ └── java_passthrough.rb ├── template ├── change_variable.rb ├── invoke_method.rb └── single_step.rb └── test ├── hello_world.rb ├── helper.rb ├── log_intent.rb ├── sample.rb ├── snapchat.rb ├── test.rb ├── test_adb_native.rb └── test_android_debug.rb /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/snapchat.rb 2 | 3 | # rcov generated 4 | coverage 5 | coverage.data 6 | 7 | # rdoc generated 8 | rdoc 9 | 10 | # yard generated 11 | doc 12 | .yardoc 13 | 14 | # bundler 15 | .bundle 16 | 17 | # jeweler generated 18 | pkg 19 | 20 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 21 | # 22 | # * Create a file at ~/.gitignore 23 | # * Include files you want ignored 24 | # * Run: git config --global core.excludesfile ~/.gitignore 25 | # 26 | # After doing this, these files will be ignored in all your git projects, 27 | # saving you from having to 'pollute' every project you touch with them 28 | # 29 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 30 | # 31 | # For MacOS: 32 | # 33 | #.DS_Store 34 | 35 | # For TextMate 36 | #*.tmproj 37 | #tmtags 38 | 39 | # For emacs: 40 | #*~ 41 | #\#* 42 | #.\#* 43 | 44 | # For vim: 45 | #*.swp 46 | 47 | # For redcar: 48 | #.redcar 49 | 50 | # For rubinius: 51 | #*.rbc 52 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use jruby-1.7.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | # Add dependencies required to use your gem here. 3 | # Example: 4 | # gem "activesupport", ">= 2.3.5" 5 | 6 | # Add dependencies to develop your gem here. 7 | # Include everything needed to run rake, tests, features, etc. 8 | group :development do 9 | # Testing 10 | gem "shoulda", ">= 0" 11 | 12 | # Development 13 | gem "bundler", "~> 1.3.5" 14 | gem "jeweler", "~> 1.8.4" 15 | 16 | # Documentation 17 | gem 'yard' 18 | gem 'kramdown' 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activesupport (3.2.13) 5 | i18n (= 0.6.1) 6 | multi_json (~> 1.0) 7 | git (1.2.5) 8 | i18n (0.6.1) 9 | jeweler (1.8.4) 10 | bundler (~> 1.0) 11 | git (>= 1.2.5) 12 | rake 13 | rdoc 14 | json (1.8.0) 15 | json (1.8.0-java) 16 | kramdown (1.0.2) 17 | multi_json (1.7.6) 18 | rake (10.0.4) 19 | rdoc (3.12.2) 20 | json (~> 1.4) 21 | shoulda (3.5.0) 22 | shoulda-context (~> 1.0, >= 1.0.1) 23 | shoulda-matchers (>= 1.4.1, < 3.0) 24 | shoulda-context (1.1.2) 25 | shoulda-matchers (2.2.0) 26 | activesupport (>= 3.0.0) 27 | yard (0.8.6.1) 28 | 29 | PLATFORMS 30 | java 31 | ruby 32 | 33 | DEPENDENCIES 34 | bundler (~> 1.3.5) 35 | jeweler (~> 1.8.4) 36 | kramdown 37 | shoulda 38 | yard 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 wuntee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | android_debug 2 | ============= 3 | 4 | This is a scriptable debugger for Android applications. Sample usage: 5 | 6 | require 'android_debug' 7 | dbg = AndroidDebug.launch_and_attach("com.android.browser", "com.android.browser.BrowserActivity") 8 | dbg.on_break do |event| 9 | puts("Break type: #{event}") 10 | if(dbg.frame.method_name == "registerReceiver") 11 | puts("#{dbg.frame.method_signature}") 12 | puts("Frame variables:") 13 | dbg.frame.variables.each do |var| 14 | puts("\t#{var}") 15 | end 16 | end 17 | dbg.resume 18 | end 19 | dbg.add_class_entry_breakpoint("android.content.Context") 20 | dbg.go 21 | 22 | Core concepts: 23 | -------------- 24 | 25 | * Environmental or global vairable ANDROID_HOME must be set to your SDK home (ex: /Users/wuntee/android-sdk-macosx). This is because it uses the 'adb' binary to launch applications for the debugger, and utilizes some of the DDMS java libraries. 26 | * On a breakpoint, you can access variables, methods, locations via the initial 'AndroidDebug::Debugger' object 27 | * Create/Launch a debugger via AndroidDebug.launch_and_attach(classpath, classname) 28 | * AndroidDebug::Debugger.this is the "this" object within the class you broken in. 29 | * AndroidDebug::Debugger.frame is the stackframe on where the break occured and is a AndroidDebug::Jpda::Frame object. This class has a bunch of helper methods to get fun data. 30 | * You can invoke remote methods by calling Object.method(args) 31 | * Invoking methods will re-start the application, and you will lose your break. 32 | * Most classes are extending existing Java classes. Any method you will typically call on a java class will percilate down to the core Java class, if it exists. (See the JavaPassthrough mixin for specifics) 33 | * You must resume the debugger in the 'on_break' block ('dbg.resume') before returning. This is because there is the ability to step within the block as well. 34 | 35 | Linux users: 36 | ------------ 37 | 38 | * You must use the sun 'JDK' as it is the only packages that has the com.sun.jdi classes 39 | * You also may have to explicitally add the tools.jar to teh $CLASSPATH variable in your script. (This will be the case if you see the following exception: NameError: cannot load Java class com.sun.jdi.Bootstrap) 40 | 41 | Contributing to android_debug 42 | ============================= 43 | 44 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. 45 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. 46 | * Fork the project. 47 | * Start a feature/bugfix branch. 48 | * Commit and push until you are happy with your contribution. 49 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 50 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 51 | 52 | Copyright 53 | ========= 54 | 55 | Copyright (c) 2013 wuntee. See LICENSE.txt for 56 | further details. 57 | 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'jeweler' 15 | Jeweler::Tasks.new do |gem| 16 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 17 | gem.name = "android_debug" 18 | gem.homepage = "http://github.com/wuntee/android_debug" 19 | gem.license = "MIT" 20 | gem.summary = "A scriptable debugger library to interact with Android applications" 21 | gem.description = "" 22 | gem.email = "mathew.rowley@gmail.com" 23 | gem.authors = ["wuntee"] 24 | gem.platform = "java" 25 | gem.files = Dir.glob('lib/**/*.rb') 26 | # dependencies defined in Gemfile 27 | end 28 | 29 | # Not yet... 30 | #Jeweler::RubygemsDotOrgTasks.new 31 | 32 | =begin 33 | require 'rake/testtask' 34 | Rake::TestTask.new(:test) do |test| 35 | test.libs << 'lib' << 'test' 36 | test.pattern = 'test/**/test_*.rb' 37 | test.verbose = true 38 | end 39 | 40 | require 'rcov/rcovtask' 41 | Rcov::RcovTask.new do |test| 42 | test.libs << 'test' 43 | test.pattern = 'test/**/test_*.rb' 44 | test.verbose = true 45 | test.rcov_opts << '--exclude "gems/*"' 46 | end 47 | 48 | task :default => :test 49 | 50 | require 'rdoc/task' 51 | Rake::RDocTask.new do |rdoc| 52 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 53 | 54 | rdoc.rdoc_dir = 'rdoc' 55 | rdoc.title = "android_debug #{version}" 56 | rdoc.rdoc_files.include('README*') 57 | rdoc.rdoc_files.include('lib/**/*.rb') 58 | end 59 | =end 60 | require 'yard' 61 | 62 | #module YARD::Templates::Helpers::HtmlHelper 63 | # def html_markup_markdown(text) 64 | # markup_class(:markdown).new(text, :gh_blockcode, :fenced_code, :autolink, :tables).to_html 65 | # end 66 | #end 67 | 68 | YARD::Rake::YardocTask.new('doc') do |doc| 69 | doc.options << '-m' << 'markdown' << '-M' << 'kramdown' 70 | doc.options << '--protected' << '--no-private' 71 | doc.options << '-r' << 'README.rdoc' 72 | doc.options << '-o' << 'doc' 73 | doc.options << '--title' << "Android Scriptable Debugger Framework" 74 | 75 | doc.files = %w( lib/**/*.rb README.rdoc ) 76 | end 77 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 -------------------------------------------------------------------------------- /bin/hit_tracer.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'pp' 3 | require_relative '../lib/android_debug.rb' 4 | 5 | 6 | options = {} 7 | OptionParser.new do |opts| 8 | opts.banner = "Usage: hit_tracer.rb [options]" 9 | opts.on("-d", '--debug', "Enable debug logging.") do |v| 10 | $DEBUG = true 11 | end 12 | opts.on("-l", "--launch STR", "Launch an application. Ex: com.android.browser/com.android.browser.BrowserActivity") do |l| 13 | split = l.split("\/") 14 | options[:app_class] = split[0] 15 | options[:app_pkg] = split[1] 16 | end 17 | opts.on("-c", "--class STR", "Regular expression for classes to track.") do |c| 18 | options[:class_regex] = c 19 | end 20 | end.parse! 21 | raise OptionParser::MissingArgument if 22 | options[:app_class].nil? or 23 | options[:app_pkg].nil? or 24 | options[:class_regex].nil? 25 | 26 | dbg = AndroidDebug.launch_and_attach(options[:app_class], options[:app_pkg]) 27 | dbg.add_class_entry_breakpoint(options[:class_regex]) 28 | vars = {} 29 | dbg.on_break do |event| 30 | breakpoint = dbg.frame.method_signature 31 | vars[breakpoint].nil? ? (vars[breakpoint]=1) : 32 | (vars[breakpoint]=vars[breakpoint]+1) 33 | puts("Hits:") 34 | pp(vars) 35 | puts 36 | dbg.resume 37 | end 38 | dbg.go 39 | -------------------------------------------------------------------------------- /bin/print_variables.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require_relative '../lib/android_debug.rb' 3 | 4 | 5 | options = {} 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: print _variables.rb [options]" 8 | opts.on("-d", '--debug', "Enable debug logging.") do |v| 9 | $DEBUG = true 10 | end 11 | opts.on("-l", "--launch STR", "Launch an application. Ex: com.android.browser/com.android.browser.BrowserActivity") do |l| 12 | split = l.split("\/") 13 | options[:app_class] = split[0] 14 | options[:app_pkg] = split[1] 15 | end 16 | opts.on("-c", "--class STR", "Regular expression for classes to track.") do |c| 17 | options[:class_regex] = c 18 | end 19 | opts.on("-m", "--method STR", "Method that you would like to print variables from (optional). Use '' for constructor.") do |m| 20 | options[:method] = m 21 | end 22 | end.parse! 23 | raise OptionParser::MissingArgument if 24 | options[:app_class].nil? or 25 | options[:app_pkg].nil? or 26 | options[:class_regex].nil? 27 | 28 | dbg = AndroidDebug.launch_and_attach(options[:app_class], options[:app_pkg]) 29 | dbg.add_class_entry_breakpoint(options[:class_regex]) 30 | vars = {} 31 | dbg.on_break do |event| 32 | if(options[:method].nil? or (dbg.frame.method_name == options[:method])) 33 | puts(dbg.frame.method_signature) 34 | dbg.frame.variables.each do |var| 35 | puts("\t#{var}") 36 | end 37 | puts 38 | end 39 | dbg.resume 40 | end 41 | dbg.go 42 | -------------------------------------------------------------------------------- /lib/android_debug.rb: -------------------------------------------------------------------------------- 1 | require 'java' 2 | require_relative 'android_debug/mixin/java_passthrough.rb' 3 | require_relative 'android_debug/mixin/invokable.rb' 4 | require_relative 'android_debug/jpda/jpda_helper.rb' 5 | require_relative 'android_debug/jpda/local_variable.rb' 6 | require_relative 'android_debug/jpda/object_in_frame.rb' 7 | require_relative 'android_debug/jpda/event.rb' 8 | require_relative 'android_debug/debugger.rb' 9 | require_relative 'android_debug.rb' 10 | require_relative 'android_debug/adb.rb' 11 | require_relative 'android_debug/jpda/frame.rb' 12 | require_relative 'android_debug/adb_native.rb' 13 | 14 | module AndroidDebug 15 | 16 | # Static helper function to launch a specific activity, attach the debugger, 17 | # and return the debugger object 18 | # @return [AndroidDebug::Debugger] 19 | def self.launch_and_attach(package, clazz) 20 | AndroidDebug::Adb.set_activity_debug(package) 21 | AndroidDebug::Adb.launch_activity(package, clazz) 22 | sleep(0.1) 23 | 24 | dbg = AndroidDebug::Debugger.new 25 | dbg.connect() 26 | 27 | return(dbg) 28 | end 29 | 30 | def self.launch_and_attach_native(package, clazz) 31 | adb = AndroidDebug::AdbNative.new 32 | devices = adb.devices 33 | device = adb.devices[0] 34 | 35 | port = 5570 36 | 37 | adb.set_activity_debug(package) 38 | adb.launch_activity(package, clazz) 39 | sleep(0.5) 40 | pid = adb.jdwp_pids(device)[-1] 41 | adb.forward_jdwp(device, port, pid) 42 | 43 | dbg = AndroidDebug::Debugger.new 44 | dbg.connect_port(port) 45 | 46 | return(dbg) 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /lib/android_debug/adb.rb: -------------------------------------------------------------------------------- 1 | module AndroidDebug 2 | # Helper static methods for interacting with ADB 3 | module Adb 4 | 5 | # Set an activity to wait for the debugger upon next launch 6 | # @param activity [String] 7 | def self.set_activity_debug(activity) 8 | system("#{self.get_adb} shell am set-debug-app -w #{activity}") 9 | end 10 | 11 | # Launch a specific activity 12 | # @param activity [String] 13 | # @param clazz [String] 14 | # Example: 15 | # # Launches browser 16 | # dbg = AndroidDebug.launch_and_attach("com.android.browser", "com.android.browser.BrowserActivity") 17 | def self.launch_activity(activity, clazz) 18 | system("#{self.get_adb} shell am start #{activity}/#{clazz}") 19 | end 20 | 21 | # @return The location of the adb binary 22 | def self.get_adb 23 | android_home = self.get_android_home 24 | return("#{android_home}/platform-tools/adb") 25 | end 26 | 27 | # @return the location of the the android SDK home directory 28 | def self.get_android_home 29 | android_home = ENV['ANDROID_HOME'] || $ANDROID_HOME or throw "ANDROID_HOME is not set in env or as a global variable." 30 | return(android_home) 31 | end 32 | 33 | # This initializes the ADB bridge in order to enumerate applications, debug ports, etc. 34 | # @return An AndroidDebugBridge object 35 | def self.init_adb 36 | AndroidDebugBridge.init(true) 37 | adb = AndroidDebugBridge.createBridge("#{self.get_android_home}/platform-tools/adb", true) 38 | sleep(0.1) 39 | return(adb) 40 | end 41 | 42 | # Destructor to clean up the ADB bridge. Should be called on exit. 43 | def self.cleanup_adb 44 | AndroidDebugBridge.disconnectBridge 45 | AndroidDebugBridge.terminate 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/android_debug/adb_native.rb: -------------------------------------------------------------------------------- 1 | module AndroidDebug 2 | # Native ADB implementation so we dont need the ANDROID_HOME 3 | class AdbNative 4 | require 'socket' 5 | 6 | attr_reader :soc 7 | 8 | def send_command(command, new_connection = true) 9 | host = "localhost" 10 | port = 5037 11 | @soc.nil? || @soc.closed? || new_connection and @soc = TCPSocket.open(host, port) 12 | begin 13 | @soc.print("%04x" % command.size) 14 | @soc.print(command) 15 | response = read_soc 16 | if(response.match(/OKAY\d{4}.+/)) 17 | return(response["OKAY0000".length, response.size]) 18 | else 19 | return(response) 20 | end 21 | rescue Errno::EPIPE 22 | puts("Connection to the ADB has been torn down.") 23 | end 24 | end 25 | 26 | def read_soc 27 | return @soc.recvfrom(1024)[0].strip 28 | end 29 | 30 | def devices 31 | devices = send_command("host:devices") 32 | if(devices == "") 33 | return({}) 34 | else 35 | # Converts an array to a hash 36 | return(Hash[*devices.split(/\t/)]) 37 | end 38 | end 39 | 40 | def version 41 | return(send_command("host:version")) 42 | end 43 | 44 | def jdwp_pids(device) 45 | send_command("host:transport-#{device}") 46 | send_command("jdwp", false) 47 | 48 | # format [4-size][rest] 49 | size = @soc.recvfrom(4)[0].to_i(16) 50 | ret = @soc.recvfrom(size)[0].strip 51 | return(ret[4, ret.size].split("\n")) 52 | end 53 | 54 | def forward_jdwp(device, local_port, remote_pid) 55 | return send_command("host:forward:tcp:#{local_port};jdwp:#{remote_pid}") 56 | end 57 | 58 | def shell_command(command, transport = "any", new_connection = true) 59 | send_command("host:transport-#{transport}", new_connection) 60 | send_command("shell:#{command}", false) 61 | return(@soc.read.strip) 62 | end 63 | 64 | def launch_activity(activity, clazz) 65 | shell_command("am start #{activity}/#{clazz}") 66 | end 67 | 68 | def set_activity_debug(activity) 69 | shell_command("am set-debug-app -w #{activity}") 70 | end 71 | end 72 | end 73 | 74 | -------------------------------------------------------------------------------- /lib/android_debug/debugger.rb: -------------------------------------------------------------------------------- 1 | require 'java' 2 | java_import 'com.sun.jdi.request.EventRequest' 3 | java_import 'java.util.ArrayList' 4 | 5 | module AndroidDebug 6 | # This is the main interaction point for the user 7 | class Debugger 8 | INVOKE_SINGLE_THREADED = 1 9 | 10 | attr_accessor :android_home, :on_break 11 | 12 | # Private virtual machine object 13 | attr_reader :vm 14 | 15 | # Private VM manager object 16 | attr_reader :mgr 17 | 18 | # Private ADB object 19 | attr_reader :adb 20 | 21 | # Private array of entry brakepoints 22 | attr_reader :class_entry_breakpoints 23 | 24 | # Private array of exit brakepoints 25 | attr_reader :class_exit_breakpoints 26 | 27 | # "this" object of the current breakpoint. Or, the current instance of the object that the breakpoint was set in. 28 | attr_reader :this 29 | 30 | # The current [AndroidDebug::Jpda::Frame] object. Methods and instance variables. 31 | attr_reader :frame 32 | 33 | # Constructor 34 | # @param android_home [String] the SDK location 35 | def initialize(android_home = AndroidDebug::Adb.get_android_home) 36 | @android_home = android_home 37 | 38 | @class_entry_breakpoints = {} 39 | @class_exit_breakpoints = {} 40 | @method_breakpoints = {} 41 | 42 | $CLASSPATH << "#{@android_home}/tools/lib/ddmlib.jar" 43 | java_import 'com.android.ddmlib.AndroidDebugBridge' 44 | 45 | @adb = AndroidDebug::Adb.init_adb 46 | end 47 | 48 | # Connect the android debugger to a remote host/port 49 | # @param host 50 | # @param port 51 | def connect_host_port(host, port) 52 | $DEBUG and puts("Connecting to debugger on #{host}:#{port}") 53 | conn, args = AndroidDebug::Jpda.get_socket_connector_and_args(host, port) 54 | $DEBUG and puts("Connected. Attaching to Java VM.") 55 | @vm = conn.attach(args) 56 | @mgr = @vm.eventRequestManager 57 | $DEBUG and puts("Attached.") 58 | end 59 | 60 | # Helper to connect to a localhost port 61 | # @param port 62 | def connect_port(port) 63 | connect_host_port("localhost", port) 64 | end 65 | 66 | # Helper method to connect to whatever port is listening for a debugger 67 | def connect 68 | begin 69 | port = get_debugger_port 70 | rescue 71 | # Sometimes this will fail if we don't wait long enough, try one more time 72 | sleep(2) 73 | port = get_debugger_port 74 | end 75 | connect_host_port("localhost", port) 76 | end 77 | 78 | # Communication with the Android debugger is performed over an open localhost 79 | # port. This helper function utilizes the Android DDMS libraries to determine 80 | # what ports are currently open and waiting for a debugger. 81 | # @return the local port that is listening for a debugger 82 | def get_debugger_port 83 | throw "Could not get devices from adb" if @adb.getDevices.size == 0 84 | dev = @adb.getDevices[0] 85 | sleep(1) 86 | throw "Could not get clients for device (#{dev})" if dev.getClients.size == 0 87 | dev.getClients.each do |cli| 88 | $DEBUG and puts("Found process: #{cli}") 89 | if(cli.getClientData.getDebuggerConnectionStatus.to_s == "WAITING") 90 | $DEBUG and puts("Found process waiting for debugger: #{cli} : #{cli.getDebuggerListenPort}") 91 | return(cli.getDebuggerListenPort) 92 | end 93 | end 94 | throw("Could not find a process waiting for debugger.") 95 | return(nil) 96 | end 97 | 98 | # Disconnect the debugger 99 | def disconnect 100 | #@vm.exit(0) 101 | AndroidDebug::Adb.cleanup_adb 102 | end 103 | 104 | # Main loop of the library. This will launch the application, wait for 105 | # breakpoints and process them accordingly. 106 | def go 107 | while(true) 108 | process_event(wait_for_event) 109 | end 110 | end 111 | 112 | # Private method that will process an event when it comes in. On Breakpoint 113 | # events, it will call on_break, which is the user supplied method. 114 | def process_event(event) 115 | if(event.type == AndroidDebug::Jpda::Event::ENTRY or 116 | event.type == AndroidDebug::Jpda::Event::EXIT) 117 | on_break(event) 118 | elsif(event.type == AndroidDebug::Jpda::Event::VM_EXIT) 119 | puts("The VM was disconnected.") 120 | exit(0) 121 | else 122 | $DEBUG and puts("Received unknown event type: #{event}") 123 | end 124 | end 125 | 126 | # Private method that will wait for an incoming event and return it. This 127 | # exists because different portions of the library need to retrieve events 128 | # such as a Breakpoint event is handled differently than a Step event. 129 | # @return [AndroidDebug::Jpda::Event] 130 | def wait_for_event 131 | q = @vm.eventQueue() 132 | while(true) 133 | event_set = q.remove() 134 | it = event_set.iterator() 135 | while(it.hasNext) 136 | event = it.next 137 | $DEBUG and puts("Received an event: #{event.java_class}") 138 | @frame = AndroidDebug::Jpda::Frame.new(event.thread.frame(0), event.location) 139 | @this = @frame.this 140 | return(AndroidDebug::Jpda::Event.new(event)) 141 | end 142 | end 143 | end 144 | 145 | # Private helper method that will process events until a step event occurs. 146 | # It will also process all breakpoint events as they occur. There may be an 147 | # occasion where a step will run into a break. 148 | def wait_for_step_event 149 | while(true) 150 | event = wait_for_event 151 | if(event.java_event.java_kind_of?(com.sun.jdi.event.StepEvent)) 152 | return 153 | else 154 | process_event(event) 155 | end 156 | end 157 | end 158 | 159 | # Add a breakpoint for when application control flow enters a class. 160 | # @param class_filter [String] that represents a regular expression of where to 161 | # break. 162 | # Example: 163 | # => "android.content.Context" 164 | # => "android.content.*" 165 | def add_class_entry_breakpoint(class_filter) 166 | return if @class_entry_breakpoints[class_filter] 167 | 168 | bp = @mgr.createMethodEntryRequest 169 | bp.setSuspendPolicy(EventRequest.SUSPEND_ALL); 170 | bp.addClassFilter(class_filter); 171 | bp.enable(); 172 | 173 | @class_entry_breakpoints[class_filter] = bp 174 | end 175 | 176 | # Add a breakpoint that will only trigger once. 177 | # @param class_filter [String] see #{AndroidDebug::Debugger::add_class_entry_breakpoint} 178 | def add_temporary_class_entry_breakpoint(class_filter) 179 | bp = @mgr.createMethodEntryRequest 180 | bp.setSuspendPolicy(EventRequest.SUSPEND_ALL) 181 | bp.addClassFilter(class_filter) 182 | bp.addCountFilter(1) 183 | bp.enable() 184 | end 185 | 186 | # Add a breakpoint for when application control flow exits a class. 187 | # Sometimes, the local variables of the [AndroidDebug::Jpda::Frame] object 188 | # will contain the return value. 189 | # Android does not support the JPDA method to get the return value of a method. 190 | # @param class_filter [String] see #{AndroidDebug::Debugger::add_class_entry_breakpoint} 191 | def add_class_exit_breakpoint(class_filter) 192 | return if @class_exit_breakpoints[class_filter] 193 | 194 | bp = @mgr.createMethodExitRequest 195 | bp.setSuspendPolicy(EventRequest.SUSPEND_ALL) 196 | bp.addClassFilter(class_filter) 197 | bp.enable() 198 | 199 | @class_exit_breakpoints[class_filter] = bp 200 | end 201 | 202 | # Add a breakpoint that will only trigger once. 203 | # @param class_filter [String] see #{AndroidDebug::Debugger::add_class_entry_breakpoint} 204 | def add_temporary_class_exit_breakpoint(class_filter) 205 | bp = @mgr.createMethodExitRequest 206 | bp.setSuspendPolicy(EventRequest.SUSPEND_ALL) 207 | bp.addClassFilter(class_filter) 208 | bp.addCountFilter(1) 209 | bp.enable() 210 | end 211 | 212 | # Remove an entry breakpoint that was already created 213 | # @param class_filter [String] see #{AndroidDebug::Debugger::add_class_entry_breakpoint} 214 | def remove_class_entry_breakpoint(class_filter) 215 | @mgr.deleteEventRequest(@class_entry_breakpoints.delete(class_filter)) 216 | end 217 | 218 | # Remove an exit breakpoint that was already created 219 | # @param class_filter [String] see #{AndroidDebug::Debugger::add_class_entry_breakpoint} 220 | def remove_class_exit_breakpoint(class_filter) 221 | @mgr.deleteEventRequest(@class_exit_breakpoints.delete(class_filter)) 222 | end 223 | 224 | # The code block that will process breakpoints. It will pass the 225 | # [AndroidDebug::Jpda::Event] object 226 | # @param block is the block 227 | # Example: 228 | # => dbg.on_break |event| do 229 | # => puts "Local variables:" 230 | # => puts dbg.frame.variables 231 | # => end 232 | def on_break(*args, &block) 233 | if block_given? 234 | @on_break = block 235 | else 236 | @on_break.call(*args) if @on_break 237 | end 238 | end 239 | 240 | # Must be called after a breakpoint. Will resume application flow. 241 | def resume 242 | @vm.resume 243 | end 244 | 245 | def step_over 246 | step_request = get_step_request(@frame.thread, com.sun.jdi.request.StepRequest.STEP_LINE, com.sun.jdi.request.StepRequest.STEP_OVER) 247 | step_request.enable 248 | @vm.resume 249 | wait_for_step_event 250 | @mgr.deleteEventRequest(step_request) 251 | end 252 | 253 | def step_into 254 | step_request = get_step_request(@frame.thread, com.sun.jdi.request.StepRequest.STEP_MIN, com.sun.jdi.request.StepRequest.STEP_INTO) 255 | step_request.enable() 256 | @vm.resume 257 | wait_for_step_event 258 | @mgr.deleteEventRequest(step_request) 259 | end 260 | 261 | def step_out 262 | step_request = get_step_request(@frame.thread, com.sun.jdi.request.StepRequest.STEP_LINE, com.sun.jdi.request.StepRequest.STEP_OUT) 263 | step_request.enable() 264 | @vm.resume 265 | wait_for_step_event 266 | @mgr.deleteEventRequest(step_request) 267 | end 268 | 269 | 270 | # There is only one StepRequest per thread. If the thread contains one, return it, else create. 271 | def get_step_request(thread, size, depth, count_filter=1) 272 | =begin 273 | @mgr.stepRequests.each do |step_request| 274 | if(step_request.thread == thread) 275 | # TODO: Need to figure out how to modify the step request 276 | 277 | # attempt to delete the request, but it may be the one we are currently broke on 278 | #begin 279 | #@mgr.deleteEventRequest(step_request) 280 | 281 | #rescue java.util.ConcurrentModificationException => e 282 | # Couldnt delete it, so return it 283 | return(step_request) 284 | #end 285 | end 286 | end 287 | =end 288 | step_request = @mgr.createStepRequest(thread, size, depth) # Will thorw java.util.ConcurrentModificationException if attempted after a delete 289 | step_request.addCountFilter(count_filter) 290 | return(step_request) 291 | end 292 | 293 | # @return An array of frames [com.sun.jdi.StackFrame] 294 | def backtrace 295 | return(@frame.thread.frames) 296 | end 297 | 298 | # Prints the backtrace 299 | def print_backtrace 300 | self.get_backtrace.each do |f| 301 | puts(f) 302 | end 303 | end 304 | 305 | # Finds the proper method object when you want to call a specific method on a 306 | # variable from within the current frame. 307 | # @param clazz [String] The class name you are looking for (may be the full 308 | # classpath or just the name) 309 | # @param method [String] The method name to be returned 310 | # @return An array of [com.sun.jdi.Method] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/Method.html} 311 | def find_methods(clazz, method) 312 | ret = [] 313 | 314 | classes = @vm.allClasses 315 | classes.each do |c| 316 | if(c.name == clazz or c.name.end_with?(clazz)) 317 | c.allMethods.each do |m| 318 | if(m.name == method) 319 | ret.push(m) 320 | end 321 | end 322 | end 323 | end 324 | return(ret) 325 | end 326 | 327 | 328 | end 329 | end -------------------------------------------------------------------------------- /lib/android_debug/jpda/event.rb: -------------------------------------------------------------------------------- 1 | module AndroidDebug 2 | module Jpda 3 | class Event 4 | include AndroidDebug::Mixin::JavaPassthrough 5 | 6 | ENTRY = "entry" 7 | EXIT = "exit" 8 | BREAKPOINT = "breakpoint" 9 | VM_EXIT = "vm_destry" 10 | UNKNOWN = "unknown" 11 | 12 | attr_reader :java_event 13 | 14 | # Constructor for the wrapper of com.sun.jdi.event 15 | # @param event [com.sun.jdi.event.Event] 16 | def initialize(event) 17 | @java_object = @java_event = event 18 | end 19 | 20 | # @return [String] type of this event 21 | def type 22 | @java_event.java_kind_of?(com.sun.jdi.event.MethodEntryEvent) and return ENTRY 23 | @java_event.java_kind_of?(com.sun.jdi.event.MethodExitEvent) and return EXIT 24 | @java_event.java_kind_of?(com.sun.jdi.event.BreakpointEvent) and return BREAKPOINT 25 | @java_event.java_kind_of?(com.sun.jdi.event.VMDeathEvent) and return VM_EXIT 26 | @java_event.java_kind_of?(com.sun.jdi.event.VMDisconnectEvent) and return VM_EXIT 27 | return(UNKNOWN) 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/android_debug/jpda/frame.rb: -------------------------------------------------------------------------------- 1 | module AndroidDebug 2 | module Jpda 3 | class Frame 4 | include AndroidDebug::Mixin::JavaPassthrough 5 | 6 | attr_reader :java_frame, :java_location 7 | 8 | # Constructor 9 | # @param frame [com.sun.jdi.StackFrame] 10 | # @param location [com.sun.jdi.Location] 11 | def initialize(frame, location) 12 | @java_object = @java_frame = frame 13 | @java_location = location 14 | end 15 | 16 | # @return A string representation of the current frame's method name at the current frame 17 | def method_name 18 | return(@java_location.method.name) 19 | end 20 | 21 | # @return A string representation of the current frame's method signature 22 | def method_signature 23 | return(@java_location.method.to_s) 24 | end 25 | 26 | # @return A string representation of the return type for the current frame's method 27 | def method_return_type 28 | return(@java_location.method.returnTypeName) 29 | end 30 | 31 | # @return An array of [AndroidDebug::Jpda::LocalVariable] objects which represent the local variables in the current frame 32 | def variables 33 | ret = [] 34 | @java_frame.getValues(@java_frame.visibleVariables).each do |local_variable, value| 35 | ret.push(AndroidDebug::Jpda::LocalVariable.new(local_variable, value, @java_frame)) 36 | end 37 | 38 | return(ret) 39 | end 40 | 41 | # @return [AndroidDebug::Jpda::ObjectInFrame] which represents the "this" object at the current frame. 42 | def this 43 | return(AndroidDebug::Jpda::ObjectInFrame.new(@java_frame, @java_frame.thisObject)) 44 | end 45 | 46 | 47 | # @param variable [AndroidDebug::Jpda::Variable] 48 | # @param value Basic type or [com.sun.jdi.Value] 49 | def set_variable(variable, value) 50 | new_value = @java_frame.virtualMachine.mirrorOf(value) 51 | $DEBUG and puts("Variable: #{variable.java_local_variable.class}") 52 | $DEBUG and puts("Variable type: #{variable}") 53 | $DEBUG and puts("New Value: #{new_value.class}") 54 | $DEBUG and puts("New Value value: #{new_value.value}") 55 | 56 | ''' 57 | For some reason in this call either variable.java_value or new_value are of the wrong type 58 | TypeError: cannot convert instance of class org.jruby.java.proxies.ConcreteJavaProxy to interface com.sun.jdi.Field 59 | ''' 60 | @java_frame.setValue(variable.java_local_variable.to_java, new_value.to_java) 61 | end 62 | 63 | # Sets the value of a variable in the current frame 64 | # @param name [String] of the variable that will be modified 65 | # @param value basic type of the value of the variable you are changing 66 | def set_variable_by_name(name, value) 67 | new_value = @java_frame.virtualMachine.mirrorOf(value) 68 | variables.each do |var| 69 | if(var.name == name) 70 | set_variable(var, value) 71 | return 72 | end 73 | end 74 | end 75 | 76 | # Prints the variables that are in the current frame 77 | def print_variables 78 | variables.each do |var| 79 | puts(var) 80 | end 81 | end 82 | 83 | end 84 | end 85 | end -------------------------------------------------------------------------------- /lib/android_debug/jpda/jpda_helper.rb: -------------------------------------------------------------------------------- 1 | require 'java' 2 | java_import "com.sun.jdi.Bootstrap" 3 | 4 | module AndroidDebug 5 | module Jpda 6 | CONNECTOR_PROCESS = "com.sun.jdi.ProcessAttach" 7 | CONNECTOR_SOCKET = "com.sun.jdi.SocketAttach" 8 | CONNECTOR_COMMAND_LINE = "com.sun.jdi.CommandLineLaunch" 9 | 10 | # Static method to get a 'connector' object which allows you to connect to a remote Java debugging process 11 | # @return [com.sun.jdi.connect.Connector] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/connect/Connector.html} 12 | def self.get_connector(name) 13 | connectors = Bootstrap.virtualMachineManager().allConnectors() 14 | connectors.each do |c| 15 | if(c.name == name) 16 | return(c) 17 | end 18 | end 19 | end 20 | 21 | # @return A fully configured [com.sun.jdi.connect.AttachingConnector] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/connect/AttachingConnector.html} 22 | def self.get_socket_connector_and_args(host, port) 23 | ret = self.get_connector(CONNECTOR_SOCKET) 24 | arg = ret.defaultArguments() 25 | arg.get("hostname").setValue(host) 26 | arg.get("port").setValue(port) 27 | return([ret, arg]) 28 | end 29 | 30 | 31 | end 32 | end -------------------------------------------------------------------------------- /lib/android_debug/jpda/local_variable.rb: -------------------------------------------------------------------------------- 1 | require 'java' 2 | 3 | module AndroidDebug 4 | module Jpda 5 | class LocalVariable 6 | include AndroidDebug::Mixin::JavaPassthrough 7 | include AndroidDebug::Mixin::Invokable 8 | 9 | attr_reader :java_frame # Defined in Invokable 10 | attr_reader :java_invokable_object # Defined in Invokable 11 | 12 | attr_accessor :name 13 | attr_reader :java_local_variable, :java_value, :value, :type 14 | 15 | # Constructor 16 | # @param local_variable [com.sun.jdi.LocalVariable] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/LocalVariable.html} 17 | # @param value [com.sun.jdi.Value] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/Value.html} 18 | # @param frame [com.sun.jdi.StackFrame] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/StackFrame.html} 19 | def initialize(local_variable, value, frame) 20 | @java_frame = frame 21 | @java_object = @java_local_variable = local_variable 22 | @java_value = @java_invokable_object = value 23 | 24 | value.nil? ? @type = "UNKNOWN_TYPE" : @type = value.type.name 25 | local_variable.nil? ? @name = "UNKNOWN_NAME" : @name = local_variable.name 26 | 27 | 28 | #@value = @java_value.invokeMethod(1, 2, 3, 4) 29 | @value = @java_value.to_s 30 | end 31 | 32 | # @return A string representation of the object 33 | def to_s 34 | return("#{@type} #{@name} = #{@value}") 35 | end 36 | 37 | # @return A string representation of the object 38 | def to_str 39 | return("#{@type} #{@name} = #{@value}") 40 | end 41 | 42 | # @return A detailed string representation of the object 43 | def inspect 44 | print("#{@type} #{@name} = ") 45 | if(@java_value.java_kind_of?(com.sun.jdi.ArrayReference)) 46 | inspect_array_reference(@java_value) 47 | else 48 | puts @value 49 | end 50 | end 51 | 52 | # @return A detailed string representation of an array reference 53 | def inspect_array_reference(array_reference, tabs=1) 54 | puts("[") 55 | array_reference.getValues.each_with_index do |val, i| 56 | if(val.java_kind_of?(com.sun.jdi.ArrayReference)) 57 | puts("[") 58 | inspect_array_reference(val, tabs+1) 59 | else 60 | print("\t"*tabs) 61 | puts(val) 62 | end 63 | end 64 | puts("]") 65 | end 66 | 67 | # @param obj Object to compare, either [AndroidDebug::Jpda::LocalVariable] or [com.sun.jdi.LocalVariable] 68 | def ==(obj) 69 | obj.instance_of?(AndroidDebug::Jpda::LocalVariable) and return(@java_local_variable.equals(obj.java_local_variable)) 70 | obj.java_kind_of?(com.sun.jdi.LocalVariable) and return(@java_local_variable.equals(obj)) 71 | return(false) 72 | end 73 | end 74 | end 75 | end -------------------------------------------------------------------------------- /lib/android_debug/jpda/object_in_frame.rb: -------------------------------------------------------------------------------- 1 | module AndroidDebug 2 | module Jpda 3 | class ObjectInFrame 4 | include AndroidDebug::Mixin::JavaPassthrough 5 | include AndroidDebug::Mixin::Invokable 6 | 7 | attr_reader :java_object_reference 8 | attr_reader :java_frame # Defined in Invokable 9 | attr_reader :java_invokable_object # Defined in Invokable 10 | 11 | # Constructor 12 | # @param frame [com.sun.jdi.StackFrame] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/StackFrame.html} 13 | # @param object_reference [com.sun.jdi.ObjectReference] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/ObjectReference.html} 14 | def initialize(frame, object_reference) 15 | @java_frame = frame 16 | @java_object = @java_invokable_object = @java_object_reference = object_reference 17 | end 18 | 19 | # @return A string representation of the object 20 | def to_s 21 | return(@java_object_reference.to_s) 22 | end 23 | 24 | # @return A string representation of the object's class name 25 | def class_name 26 | return(@java_object_reference.to_s) 27 | end 28 | 29 | # @return A string representation of the unique ID of the object 30 | def id 31 | return(@java_object_reference.uniqueID) 32 | end 33 | 34 | # @param obj Object to compare, either [AndroidDebug::Jpda::ObjectInFrame] or [com.sun.jdi.ObjectReference] 35 | def ==(obj) 36 | obj.instance_of(AndroidDebug::Jpda::ObjectInFrame) and return(obj.java_object_reference.uniqueID == @java_object_reference.uniqueID) 37 | obj.java_kind_of?(com.sun.jdi.ObjectReference) and return(obj.uniqueID == @java_object_reference.uniqueID) 38 | return(false) 39 | end 40 | 41 | end 42 | 43 | end 44 | end -------------------------------------------------------------------------------- /lib/android_debug/mixin/invokable.rb: -------------------------------------------------------------------------------- 1 | module AndroidDebug 2 | module Mixin 3 | module Invokable 4 | # This is a mixin that provides the ability to invoke methods from objects. The 5 | # object must be of a type that has the 'invokeMethod' method. To specify the 6 | # object which the invoking will be performed, you just set the @java_invokable_object 7 | # to the specific object. 8 | 9 | attr_reader :java_invokable_object, :java_frame 10 | 11 | # Invoke a method on the @java_invokable_object. 12 | # @param method either a [String] or a [com.sun.jdi.Method] {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/Method.html} 13 | # @param args [Array] of basic types that will be the argumets of the method being called 14 | def invoke_method(method, args={}) 15 | $DEBUG and puts("Invoking method #{method} with args: #{args}") 16 | if(@java_frame.nil?) 17 | throw("The current event does not have an associated thread, cant invoke a method.") 18 | end 19 | 20 | java_args = ArrayList.new 21 | args.each do |a| 22 | java_args.add(@java_frame.virtualMachine.mirrorOf(a)) 23 | end 24 | 25 | if(method.is_a?(String)) 26 | method = get_method(method) 27 | end 28 | 29 | 30 | $DEBUG and puts("Calling method #{method}") 31 | $DEBUG and puts("Thread: #{@java_frame.thread}") 32 | $DEBUG and puts("Method: #{method}") 33 | $DEBUG and puts("Args: #{java_args}") 34 | @java_invokable_object.invokeMethod(@java_frame.thread, method, java_args, com.sun.jdi.ObjectReference.INVOKE_SINGLE_THREADED) 35 | end 36 | 37 | 38 | 39 | # @return An array of methods {http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/Method.html} 40 | def methods 41 | # In java, primitive values dont have methods, this ensures we can call 'referenceType' 42 | !@java_invokable_object.java_kind_of?(com.sun.jdi.ObjectReference) and return([]) 43 | 44 | # This happens for certain unknown LocalVariable object types 45 | @java_invokable_object.nil? and return([]) 46 | 47 | return(@java_invokable_object.referenceType.methods) 48 | end 49 | 50 | # @return An array of method names as strings 51 | def method_names 52 | ret = [] 53 | methods.each do |m| 54 | ret.push(m.name) 55 | end 56 | return(ret) 57 | end 58 | 59 | # @param name [String] of the method you are looking to find 60 | # @param index of the name, default 0. This takes care of when Java has overloaded methods. They will all have the same name. 61 | # @return A java Method object for a specific method of the @java_invokable_object 62 | def get_method(name, index=0) 63 | ret = [] 64 | methods.each do |m| 65 | if(m.name == name) 66 | ret.push(m) 67 | end 68 | end 69 | return(ret[index]) 70 | end 71 | 72 | # Handler when attempting to call a method of an object that has this mixin. It will attempt to call the Java method of that object. 73 | # For example, if you have some ObjectReference of a Java String. You can directly call ObjectReference.toString() and it will pass 74 | # through to attempt to call that native Java method. 75 | def method_missing(m, *args, &block) 76 | # Attempt to call a method on the object directly 77 | if(method_names.index(m.to_s)) 78 | $DEBUG and puts("Attempting to invoke_method '#{m}'") 79 | invoke_method(get_method(m.to_s), args) 80 | else 81 | super.method_missing(m, *args, &block) 82 | end 83 | end 84 | 85 | # @return A detailed string representation of the object 86 | def inspect 87 | puts(@java_invokable_object.to_s) 88 | puts("\tMethods:") 89 | methods.each do |m| 90 | puts("\t\t#{m.to_s}") 91 | end 92 | end 93 | end 94 | end 95 | end -------------------------------------------------------------------------------- /lib/android_debug/mixin/java_passthrough.rb: -------------------------------------------------------------------------------- 1 | module AndroidDebug 2 | module Mixin 3 | module JavaPassthrough 4 | attr_reader :java_object 5 | 6 | # Most of the classes we implement are just Java class wrappers. This will attempt to call the local Java method of the object we are wrapping. For example, AndroidDebug::Jpda::Event wraps the com.sun.jdi.event.Event object. It is possible to call any method of that Java object, simply by using the dot notation. Event.requests() 7 | def method_missing(m, *args, &block) 8 | @java_object.nil? and throw("The @java_object is not set for this class") 9 | !@java_object.respond_to?(m) and throw("The Java object does not respond to methd '#{m}' (#{@java_object.class})") 10 | $DEBUG and puts("Calling java native method: #{m}(#{args})") 11 | @java_object.send(m, *args) 12 | end 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /template/change_variable.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/android_debug.rb' 2 | 3 | $DEBUG = true 4 | 5 | dbg = AndroidDebug.launch_and_attach_native("com.android.browser", "com.android.browser.BrowserActivity") 6 | 7 | # Set the breakpoint method 8 | dbg.add_class_entry_breakpoint("android.content.Intent") 9 | 10 | # Find the variable you want to call the method on 11 | dbg.on_break do |event| 12 | 13 | # If were in the correct method 14 | if(dbg.frame.method_name == "getParcelableExtra") 15 | puts(dbg.frame.method_signature) 16 | 17 | =begin 18 | # Find the variable 19 | dbg.frame.variables.each do |var| 20 | 21 | # Make sure its the variable we want to change 22 | if(var.name = "name") 23 | 24 | # Change the value 25 | dbg.frame.set_variable(var, "networkInfo1") 26 | end 27 | end 28 | =end 29 | 30 | # Or we can just use the helper method 31 | dbg.frame.set_variable_by_name("name", "networkInfo1") 32 | 33 | puts 34 | end 35 | dbg.resume 36 | end 37 | dbg.go 38 | -------------------------------------------------------------------------------- /template/invoke_method.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/android_debug.rb' 2 | 3 | counter = 0 4 | 5 | # Launch the activity and attach the debugger 6 | dbg = AndroidDebug.launch_and_attach_native("com.android.browser", "com.android.browser.BrowserActivity") 7 | 8 | # Set the breakpoint method 9 | dbg.add_class_entry_breakpoint("android.content.Intent") 10 | 11 | # Find the variable you want to call the method on 12 | dbg.on_break do |event| 13 | 14 | # You can only call ONE method on a breakpoint (at the moment) 15 | # because the method may cause a loop depending on the other 16 | # breakpoints. The internal debugger state does not properly 17 | # handle updating the 'this' variable after calling 18 | 19 | if(counter % 2 == 0) 20 | # Invoke a method by fidning it and calling 'invoke_method' 21 | this_method = dbg.this.get_method("toString") 22 | response = dbg.this.invoke_method(this_method) 23 | puts("Invoke Method Response(explicit): #{response}") 24 | else 25 | # Invoke a method by just calling it on the 'this' object 26 | response = dbg.this.toString 27 | puts("Invoke Method Response(implicit): #{response}") 28 | end 29 | counter = counter + 1 30 | dbg.resume 31 | end 32 | 33 | # Start the process 34 | dbg.go 35 | -------------------------------------------------------------------------------- /template/single_step.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/android_debug.rb' 2 | 3 | def print_variables(frame) 4 | frame.variables.each do |var| 5 | puts(var) 6 | end 7 | end 8 | 9 | counter = 0 10 | 11 | dbg = AndroidDebug.launch_and_attach_native("com.android.browser", "com.android.browser.BrowserActivity") 12 | 13 | # Set the breakpoint method 14 | dbg.add_class_entry_breakpoint("android.content.Intent") 15 | 16 | # Find the variable you want to call the method on 17 | dbg.on_break do |event| 18 | puts("Counter: #{counter}") 19 | print_variables(dbg.frame) 20 | dbg.step_into 21 | print_variables(dbg.frame) 22 | dbg.step_into 23 | print_variables(dbg.frame) 24 | puts 25 | dbg.resume 26 | counter = counter + 1 27 | end 28 | 29 | dbg.go -------------------------------------------------------------------------------- /test/hello_world.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/android_debug.rb' 2 | 3 | $DEBUG = true 4 | 5 | dbg = AndroidDebug.launch_and_attach("com.wuntee.dummyandroidproject", "com.wuntee.dummyandroidproject.DummyAndroidProjectActivity") 6 | #dbg = AndroidDebug.launch_and_attach("com.wuntee.dummyandroidproject", "com.wuntee.dummyandroidproject.DummyAndroidActivity") 7 | dbg.on_break do 8 | 9 | puts("Frame variables:") 10 | dbg.frame.variables.each do |var| 11 | puts("\t#{var}") 12 | #if(var.name == "test2") 13 | # dbg.frame.set_variable_by_name("test2", "changin variable") 14 | #end 15 | end 16 | puts("This object:") 17 | puts(dbg.this.inspect) 18 | if(dbg.this.method_names.index("log_int")) 19 | puts("CALLING LOG_NOARG") 20 | dbg.this.log_int(1234) 21 | end 22 | 23 | #if(event.method == "log") 24 | # puts("Attempting to set log variable") 25 | # dbg.set_variable_value(dbg.current_variables[0], "i changed the value via the debugger") 26 | #end 27 | dbg.resume 28 | end 29 | dbg.add_class_entry_breakpoint("com.wuntee.*") 30 | dbg.go 31 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'test/unit' 11 | require 'shoulda' 12 | 13 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 14 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 15 | require 'android_debug' 16 | 17 | class Test::Unit::TestCase 18 | end 19 | -------------------------------------------------------------------------------- /test/log_intent.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/android_debug.rb' 2 | 3 | $DEBUG = false 4 | 5 | dbg = AndroidDebug.launch_and_attach("com.android.browser", "com.android.browser.BrowserActivity") 6 | #dbg = AndroidDebug.launch_and_attach("com.wuntee.dummyandroidproject", "com.wuntee.dummyandroidproject.DummyAndroidActivity") 7 | 8 | vars = {} 9 | 10 | dbg.on_break do |event| 11 | !vars[dbg.this.to_s] and vars[dbg.this.to_s] = [] 12 | vars[dbg.this.to_s].push("#{dbg.frame.method_name}(#{dbg.frame.variables.join(',')})") 13 | 14 | puts("New method called on object: #{dbg.this.to_s}") 15 | vars[dbg.this.to_s].each do |method| 16 | puts("\t#{method}") 17 | end 18 | 19 | puts 20 | 21 | dbg.resume 22 | end 23 | dbg.add_class_entry_breakpoint("android.content.Intent") 24 | #dbg.add_class_exit_breakpoint("android.content.Intent") 25 | dbg.go 26 | -------------------------------------------------------------------------------- /test/sample.rb: -------------------------------------------------------------------------------- 1 | require 'android_debug' 2 | dbg = AndroidDebug.launch_and_attach("com.android.browser", "com.android.browser.BrowserActivity") 3 | dbg.on_break do 4 | if(dbg.frame.method_name == "registerReceiver") 5 | puts("#{dbg.frame.method_signature}") 6 | puts("Frame variables:") 7 | dbg.frame.variables.each do |var| 8 | puts("\t#{var}") 9 | end 10 | end 11 | dbg.resume 12 | end 13 | dbg.add_class_entry_breakpoint("android.content.Context") 14 | dbg.go 15 | -------------------------------------------------------------------------------- /test/snapchat.rb: -------------------------------------------------------------------------------- 1 | require 'android_debug' 2 | dbg = AndroidDebug.launch_and_attach("com.snapchat.android", "com.snapchat.android.LandingPageActivity") 3 | dbg.on_break do 4 | ''' 5 | if(dbg.frame.method_name == "doFinanl") 6 | puts("#{dbg.frame.method_signature}") 7 | puts("Frame variables:") 8 | dbg.frame.variables.each do |var| 9 | puts("\t#{var}") 10 | end 11 | end 12 | ''' 13 | puts(dbg.frame.method_name) 14 | dbg.resume 15 | end 16 | dbg.add_class_entry_breakpoint("javax.crypto.Cipher") 17 | dbg.go 18 | -------------------------------------------------------------------------------- /test/test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/android_debug.rb' 2 | 3 | $DEBUG = true 4 | 5 | dbg = AndroidDebug.launch_and_attach_native("com.android.browser", "com.android.browser.BrowserActivity") 6 | #dbg = AndroidDebug.launch_and_attach("com.wuntee.dummyandroidproject", "com.wuntee.dummyandroidproject.DummyAndroidActivity") 7 | 8 | vars = {} 9 | 10 | dbg.on_break do |event| 11 | if(dbg.frame.variables.size >= 2) 12 | puts("Number of vars: #{dbg.frame.variables.size}") 13 | dbg.frame.variables.each_with_index do |var, i| 14 | puts("-var[#{i}] #{var}") 15 | if(var.get_method("toString")) 16 | begin 17 | puts("--RESPONSE: #{var.invoke_method('toString')}") 18 | rescue Exception => e 19 | puts("--Could not invoke: #{e} ") 20 | end 21 | end 22 | end 23 | end 24 | 25 | =begin 26 | puts(var.java_value.class) 27 | var.java_value.methods.each do |method| 28 | if(method.to_s == "toString") 29 | args = ArrayList.new 30 | puts("- Value: #{var.java_value.class}") 31 | puts("- Thread: #{event.java_object.thread.class}") 32 | puts("- Method: #{method.class}") 33 | puts("- Args: #{args.class}") 34 | begin 35 | ret = var.java_value.invokeMethod(event.java_object.thread, method, args, 2) 36 | puts("------INVOKED: #{var.java_variable}.toString() = #{ret}") 37 | rescue Exception => e 38 | puts("- Could not invoke method: #{e}") 39 | end 40 | end 41 | end 42 | end 43 | =end 44 | dbg.resume 45 | end 46 | dbg.add_class_entry_breakpoint("android.content.Intent") 47 | dbg.go 48 | -------------------------------------------------------------------------------- /test/test_adb_native.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require_relative "../lib/android_debug/adb_native.rb" 3 | 4 | class TestAdbNative < Test::Unit::TestCase 5 | adb = AndroidDebug::AdbNative.new 6 | 7 | devices = adb.devices 8 | puts("Devices: #{devices}") 9 | 10 | if(devices == "") 11 | puts("WARNING: Could not test AdbNative. No ADB attached.") 12 | else 13 | should "Properly run shell commands properly" do 14 | pwd = adb.shell("cd / && pwd") 15 | puts("pwd: #{pwd}") 16 | assert_equal(pwd, "/") 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /test/test_android_debug.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestAndroidDebug < Test::Unit::TestCase 4 | should "probably rename this file and start testing for real" do 5 | flunk "hey buddy, you should probably rename this file and start testing for real" 6 | end 7 | end 8 | --------------------------------------------------------------------------------