├── devices └── .placeholder ├── Makefile ├── .gitignore ├── lib ├── plural.rb ├── config.rb ├── devices │ └── pool.rb.sample ├── devices.rb └── madb.rb ├── mcmd ├── mbb ├── display.rb ├── shell.rb ├── init.rb ├── scan.rb ├── mpull ├── mdo ├── reconfig.rb ├── README.md └── LICENSE /devices/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | ./init.rb && ./reconfig.rb 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/devices/pool.rb 2 | lib/devices/connected.rb 3 | -------------------------------------------------------------------------------- /lib/plural.rb: -------------------------------------------------------------------------------- 1 | def plural(num) 2 | return "" if num == 1 3 | return "s" 4 | end 5 | -------------------------------------------------------------------------------- /lib/config.rb: -------------------------------------------------------------------------------- 1 | # 2 | # ADB server configuration (for using multiple servers) 3 | # 4 | $adb_ports = [ 5037 ] 5 | 6 | -------------------------------------------------------------------------------- /mcmd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # mcmd - execute a command on the selected device(s) 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | 18 | require 'devices' 19 | load_connected() 20 | 21 | require 'madb' 22 | 23 | 24 | # process args 25 | if not parse_global_options() or ARGV.length < 1 26 | $stderr.puts "usage: mcmd [-1v] -d " 27 | exit(1) 28 | end 29 | 30 | 31 | # run the commands 32 | multi_adb(['shell']) 33 | -------------------------------------------------------------------------------- /mbb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # mbb - execute a command on the selected device(s) using busybox 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | 18 | require 'devices' 19 | load_connected(false) 20 | 21 | require 'madb' 22 | 23 | 24 | # process args 25 | if not parse_global_options() or ARGV.length < 1 26 | $stderr.puts "usage: mbb [-1v] -d " 27 | exit(1) 28 | end 29 | 30 | 31 | # run the commands 32 | multi_adb(['shell', '/data/local/tmp/busybox']) 33 | -------------------------------------------------------------------------------- /lib/devices/pool.rb.sample: -------------------------------------------------------------------------------- 1 | # 2 | # Android Cluster Toolkit - pool.rb 3 | # 4 | # (c) 2012-2015 Joshua J. Drake (jduck) 5 | # 6 | 7 | $devices[:pool] = [ 8 | =begin 9 | # 10 | # The following Hash entry describes one device in the cluster. 11 | # 12 | # Required fields: 13 | # :name -- a human friendly name, chosen by you 14 | # :serial -- the device's serial number (from 'adb devices') 15 | # 16 | # Optional fields: 17 | # :disabled -- disable the device from use 18 | # 19 | # Use the ./scan.rb script to generate a basis for new entries. 20 | # 21 | # Use ./reconfig.rb to generate devices/connected.rb based on which devices 22 | # are actually connected. 23 | # 24 | { 25 | :name => 'g1', # htc g1 26 | :serial => 'HT333GZ31337', 27 | }, 28 | =end 29 | ] 30 | 31 | -------------------------------------------------------------------------------- /display.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # display.rb - list which android devices are plugged in 7 | # (in both $devices[:connected] and adb_devices) 8 | # 9 | # (c) 2012-2015 Joshua J. Drake (jduck) 10 | # 11 | 12 | bfn = __FILE__ 13 | while File.symlink?(bfn) 14 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 15 | end 16 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 17 | 18 | 19 | # load connected devices 20 | require 'devices' 21 | load_connected(true) 22 | 23 | # get a list of devices via 'adb devices' 24 | require 'madb' 25 | adb_devices = adb_scan(true) 26 | 27 | 28 | # show devices in both sets 29 | $devices[:connected].each { |dev| 30 | adb_devices.each { |port,serial| 31 | if dev[:serial] == serial 32 | puts " #{dev[:name]} / #{dev[:serial]}" 33 | break 34 | end 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shell.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # shell.rb - spawn a shell for the specified device 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | 18 | require 'devices' 19 | 20 | 21 | devid = parse_device_arg("shell") 22 | load_connected() 23 | dev = get_one_device(devid) 24 | 25 | 26 | # build the environment vars to set... 27 | envs = get_device_envs(dev) 28 | envstr = get_device_env_str(envs) 29 | 30 | # show! 31 | puts "[*] starting shell for #{dev[:name]} (#{envstr}) ..." 32 | 33 | # add one final var and apply them. 34 | envs.merge!("debian_chroot" => dev[:name]) 35 | envs.each { |k,v| ENV[k] = v } 36 | 37 | # spawn the shell! 38 | system(ENV['SHELL']) #, '--norc') 39 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # init.rb - generate devices/pool.rb based on 'adb devices' 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | 18 | # get a list of devices via 'adb devices' 19 | require 'madb' 20 | adb_devices = adb_scan(true) 21 | 22 | 23 | device_pool = File.join(File.dirname(bfn), 'lib', 'devices', 'pool.rb') 24 | if File.exists? device_pool 25 | $stderr.puts "[!] devices/pool.rb exists! rm it to start over" 26 | exit(1) 27 | end 28 | 29 | 30 | template = nil 31 | File.open("#{device_pool}.sample", 'rb') { |f| 32 | template = f.read 33 | } 34 | 35 | File.open(device_pool, 'wb') { |f| 36 | f.puts template.split(/^=end$/).first + "=end" 37 | adb_devices.each { |port,serial| 38 | f.puts %Q| 39 | { 40 | :name => 'name', # description 41 | :serial => '#{serial}', 42 | }, 43 | | 44 | } 45 | f.puts "]" 46 | } 47 | -------------------------------------------------------------------------------- /scan.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # scan.rb - scan for new android devices (not in $devices) 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | 18 | # load connected set of devices 19 | require 'devices' 20 | load_connected(true) 21 | 22 | # get a list of devices via 'adb devices' 23 | require 'madb' 24 | adb_devices = adb_scan(true) 25 | 26 | 27 | # intersect this with $devices 28 | new_devices = [] 29 | adb_devices.each { |port,serial| 30 | found = false 31 | $devices[:connected].each { |dev| 32 | if dev[:serial] == serial 33 | found = true 34 | break 35 | end 36 | } 37 | 38 | new_devices << [ port, serial ] if not found 39 | } 40 | 41 | $stderr.puts "[*] Found #{new_devices.length} new device#{plural(new_devices.length)}!" 42 | 43 | 44 | # print any new ones in the format used to add to devices-orig.rb 45 | new_devices.each { |port,serial| 46 | puts %Q| 47 | { 48 | :name => 'name', # description 49 | :serial => '#{serial}', 50 | }, 51 | | 52 | } 53 | -------------------------------------------------------------------------------- /mpull: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # mpull - pull a path from the selected device(s) 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | 18 | require 'devices' 19 | load_connected() 20 | 21 | require 'madb' 22 | 23 | 24 | # process args 25 | if not parse_global_options() or ARGV.length < 1 26 | $stderr.puts "usage: mpull [-1v] -d " 27 | exit(1) 28 | end 29 | 30 | path = ARGV.shift 31 | 32 | 33 | # 34 | # NOTE: this can't use multi_adb because it has a device-dependant argument 35 | # 36 | $devices[:connected].each { |dev| 37 | 38 | next if not is_selected(dev) 39 | 40 | if dev[:disabled] 41 | puts "[!] Warning: The selected device is marked disabled. It may not be present." 42 | end 43 | 44 | print_col_prefix(dev) 45 | 46 | cmd = [ 'adb', '-s', dev[:serial], 'pull' ] 47 | cmd << path 48 | cmd << File.join([ 'devices', dev[:name], path ]) 49 | #puts cmd 50 | system(*cmd) 51 | 52 | puts "" if not $one_line 53 | } 54 | -------------------------------------------------------------------------------- /mdo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # mdo - execute an ADB command on the selected device(s) 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | 18 | require 'devices' 19 | load_connected() 20 | 21 | require 'madb' 22 | 23 | 24 | # process args 25 | if not parse_global_options() or ARGV.length < 1 26 | $stderr.puts "usage: mdo [-1v] -d " 27 | exit(1) 28 | end 29 | 30 | 31 | # validate the adb command line 32 | cmd = ARGV.first 33 | if cmd == 'wait-for-device' 34 | ARGV.shift 35 | cmd = ARGV.first 36 | end 37 | case cmd 38 | when 'bugreport', 'get-state', 'get-serialno', 'get-devpath', 'reboot', 'reboot-bootloader', 'root', 'remount', 'jdwp' 39 | # allow for all usage patterns 40 | 41 | when 'shell' 42 | if ARGV.length < 2 and $one_line 43 | $stderr.puts "[!] the #{cmd} command requires an argument when used in single-line mode" 44 | exit(1) 45 | end 46 | 47 | when 'push', 'install' 48 | if ARGV.length < 2 49 | $stderr.puts "[!] the #{cmd} command requires an argument" 50 | exit(1) 51 | end 52 | 53 | when 'pull' 54 | if $do_all or $selected_devices.length > 1 55 | $stderr.puts "[!] use ./mpull to pull from multiple devices" 56 | exit(1) 57 | elsif ARGV.length < 2 58 | $stderr.puts "[!] the #{cmd} command requires an argument" 59 | exit(1) 60 | end 61 | 62 | when 'logcat' 63 | if $do_all or $selected_devices.length > 1 64 | $stderr.puts "[!] specify a single device for logcat" 65 | exit(1) 66 | end 67 | 68 | if $one_line 69 | $stderr.puts "[!] single-line mode is not compatible with logcat" 70 | end 71 | 72 | else 73 | $stderr.puts "[!] unsupported command: #{cmd}" 74 | exit(1) 75 | end 76 | 77 | 78 | # run the commands 79 | multi_adb() 80 | -------------------------------------------------------------------------------- /lib/devices.rb: -------------------------------------------------------------------------------- 1 | require 'plural' 2 | 3 | $devices = { 4 | :pool => [], 5 | :connected => [], 6 | } 7 | 8 | 9 | 10 | def load_connected(verbose = false) 11 | begin 12 | require 'devices/connected' 13 | rescue LoadError 14 | $stderr.puts "[!] WARNING: Unable to load connected devices! Did you run reconfig.rb?" 15 | end 16 | 17 | if verbose 18 | devs = $devices[:connected] 19 | $stderr.puts "[*] Loaded #{devs.length} device#{plural(devs)} from 'devices/connected'" 20 | end 21 | end 22 | 23 | 24 | def load_pool(verbose = false) 25 | begin 26 | require 'devices/pool' 27 | rescue LoadError 28 | $stderr.puts "[!] Unable to load the device pool! Did you run init.rb or scan.rb?" 29 | end 30 | 31 | if verbose 32 | devs = $devices[:pool] 33 | $stderr.puts "[*] Loaded #{devs.length} device#{plural(devs)} from 'devices/pool'" 34 | end 35 | end 36 | 37 | 38 | def parse_device_arg(cmd) 39 | devid = nil 40 | if ARGV.length > 0 41 | devid = ARGV.shift 42 | end 43 | 44 | if devid.nil? 45 | $stderr.puts "usage: #{cmd} " 46 | exit(1) 47 | end 48 | 49 | return devid 50 | end 51 | 52 | 53 | def get_one_device(devid) 54 | if $devices[:connected].length < 1 55 | load_connected() 56 | end 57 | 58 | usedev = nil 59 | $devices[:connected].each { |dev| 60 | if dev[:name] == devid or dev[:serial] == devid 61 | usedev = dev 62 | break 63 | end 64 | } 65 | 66 | if usedev.nil? 67 | $stderr.puts "[!] unable to find device: #{devid}" 68 | exit(1) 69 | end 70 | 71 | if usedev[:disabled] 72 | puts "[!] Warning: The selected device is marked disabled. It may not be present." 73 | end 74 | 75 | return usedev 76 | end 77 | 78 | 79 | # build the environment vars to set... 80 | def get_device_envs(dev) 81 | envs = { "ANDROID_SERIAL" => dev[:serial] } 82 | envs.merge!("ANDROID_ADB_SERVER_PORT" => dev[:port].to_s) 83 | return envs 84 | end 85 | 86 | # build a string to display for the device env vars 87 | def get_device_env_str(envs) 88 | envstr = '' 89 | envs.each { |k,v| 90 | envstr << ' ' if envstr.length > 0 91 | envstr << "#{k}=\"#{v}\"" 92 | } 93 | return envstr 94 | end 95 | -------------------------------------------------------------------------------- /reconfig.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Android Cluster Toolkit 5 | # 6 | # reconfig.rb - generate devices.rb based on 'adb devices' and 'devices-orig.rb' 7 | # 8 | # (c) 2012-2015 Joshua J. Drake (jduck) 9 | # 10 | 11 | bfn = __FILE__ 12 | while File.symlink?(bfn) 13 | bfn = File.expand_path(File.readlink(bfn), File.dirname(bfn)) 14 | end 15 | $:.unshift(File.join(File.dirname(bfn), 'lib')) 16 | 17 | require 'madb' 18 | 19 | $verbose = true if ARGV.pop == "-v" 20 | 21 | # load connected set of devices 22 | require 'devices' 23 | load_connected(true) 24 | $old_devices = $devices[:connected].dup 25 | 26 | # load total device pool 27 | load_pool(true) 28 | 29 | # get a list of devices via 'adb devices' 30 | adb_devices = adb_scan(true) 31 | 32 | 33 | def find_device(pool, serial) 34 | pool.each { |dev| 35 | if dev[:serial] == serial 36 | return dev 37 | end 38 | } 39 | nil 40 | end 41 | 42 | 43 | # determine new set and missing devices 44 | if $verbose 45 | # The following will evolve into devices that are no longer connected, 46 | # but are in the pool. 47 | missing = $devices[:pool].dup 48 | end 49 | new_devices = [] # new available devices 50 | nconn = [] # newly connected 51 | dconn = $old_devices.dup # recently disconnected 52 | adb_devices.each { |port,serial| 53 | # find this device in the pool of all supported devices 54 | dev = find_device($devices[:pool], serial) 55 | if dev 56 | # got it. 57 | dev.merge!(:port => port) 58 | new_devices << dev 59 | 60 | # it's not missing. 61 | missing.delete dev if $verbose 62 | end 63 | 64 | # see if this one was present before... 65 | pdev = find_device($old_devices, serial) 66 | 67 | # if so, it didn't disconnect :) 68 | if pdev 69 | dconn.delete pdev 70 | elsif dev 71 | # if we have a device, it's new. 72 | nconn << dev 73 | end 74 | } 75 | 76 | 77 | # show status. which matched, what's new, what disappeared... 78 | $stderr.puts "[*] Matched #{new_devices.length} device#{plural(new_devices.length)}!" 79 | $stderr.puts " #{nconn.length} device#{plural(nconn.length)} added" 80 | nconn.each { |dev| 81 | $stderr.puts " #{dev[:name]} (#{dev[:serial]})" 82 | } 83 | $stderr.puts " #{dconn.length} device#{plural(dconn.length)} removed" 84 | dconn.each { |dev| 85 | $stderr.puts " #{dev[:name]} (#{dev[:serial]})" 86 | } 87 | 88 | 89 | # show messing devices (verbose only) 90 | if $verbose 91 | $stderr.puts "[*] Missing #{missing.length} device#{plural(missing.length)}:" 92 | missing.each { |dev| 93 | $stderr.puts " #{dev[:name]} (#{dev[:serial]})" 94 | } 95 | end 96 | 97 | 98 | # produce a new devices.rb with the currently connected devices only 99 | devices = File.join(File.dirname(bfn), 'lib', 'devices', 'connected.rb') 100 | 101 | File.open(devices, "wb") { |f| 102 | f.puts "$devices[:connected] = [" 103 | new_devices.each { |dev| 104 | name = "'#{dev[:name]}'," 105 | serial = "'#{dev[:serial]}'," 106 | line = " { :name => %-16s :serial => %-24s :port => %u" % [name, serial, dev[:port]] 107 | if dev[:codename] 108 | line << ":codename => #{dev[:codename].inspect}" 109 | end 110 | line << " }," 111 | 112 | f.puts line 113 | } 114 | f.puts "]" 115 | } 116 | -------------------------------------------------------------------------------- /lib/madb.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Android Cluster Toolkit - madb.rb 3 | # 4 | # shared implementation of multi-adb scripts 5 | # 6 | # (c) 2012-2015 Joshua J. Drake (jduck) 7 | # 8 | 9 | require 'open3' 10 | require 'getoptlong' 11 | 12 | require 'plural' 13 | require 'config' 14 | 15 | # 16 | # set globals based on the command line options 17 | # 18 | def parse_global_options() 19 | opts = GetoptLong.new( 20 | [ '--one-line', '-1', GetoptLong::NO_ARGUMENT ], 21 | [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], 22 | [ '--device', '-d', GetoptLong::REQUIRED_ARGUMENT ] 23 | ) 24 | 25 | opts.each { |opt, arg| 26 | case opt 27 | when '--one-line' 28 | $one_line = true 29 | when '--verbose' 30 | $verbose = true 31 | when '--device' 32 | return false if arg == "--" 33 | 34 | $selected_devices = arg.split(',') 35 | $do_all = false 36 | if $selected_devices.length == 0 37 | $do_all = true 38 | elsif $selected_devices.first == "." 39 | $do_all = true 40 | discard = $selected_devices.shift 41 | end 42 | 43 | opts.terminate() 44 | else 45 | return false 46 | end 47 | } 48 | 49 | if $one_line 50 | $widths = get_col_widths() 51 | end 52 | 53 | return true 54 | end 55 | 56 | 57 | # 58 | # return the selection (regex) that matches the provided device 59 | # 60 | def which_matches(sel, dev) 61 | # regex match :) 62 | sel.each { |str| 63 | return str if dev[:name] =~ /^#{str}$/ 64 | } 65 | return nil 66 | end 67 | 68 | # 69 | # return if the specified device is selected 70 | # 71 | def is_selected(dev) 72 | return true if $do_all 73 | 74 | sel = $selected_devices 75 | return false if not sel or sel.length == 0 76 | 77 | if (sel.include? dev[:name] or sel.include? dev[:serial]) 78 | return true 79 | end 80 | 81 | # regex match :) 82 | str = which_matches(sel, dev) 83 | return true if not str.nil? 84 | 85 | return false 86 | end 87 | 88 | 89 | # 90 | # return widths for columns to align columnar output 91 | # 92 | def get_col_widths() 93 | w_name = 0 94 | w_serial = 0 95 | 96 | $devices[:connected].each { |dev| 97 | next if dev[:disabled] 98 | next if not is_selected(dev) 99 | 100 | l_name = dev[:name].length 101 | w_name = l_name if l_name > w_name 102 | 103 | if $verbose 104 | l_serial = dev[:serial].length 105 | w_serial = l_serial if l_serial > w_serial 106 | end 107 | } 108 | 109 | if $verbose 110 | return [ w_name, w_serial + 1 ] 111 | end 112 | return w_name 113 | end 114 | 115 | 116 | # 117 | # print the line prefix for single device 118 | # 119 | def print_col_prefix(dev) 120 | if $verbose 121 | if $widths 122 | w_name, w_serial = $widths 123 | w_serial *= -1 124 | else 125 | w_name = w_serial = '' 126 | end 127 | fmt = "[*] %#{w_name}s / %#{w_serial}s: " 128 | $stdout.write fmt % [ dev[:name], dev[:serial] ] 129 | 130 | else 131 | fmt = "[*] %#{$widths}s: " 132 | $stdout.write fmt % [ dev[:name] ] 133 | 134 | end 135 | $stdout.flush 136 | end 137 | 138 | 139 | # 140 | # get a list of devices via 'adb devices' 141 | # 142 | def adb_scan(verbose = false) 143 | adb_devices = [] 144 | 145 | $adb_ports.each { |port| 146 | cmd = [ 'adb', '-P', port.to_s, 'devices' ] 147 | Open3.popen3(*cmd) { |sin, sout, serr, thr| 148 | pid = thr[:pid] 149 | outlines = sout.readlines 150 | errlines = serr.readlines 151 | 152 | if errlines.length > 0 153 | $stderr.puts "ERROR:" 154 | 155 | errlines.each { |ln| 156 | $stderr.puts ln 157 | } 158 | end 159 | 160 | outlines.each { |ln| 161 | ln.chomp! 162 | ln.strip! 163 | next if ln.length < 1 164 | next if ln == "List of devices attached" 165 | next if ln[0,1] == "*" 166 | 167 | parts = ln.split("\t") 168 | serial = parts.first 169 | adb_devices << [ port, serial ] 170 | } 171 | } 172 | } 173 | 174 | if verbose 175 | adl = adb_devices.length 176 | apl = $adb_ports.length 177 | $stderr.puts "[*] Found #{adl} device#{plural(adl)} on #{apl} server#{plural(apl)} via 'adb devices'" 178 | end 179 | 180 | return adb_devices 181 | end 182 | 183 | 184 | # 185 | # run an adb command, capture and return the lines of output 186 | # 187 | # NOTE: this is not interactive! 188 | # 189 | def adb_get_lines(incmd) 190 | adb_devices = [] 191 | 192 | lines = [] 193 | 194 | cmd = [ 'adb' ] 195 | cmd += incmd 196 | Open3.popen3(*cmd) { |sin, sout, serr, thr| 197 | pid = thr[:pid] 198 | outlines = sout.readlines 199 | errlines = serr.readlines 200 | 201 | if errlines.length > 0 202 | lines << "ERROR:" 203 | errlines.each { |ln| 204 | lines << ln 205 | } 206 | end 207 | 208 | outlines.each { |ln| 209 | lines << ln.chomp 210 | } 211 | } 212 | 213 | return lines 214 | end 215 | 216 | 217 | # 218 | # run the adb binary once for each selected device 219 | # each time, specifying the remaining ARGV 220 | # 221 | def multi_adb(base = nil, argv = nil) 222 | 223 | not_found = [] 224 | not_found = $selected_devices.dup if $selected_devices 225 | 226 | $devices[:connected].each { |dev| 227 | 228 | # skip this device if it wasn't selected 229 | next if not is_selected(dev) 230 | 231 | # it was selected, remove it from the "not_found" list 232 | sel = which_matches(not_found, dev) 233 | if not sel.nil? 234 | not_found.delete sel 235 | else 236 | not_found.delete dev[:name] 237 | not_found.delete dev[:serial] 238 | end 239 | 240 | argv ||= ARGV 241 | 242 | if dev[:disabled] 243 | puts "[!] Warning: The selected device is marked disabled. It may not be present." 244 | end 245 | 246 | if $one_line 247 | print_col_prefix(dev) 248 | 249 | args = [ '-s', dev[:serial], '-P', dev[:port].to_s ] 250 | args += base if base 251 | args += argv 252 | puts adb_get_lines(args).join("\\n") 253 | 254 | else 255 | print_col_prefix(dev) 256 | puts "" 257 | 258 | cmd = [ 'adb', '-s', dev[:serial], '-P', dev[:port].to_s ] 259 | cmd += base if base 260 | cmd += argv 261 | system(*cmd) 262 | puts "" 263 | end 264 | } 265 | 266 | not_found.each { |sel| 267 | puts "[!] didn't find device \"#{sel}\" - typo? device not connected?" 268 | } 269 | 270 | end 271 | 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | android-cluster-toolkit 2 | ======================= 3 | 4 | The Android Cluster Toolkit helps organize and manipulate a collection of Android devices. It was designed to work with a collection of devices connected to the same host machine, either directly or via one or more tiers of powered USB hubs. The tools within can operate on single devices, a selected subset, or all connected devices at once. 5 | 6 | ## Requirements 7 | 8 | - A Ruby Interpreter (Tested with Ruby 1.9.3 and 2.0.0) 9 | - A Linux Host machine (YMMV on other OSes) 10 | - One or more Android devices 11 | - Open USB ports (one per device) 12 | 13 | ## Hardware Components 14 | 15 | There are no special hardware components, though various hardware has been used with varying results. 16 | 17 | ### Good Hardware 18 | D-Link DUB-H7 hubs seem to work well. 19 | 20 | ### Bad Hardware 21 | Although it looks cool and has a lot of ports, the Manhattan MondoHub is not recommended. It simply doesn't have enough power to support all ports being in use at the same time. 22 | 23 | ## Software Components 24 | 25 | Several scripts comprise the Android Cluster Toolkit. These scripts are described below. 26 | 27 | ### Library Components 28 | 29 | The toolkit contains three scripts that are included by the other scripts. These scripts each serve a particular purpose. 30 | 31 | #### lib/devices/pool.rb 32 | 33 | This file contains a Ruby Hash that describes various properties of the devices within your Android cluster. A sample file is included as "lib/devices/pool.rb.sample". This file is never changed by the toolkit, so this is a great place to put manual comments and so forth. You can generate one from the sample by running './init.rb' or 'make'. 34 | 35 | #### lib/devices/connected.rb 36 | 37 | The lib/devices/connected.rb script is generated from the Hash in lib/devices/pool.rb. It contains the list of devices that are currently connected to the host machine (the ADB Server). It's created by the reconfig.rb script. 38 | 39 | #### lib/plural.rb 40 | 41 | This file only contains a simple helper function to help pluralize words talking about arrays. 42 | 43 | #### lib/devices.rb 44 | 45 | This file contains functionality for dealing with devices or the lists of devices in the lib/devices directory. 46 | 47 | #### lib/plural.rb 48 | 49 | This file only contains a simple helper function to help pluralize words talking about arrays. 50 | 51 | #### lib/madb.rb 52 | 53 | This file contains functionality that is shared between several of the scripts in the Android Cluster Toolkit. It is included by those scripts and currently only contains a few utility method implementations. 54 | 55 | ### Management Scripts 56 | 57 | Dealing with a large collection of Android devices isn't painless. A couple of scripts in the toolkit ease the pain. These scripts take no arguments and just do their thing when you run them. 58 | 59 | #### scan.rb 60 | 61 | The scan.rb script exists to make adding new devices easier. Once you obtain a new device, plug it in and run the scan.rb script. If it finds any devices that are not in lib/devices/pool.rb it will output suitable Hash entries for them. Add these entries to lib/devices/pool.rb to make these devices accessible to the multi-device scripts. Make sure to set a unique name for the device (perhaps the model number or slang name, ie. GT-I9505 or sgs4). Re-run reconfig.rb to activate the changes. 62 | 63 | #### reconfig.rb 64 | 65 | The reconfig.rb script automatically generates the Hash in lib/devices/connected.rb. When executed, it copies the entries in lib/devices/pool.rb that are currently connected to the ADB Server. This way, you can avoid silly "no such device" output from the multi-device scripts. 66 | 67 | ### Single Device Scripts 68 | 69 | Although there used to be more single device scripts, they were deprecated in favor of a more unified interface (described in the next section). That said, one single-device script remains: shell.rb. 70 | 71 | #### shell.rb <device|serial> 72 | 73 | This script executes a shell suitable for operating on the specified device only. This is useful for maintenance or doing more focused research on a single device. When executed, it simply sets up the environment so that other ADB-aware tools will operate on the specified device. Once the environment is set up, the script spawns a shell for your pleasure. Exit the new shell to return to the original shell. 74 | 75 | ### Multi-device scripts 76 | 77 | The Android Cluster Toolkit's best feature is its multi-device scripts. These scripts can be used to perform some action against one, several, or all of the devices in your Android cluster at once. The first argument to these scripts specifies which device(s) to act upon as follows: 78 | 79 | 1. "." (a literal period, without the quotes): Act upon all devices 80 | 2. One or more device names or serial numbers separated by commas (quote your spaces if necessary): Act upon only the selected devices. 81 | 82 | The arguments that follow the device selector are specific to each script. In some cases they are optional, but in other cases they are not. 83 | 84 | A couple of options, -v and -1, toggle verbosity and single-line mode. NOTE: Single-line mode does not accept input and will block if the device reads from stdin. 85 | 86 | #### mdo [-1v] -d <device selector> <adb args> 87 | 88 | This multi-device script enables you to run an arbitrary ADB command against the selected device(s). Any command supported by the ADB client should work (ie, push, shell, reboot, etc). Validation is done to prevent non-desirable results; relax it at your own risk. 89 | 90 | ./mdo -1d . install /path/to/my/app.apk 91 | 92 | This will install app.apk to all connected devices. 93 | 94 | #### mcmd [-1v] -d <device selector> <command and args> 95 | 96 | This multi-device script enables you to run an arbitrary shell command on the selected device(s). For example: 97 | 98 | ./mcmd -d . getprop ro.build.fingerprint 99 | 100 | This will list the build fingerprint of all connected devices. 101 | 102 | #### mbb [-1v] -d <device selector> <command and args> 103 | 104 | During research, the need arose to use tools from the BusyBox binary in lieu of the default versions. For example, the ls(1) binary inside BusyBox offers color output, among other features whereas the default one (usually from toolbox) does not. This script accomplishes this goal by prefixing the specified command with "/data/local/tmp/busybox". Placing a suitable busybox binary in this location is up to you. You can find a suitable binary at: http://cache.saurik.com/android/armeabi/busybox 105 | 106 | #### mpull [-1v] -d <device selector> <path to pull> 107 | 108 | Another common task is pulling a particular file or path from all devices so that the data can be inspected on the host machine. This script will pull the specified paths from all devices and store them into a device-specific sub-directory within the "devices" directory in the Android Cluster Toolkit directory. For example: 109 | 110 | ./mpull -d sgs4 /proc/version 111 | 112 | This command will pull the kernel version information from /proc/version and save it to devices/sgs4/proc/version. 113 | 114 | ## Getting Set Up 115 | 116 | There are only a handful steps to getting up and running. 117 | 118 | 1. Clone this repository 119 | 2. Go into the directory 120 | 3. Connect all of the desired devices 121 | 122 | The relevant commands are: 123 | 124 | ``` 125 | $ git clone https://github.com/jduck/android-cluster-toolkit.git 126 | $ cd android-cluster-toolkit 127 | ``` 128 | 129 | At this point you can follow either the *QUICK* or *MANUAL* methods to provision devices. 130 | 131 | ### QUICK 132 | 133 | 1. Run the ./init.rb to generate lib/devices/pool.rb from the connected devices 134 | 2. Run ./reconfig.rb to generate lib/devices/connected.rb from lib/devices/pool.rb 135 | 136 | The relevant commands are: 137 | 138 | ``` 139 | $ ./init.rb 140 | $ $EDITOR lib/devices/pool.rb # optionally set names for the devices 141 | $ ./reconfig.rb 142 | ``` 143 | 144 | ### MANUAL 145 | 146 | 1. Run the ./scan.rb script to see newly connected devices 147 | 2. Add the Hash entries to lib/devices/pool.rb, assigning names as you go 148 | 3. Run ./reconfig.rb 149 | 150 | The relevant commands are: 151 | 152 | ``` 153 | $ ./scan.rb 154 | $ $EDITOR lib/devices/pool.rb # add and set names for the devices 155 | $ ./reconfig.rb 156 | ``` 157 | 158 | At this point you're ready to go. To confirm everything is working, run something against all the devices. 159 | 160 | ./mcmd -1d . getprop ro.build.fingerprint 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2012-2015, Joshua J. Drake (jduck) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | 13 | 14 | Apache License 15 | Version 2.0, January 2004 16 | http://www.apache.org/licenses/ 17 | 18 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 19 | 20 | 1. Definitions. 21 | 22 | "License" shall mean the terms and conditions for use, reproduction, 23 | and distribution as defined by Sections 1 through 9 of this document. 24 | 25 | "Licensor" shall mean the copyright owner or entity authorized by 26 | the copyright owner that is granting the License. 27 | 28 | "Legal Entity" shall mean the union of the acting entity and all 29 | other entities that control, are controlled by, or are under common 30 | control with that entity. For the purposes of this definition, 31 | "control" means (i) the power, direct or indirect, to cause the 32 | direction or management of such entity, whether by contract or 33 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 34 | outstanding shares, or (iii) beneficial ownership of such entity. 35 | 36 | "You" (or "Your") shall mean an individual or Legal Entity 37 | exercising permissions granted by this License. 38 | 39 | "Source" form shall mean the preferred form for making modifications, 40 | including but not limited to software source code, documentation 41 | source, and configuration files. 42 | 43 | "Object" form shall mean any form resulting from mechanical 44 | transformation or translation of a Source form, including but 45 | not limited to compiled object code, generated documentation, 46 | and conversions to other media types. 47 | 48 | "Work" shall mean the work of authorship, whether in Source or 49 | Object form, made available under the License, as indicated by a 50 | copyright notice that is included in or attached to the work 51 | (an example is provided in the Appendix below). 52 | 53 | "Derivative Works" shall mean any work, whether in Source or Object 54 | form, that is based on (or derived from) the Work and for which the 55 | editorial revisions, annotations, elaborations, or other modifications 56 | represent, as a whole, an original work of authorship. For the purposes 57 | of this License, Derivative Works shall not include works that remain 58 | separable from, or merely link (or bind by name) to the interfaces of, 59 | the Work and Derivative Works thereof. 60 | 61 | "Contribution" shall mean any work of authorship, including 62 | the original version of the Work and any modifications or additions 63 | to that Work or Derivative Works thereof, that is intentionally 64 | submitted to Licensor for inclusion in the Work by the copyright owner 65 | or by an individual or Legal Entity authorized to submit on behalf of 66 | the copyright owner. For the purposes of this definition, "submitted" 67 | means any form of electronic, verbal, or written communication sent 68 | to the Licensor or its representatives, including but not limited to 69 | communication on electronic mailing lists, source code control systems, 70 | and issue tracking systems that are managed by, or on behalf of, the 71 | Licensor for the purpose of discussing and improving the Work, but 72 | excluding communication that is conspicuously marked or otherwise 73 | designated in writing by the copyright owner as "Not a Contribution." 74 | 75 | "Contributor" shall mean Licensor and any individual or Legal Entity 76 | on behalf of whom a Contribution has been received by Licensor and 77 | subsequently incorporated within the Work. 78 | 79 | 2. Grant of Copyright License. Subject to the terms and conditions of 80 | this License, each Contributor hereby grants to You a perpetual, 81 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 82 | copyright license to reproduce, prepare Derivative Works of, 83 | publicly display, publicly perform, sublicense, and distribute the 84 | Work and such Derivative Works in Source or Object form. 85 | 86 | 3. Grant of Patent License. Subject to the terms and conditions of 87 | this License, each Contributor hereby grants to You a perpetual, 88 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 89 | (except as stated in this section) patent license to make, have made, 90 | use, offer to sell, sell, import, and otherwise transfer the Work, 91 | where such license applies only to those patent claims licensable 92 | by such Contributor that are necessarily infringed by their 93 | Contribution(s) alone or by combination of their Contribution(s) 94 | with the Work to which such Contribution(s) was submitted. If You 95 | institute patent litigation against any entity (including a 96 | cross-claim or counterclaim in a lawsuit) alleging that the Work 97 | or a Contribution incorporated within the Work constitutes direct 98 | or contributory patent infringement, then any patent licenses 99 | granted to You under this License for that Work shall terminate 100 | as of the date such litigation is filed. 101 | 102 | 4. Redistribution. You may reproduce and distribute copies of the 103 | Work or Derivative Works thereof in any medium, with or without 104 | modifications, and in Source or Object form, provided that You 105 | meet the following conditions: 106 | 107 | (a) You must give any other recipients of the Work or 108 | Derivative Works a copy of this License; and 109 | 110 | (b) You must cause any modified files to carry prominent notices 111 | stating that You changed the files; and 112 | 113 | (c) You must retain, in the Source form of any Derivative Works 114 | that You distribute, all copyright, patent, trademark, and 115 | attribution notices from the Source form of the Work, 116 | excluding those notices that do not pertain to any part of 117 | the Derivative Works; and 118 | 119 | (d) If the Work includes a "NOTICE" text file as part of its 120 | distribution, then any Derivative Works that You distribute must 121 | include a readable copy of the attribution notices contained 122 | within such NOTICE file, excluding those notices that do not 123 | pertain to any part of the Derivative Works, in at least one 124 | of the following places: within a NOTICE text file distributed 125 | as part of the Derivative Works; within the Source form or 126 | documentation, if provided along with the Derivative Works; or, 127 | within a display generated by the Derivative Works, if and 128 | wherever such third-party notices normally appear. The contents 129 | of the NOTICE file are for informational purposes only and 130 | do not modify the License. You may add Your own attribution 131 | notices within Derivative Works that You distribute, alongside 132 | or as an addendum to the NOTICE text from the Work, provided 133 | that such additional attribution notices cannot be construed 134 | as modifying the License. 135 | 136 | You may add Your own copyright statement to Your modifications and 137 | may provide additional or different license terms and conditions 138 | for use, reproduction, or distribution of Your modifications, or 139 | for any such Derivative Works as a whole, provided Your use, 140 | reproduction, and distribution of the Work otherwise complies with 141 | the conditions stated in this License. 142 | 143 | 5. Submission of Contributions. Unless You explicitly state otherwise, 144 | any Contribution intentionally submitted for inclusion in the Work 145 | by You to the Licensor shall be under the terms and conditions of 146 | this License, without any additional terms or conditions. 147 | Notwithstanding the above, nothing herein shall supersede or modify 148 | the terms of any separate license agreement you may have executed 149 | with Licensor regarding such Contributions. 150 | 151 | 6. Trademarks. This License does not grant permission to use the trade 152 | names, trademarks, service marks, or product names of the Licensor, 153 | except as required for reasonable and customary use in describing the 154 | origin of the Work and reproducing the content of the NOTICE file. 155 | 156 | 7. Disclaimer of Warranty. Unless required by applicable law or 157 | agreed to in writing, Licensor provides the Work (and each 158 | Contributor provides its Contributions) on an "AS IS" BASIS, 159 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 160 | implied, including, without limitation, any warranties or conditions 161 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 162 | PARTICULAR PURPOSE. You are solely responsible for determining the 163 | appropriateness of using or redistributing the Work and assume any 164 | risks associated with Your exercise of permissions under this License. 165 | 166 | 8. Limitation of Liability. In no event and under no legal theory, 167 | whether in tort (including negligence), contract, or otherwise, 168 | unless required by applicable law (such as deliberate and grossly 169 | negligent acts) or agreed to in writing, shall any Contributor be 170 | liable to You for damages, including any direct, indirect, special, 171 | incidental, or consequential damages of any character arising as a 172 | result of this License or out of the use or inability to use the 173 | Work (including but not limited to damages for loss of goodwill, 174 | work stoppage, computer failure or malfunction, or any and all 175 | other commercial damages or losses), even if such Contributor 176 | has been advised of the possibility of such damages. 177 | 178 | 9. Accepting Warranty or Additional Liability. While redistributing 179 | the Work or Derivative Works thereof, You may choose to offer, 180 | and charge a fee for, acceptance of support, warranty, indemnity, 181 | or other liability obligations and/or rights consistent with this 182 | License. However, in accepting such obligations, You may act only 183 | on Your own behalf and on Your sole responsibility, not on behalf 184 | of any other Contributor, and only if You agree to indemnify, 185 | defend, and hold each Contributor harmless for any liability 186 | incurred by, or claims asserted against, such Contributor by reason 187 | of your accepting any such warranty or additional liability. 188 | 189 | END OF TERMS AND CONDITIONS 190 | --------------------------------------------------------------------------------