smartdust-client 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,156 @@
1
+ require 'ADB'
2
+
3
+ require 'stf/client'
4
+ require 'stf/log/log'
5
+ require 'stf/model/device_list'
6
+
7
+ module Stf
8
+ class StartDebugSessionInteractor
9
+
10
+ include Log
11
+ include ADB
12
+
13
+ def execute(opts = {})
14
+ all_flag = opts[:all]
15
+ nodaemon_flag = opts[:nodaemon]
16
+ filter = opts[:filter]
17
+ max_n = opts[:n].to_i > 0 ? opts[:n].to_i : 1
18
+ start_timeout = opts[:starttime].to_i > 0 ? opts[:starttime].to_i : 120
19
+ session = opts[:worktime].to_i > 0 ? opts[:session].to_i : 10800
20
+ min_n = opts[:min].to_s.empty? ? (max_n + 1) / 2 : [opts[:min].to_i, max_n].min
21
+ healthcheck = opts[:health]
22
+ force_filter = opts[:forcefilter]
23
+
24
+ DI[:demonizer].kill unless opts[:nokill]
25
+
26
+ wanted = nodaemon_flag ? max_n : min_n
27
+
28
+ begin
29
+ connect_loop(all_flag: all_flag,
30
+ wanted: wanted,
31
+ filter: filter,
32
+ force_filter: force_filter,
33
+ healthcheck: healthcheck,
34
+ delay: 5,
35
+ timeout: start_timeout)
36
+
37
+ rescue SignalException => e
38
+ logger.info "Caught signal \"#{e.message}\""
39
+ DI[:stop_all_debug_sessions_interactor].execute
40
+ return false
41
+ rescue Exception => e
42
+ logger.info "Exception \"#{e.message}\" during initial connect loop"
43
+ DI[:stop_all_debug_sessions_interactor].execute
44
+ return false
45
+ end
46
+
47
+ connected_count = count_connected_devices(filter)
48
+ logger.info "Lower quantity achieved, already connected #{connected_count}"
49
+
50
+ return true if nodaemon_flag
51
+
52
+ # will be daemon here
53
+ DI[:demonizer].run do
54
+ connect_loop(all_flag: all_flag,
55
+ wanted: max_n,
56
+ filter: filter,
57
+ force_filter: force_filter,
58
+ healthcheck: healthcheck,
59
+ daemon_mode: true,
60
+ delay: 30,
61
+ timeout: session)
62
+
63
+ DI[:stop_all_debug_sessions_interactor].execute(byFilter: filter, nokill: true)
64
+ end
65
+
66
+ return true
67
+ end
68
+
69
+ def connect_loop(all_flag: false,
70
+ wanted: 1,
71
+
72
+ filter: nil,
73
+ force_filter: false,
74
+ healthcheck: nil,
75
+
76
+ daemon_mode: false,
77
+ delay: 5,
78
+ timeout: 120)
79
+ finish_time = Time.now + timeout
80
+ one_time_mode = !daemon_mode
81
+
82
+ while true do
83
+ cleanup_disconnected_devices(filter, force_filter, healthcheck)
84
+
85
+ if one_time_mode && Time.now > finish_time
86
+ raise "Connect loop timeout reached"
87
+ end
88
+
89
+ all_devices = DeviceList.new(DI[:stf].get_devices)
90
+ stf_devices = all_devices.select_ready_to_connect
91
+ stf_devices = stf_devices.by_filter(filter) if filter
92
+ stf_devices = stf_devices.select_healthy_for_connect(healthcheck) if healthcheck
93
+
94
+ if all_flag
95
+ to_connect = stf_devices.size
96
+ else
97
+ connected = devices & all_devices.as_connect_url_list
98
+ to_connect = wanted - connected.size
99
+ end
100
+
101
+ return if one_time_mode && to_connect <= 0
102
+
103
+ if to_connect > 0
104
+ if stf_devices.empty?
105
+ logger.error 'There is no available devices with criteria ' + filter
106
+ else
107
+ random_device = stf_devices.asArray.sample
108
+ DI[:start_one_debug_session_interactor].execute(random_device)
109
+ next
110
+ end
111
+ end
112
+
113
+ sleep delay
114
+ end
115
+ end
116
+
117
+ def count_connected_devices(filter)
118
+ stf_devices = DeviceList.new(DI[:stf].get_user_devices)
119
+ stf_devices = stf_devices.by_filter(filter) if filter
120
+ connected = devices & stf_devices.as_connect_url_list
121
+ connected.size
122
+ end
123
+
124
+ def cleanup_disconnected_devices(filter, force_filter, healthcheck)
125
+ to_disconnect = []
126
+ stf_devices = DeviceList.new(DI[:stf].get_user_devices)
127
+
128
+ if filter && force_filter
129
+ disconnect_because_filter = stf_devices.except_filter(filter).as_connect_url_list
130
+ unless disconnect_because_filter.empty?
131
+ logger.info 'will be disconnected by filter: ' + disconnect_because_filter.join(',')
132
+ to_disconnect += disconnect_because_filter
133
+ end
134
+ end
135
+
136
+ if healthcheck
137
+ disconnect_by_health = stf_devices.select_not_healthy(healthcheck).as_connect_url_list
138
+ unless disconnect_by_health.empty?
139
+ logger.info 'will be disconnected by health check: ' + disconnect_by_health.join(',')
140
+ to_disconnect += disconnect_by_health
141
+ end
142
+ end
143
+
144
+ dead_persons = stf_devices.as_connect_url_list - devices
145
+ unless dead_persons.empty?
146
+ logger.info 'will be disconnected because not present locally: ' + dead_persons.join(',')
147
+ to_disconnect += dead_persons
148
+ end
149
+
150
+ to_disconnect.reject {|url| url.to_s.empty?}.uniq.each do |url|
151
+ logger.info 'Cleanup the device ' + url.to_s
152
+ DI[:stop_debug_session_interactor].execute(url)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,76 @@
1
+ require 'di'
2
+ require 'ADB'
3
+
4
+ require 'stf/client'
5
+ require 'stf/log/log'
6
+
7
+ module Stf
8
+ class StartOneDebugSessionInteractor
9
+
10
+ include Log
11
+ include ADB
12
+
13
+ def execute(device)
14
+ return false if device.nil?
15
+ serial = device.serial
16
+
17
+ begin
18
+ success = DI[:stf].add_device serial
19
+ if success
20
+ logger.info "Device added #{serial}"
21
+ else
22
+ logger.error "Can't add device #{serial}"
23
+ raise
24
+ end
25
+
26
+ result = DI[:stf].start_debug serial
27
+ if result.success
28
+ logger.info "Debug started #{serial}"
29
+ else
30
+ logger.error "Can't start debugging session for device #{serial}"
31
+ raise
32
+ end
33
+
34
+ provider = device.getValue "provider"
35
+ provider_ip = provider["ip"]
36
+ remote_connect_url_split = result.remoteConnectUrl.split ":"
37
+ remote_connect_port = remote_connect_url_split[1]
38
+ result = DI[:stf].open_tunnel provider_ip, remote_connect_port.to_i
39
+ if result.success
40
+ logger.info "Opened tunnel to provider with IP #{provider_ip}"
41
+ else
42
+ logger.error "Can't open tunnel to provider with IP #{provider_ip}"
43
+ raise
44
+ end
45
+ remote_connect_tunneled_url = DI[:device_enhancer].get_tunneled_remote_connect_url(device)
46
+ logger.info remote_connect_tunneled_url
47
+ execute_adb_with 30, "connect #{remote_connect_tunneled_url}"
48
+
49
+ shell('echo adbtest', {serial: "#{remote_connect_tunneled_url}"}, 30)
50
+ raise ADBError, "Could not execute shell test" unless stdout_contains "adbtest"
51
+
52
+ return true
53
+
54
+ rescue StandardError, SignalException => e
55
+ begin
56
+ # we will try clean anyway
57
+ DI[:stf].remove_device serial
58
+ if test ?d, '/custom-metrics'
59
+ File.open('/custom-metrics/openstf_connect_fail', 'a') do |f|
60
+ message = (!e.nil? || !e.message.nil?) ? e.message : ""
61
+ f.write("openstf_connect_fail,reason=\"#{escape(message)}\",serial=\"#{escape(serial)}\" count=1i #{Time.now.to_i}\n")
62
+ end
63
+ end
64
+ rescue
65
+ end
66
+
67
+ logger.error "Failed to connect to #{serial}: " + e&.message
68
+ return false
69
+ end
70
+ end
71
+
72
+ def escape(s)
73
+ s.gsub(/["]/, '\"').gsub(/[ ]/, '\ ').gsub(/[=]/, '\=').gsub(/[,]/, '\,')
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,27 @@
1
+ require 'di'
2
+ require 'ADB'
3
+
4
+ require 'stf/client'
5
+ require 'stf/log/log'
6
+ require 'stf/interactor/stop_debug_session_interactor'
7
+ require 'stf/model/device_list'
8
+
9
+ module Stf
10
+ class StopAllDebugSessionsInteractor
11
+ include Log
12
+ include ADB
13
+
14
+ # byFilter:
15
+ def execute(options = {})
16
+ DI[:demonizer].kill unless options[:nokill]
17
+
18
+ stf_devices = DeviceList.new(DI[:stf].get_user_devices)
19
+
20
+ stf_devices = stf_devices.by_filter options[:byFilter] if options[:byFilter]
21
+
22
+ pending_disconnect = stf_devices.as_connect_url_list
23
+
24
+ pending_disconnect.each {|d| DI[:stop_debug_session_interactor].execute d}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ require 'di'
2
+ require 'ADB'
3
+
4
+ require 'stf/client'
5
+ require 'stf/log/log'
6
+
7
+ module Stf
8
+ class StopDebugSessionInteractor
9
+ include Log
10
+ include ADB
11
+
12
+ def execute(remote_connect_url)
13
+ remote_devices = DI[:stf].get_user_devices
14
+ device = remote_devices.find {|d| d.remoteConnect == true && DI[:device_enhancer].get_tunneled_remote_connect_url(d).eql?(remote_connect_url)}
15
+
16
+ # try to disconnect anyway
17
+ execute_adb_with 30, "disconnect #{remote_connect_url}"
18
+
19
+ if device.nil?
20
+ logger.error "Device #{remote_connect_url} is not available"
21
+ return false
22
+ end
23
+
24
+ success = false
25
+
26
+ 1..10.times do
27
+ begin
28
+ success = DI[:stf].stop_debug(device.serial)
29
+ break if success
30
+ rescue
31
+ end
32
+
33
+ logger.error 'Can\'t stop debug session. Retrying'
34
+ end
35
+
36
+ 1..10.times do
37
+ begin
38
+ success = DI[:stf].remove_device(device.serial)
39
+ break if success
40
+ rescue
41
+ end
42
+ logger.error 'Can\'t remove device from user devices. Retrying'
43
+ end
44
+
45
+ if success
46
+ logger.info "Successfully removed #{remote_connect_url}"
47
+ else
48
+ logger.error "Error removing #{remote_connect_url}"
49
+ end
50
+
51
+ success
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,18 @@
1
+ require 'logger'
2
+
3
+ module Stf
4
+ module Log
5
+
6
+ @@logger = Logger.new(STDOUT)
7
+ @@logger.level = Logger::INFO
8
+
9
+ def logger
10
+ @@logger
11
+ end
12
+
13
+ def self.verbose(enable)
14
+ @@logger.level = enable ? Logger::DEBUG : Logger::INFO
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ module Stf
2
+ require 'facets/boolean'
3
+ class Device < OpenStruct
4
+ def getValue(key)
5
+ getValueFromObject(self, key)
6
+ end
7
+
8
+ def getKeys
9
+ getKeysNextLevel('', self)
10
+ end
11
+
12
+ # more pessimistic decision
13
+ def healthy_for_connect?(pattern)
14
+ return true if pattern.nil?
15
+ health = healthy?(pattern)
16
+ ppp = pattern.split(',')
17
+ ppp.each do |p|
18
+ health &&= getValue('battery.temp').to_i < 30 if ['t', 'temp', 'temperature'].include? p
19
+ health &&= getValue('battery.level').to_f > 30.0 if ['b', 'batt', 'battery'].include? p
20
+ end
21
+ health
22
+ end
23
+
24
+ def healthy?(pattern)
25
+ return true if pattern.nil?
26
+ ppp = pattern.split(',')
27
+ health = true
28
+ ppp.each do |p|
29
+ health &&= getValue('battery.temp').to_i < 32 if ['t', 'temp', 'temperature'].include? p
30
+ health &&= getValue('battery.level').to_f > 20.0 if ['b', 'batt', 'battery'].include? p
31
+ health &&= getValue('network.connected') if ['n', 'net', 'network'].include? p
32
+ health &&= getValue('network.type') == 'VPN' if ['vpn'].include? p
33
+ health &&= getValue('network.type') == 'WIFI' if ['wifi'].include? p
34
+ end
35
+ health
36
+ end
37
+
38
+ def checkFilter?(filter)
39
+ return true if filter.nil?
40
+ key, value = filter.split(':', 2)
41
+ if value == 'true' || value == 'false'
42
+ value = value.to_b
43
+ end
44
+ getValue(key) == value
45
+ end
46
+
47
+ def getKeysNextLevel(prefix, o)
48
+ return [] if o.nil?
49
+
50
+ o.each_pair.flat_map do |k, v|
51
+ if v.is_a? OpenStruct
52
+ getKeysNextLevel(concat(prefix, k.to_s), v)
53
+ else
54
+ [concat(prefix, k.to_s)]
55
+ end
56
+ end
57
+ end
58
+
59
+ def concat(prefix, key)
60
+ prefix.to_s.empty? ? key : prefix + '.' + key
61
+ end
62
+
63
+ def getValueFromObject(obj, key)
64
+ keys = key.split('.', 2)
65
+ if keys[1].nil?
66
+ obj[key]
67
+ else
68
+ getValueFromObject(obj[keys[0]], keys[1])
69
+ end
70
+ end
71
+
72
+ private :getValueFromObject, :concat, :getKeysNextLevel
73
+
74
+ end
75
+ end
@@ -0,0 +1,17 @@
1
+ module Stf
2
+ class DeviceEnhancer
3
+ def get_tunneled_remote_connect_url(device)
4
+ provider = device.provider
5
+ provider_ip = provider["ip"]
6
+ result = DI[:stf].start_debug device.serial
7
+ remote_connect_url_split = result.remoteConnectUrl.split ":"
8
+ remote_connect_port = remote_connect_url_split[1]
9
+ result = DI[:stf].check_tunnel provider_ip, remote_connect_port
10
+ if result.success
11
+ remote_connect_hostname = remote_connect_url_split[0]
12
+ remote_connect_hostname + ":" + result.port.to_s
13
+ else nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,80 @@
1
+ require 'stf/model/device'
2
+
3
+ module Stf
4
+ # can not inherite from Array because http://words.steveklabnik.com/beware-subclassing-ruby-core-classes
5
+ class DeviceList
6
+ def initialize(devices)
7
+ if devices.nil?
8
+ @devices = Array.new
9
+ else
10
+ @devices = devices.map {|d| (d.kind_of? Device) ? d : Device.new(d)}
11
+ end
12
+ end
13
+
14
+ def by_filter(filter)
15
+ filter ? select {|d| d.checkFilter?(filter)} : []
16
+ end
17
+
18
+ def except_filter(filter)
19
+ filter ? reject {|d| d.checkFilter?(filter)} : this
20
+ end
21
+
22
+ def select_healthy(pattern)
23
+ pattern ? select { |d| d.healthy?(pattern) } : this
24
+ end
25
+
26
+ # more pessimistic than healthy()
27
+ def select_healthy_for_connect(pattern)
28
+ pattern ? select { |d| d.healthy_for_connect?(pattern) } : this
29
+ end
30
+
31
+ def select_not_healthy(pattern)
32
+ pattern ? reject { |d| d.healthy?(pattern) } : []
33
+ end
34
+
35
+ def select_ready_to_connect
36
+ # https://github.com/openstf/stf/blob/93d9d7fe859bb7ca71669f375d841d94fa47d751/lib/wire/wire.proto#L170
37
+ # enum DeviceStatus {
38
+ # OFFLINE = 1;
39
+ # UNAUTHORIZED = 2;
40
+ # ONLINE = 3;
41
+ # CONNECTING = 4;
42
+ # AUTHORIZING = 5;
43
+ # }
44
+ #
45
+ # https://github.com/openstf/stf/blob/93d9d7fe859bb7ca71669f375d841d94fa47d751/res/app/components/stf/device/enhance-device/enhance-device-service.js
46
+ select {|d|
47
+ d.present == true &&
48
+ d.status == 3 &&
49
+ d.ready == true &&
50
+ d.using == false &&
51
+ d.owner.nil?
52
+ }
53
+ end
54
+
55
+ def as_connect_url_list
56
+ @devices.reject { |d| d.remoteConnectUrl.nil? || d.remoteConnectUrl.empty? }
57
+ .map{|d| DI[:device_enhancer].get_tunneled_remote_connect_url(d)}
58
+ end
59
+
60
+ def select
61
+ DeviceList.new(@devices.select {|d| yield(d)})
62
+ end
63
+
64
+ def reject
65
+ DeviceList.new(@devices.select {|d| !yield(d)})
66
+ end
67
+
68
+ def empty?
69
+ @devices.empty?
70
+ end
71
+
72
+ def size
73
+ @devices.size
74
+ end
75
+
76
+ def asArray
77
+ @devices
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,20 @@
1
+ module Stf
2
+ class Demonizer
3
+ def initialize(dante, opts = {})
4
+ @dante = dante
5
+
6
+ @pid_path = opts[:pid_path].to_s.empty? ? '/tmp/stf-client.pid' : opts[:pid_path]
7
+ @log_path = opts[:log_path].to_s.empty? ? '/tmp/stf-client.log' : opts[:log_path]
8
+ end
9
+
10
+ def run
11
+ @dante.execute(daemonize: true,
12
+ pid_path: @pid_path,
13
+ log_path: @log_path) {yield}
14
+ end
15
+
16
+ def kill
17
+ @dante.execute(kill: true, pid_path: @pid_path)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ module Stf
2
+ class URIValidator
3
+ def validate(uri)
4
+ return (uri =~ /\A#{URI::regexp(%w(http https))}\z/) != nil
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Stf
2
+ VERSION = '1.1.0'
3
+ end
@@ -0,0 +1,121 @@
1
+ module Stf
2
+ module CLI
3
+ require 'di'
4
+ require 'gli'
5
+
6
+ include GLI::App
7
+
8
+ extend self
9
+
10
+ program_desc "Smartdust Lab client (version #{Stf::VERSION})"
11
+
12
+ desc 'Be verbose'
13
+ switch [:v, :verbose]
14
+
15
+ desc 'PID file'
16
+ flag [:pid]
17
+
18
+ desc 'Log file'
19
+ flag [:log]
20
+
21
+ desc 'Authorization token, can also be set by environment variable SD_TOKEN'
22
+ flag [:t, :token]
23
+
24
+ desc 'URL to Smartdust Lab, can also be set by environment variable SD_URL'
25
+ flag [:u, :url]
26
+
27
+ pre do |global_options, command, options, args|
28
+
29
+ global_options[:url] = ENV['SD_URL'] if global_options[:url].nil?
30
+ global_options[:token] = ENV['SD_TOKEN'] if global_options[:token].nil?
31
+
32
+ help_now!('Smartdust Lab url is required') if global_options[:url].nil?
33
+ help_now!('Authorization token is required') if global_options[:token].nil?
34
+
35
+ Log::verbose(global_options[:verbose])
36
+
37
+ DI.init(global_options)
38
+
39
+ help_now!('Valid Smartdust Lab url is required, e.g. https://public.smartdust.me') if !DI[:uri_validator].validate(global_options[:url])
40
+ true
41
+ end
42
+
43
+ desc 'Search for a device available in Smartdust Lab and attach it to local adb server'
44
+ command :connect do |c|
45
+ c.desc 'Connect to all available devices'
46
+ c.switch [:all]
47
+ c.desc 'Required quantity of devices'
48
+ c.flag [:n]
49
+ c.desc 'Minimal quantity of devices, n/2 by default'
50
+ c.flag [:min]
51
+ c.desc 'Filter key:value for devices'
52
+ c.flag [:f, :filter]
53
+ c.desc 'Force filter check for connected devices'
54
+ c.switch [:forcefilter, :ff]
55
+ c.desc 'Check selected health parameters, could be any of the: battery,temperature,network,vpn,wifi'
56
+ c.flag [:health]
57
+ c.desc 'Maximum session duration in seconds, 10800 (3h) by default'
58
+ c.flag [:session]
59
+ c.desc 'Maximum time to connect minimal quantity of devices in seconds, 120 (2m) by default'
60
+ c.flag [:starttime]
61
+ c.desc 'Do not start daemon'
62
+ c.switch [:nodaemon]
63
+
64
+ c.action do |_, options, _|
65
+ unless DI[:start_debug_session_interactor].execute(options)
66
+ raise GLI::CustomExit.new('Connect failed', 1)
67
+ end
68
+ end
69
+ end
70
+
71
+ desc 'Show available keys for filtering'
72
+ command :keys do |c|
73
+ c.action {puts DI[:get_keys_interactor].execute}
74
+ end
75
+
76
+ desc 'Show known values for the filtering key'
77
+ command :values do |c|
78
+ c.action do |_, _, args|
79
+ exit_now!('Please specify one key') if args.empty?
80
+
81
+ puts DI[:get_values_interactor].execute(args.first)
82
+ end
83
+ end
84
+
85
+ desc 'Disconnect device(s) from local adb server and remove device(s) from user devices in Smartdust Lab'
86
+ command :disconnect do |c|
87
+ c.desc '(optional) ADB connection url of the device'
88
+ c.switch [:all]
89
+
90
+ c.action do |_, options, args|
91
+ if args.empty? && options[:all] == true
92
+ DI[:stop_all_debug_sessions_interactor].execute
93
+ elsif !args.empty? && options[:all] == false
94
+ DI[:stop_debug_session_interactor].execute(args.first)
95
+ elsif exit_now!('Please specify one device or mode --all')
96
+ end
97
+ end
98
+ end
99
+
100
+ desc 'Frees all devices that are assigned to current user in Smartdust Lab. Doesn\'t modify local adb'
101
+ command :clean do |c|
102
+ c.action {DI[:remove_all_user_devices_interactor].execute}
103
+ end
104
+
105
+ desc 'Add adb public key into Smartdust Lab'
106
+ command :trustme do |c|
107
+ c.desc 'Location of adb public key'
108
+ c.flag [:k, :adb_public_key_location]
109
+ c.default_value '~/.android/adbkey.pub'
110
+
111
+ c.action do |_, options, _|
112
+ filename = File.expand_path(options[:adb_public_key_location])
113
+ exit_now!("File does not exist: '#{options[:adb_public_key_location]}'") unless File.exist? filename
114
+ DI[:add_adb_public_key_interactor].execute(options[:adb_public_key_location])
115
+ end
116
+ end
117
+
118
+ exit run(ARGV)
119
+ end
120
+ end
121
+
data/lib/stf.rb ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'stf/view/cli'
4
+
5
+ module Stf
6
+ include Stf::CLI
7
+ end