manymo 2.0.0.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +37 -0
- data/Rakefile +1 -0
- data/bin/manymo +5 -0
- data/bin/tunnel_test.rb +2 -0
- data/lib/manymo.rb +12 -0
- data/lib/manymo/adb.rb +33 -0
- data/lib/manymo/adb_connection_verifier.rb +37 -0
- data/lib/manymo/adb_packet.rb +90 -0
- data/lib/manymo/adb_stream.rb +18 -0
- data/lib/manymo/adb_tunnel.rb +132 -0
- data/lib/manymo/command.rb +106 -0
- data/lib/manymo/console_tunnel.rb +68 -0
- data/lib/manymo/service.rb +71 -0
- data/lib/manymo/tunnel.rb +145 -0
- data/lib/manymo/tunnel_close_event.rb +23 -0
- data/lib/manymo/version.rb +3 -0
- data/manymo.gemspec +24 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 821c784a5b085a534bc0589b61a990ab57544780
|
4
|
+
data.tar.gz: 05416b27426bf6c3c54cff2979a0ec446ed3c017
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ace13b861a68a2f7a2420662cf8851e533639acccc22d8e07cb3d1ddeb0afaef29e4aa7d64da131eefc79a2c82e84f2d2297e4445f8fe4c001f882074e177f1b
|
7
|
+
data.tar.gz: 2a254bf0950e742c68b6eb3cf8570e4a1e501ef216463948cb150f940757819aac78057c178c71416d65c779fea3ddd4d6dc3525c965233568f2c11ae2734700
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Pete Schwamb
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Manymo
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Dependencies
|
6
|
+
|
7
|
+
You will need adb (from the [Android SDK](http://developer.android.com/sdk)) in your PATH.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
$ gem install manymo
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```
|
16
|
+
manymo [options] COMMAND [ARGUMENTS]
|
17
|
+
|
18
|
+
Commands:
|
19
|
+
launch EMULATORNAME Launch a headless emulator and make it appear like a local device
|
20
|
+
list List emulators; use the name attribute with the launch command
|
21
|
+
shutdown [SERIALNUMBER] Shutdown specified headless emulator or tunnel, or all if serial number omitted
|
22
|
+
token Display a prompt to enter authorization token
|
23
|
+
tunnel TUNNELKEY Make an in-browser emulator appear like a local device
|
24
|
+
|
25
|
+
Options:
|
26
|
+
--adb-path PATH_TO_ADB Specify path to adb executable; otherwise uses the one in your path
|
27
|
+
```
|
28
|
+
|
29
|
+
More documentation is at https://www.manymo.com/pages/documentation/manymo-command-line-tool
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
1. Fork it
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
36
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
37
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/manymo
ADDED
data/bin/tunnel_test.rb
ADDED
data/lib/manymo.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "manymo/version"
|
2
|
+
require "manymo/adb"
|
3
|
+
require "manymo/adb_connection_verifier"
|
4
|
+
require "manymo/adb_packet"
|
5
|
+
require "manymo/adb_stream"
|
6
|
+
require "manymo/adb_tunnel"
|
7
|
+
require "manymo/console_tunnel"
|
8
|
+
require "manymo/command"
|
9
|
+
require "manymo/service"
|
10
|
+
require "manymo/tunnel"
|
11
|
+
require "manymo/tunnel_close_event"
|
12
|
+
|
data/lib/manymo/adb.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
module Manymo
|
3
|
+
class ADB
|
4
|
+
attr_accessor :path
|
5
|
+
def initialize(path = nil)
|
6
|
+
@path = path
|
7
|
+
end
|
8
|
+
|
9
|
+
def path
|
10
|
+
@path || 'adb'
|
11
|
+
end
|
12
|
+
|
13
|
+
def ensure_available
|
14
|
+
if @path
|
15
|
+
if !File.exists?(@path)
|
16
|
+
STDERR.puts "Specified ADB executable (#{@path}) not found."
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
else
|
20
|
+
found_adb = false
|
21
|
+
begin
|
22
|
+
`adb version`
|
23
|
+
found_adb = true
|
24
|
+
rescue Errno::ENOENT
|
25
|
+
end
|
26
|
+
if (!found_adb)
|
27
|
+
STDERR.puts "Could not find adb. Please install the Android SDK and make sure its platform-tools directory is in your PATH."
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Manymo
|
4
|
+
class ADBConnectionVerifier
|
5
|
+
include EM::Deferrable
|
6
|
+
|
7
|
+
module ADBConnection
|
8
|
+
def initialize(verifier)
|
9
|
+
@verifier = verifier
|
10
|
+
@packet = ADBPacket.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def post_init
|
14
|
+
#puts "Local adb socket opened. Sending CNXN packet."
|
15
|
+
send_data ADBPacket.build("CNXN", 0x01000000, 0x00001000, "host::\x00").data
|
16
|
+
end
|
17
|
+
|
18
|
+
def receive_data(data)
|
19
|
+
#puts "Client receive data!"
|
20
|
+
remainder = @packet.consume(data)
|
21
|
+
while @packet.complete? do
|
22
|
+
@verifier.succeed
|
23
|
+
@packet = ADBPacket.new
|
24
|
+
remainder = @packet.consume(remainder)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def unbind
|
29
|
+
#@verifier.fail("Could not connect to adb over tunnel.")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(port)
|
34
|
+
EM.connect('127.0.0.1', port+1, ADBConnection, self)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
module Manymo
|
3
|
+
class ADBPacket
|
4
|
+
attr_accessor :data
|
5
|
+
|
6
|
+
def initialize(data = "")
|
7
|
+
@data = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.build(cmd, arg0, arg1, body = "")
|
11
|
+
# Should be 0x00000232 for a CNXN packet
|
12
|
+
new(cmd + [arg0, arg1, body.length, compute_checksum(body), compute_magic(cmd)].pack("V*") + body)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.compute_magic(cmd)
|
16
|
+
cmd.unpack("V").first ^ 0xffffffff
|
17
|
+
end
|
18
|
+
|
19
|
+
def consume(data)
|
20
|
+
@data = @data + data
|
21
|
+
remainder = ""
|
22
|
+
if @data.length >= 16 && @data.length > data_length
|
23
|
+
remainder = @data[data_length..-1]
|
24
|
+
@data = @data[0,data_length]
|
25
|
+
end
|
26
|
+
remainder
|
27
|
+
end
|
28
|
+
|
29
|
+
def complete?
|
30
|
+
@data.length >= 16 && @data.length == data_length
|
31
|
+
end
|
32
|
+
|
33
|
+
def command
|
34
|
+
@data[0,4]
|
35
|
+
end
|
36
|
+
|
37
|
+
def arg0
|
38
|
+
@data[4,4].unpack('V').first
|
39
|
+
end
|
40
|
+
|
41
|
+
def arg1
|
42
|
+
@data[8,4].unpack('V').first
|
43
|
+
end
|
44
|
+
|
45
|
+
def payload_size
|
46
|
+
@data[12,4].unpack('V').first
|
47
|
+
end
|
48
|
+
|
49
|
+
def checksum
|
50
|
+
@data[16,4].unpack('V').first
|
51
|
+
end
|
52
|
+
|
53
|
+
def body
|
54
|
+
@data[header_size..-1]
|
55
|
+
end
|
56
|
+
|
57
|
+
def magic
|
58
|
+
@data[20,4].unpack('V').first
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.compute_checksum(data)
|
62
|
+
# Not really a crc32
|
63
|
+
data.bytes.inject{|sum,x| sum + x } || 0
|
64
|
+
end
|
65
|
+
|
66
|
+
def computed_checksum
|
67
|
+
self.class.compute_checksum(body)
|
68
|
+
end
|
69
|
+
|
70
|
+
def header_size
|
71
|
+
24
|
72
|
+
end
|
73
|
+
|
74
|
+
def data_length
|
75
|
+
payload_size + header_size
|
76
|
+
end
|
77
|
+
|
78
|
+
def checksum_ok?
|
79
|
+
computed_checksum == checksum
|
80
|
+
end
|
81
|
+
|
82
|
+
def hex_str(v)
|
83
|
+
"0x%08x" % v unless v.nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
"#{command}(#{hex_str(arg0)}, #{hex_str(arg1)}), payload_size=#{payload_size}, checksum=#{hex_str(checksum)}, computed_checksum=#{hex_str(computed_checksum)}, magic=#{hex_str(magic)}, body=#{body.inspect}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Manymo
|
2
|
+
class ADBStream
|
3
|
+
attr_accessor :local_id, :remote_id
|
4
|
+
attr_accessor :systemtype, :banner
|
5
|
+
attr_accessor :window_length, :spoofed_ack_count
|
6
|
+
|
7
|
+
def initialize(local_id, open_data)
|
8
|
+
@spoofed_ack_count = 0
|
9
|
+
@local_id = local_id
|
10
|
+
@system_type, @banner = open_data.split(':')
|
11
|
+
end
|
12
|
+
|
13
|
+
def quick_ack
|
14
|
+
@spoofed_ack_count += 1
|
15
|
+
ADBPacket.build("OKAY", remote_id, local_id)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'faye'
|
2
|
+
|
3
|
+
module Manymo
|
4
|
+
module ADBTunnel
|
5
|
+
|
6
|
+
def initialize(server, display, password)
|
7
|
+
@server = server
|
8
|
+
@display = display
|
9
|
+
@password = password
|
10
|
+
@connected = false
|
11
|
+
@q = []
|
12
|
+
@in_packet = ADBPacket.new
|
13
|
+
@out_packet = ADBPacket.new
|
14
|
+
@out_streams = {}
|
15
|
+
@local_port, @local_ip = Socket.unpack_sockaddr_in(get_sockname)
|
16
|
+
@peer_port, @peer_ip = Socket.unpack_sockaddr_in(get_peername)
|
17
|
+
end
|
18
|
+
|
19
|
+
def onclose(&blk)
|
20
|
+
@onclose = blk
|
21
|
+
end
|
22
|
+
|
23
|
+
def log_packet(prefix, pkt)
|
24
|
+
#puts prefix + ": " + pkt.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
# Packet from local socket out to websocket
|
28
|
+
def handle_outgoing_packet(p)
|
29
|
+
log_packet "#{@peer_port} -> ws", p
|
30
|
+
@ws.send p.data.bytes.to_a
|
31
|
+
case p.command
|
32
|
+
when "OPEN"
|
33
|
+
@out_streams[p.arg0] = ADBStream.new(p.arg0, p.body)
|
34
|
+
when "WRTE"
|
35
|
+
stream = @out_streams[p.arg0]
|
36
|
+
if stream
|
37
|
+
ack = stream.quick_ack
|
38
|
+
log_packet "+ws -> #{@peer_port}", ack
|
39
|
+
send_data ack.data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Packet from remote websocket to local socket
|
45
|
+
def handle_incoming_packet(p)
|
46
|
+
should_send = true
|
47
|
+
case p.command
|
48
|
+
when "CLSE"
|
49
|
+
@out_streams.delete p.arg1
|
50
|
+
when "OKAY"
|
51
|
+
stream = @out_streams[p.arg1]
|
52
|
+
if stream
|
53
|
+
if stream.spoofed_ack_count > 0
|
54
|
+
stream.spoofed_ack_count -= 1
|
55
|
+
should_send = false
|
56
|
+
log_packet "-ws -> #{@peer_port}", p
|
57
|
+
else
|
58
|
+
stream.remote_id = p.arg0
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
if should_send
|
63
|
+
log_packet "ws -> #{@peer_port}", p
|
64
|
+
send_data p.data
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def post_init
|
69
|
+
#puts "opening ws wss://#{@server}/adb_tunnel?display=#{@display}&password=#{@password}"
|
70
|
+
@ws = Faye::WebSocket::Client.new("wss://#{@server}/adb_tunnel?display=#{@display}&password=#{@password}", nil, {ping: 10})
|
71
|
+
@ws.on :open do
|
72
|
+
@connected = true
|
73
|
+
flush
|
74
|
+
end
|
75
|
+
|
76
|
+
@ws.on :message do |msg|
|
77
|
+
data = msg.data.pack('c*')
|
78
|
+
remainder = @in_packet.consume(data)
|
79
|
+
while @in_packet.complete? do
|
80
|
+
handle_incoming_packet(@in_packet)
|
81
|
+
@in_packet = ADBPacket.new
|
82
|
+
remainder = @in_packet.consume(remainder)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
@ws.on :close do |event|
|
87
|
+
puts "remote adb ws closed connection: #{event.code}"
|
88
|
+
if @onclose
|
89
|
+
close_event = TunnelCloseEvent.new(:websocket)
|
90
|
+
close_event.websocket_event = event
|
91
|
+
@onclose.call(close_event)
|
92
|
+
@onclose = nil
|
93
|
+
end
|
94
|
+
close_connection
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def unbind
|
99
|
+
#puts "unbind local adb"
|
100
|
+
if @onclose
|
101
|
+
close_event = TunnelCloseEvent.new(:local)
|
102
|
+
@onclose.call(close_event)
|
103
|
+
@onclose = nil
|
104
|
+
end
|
105
|
+
|
106
|
+
if @ws
|
107
|
+
@ws.close
|
108
|
+
@ws = nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def flush
|
113
|
+
if @connected
|
114
|
+
@q.each do |data|
|
115
|
+
remainder = @out_packet.consume(data)
|
116
|
+
while @out_packet.complete? do
|
117
|
+
handle_outgoing_packet(@out_packet)
|
118
|
+
@out_packet = ADBPacket.new
|
119
|
+
remainder = @out_packet.consume(remainder)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
@q = []
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def receive_data(data)
|
127
|
+
#puts "local data: #{data.inspect}"
|
128
|
+
@q << data
|
129
|
+
flush
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module Manymo
|
4
|
+
class Command
|
5
|
+
class <<self
|
6
|
+
def start(argv)
|
7
|
+
@argv = argv
|
8
|
+
@options = {}
|
9
|
+
@parser = OptionParser.new do |opts|
|
10
|
+
opts.banner = <<EOT
|
11
|
+
manymo - Manymo Command Line Tool (Version #{Manymo::VERSION})
|
12
|
+
|
13
|
+
Full documentation at https://www.manymo.com/pages/documentation/manymo-command-line-tool
|
14
|
+
|
15
|
+
Usage:
|
16
|
+
|
17
|
+
manymo [options] COMMAND [ARGUMENTS]
|
18
|
+
|
19
|
+
Commands:
|
20
|
+
launch EMULATORNAME Launch a headless emulator and make it appear like a local device
|
21
|
+
list List emulators; use the name attribute with the launch command
|
22
|
+
token Display a prompt to enter authorization token
|
23
|
+
tunnel TUNNELKEY Make an in-browser emulator appear like a local device
|
24
|
+
|
25
|
+
Options:
|
26
|
+
EOT
|
27
|
+
|
28
|
+
opts.on("--adb-path PATH_TO_ADB", "Specify path to adb executable; otherwise uses the one in your path") do |v|
|
29
|
+
@options[:adb_path] = v
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
@parser.parse!
|
35
|
+
rescue
|
36
|
+
usage
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
command = @argv[0]
|
41
|
+
|
42
|
+
@adb = ADB.new(@options[:adb_path])
|
43
|
+
|
44
|
+
@adb.ensure_available
|
45
|
+
|
46
|
+
@service = Service.new
|
47
|
+
|
48
|
+
case command
|
49
|
+
when /token/
|
50
|
+
get_auth_token(true)
|
51
|
+
when /tunnel/
|
52
|
+
args = @argv[1].split(':')
|
53
|
+
if args.count == 3
|
54
|
+
start_tunnel(args[0], args[1], args[2])
|
55
|
+
else
|
56
|
+
usage
|
57
|
+
end
|
58
|
+
when /list/
|
59
|
+
list_emulators
|
60
|
+
when /launch/
|
61
|
+
if @argv[1]
|
62
|
+
launch(@argv[1])
|
63
|
+
else
|
64
|
+
usage
|
65
|
+
end
|
66
|
+
when /version/
|
67
|
+
puts "Version #{Manymo::VERSION}"
|
68
|
+
else
|
69
|
+
usage
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def usage
|
74
|
+
puts "#{@parser.banner}#{@parser.summarize.join("\n")}"
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def list_emulators
|
79
|
+
puts @service.get("/emulators")
|
80
|
+
end
|
81
|
+
|
82
|
+
def launch(name)
|
83
|
+
hostname, emulator_console_port, password = @service.get("/emulators/launch_emulator/#{name}").split(":")
|
84
|
+
start_tunnel(hostname, emulator_console_port, password)
|
85
|
+
end
|
86
|
+
|
87
|
+
def start_tunnel(server, port, password)
|
88
|
+
EM.run {
|
89
|
+
# hit Control + C to stop
|
90
|
+
Signal.trap("INT") { EventMachine.stop }
|
91
|
+
Signal.trap("TERM") { EventMachine.stop }
|
92
|
+
EM
|
93
|
+
tunnel = Tunnel.new(server, port, password, @adb)
|
94
|
+
|
95
|
+
EM.add_shutdown_hook {
|
96
|
+
tunnel.shutdown("Process terminated")
|
97
|
+
}
|
98
|
+
tunnel.errback{ |message|
|
99
|
+
STDERR.puts message
|
100
|
+
exit 1
|
101
|
+
}
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Manymo
|
2
|
+
module ConsoleTunnel
|
3
|
+
def initialize(server, display, password)
|
4
|
+
@server = server
|
5
|
+
@display = display
|
6
|
+
@password = password
|
7
|
+
@connected = false
|
8
|
+
@q = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def onclose(&blk)
|
12
|
+
@onclose = blk
|
13
|
+
end
|
14
|
+
|
15
|
+
def post_init
|
16
|
+
#puts "opening ws wss://#{@server}/console_tunnel?display=#{@display}&password=#{@password}"
|
17
|
+
@ws = Faye::WebSocket::Client.new("wss://#{@server}/console_tunnel?display=#{@display}&password=#{@password}")
|
18
|
+
@ws.on :open do
|
19
|
+
@connected = true
|
20
|
+
flush
|
21
|
+
end
|
22
|
+
|
23
|
+
@ws.on :message do |msg|
|
24
|
+
#puts "console incoming: #{msg.data}"
|
25
|
+
send_data msg.data
|
26
|
+
end
|
27
|
+
|
28
|
+
@ws.on :close do |event|
|
29
|
+
if @onclose
|
30
|
+
close_event = TunnelCloseEvent.new(:websocket)
|
31
|
+
close_event.websocket_event = event
|
32
|
+
@onclose.call(close_event)
|
33
|
+
@onclose = nil
|
34
|
+
end
|
35
|
+
@ws = nil
|
36
|
+
close_connection
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def unbind
|
41
|
+
if @onclose
|
42
|
+
close_event = TunnelCloseEvent.new(:local)
|
43
|
+
@onclose.call(close_event)
|
44
|
+
@onclose = nil
|
45
|
+
end
|
46
|
+
if @ws
|
47
|
+
@ws.close
|
48
|
+
@ws = nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def flush
|
53
|
+
if @connected
|
54
|
+
@q.each do |msg|
|
55
|
+
@ws.send msg.bytes.to_a
|
56
|
+
end
|
57
|
+
@q = []
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def receive_data(data)
|
62
|
+
#puts "console outgoing: #{data.inspect}"
|
63
|
+
@q << data
|
64
|
+
flush
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "net/https"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module Manymo
|
5
|
+
class Service
|
6
|
+
HOST= ENV["MANYMO_API_HOST"] || "www.manymo.com"
|
7
|
+
BASE_URL="https://#{HOST}/api/v1"
|
8
|
+
def get_auth_token(force = false)
|
9
|
+
manymo_config_dir = File.expand_path('~/.manymo')
|
10
|
+
auth_token_path = manymo_config_dir + '/auth_token'
|
11
|
+
if ! force and File.exists?(auth_token_path)
|
12
|
+
return File.read(auth_token_path)
|
13
|
+
else
|
14
|
+
print "Please visit https://#{HOST}/user/client_applications to get your authorization token. Enter it here: "
|
15
|
+
STDOUT.flush
|
16
|
+
auth_token = STDIN.gets.chomp
|
17
|
+
if auth_token.empty?
|
18
|
+
STDERR.puts "No token supplied. Exiting."
|
19
|
+
exit 1
|
20
|
+
else
|
21
|
+
begin
|
22
|
+
if !File.exists?(manymo_config_dir)
|
23
|
+
FileUtils.mkdir(manymo_config_dir)
|
24
|
+
end
|
25
|
+
File.open(auth_token_path, "w") do |auth_token_path_file_handle|
|
26
|
+
auth_token_path_file_handle.write(auth_token)
|
27
|
+
end
|
28
|
+
rescue Errno::EACCES
|
29
|
+
STDERR.puts "Unable to store api token in #{auth_token_path}. You will be prompted again, next time this command is run."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
return auth_token
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def get(endpoint)
|
37
|
+
auth_token = get_auth_token
|
38
|
+
#puts "Auth token: #{auth_token}"
|
39
|
+
uri = URI.parse(BASE_URL + endpoint)
|
40
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
41
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
42
|
+
if uri.scheme == 'https'
|
43
|
+
http.use_ssl = true
|
44
|
+
end
|
45
|
+
|
46
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
47
|
+
request["Authorization"] = "OAuth token=#{auth_token}"
|
48
|
+
request["Accept"] = "text/plain"
|
49
|
+
|
50
|
+
begin
|
51
|
+
response = http.request(request)
|
52
|
+
if response.code.to_i / 100 == 2
|
53
|
+
response.body
|
54
|
+
elsif response.code.to_i == 401
|
55
|
+
puts "Invalid authorization token."
|
56
|
+
get_auth_token(true)
|
57
|
+
get(endpoint)
|
58
|
+
else
|
59
|
+
STDERR.puts "Error #{response.code}: #{response.body}"
|
60
|
+
exit 1
|
61
|
+
end
|
62
|
+
rescue SystemExit, Interrupt
|
63
|
+
exit 1
|
64
|
+
rescue Exception => e
|
65
|
+
STDERR.puts "Error: #{e.inspect}"
|
66
|
+
STDERR.puts "Please check that your network is connected and that no firewall rules are blocking #{uri.scheme} requests."
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require "socket"
|
3
|
+
require 'fileutils'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module Manymo
|
7
|
+
class Tunnel
|
8
|
+
include EM::Deferrable
|
9
|
+
attr_accessor :serial_number
|
10
|
+
|
11
|
+
def initialize(server, port, password, adb)
|
12
|
+
@shutdown = false
|
13
|
+
@server = server
|
14
|
+
@port = port
|
15
|
+
@password = password
|
16
|
+
@display = (port.to_i - 5554) / 2
|
17
|
+
@adb = adb
|
18
|
+
EM::next_tick {
|
19
|
+
start
|
20
|
+
}
|
21
|
+
@adb_tunnels = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def lockfile_for_port(port)
|
25
|
+
File.expand_path("~/.manymo/.tunnel_#{port}.lock")
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_lockfile(port)
|
29
|
+
# Raises Errno::EEXIST if lockfile cannot be created
|
30
|
+
tunnel_lockfile = lockfile_for_port(port)
|
31
|
+
if File.exists?(tunnel_lockfile) && (Time.new - File.mtime(tunnel_lockfile)) > 60
|
32
|
+
File.unlink(tunnel_lockfile)
|
33
|
+
end
|
34
|
+
File.open(tunnel_lockfile, File::WRONLY|File::CREAT|File::EXCL) { |file| file.write $$ }
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_lockfile(port)
|
38
|
+
begin
|
39
|
+
File.unlink(lockfile_for_port(port))
|
40
|
+
rescue Errno::ENOENT
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_available_local_emulator_port
|
45
|
+
(5554...5754).step(2) do |local_port|
|
46
|
+
created_lock = false
|
47
|
+
begin
|
48
|
+
create_lockfile(local_port)
|
49
|
+
created_lock = true
|
50
|
+
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
51
|
+
socket.bind(Socket.pack_sockaddr_in( local_port, '127.0.0.1' ))
|
52
|
+
socket.close
|
53
|
+
return local_port
|
54
|
+
rescue Errno::EADDRINUSE, Errno::EEXIST
|
55
|
+
remove_lockfile(local_port) if created_lock
|
56
|
+
end
|
57
|
+
end
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def is_device_listed?(console_port)
|
62
|
+
devices_output = `#{@adb.path.shellescape} devices`
|
63
|
+
#puts "output #{devices_output}"
|
64
|
+
devices_output.match(/emulator-#{console_port}/)
|
65
|
+
end
|
66
|
+
|
67
|
+
def start_tunnels_with_local_port(local_port)
|
68
|
+
#puts "Starting tunnels for #{local_port}"
|
69
|
+
EventMachine::start_server '0.0.0.0', local_port, ConsoleTunnel, @server, @display, @password do |tunnel|
|
70
|
+
tunnel.onclose { |event|
|
71
|
+
if event.authorization_denied?
|
72
|
+
self.fail("Authorization denied for console connection.")
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
EventMachine::start_server '0.0.0.0', local_port+1, ADBTunnel, @server, @display, @password do |tunnel|
|
77
|
+
@adb_tunnels << tunnel
|
78
|
+
tunnel.onclose { |event|
|
79
|
+
#puts "adb closed: #{event}"
|
80
|
+
@adb_tunnels.delete(tunnel)
|
81
|
+
if event.authorization_denied?
|
82
|
+
shutdown("Authorization denied for adb connection.")
|
83
|
+
elsif event.emulator_terminated?
|
84
|
+
shutdown("Emulator terminated.")
|
85
|
+
elsif @adb_tunnels.empty? && !@shutdown
|
86
|
+
# retry
|
87
|
+
puts "adb tunnel closed. retrying..."
|
88
|
+
connect_emulator_to_adb_server(local_port)
|
89
|
+
end
|
90
|
+
}
|
91
|
+
end
|
92
|
+
connect_emulator_to_adb_server(local_port)
|
93
|
+
|
94
|
+
@timeout_timer = EM::Timer.new(50) do
|
95
|
+
shutdown("Timed out attempting to connect to emulator.")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def connect_emulator_to_adb_server(port)
|
100
|
+
connection_verifier = ADBConnectionVerifier.new(port)
|
101
|
+
connection_verifier.callback {
|
102
|
+
@serial_number = "emulator-#{port}"
|
103
|
+
|
104
|
+
# This will start adb server if it isn't started
|
105
|
+
listed = is_device_listed?(port)
|
106
|
+
|
107
|
+
if !listed
|
108
|
+
s = TCPSocket.open('localhost', 5037)
|
109
|
+
#s.puts("0012host:emulator:#{port+1}")
|
110
|
+
# Check again
|
111
|
+
listed = is_device_listed?(port)
|
112
|
+
listed = true
|
113
|
+
end
|
114
|
+
|
115
|
+
if listed
|
116
|
+
@timeout_timer.cancel if @timeout_timer
|
117
|
+
puts "Tunnel established; local serial number is: " + @serial_number
|
118
|
+
else
|
119
|
+
shutdown("Error connecting emulator to adb server.")
|
120
|
+
end
|
121
|
+
}
|
122
|
+
connection_verifier.errback {
|
123
|
+
shutdown("Error connecting to emulator emulator.")
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def shutdown(reason)
|
128
|
+
@shutdown = true
|
129
|
+
self.fail(reason)
|
130
|
+
end
|
131
|
+
|
132
|
+
def start
|
133
|
+
local_port = find_available_local_emulator_port
|
134
|
+
if local_port
|
135
|
+
begin
|
136
|
+
start_tunnels_with_local_port(local_port)
|
137
|
+
ensure
|
138
|
+
remove_lockfile(local_port)
|
139
|
+
end
|
140
|
+
else
|
141
|
+
self.fail("Unable to allocate local port in range 5554-5754 for tunnel")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Manymo
|
2
|
+
class TunnelCloseEvent
|
3
|
+
attr_accessor :reason
|
4
|
+
attr_accessor :websocket_event
|
5
|
+
|
6
|
+
def initialize(reason)
|
7
|
+
@reason = reason
|
8
|
+
end
|
9
|
+
|
10
|
+
def normal_closure?
|
11
|
+
@reason == :websocket && @websocket_event.code == 1000
|
12
|
+
end
|
13
|
+
|
14
|
+
def authorization_denied?
|
15
|
+
@reason == :websocket && @websocket_event.code == 4008
|
16
|
+
end
|
17
|
+
|
18
|
+
def emulator_terminated?
|
19
|
+
@reason == :websocket && @websocket_event.code == 4009
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/manymo.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'manymo/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "manymo"
|
8
|
+
spec.version = Manymo::VERSION
|
9
|
+
spec.authors = ["Pete Schwamb"]
|
10
|
+
spec.email = ["pete@manymo.com"]
|
11
|
+
spec.description = %q{Manymo client tool.}
|
12
|
+
spec.summary = %q{A command line utility for connecting to remote emulators.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'faye', '~> 1.0.1'
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: manymo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0.beta
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pete Schwamb
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faye
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Manymo client tool.
|
56
|
+
email:
|
57
|
+
- pete@manymo.com
|
58
|
+
executables:
|
59
|
+
- manymo
|
60
|
+
- tunnel_test.rb
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- .gitignore
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- bin/manymo
|
70
|
+
- bin/tunnel_test.rb
|
71
|
+
- lib/manymo.rb
|
72
|
+
- lib/manymo/adb.rb
|
73
|
+
- lib/manymo/adb_connection_verifier.rb
|
74
|
+
- lib/manymo/adb_packet.rb
|
75
|
+
- lib/manymo/adb_stream.rb
|
76
|
+
- lib/manymo/adb_tunnel.rb
|
77
|
+
- lib/manymo/command.rb
|
78
|
+
- lib/manymo/console_tunnel.rb
|
79
|
+
- lib/manymo/service.rb
|
80
|
+
- lib/manymo/tunnel.rb
|
81
|
+
- lib/manymo/tunnel_close_event.rb
|
82
|
+
- lib/manymo/version.rb
|
83
|
+
- manymo.gemspec
|
84
|
+
homepage: ''
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - '>'
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 1.3.1
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 2.1.11
|
105
|
+
signing_key:
|
106
|
+
specification_version: 4
|
107
|
+
summary: A command line utility for connecting to remote emulators.
|
108
|
+
test_files: []
|