smartdust-client 1.1.0

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.
@@ -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