replicant-adb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ script: "bundle exec rake test"
data/Gemfile ADDED
@@ -0,0 +1,19 @@
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
+ gem "activesupport-core-ext", "~> 4.0", :require => false
9
+
10
+ group :development do
11
+ gem "rake", "~> 10.1"
12
+ gem "bundler", "~> 1.0"
13
+ gem "jeweler", "~> 1.8.7"
14
+ end
15
+
16
+ group :test do
17
+ gem "minitest", "~> 5.0", :require => 'minitest/autorun'
18
+ gem "mocha", "~> 0.14.0", :require => 'mocha/setup'
19
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Matthias Kaeppler
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.
@@ -0,0 +1,117 @@
1
+ replicant - a repl for adb
2
+ ==========================
3
+
4
+ `replicant` is an interactive shell (a [REPL][2]) for `adb`, the Android Debug Bridge.
5
+ It is partially based on Chris Wanstrath's excellent [repl][0] command line wrapper.
6
+
7
+ ![repl](https://raw.github.com/mttkay/replicant/master/screenshots/01_repl.png)
8
+
9
+ Overview
10
+ -------
11
+ Working with the `adb` tool directly to target connected emulators and devices is
12
+ verbose and cumbersome. `replicant` simplifies this process in a number of ways:
13
+
14
+ - being a repl, you're now working with `adb` in interactive mode
15
+ - allows fixing devices and package IDs for subsequent `adb` commands
16
+ - auto-detection of target package by project folder inspection
17
+ - command history and tab-completion via `rlwrap` (see below)
18
+
19
+ ### Example
20
+ In this example session, we start `replicant` from the folder of an existing Android
21
+ application. It detects the manifest file and sets a default package ID.
22
+ From here on we list the available commands, fix the device to the first listed
23
+ emulator, uninstall the app, then reset the session.
24
+
25
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26
+ v1.0.0
27
+ dP oo dP
28
+ 88 88
29
+ 88d888b. .d8888b. 88d888b. 88 dP .d8888b. .d8888b. 88d888b. d8888P
30
+ 88' `88 88ooood8 88' `88 88 88 88' `"" 88' `88 88' `88 88
31
+ 88 88. ... 88. .88 88 88 88. ... 88. .88 88 88 88
32
+ dP `88888P' 88Y888P' dP dP `88888P' `88888P8 dP dP dP
33
+ 88
34
+ dP (c) 2013 Matthias Kaeppler
35
+
36
+
37
+ Type !list to see a list of commands.
38
+ Commands not starting in '!' are sent to adb verbatim.
39
+ Use Ctrl-D (i.e. EOF) to exit.
40
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
41
+ Setting default package to "com.soundcloud.android"
42
+ -- com.soundcloud.android, No device set
43
+ >> !list
44
+ !device -- set a default device to work with
45
+ !devices -- print a list of connected devices
46
+ !list -- print a list of available commands
47
+ !package -- set a default package to work with
48
+ !reset -- clear current device and package
49
+ !restart -- restart ADB
50
+ OK.
51
+ -- com.soundcloud.android, No device set
52
+ >> !devices
53
+
54
+ 005de387d71505d6 [Nexus 4]
55
+ emulator-5554 [Android SDK built for x86]
56
+
57
+ OK.
58
+ -- com.soundcloud.android, No device set
59
+ >> !device emu1
60
+
61
+ 005de387d71505d6 [Nexus 4]
62
+ emulator-5554 [Android SDK built for x86]
63
+
64
+ Setting default device to emulator-5554 [Android SDK built for x86]
65
+ OK.
66
+ -- com.soundcloud.android, emulator-5554 [Android SDK built for x86]
67
+ >> uninstall
68
+ Success
69
+ OK.
70
+ -- com.soundcloud.android, emulator-5554 [Android SDK built for x86]
71
+ >> !reset
72
+ OK.
73
+ -- No package set, No device set
74
+ >>
75
+
76
+ Install
77
+ -------
78
+ `replicant` requires Ruby 1.9 and a UNIX/Linux compatible shell such as `bash` or `zsh`.
79
+ For the best experience, I strongly recommend to install [rlwrap][1] to get
80
+ command history and tab-completion, although it's not a requirement.
81
+ `replicant` integrates with `rlwrap` automatically;
82
+ it's sufficient for it to just be installed.
83
+
84
+ Replicant hasn't seen a stable release yet, so for now, has to be installed as a local Ruby gem.
85
+ To proceed, make sure that [bundler][3] is installed on your system. (e.g. `$gem install bundler`)
86
+
87
+ Clone this repository, then:
88
+
89
+ $ cd replicant
90
+ $ bundle install
91
+ $ rake install
92
+
93
+
94
+ Contributing
95
+ ------------
96
+
97
+ [![Build Status](https://travis-ci.org/mttkay/replicant.png)](https://travis-ci.org/mttkay/replicant)
98
+
99
+ Please hack on replicant and make it better and more feature complete!
100
+
101
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
102
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
103
+ * Fork the project.
104
+ * Start a feature branch to implement your bugfix or idea.
105
+ * Write an executable spec. See existing specs in the test/ folder for examples.
106
+ * Commit and push until you are happy with your contribution.
107
+
108
+ Copyright
109
+ ---------
110
+
111
+ Copyright (c) 2013 Matthias Kaeppler. See LICENSE.txt for
112
+ further details.
113
+
114
+ [0]: https://github.com/defunkt/repl
115
+ [1]: http://utopia.knoware.nl/~hlub/rlwrap/
116
+ [2]: http://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop
117
+ [3]: http://bundler.io/
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ require './lib/replicant/version'
6
+
7
+ begin
8
+ Bundler.setup(:default, :development)
9
+ rescue Bundler::BundlerError => e
10
+ $stderr.puts e.message
11
+ $stderr.puts "Run `bundle install` to install missing gems"
12
+ exit e.status_code
13
+ end
14
+ require 'rake'
15
+
16
+ require 'jeweler'
17
+ Jeweler::Tasks.new do |gem|
18
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
19
+ gem.name = "replicant-adb"
20
+ gem.version = Replicant::VERSION
21
+ gem.homepage = "https://github.com/mttkay/replicant"
22
+ gem.license = "MIT"
23
+ gem.summary = "A REPL for the Android Debug Bridge"
24
+ gem.description = "replicant is an interactive shell (a REPL) for ADB, the Android Debug Bridge"
25
+ gem.email = "m.kaeppler@gmail.com"
26
+ gem.authors = ["Matthias Kaeppler"]
27
+ gem.files.exclude 'screenshots/**'
28
+ # dependencies defined in Gemfile
29
+ end
30
+ Jeweler::RubygemsDotOrgTasks.new
31
+
32
+ require 'rake/testtask'
33
+ Rake::TestTask.new(:test) do |test|
34
+ test.libs << 'lib' << 'test'
35
+ test.pattern = 'test/**/*_spec.rb'
36
+ test.verbose = true
37
+ end
38
+
39
+ task :default => :test
40
+
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require 'replicant'
4
+ rescue LoadError
5
+ begin
6
+ require 'rubygems'
7
+ require 'replicant'
8
+ end
9
+ end
10
+
11
+ Replicant::REPL.new.run
@@ -0,0 +1,9 @@
1
+ require 'active_support/core_ext/object/blank.rb'
2
+ require 'active_support/core_ext/string/filters.rb'
3
+ require 'active_support/core_ext/object/try.rb'
4
+
5
+ require 'replicant/version'
6
+ require 'replicant/command'
7
+ require 'replicant/styles'
8
+ require 'replicant/device'
9
+ require 'replicant/repl'
@@ -0,0 +1,352 @@
1
+ require 'stringio'
2
+
3
+ class Command
4
+
5
+ def self.inherited(subclass)
6
+ @@subclasses ||= []
7
+ @@subclasses << subclass
8
+ end
9
+
10
+ def self.all
11
+ (@@subclasses - [AdbCommand, ListCommand, EnvCommand]).map do |clazz|
12
+ clazz.new(nil)
13
+ end
14
+ end
15
+
16
+ def self.load(repl, command_line)
17
+ if command_line == '!'
18
+ # load command that lists available commands
19
+ ListCommand.new(repl)
20
+ elsif command_line == '?'
21
+ EnvCommand.new(repl)
22
+ elsif command_line.start_with?('!')
23
+ # load custom command
24
+ command_parts = command_line[1..-1].split
25
+ command_name = command_parts.first
26
+ command_args = command_parts[1..-1].join(' ')
27
+ command_class = "#{command_name.capitalize}Command"
28
+ begin
29
+ clazz = Object.const_get(command_class)
30
+ clazz.new(repl, command_args)
31
+ rescue NameError => e
32
+ nil
33
+ end
34
+ else
35
+ # forward command to ADB
36
+ AdbCommand.new(repl, command_line.strip)
37
+ end
38
+ end
39
+
40
+ attr_reader :args
41
+
42
+ def initialize(repl, args = nil, options = {})
43
+ @repl = repl
44
+ @args = args.strip if args
45
+ @options = options
46
+ end
47
+
48
+ def name
49
+ "!#{self.class.name.gsub("Command", "").downcase}"
50
+ end
51
+
52
+ # subclasses override this to provide a description of their functionality
53
+ def description
54
+ "TODO: description missing"
55
+ end
56
+
57
+ # subclasses override this to provide a usage example
58
+ def usage
59
+ end
60
+
61
+ def execute
62
+ if valid_args?
63
+ run
64
+ else
65
+ output "Invalid arguments. Ex.: #{usage}"
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def valid_args?
72
+ true
73
+ end
74
+
75
+ def output(message)
76
+ puts message unless @options[:silent]
77
+ end
78
+
79
+ end
80
+
81
+ class AdbCommand < Command
82
+
83
+ # the command line program
84
+ ADB = 'adb'
85
+
86
+ def run
87
+ begin
88
+ cmd = "#{adb} #{args}"
89
+
90
+ if interactive?
91
+ system cmd
92
+ else
93
+ cmd << " #{@repl.default_package}" if @repl.default_package && package_dependent?
94
+ output cmd if @repl.debug?
95
+ result = `#{cmd}`
96
+ output result
97
+ result
98
+ end
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def adb
105
+ adb = "#{ADB}"
106
+ adb << " -s #{@repl.default_device.id}" if @repl.default_device
107
+ adb
108
+ end
109
+
110
+ def interactive?
111
+ args == "shell" || args.start_with?("logcat")
112
+ end
113
+
114
+ def package_dependent?
115
+ ["uninstall"].include?(args)
116
+ end
117
+ end
118
+
119
+ class DevicesCommand < Command
120
+ def description
121
+ "print a list of connected devices"
122
+ end
123
+
124
+ def run
125
+ adb = AdbCommand.new(@repl, "devices -l", :silent => true)
126
+ device_lines = adb.execute.lines.to_a.reject do |line|
127
+ line.strip.empty? || line.include?("daemon") || line.include?("List of devices")
128
+ end
129
+
130
+ device_ids = device_lines.map { |l| /([\S]+)\s+device/.match(l)[1] }
131
+ device_products = device_lines.map { |l| /product:([\S]+)/.match(l).try(:[], 1) }
132
+
133
+ device_names = device_lines.zip(device_ids).map do |l, id|
134
+ /model:([\S]+)/.match(l).try(:[], 1) || detect_device_name(id)
135
+ end
136
+
137
+ devices = device_ids.zip(device_names, device_products).map do |id, name, product|
138
+ Device.new(id, humanize_name(name, product))
139
+ end
140
+
141
+ output ""
142
+ output devices_string(devices)
143
+ output ""
144
+ devices
145
+ end
146
+
147
+ private
148
+
149
+ def detect_device_name(id)
150
+ if id.start_with?("emulator-")
151
+ "Android emulator"
152
+ else
153
+ "Unknown device"
154
+ end
155
+ end
156
+
157
+ def humanize_name(name_string, product)
158
+ if product == "vbox86p"
159
+ "Genymotion " + name_string.gsub(/___[\d_]+___/, "_")
160
+ else
161
+ name_string
162
+ end.gsub('_', ' ').squish
163
+ end
164
+
165
+ def devices_string(devices)
166
+ device_string = if devices.any?
167
+ padding = devices.map { |d| d.name.length }.max
168
+ indices = (0..devices.length - 1).to_a
169
+ indices.zip(devices).map { |i, d| "[#{i}] #{d.name}#{' ' * (padding - d.name.length)} | #{d.id}" }
170
+ else
171
+ "No devices found"
172
+ end
173
+ end
174
+ end
175
+
176
+ class PackageCommand < Command
177
+
178
+ def description
179
+ "set a default package to work with"
180
+ end
181
+
182
+ def usage
183
+ "#{name} com.mydomain.mypackage"
184
+ end
185
+
186
+ def valid_args?
187
+ args.present? && /^\w+(\.\w+)*$/ =~ args
188
+ end
189
+
190
+ def run
191
+ output "Setting default package to #{args.inspect}"
192
+ @repl.default_package = args
193
+ end
194
+ end
195
+
196
+ class DeviceCommand < Command
197
+ def description
198
+ "set a default device to work with"
199
+ end
200
+
201
+ def usage
202
+ "#{name} [<index>|<device_id>]"
203
+ end
204
+
205
+ def valid_args?
206
+ args.present? && /\S+/ =~ args
207
+ end
208
+
209
+ def run
210
+ default_device = if index?
211
+ # user selected by index
212
+ devices[args.to_i]
213
+ else
214
+ # user selected by device ID
215
+ devices.detect { |d| d.id == args }
216
+ end
217
+
218
+ if default_device
219
+ output "Setting default device to #{default_device.inspect}"
220
+ @repl.default_device = default_device
221
+ else
222
+ output "No such device"
223
+ end
224
+ end
225
+
226
+ private
227
+
228
+ def index?
229
+ /^\d+$/ =~ args
230
+ end
231
+
232
+ def devices
233
+ @devices ||= DevicesCommand.new(@repl, nil, :silent => true).execute
234
+ end
235
+
236
+ end
237
+
238
+ class ResetCommand < Command
239
+
240
+ def description
241
+ "clear current device and package"
242
+ end
243
+
244
+ def valid_args?
245
+ args.blank?
246
+ end
247
+
248
+ def run
249
+ @repl.default_device = nil
250
+ @repl.default_package = nil
251
+ end
252
+
253
+ end
254
+
255
+ class ListCommand < Command
256
+ def valid_args?
257
+ args.blank?
258
+ end
259
+
260
+ def description
261
+ "print a list of available commands"
262
+ end
263
+
264
+ def run
265
+ command_list = Command.all.sort_by {|c| c.name}.map do |command|
266
+ padding = 20 - command.name.length
267
+ desc = "#{command.name} #{' ' * padding} -- #{command.description}"
268
+ desc
269
+ end
270
+ output command_list.join("\n")
271
+ end
272
+ end
273
+
274
+ class RestartCommand < Command
275
+ def description
276
+ "restart ADB"
277
+ end
278
+
279
+ def run
280
+ # Faster than kill-server, and also catches ADB instances launched by
281
+ # IntelliJ. Moreover, start-server after kill-server sometimes makes the
282
+ # server fail to start up unless you sleep for a second or so
283
+ `killall adb`
284
+ AdbCommand.new(@repl, "start-server").execute
285
+ end
286
+ end
287
+
288
+ class LogcatCommand < Command
289
+
290
+ def description
291
+ "access device logs"
292
+ end
293
+
294
+ def valid_args?
295
+ args.blank?
296
+ end
297
+
298
+ def run
299
+ pid = if @repl.default_package
300
+ processes = AdbCommand.new(@repl, "shell ps", :silent => true).execute
301
+ pid_line = processes.lines.detect {|l| l.include?(@repl.default_package)}
302
+ pid_line.split[1].strip if pid_line
303
+ end
304
+
305
+ logcat = "logcat -v time"
306
+ logcat << " | grep -E '\(\s*#{pid}\)'"
307
+ AdbCommand.new(@repl, logcat).execute
308
+ end
309
+ end
310
+
311
+ class ClearCommand < Command
312
+
313
+ def description
314
+ "clear application data"
315
+ end
316
+
317
+ # TODO: this is not a very good argument validator
318
+ def valid_args?
319
+ args.present? || @repl.default_package
320
+ end
321
+
322
+ def usage
323
+ "#{name} [com.example.package|<empty>(when default package is set)]"
324
+ end
325
+
326
+ def run
327
+ package = args.present? ? args : @repl.default_package
328
+ # Clear app data - cache, SharedPreferences, Databases
329
+ AdbCommand.new(@repl, "shell su -c \"rm -r /data/data/#{package}/*\"").execute
330
+ # Force application stop to recreate shared preferences, databases with new launch
331
+ AdbCommand.new(@repl, "shell am force-stop #{package}").execute
332
+ end
333
+ end
334
+
335
+ class EnvCommand < Command
336
+
337
+ def valid_args?
338
+ args.blank?
339
+ end
340
+
341
+ def run
342
+ env = "Package: #{@repl.default_package || 'Not set'}\n"
343
+ env << "Device: "
344
+ device = @repl.default_device
345
+ env << if device
346
+ "#{device.name} (#{device.id})"
347
+ else
348
+ 'Not set'
349
+ end
350
+ output env
351
+ end
352
+ end
@@ -0,0 +1,20 @@
1
+ class Device
2
+ attr_reader :id, :name
3
+
4
+ def initialize(id, name)
5
+ @id = id
6
+ @name = name
7
+ end
8
+
9
+ def emulator?
10
+ @id.start_with?('emulator-')
11
+ end
12
+
13
+ def physical_device?
14
+ !emulator?
15
+ end
16
+
17
+ def to_s
18
+ "#{@id} [#{@name}]"
19
+ end
20
+ end
@@ -0,0 +1,156 @@
1
+ require 'find'
2
+ require 'rexml/document'
3
+
4
+ module Replicant
5
+ class REPL
6
+
7
+ include Styles
8
+
9
+ # for auto-complete via rlwrap; should probably go in Rakefile at some point
10
+ ADB_COMMANDS = %w(devices connect disconnect push pull sync shell emu logcat
11
+ forward jdwp install uninstall bugreport backup restore help version
12
+ wait-for-device start-server kill-server get-state get-serialno get-devpath
13
+ status-window remount reboot reboot-bootloader root usb tcpip ppp)
14
+
15
+ attr_accessor :default_package
16
+ attr_accessor :default_device
17
+
18
+ def initialize
19
+ end
20
+
21
+ def run
22
+ # must be the first thing to execute, since it wraps the script
23
+ setup_rlwrap
24
+
25
+ show_greeting
26
+ if ARGV.any? { |arg| %w( -h --help -help help ).include?(arg) }
27
+ show_help
28
+ end
29
+
30
+ # try to detect a default package to work with from an AndroidManifest
31
+ # file somewhere close by
32
+ if manifest_path = detect_android_manifest_path
33
+ app_package = get_package_from_manifest(manifest_path)
34
+ PackageCommand.new(self, app_package).execute
35
+ end
36
+
37
+ # reset terminal colors on exit
38
+ at_exit { puts unstyled }
39
+
40
+ loop do
41
+ command_loop
42
+ end
43
+ end
44
+
45
+ def command_loop
46
+ print prompt
47
+
48
+ begin
49
+ command_line = $stdin.gets.chomp
50
+ rescue NoMethodError, Interrupt
51
+ exit
52
+ end
53
+
54
+ return if command_line.strip.empty?
55
+
56
+ command = Command.load(self, command_line)
57
+ if command
58
+ command.execute
59
+ puts span("OK.", :white_fg, :bold) { unstyled }
60
+ else
61
+ puts "No such command"
62
+ end
63
+ end
64
+
65
+ def debug?
66
+ ARGV.include?('--debug')
67
+ end
68
+
69
+ private
70
+
71
+ def prompt
72
+ prompt = ENV['REPL_PROMPT'] || begin
73
+ span('>> ', :white_fg, :bold) { styled(:green_fg) }
74
+ end.lstrip
75
+ end
76
+
77
+ def show_greeting
78
+ style = styled(:white_fg, :black_bg, :bold)
79
+ green = lambda { |text| span(text, :green_fg) { style } }
80
+
81
+ logo = <<-logo
82
+ dP oo dP
83
+ 88 88
84
+ 88d888b. .d8888b. 88d888b. 88 dP .d8888b. .d8888b. 88d888b. d8888P
85
+ 88' `88 88ooood8 88' `88 88 88 88' `"" 88' `88 88' `88 88
86
+ 88 88. ... 88. .88 88 88 88. ... 88. .88 88 88 88
87
+ dP `88888P' 88Y888P' dP dP `88888P' `88888P8 dP dP dP
88
+ 88
89
+ dP (c) 2013 Matthias Kaeppler
90
+ logo
91
+ puts style + ("~" * 70)
92
+ puts " v" + Replicant::VERSION
93
+ puts green[logo]
94
+ puts ""
95
+ puts " Type '#{green['!']}' to see a list of commands, '#{green['?']}' for environment info."
96
+ puts " Commands not starting in '#{green['!']}' are sent to adb verbatim."
97
+ puts " Use #{green['Ctrl-D']} (i.e. EOF) to exit."
98
+ puts ("~" * 70) + unstyled
99
+ end
100
+
101
+ def show_help
102
+ puts <<-help
103
+ Usage: replicant [options]
104
+
105
+ Options:
106
+ --help Display this message
107
+ --debug Display debug info
108
+
109
+ Bug reports, suggestions, updates:
110
+ http://github.com/mttkay/replicant/issues
111
+ help
112
+ exit
113
+ end
114
+
115
+ def setup_rlwrap
116
+ if !ENV['__REPL_WRAPPED'] && system("which rlwrap > /dev/null 2> /dev/null")
117
+ ENV['__REPL_WRAPPED'] = '0'
118
+
119
+ # set up auto-completion commands
120
+ completion_file = File.expand_path('~/.adb_completion')
121
+ File.open(completion_file, 'w') { |file| file.write(ADB_COMMANDS.join(' ')) }
122
+
123
+ # set up command history
124
+ if File.exists?(history_dir = File.expand_path(ENV['REPL_HISTORY_DIR'] || "~/"))
125
+ history_file = "#{history_dir}/.adb_history"
126
+ end
127
+
128
+ rlargs = "-c" # complete file names
129
+ rlargs << " --break-chars=" # we don't want to break any of the commands
130
+ rlargs << " -f #{completion_file}" if completion_file
131
+ rlargs << " -H #{history_file}" if history_file
132
+
133
+ exec "rlwrap #{rlargs} #$0 #{ARGV.join(' ')}"
134
+ end
135
+ end
136
+
137
+ # best effort function to detect the manifest path.
138
+ # checks for well known locations and falls back to a recursive search with
139
+ # a maximum depth of 2 directory levels
140
+ def detect_android_manifest_path
141
+ manifest_file = 'AndroidManifest.xml'
142
+ known_locations = %W(./#{manifest_file} ./src/main/#{manifest_file})
143
+ known_locations.find {|loc| File.exist?(loc)} || begin
144
+ Find.find('.') do |path|
145
+ Find.prune if path.start_with?('./.') || path.split('/').size > 3
146
+ return path if path.include?(manifest_file)
147
+ end
148
+ end
149
+ end
150
+
151
+ def get_package_from_manifest(manifest_path)
152
+ manifest = REXML::Document.new(File.new(manifest_path))
153
+ manifest.root.attributes['package']
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,30 @@
1
+ module Styles
2
+
3
+ STYLES = {
4
+ # foreground text
5
+ :white_fg => 37,
6
+ :black_fg => 30,
7
+ :green_fg => 32,
8
+
9
+ # background
10
+ :white_bg => 47,
11
+
12
+ # text styles
13
+ :bold => 1
14
+ }
15
+
16
+ def span(text, *styles)
17
+ styled = "\e[#{STYLES.values_at(*styles).join(';')}m#{text}"
18
+ styled << yield if block_given?
19
+ styled
20
+ end
21
+
22
+ def styled(*styles)
23
+ "\e[#{STYLES.values_at(*styles).join(';')}m"
24
+ end
25
+
26
+ def unstyled
27
+ "\e[0m"
28
+ end
29
+
30
+ end
@@ -0,0 +1,3 @@
1
+ module Replicant
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,58 @@
1
+ require 'helper'
2
+
3
+ class AdbCommandSpec < CommandSpecBase
4
+
5
+ describe "a basic adb command" do
6
+ before do
7
+ @command = silent AdbCommand.new(@repl, "devices")
8
+ @command.execute
9
+ end
10
+
11
+ it "sends a command to adb and captures the output" do
12
+ @command.backtick_capture.must_equal "adb devices"
13
+ end
14
+
15
+ it "does not use Kernel#system" do
16
+ @command.system_capture.must_be_nil
17
+ end
18
+ end
19
+
20
+ describe "an interactive command" do
21
+ before do
22
+ @command = AdbCommand.new(@repl, "shell")
23
+ @command.execute
24
+ end
25
+
26
+ it "is executed using a Kernel#system call" do
27
+ @command.system_capture.must_equal "adb shell"
28
+ end
29
+
30
+ describe "when it's 'shell'" do
31
+ it "is not treated as interactive when arguments are present" do
32
+ command = AdbCommand.new(@repl, "shell ps")
33
+ command.execute
34
+ command.system_capture.must_be_nil
35
+ command.backtick_capture.must_equal "adb shell ps"
36
+ end
37
+ end
38
+ end
39
+
40
+ describe "with a default package set" do
41
+ before do
42
+ @repl.stubs(:default_package).returns("com.myapp")
43
+ end
44
+
45
+ it "does not set the default package if command is not package dependent" do
46
+ command = silent AdbCommand.new(@repl, "devices")
47
+ command.execute
48
+ command.backtick_capture.must_equal "adb devices"
49
+ end
50
+
51
+ it "adds the default package if command is package dependent" do
52
+ command = silent AdbCommand.new(@repl, "uninstall")
53
+ command.execute
54
+ command.backtick_capture.must_equal "adb uninstall com.myapp"
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,90 @@
1
+ require 'helper'
2
+
3
+ class CommandSpec < CommandSpecBase
4
+
5
+ describe "command loading" do
6
+ it "loads the EnvCommand when command is '?'" do
7
+ command = Command.load(@repl, "?")
8
+ command.must_be_instance_of EnvCommand
9
+ end
10
+
11
+ it "loads the ListCommand when command is '!'" do
12
+ command = Command.load(@repl, "!")
13
+ command.must_be_instance_of ListCommand
14
+ end
15
+
16
+ it "returns nil if !-command cannot be resolved" do
17
+ Command.load(@repl, "!unknown").must_be_nil
18
+ end
19
+
20
+ it "loads the AdbCommand for commands not starting in '!'" do
21
+ command = Command.load(@repl, "shell ps")
22
+ command.must_be_instance_of AdbCommand
23
+ end
24
+
25
+ describe "arguments" do
26
+ it "injects the arguments for loaded command objects" do
27
+ command = Command.load(@repl, "shell ps")
28
+ command.args.must_equal "shell ps"
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "command listing" do
34
+ it "does not include the ListCommand" do
35
+ Command.all.map{|c|c.class}.wont_include(ListCommand)
36
+ end
37
+
38
+ it "does not include the AdbCommand" do
39
+ Command.all.map{|c|c.class}.wont_include(AdbCommand)
40
+ end
41
+
42
+ it "does not include the EnvCommand" do
43
+ Command.all.map{|c|c.class}.wont_include(EnvCommand)
44
+ end
45
+ end
46
+
47
+ describe "the command interface" do
48
+ before do
49
+ class ::TestCommand < Command; end
50
+ @command = silent TestCommand.new(@repl)
51
+ end
52
+
53
+ it "allows resolving the command name via type inspection" do
54
+ @command.name.must_equal "!test"
55
+ end
56
+
57
+ it "triggers the run method when calling 'execute'" do
58
+ @command.expects(:run).once
59
+ @command.execute
60
+ end
61
+
62
+ it "does not trigger the run method when arguments are invalid" do
63
+ @command.expects(:valid_args?).returns(false)
64
+ @command.expects(:run).never
65
+ @command.execute
66
+ end
67
+ end
68
+
69
+ describe "command options" do
70
+ before do
71
+ class ::TestCommand < Command
72
+ def run
73
+ output "this is a test"
74
+ end
75
+ end
76
+ end
77
+
78
+ it "can silence console output" do
79
+ command = TestCommand.new(@repl, nil, :silent => true)
80
+ lambda { command.execute }.must_be_silent
81
+ end
82
+ end
83
+
84
+ describe "arguments" do
85
+ it "strips argument whitespace when creating command instance" do
86
+ command = TestCommand.new(@repl, " ")
87
+ command.args.must_equal ""
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,30 @@
1
+ class Command
2
+ attr_reader :backtick_capture
3
+ attr_reader :system_capture
4
+ # capture system calls on all commands
5
+ def `(cmd)
6
+ @backtick_capture = cmd
7
+ nil
8
+ end
9
+ def system(cmd)
10
+ @system_capture = cmd
11
+ nil
12
+ end
13
+ end
14
+
15
+ class CommandSpecBase < MiniTest::Spec
16
+
17
+ before do
18
+ @repl = Replicant::REPL.new
19
+ end
20
+
21
+ def silent(command)
22
+ def command.output(s)
23
+ @output = s
24
+ end
25
+ def command.output_capture
26
+ @output
27
+ end
28
+ command
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ require 'helper'
2
+
3
+ class DeviceCommandSpec < CommandSpecBase
4
+
5
+ describe "given a valid list of devices" do
6
+ before do
7
+ @device0 = Device.new("emulator-5554", "Emulator 1")
8
+ @device1 = Device.new("emulator-5556", "Emulator 2")
9
+ DevicesCommand.any_instance.stubs(:execute).returns([@device0, @device1])
10
+ end
11
+
12
+ it "can select a device by index" do
13
+ silent(DeviceCommand.new(@repl, "0")).execute
14
+ @repl.default_device.must_equal @device0
15
+ silent(DeviceCommand.new(@repl, "1")).execute
16
+ @repl.default_device.must_equal @device1
17
+ end
18
+
19
+ it "can select a device by device id" do
20
+ silent(DeviceCommand.new(@repl, "emulator-5554")).execute
21
+ @repl.default_device.must_equal @device0
22
+ silent(DeviceCommand.new(@repl, "emulator-5556")).execute
23
+ @repl.default_device.must_equal @device1
24
+ end
25
+
26
+ it "outputs an error message if selected device doesn't exist" do
27
+ command = DeviceCommand.new(@repl, "emulator-bogus")
28
+ lambda { command.execute }.must_output "No such device\n"
29
+ end
30
+ end
31
+
32
+ describe "given an empty list of devices" do
33
+ it "outputs an error message when selecting a device" do
34
+ DevicesCommand.any_instance.stubs(:execute).returns([])
35
+ command = DeviceCommand.new(@repl, "emulator-5554")
36
+ lambda { command.execute }.must_output "No such device\n"
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,65 @@
1
+ require 'helper'
2
+
3
+ ADB_NO_DEVICES = <<-OUTPUT
4
+ * daemon not running. starting it now on port 5037 *
5
+ * daemon started successfully *
6
+ List of devices attached
7
+
8
+ OUTPUT
9
+
10
+ ADB_DEVICES = <<-OUTPUT
11
+ * daemon not running. starting it now on port 5037 *
12
+ * daemon started successfully *
13
+ List of devices attached
14
+ 192.168.56.101:5555 device product:vbox86p model:Nexus_4___4_3___API_18___768x1280 device:vbox86p
15
+ 005de387d71505d6 device usb:1D110000 product:occam model:Nexus_4 device:mako
16
+ emulator-5554 device
17
+
18
+ OUTPUT
19
+
20
+ REPLICANT_DEVICES = <<-OUTPUT
21
+
22
+ [0] Genymotion Nexus 4 API 18 768x1280 | 192.168.56.101:5555
23
+ [1] Nexus 4 | 005de387d71505d6
24
+ [2] Android emulator | emulator-5554
25
+
26
+ OUTPUT
27
+
28
+ class DevicesCommandSpec < CommandSpecBase
29
+
30
+ describe "when no devices were found" do
31
+ before do
32
+ AdbCommand.any_instance.expects(:execute).returns(ADB_NO_DEVICES)
33
+ end
34
+
35
+ it "returns an empty device list" do
36
+ command = silent DevicesCommand.new(@repl)
37
+ command.execute.must_equal []
38
+ end
39
+
40
+ it "prints a message and exits" do
41
+ command = DevicesCommand.new(@repl)
42
+ lambda { command.execute }.must_output("\nNo devices found\n\n")
43
+ end
44
+ end
45
+
46
+ describe "when devices were found" do
47
+ before do
48
+ AdbCommand.any_instance.expects(:execute).returns(ADB_DEVICES)
49
+ end
50
+
51
+ it "returns the list of devices" do
52
+ command = silent DevicesCommand.new(@repl)
53
+ devices = command.execute
54
+ devices.map { |d| d.id }.must_equal [
55
+ "192.168.56.101:5555", "005de387d71505d6", "emulator-5554"
56
+ ]
57
+ end
58
+
59
+ it "outputs a prettified, indexed list of devices" do
60
+ command = DevicesCommand.new(@repl)
61
+ lambda { command.execute }.must_output(REPLICANT_DEVICES)
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,30 @@
1
+ require 'helper'
2
+
3
+ class EnvCommandSpec < CommandSpecBase
4
+
5
+ BLANK_ENV_OUTPUT = <<-OUT
6
+ Package: Not set
7
+ Device: Not set
8
+ OUT
9
+
10
+ ACTIVE_ENV_OUTPUT = <<-OUT
11
+ Package: com.myapp
12
+ Device: Emulator 1 (emulator-5554)
13
+ OUT
14
+
15
+ describe "with no device and no package fixed" do
16
+ it "lists no devices and packages as selected" do
17
+ command = EnvCommand.new(@repl)
18
+ lambda { command.execute }.must_output(BLANK_ENV_OUTPUT)
19
+ end
20
+ end
21
+
22
+ describe "with a device and package fixed" do
23
+ it "lists no devices and packages as selected" do
24
+ @repl.default_device = Device.new("emulator-5554", "Emulator 1")
25
+ @repl.default_package = 'com.myapp'
26
+ command = EnvCommand.new(@repl)
27
+ lambda { command.execute }.must_output(ACTIVE_ENV_OUTPUT)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ require 'helper'
2
+
3
+ class ListCommandSpec < CommandSpecBase
4
+
5
+ describe "listing commands" do
6
+ it "prints a list of available commands" do
7
+ command = silent ListCommand.new(@repl)
8
+ command.execute
9
+ command.output_capture.must_match /^!\w+/
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,34 @@
1
+ require 'helper'
2
+
3
+ class PackageCommandSpec < CommandSpecBase
4
+
5
+ describe "with valid arguments" do
6
+ it "accepts a package with one element" do
7
+ command = silent PackageCommand.new(@repl, "myapp")
8
+ command.execute
9
+ @repl.default_package.must_equal "myapp"
10
+ end
11
+
12
+ it "accepts a package with two elements" do
13
+ command = silent PackageCommand.new(@repl, "com.myapp")
14
+ command.execute
15
+ @repl.default_package.must_equal "com.myapp"
16
+ end
17
+ end
18
+
19
+ describe "with invalid arguments" do
20
+ it "refuses a package not starting with alphanumeric characters" do
21
+ command = silent PackageCommand.new(@repl, "%myapp")
22
+ command.expects(:run).never
23
+ command.execute
24
+ @repl.default_package.must_be_nil
25
+ end
26
+
27
+ it "refuses a package containing non-alphanumeric characters" do
28
+ command = silent PackageCommand.new(@repl, "myapp.some$thing")
29
+ command.expects(:run).never
30
+ command.execute
31
+ @repl.default_package.must_be_nil
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,16 @@
1
+ begin
2
+ Bundler.require(:default, :test)
3
+ rescue Bundler::BundlerError => e
4
+ $stderr.puts e.message
5
+ $stderr.puts "Run `bundle install` to install missing gems"
6
+ exit e.status_code
7
+ end
8
+
9
+ # additional test modules
10
+ require 'minitest/pride'
11
+ require 'commands/command_spec_base'
12
+
13
+ # application module
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ require 'replicant'
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: replicant-adb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matthias Kaeppler
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport-core-ext
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '4.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '4.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '10.1'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '10.1'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: jeweler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.8.7
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.8.7
78
+ description: replicant is an interactive shell (a REPL) for ADB, the Android Debug
79
+ Bridge
80
+ email: m.kaeppler@gmail.com
81
+ executables:
82
+ - replicant
83
+ extensions: []
84
+ extra_rdoc_files:
85
+ - LICENSE.txt
86
+ - README.md
87
+ files:
88
+ - .travis.yml
89
+ - Gemfile
90
+ - LICENSE.txt
91
+ - README.md
92
+ - Rakefile
93
+ - bin/replicant
94
+ - lib/replicant.rb
95
+ - lib/replicant/command.rb
96
+ - lib/replicant/device.rb
97
+ - lib/replicant/repl.rb
98
+ - lib/replicant/styles.rb
99
+ - lib/replicant/version.rb
100
+ - test/commands/adb_command_spec.rb
101
+ - test/commands/command_spec.rb
102
+ - test/commands/command_spec_base.rb
103
+ - test/commands/device_command_spec.rb
104
+ - test/commands/devices_command_spec.rb
105
+ - test/commands/env_command_spec.rb
106
+ - test/commands/list_command_spec.rb
107
+ - test/commands/package_command_spec.rb
108
+ - test/helper.rb
109
+ homepage: https://github.com/mttkay/replicant
110
+ licenses:
111
+ - MIT
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ segments:
123
+ - 0
124
+ hash: -4199582713992118202
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 1.8.25
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: A REPL for the Android Debug Bridge
137
+ test_files: []