14
14
import urllib3
15
15
from adb_shell .adb_device import AdbDeviceTcp
16
16
from adb_shell .auth .sign_pythonrsa import PythonRSASigner
17
+ from adb_shell .constants import DEFAULT_READ_TIMEOUT_S
17
18
from bravado .client import SwaggerClient
18
19
from bravado .exception import HTTPError
19
20
from bravado .requests_client import RequestsClient
20
21
21
- logging .basicConfig (level = logging .INFO )
22
-
23
22
# Ignore cert warnings
24
23
urllib3 .disable_warnings (urllib3 .exceptions .InsecureRequestWarning )
25
24
36
35
DEFAULT_CFG_FILE = Path .home () / ".lariat/config.json"
37
36
38
37
38
+ class Config :
39
+ """Lariat Configuration"""
40
+
41
+ __config : typing .Dict [str , typing .Any ] = {}
42
+
43
+ __required_options = {
44
+ "access_token" : ("DeviceFarmer access token" , lambda s : s ),
45
+ "device_farmer_url" : ("DeviceFarmer URL" , lambda s : s ),
46
+ }
47
+
48
+ __optional_options = {
49
+ "adb_shell_default_read_timeout_s" : (
50
+ "Default adb_shell timeout in seconds" ,
51
+ lambda f : float (f ),
52
+ ),
53
+ }
54
+
55
+ @staticmethod
56
+ def get (opt : str ) -> typing .Any :
57
+ """Get the configuration option specified by opt"""
58
+ return Config .__config .get (opt )
59
+
60
+ @staticmethod
61
+ def all_opts () -> (
62
+ typing .Dict [str , typing .Tuple [str , typing .Callable [[str ], typing .Any ]]]
63
+ ):
64
+ """Returns a dict of all supported configuration options"""
65
+ d = Config .required_opts ()
66
+ d .update (Config .optional_opts ())
67
+ return d
68
+
69
+ @staticmethod
70
+ def required_opts () -> (
71
+ typing .Dict [str , typing .Tuple [str , typing .Callable [[str ], typing .Any ]]]
72
+ ):
73
+ """Returns a dict of all required configuration options"""
74
+ return Config .__required_options
75
+
76
+ @staticmethod
77
+ def optional_opts () -> (
78
+ typing .Dict [str , typing .Tuple [str , typing .Callable [[str ], typing .Any ]]]
79
+ ):
80
+ """Returns a dict of all optional configuration options"""
81
+ return Config .__optional_options
82
+
83
+ @staticmethod
84
+ def init_config (args : argparse .Namespace ) -> None :
85
+ """Initializes the Config
86
+
87
+ Args:
88
+ args (Argparse Namespace): Command line arguments
89
+ """
90
+
91
+ #
92
+ # Set default configuration values
93
+ #
94
+ Config .__config ["adb_shell_default_read_timeout_s" ] = DEFAULT_READ_TIMEOUT_S
95
+ Config .__config ["adb_private_key_path" ] = DEFAULT_ADB_PRI_KEY
96
+
97
+ #
98
+ # Override defaults with configuration file values
99
+ #
100
+ try :
101
+ with open (args .config , "r" , encoding = "utf-8" ) as file :
102
+ Config .__config .update (json .load (file ))
103
+ except FileNotFoundError as e :
104
+ logging .error (
105
+ "Config file '%s' not found. Use --config to specify if using a non-default config file" ,
106
+ args .config ,
107
+ )
108
+ raise e
109
+ except json .JSONDecodeError as e :
110
+ logging .exception ("Invalid JSON format in config file '%s'" , args .config )
111
+ raise e
112
+
113
+ #
114
+ # Override defaults with command line options
115
+ #
116
+ arg_vars = vars (args )
117
+ for opt in Config .all_opts ():
118
+ if arg_vars [opt ]:
119
+ Config .__config [opt ] = arg_vars [opt ]
120
+
121
+ #
122
+ # Ensure required options are set
123
+ #
124
+ for opt in Config .required_opts ():
125
+ if not opt in Config .__config :
126
+ raise KeyError (
127
+ "Required config option '{opt}' is missing. Update config file or specify on command line with --{opt}" .format (
128
+ opt = opt
129
+ )
130
+ )
131
+
132
+ logging .debug ("Lariat Config: %s" , Config .__config )
133
+
134
+
39
135
def parse_args () -> argparse .Namespace :
40
136
"""Parse command-line arguments for the DeviceFarmer automation tool.
41
137
@@ -95,6 +191,21 @@ def parse_args() -> argparse.Namespace:
95
191
+ "." ,
96
192
)
97
193
194
+ parser .add_argument (
195
+ "-v" ,
196
+ "--verbose" ,
197
+ action = "count" ,
198
+ default = 0 ,
199
+ help = "Increase log level. Can be supplied multiple times to further increase log verbosity (e.g. -vv)" ,
200
+ )
201
+
202
+ for opt , desc in Config .all_opts ().items ():
203
+ parser .add_argument (
204
+ "--" + opt ,
205
+ help = desc [0 ],
206
+ type = desc [1 ],
207
+ )
208
+
98
209
parsed_args = parser .parse_args ()
99
210
100
211
if not (
@@ -120,33 +231,14 @@ def parse_args() -> argparse.Namespace:
120
231
"--exec-file argument does not exist: " + str (parsed_args .exec_file )
121
232
)
122
233
123
- return parsed_args
124
-
125
-
126
- def load_config (
127
- config_file : Path ,
128
- ) -> typing .Optional [typing .Dict [typing .Any , typing .Any ]]:
129
- """Load a JSON configuration file.
130
-
131
- Args:
132
- config_file (Path): The path to the JSON configuration file.
133
-
134
- Returns:
135
- dict: The loaded configuration as a dictionary.
136
- """
234
+ log_level = logging .WARNING
235
+ if parsed_args .verbose == 1 :
236
+ log_level = logging .INFO
237
+ elif parsed_args .verbose > 1 :
238
+ log_level = logging .DEBUG
239
+ logging .basicConfig (level = log_level )
137
240
138
- cfg = None
139
- try :
140
- with open (config_file , "r" , encoding = "utf-8" ) as file :
141
- cfg = json .load (file )
142
- except FileNotFoundError :
143
- logging .error (
144
- "Config file '%s' not found. Use --config to specify if using a non-default config file" ,
145
- config_file ,
146
- )
147
- except json .JSONDecodeError :
148
- logging .exception ("Invalid JSON format in config file '%s'" , config_file )
149
- return cfg
241
+ return parsed_args
150
242
151
243
152
244
def api_connect (
@@ -168,7 +260,7 @@ def api_connect(
168
260
# The user can provide either the base URL of their Device Farmer instance (e.g myorg.devicefarmer.com)
169
261
# or a full path to their Swagger 2.0 spec file (e.g myorg.devicefarmer.com/custom/path/swagger.json)
170
262
# If the base URL is provided, the standard path for the Swagger spec file is appended
171
- if ext == ".json" or ext == ".yaml" :
263
+ if ext in ( ".json" , ".yaml" ) :
172
264
spec_url = api_url
173
265
else :
174
266
spec_url = api_url + "/api/v1/swagger.json"
@@ -333,7 +425,11 @@ def adb_connect_device(
333
425
334
426
try :
335
427
device = AdbDeviceTcp (ip_addr , port , default_transport_timeout_s = 60 )
336
- device .connect (rsa_keys = [signer ], auth_timeout_s = 1 )
428
+ device .connect (
429
+ rsa_keys = [signer ],
430
+ auth_timeout_s = 1 ,
431
+ read_timeout_s = Config .get ("adb_shell_default_read_timeout_s" ),
432
+ )
337
433
except Exception :
338
434
logging .exception ("Failed to connect to adb device %s" , device_url )
339
435
return None
@@ -357,20 +453,28 @@ def push_and_exec_file(device: AdbDeviceTcp, bin_path: Path) -> str:
357
453
binary_file = os .path .basename (bin_path )
358
454
359
455
try :
360
- device .push (bin_path .expanduser (), DEVICE_PUSH_DIR + binary_file )
456
+ device .push (
457
+ bin_path .expanduser (),
458
+ DEVICE_PUSH_DIR + binary_file ,
459
+ read_timeout_s = Config .get ("adb_shell_default_read_timeout_s" ),
460
+ )
361
461
except Exception :
362
462
logging .exception ("Failed to push file" )
363
463
return ""
364
464
365
465
try :
366
- device .shell (CHMOD_755 % (DEVICE_PUSH_DIR + binary_file ))
466
+ device .shell (
467
+ CHMOD_755 % (DEVICE_PUSH_DIR + binary_file ),
468
+ read_timeout_s = Config .get ("adb_shell_default_read_timeout_s" ),
469
+ )
367
470
except Exception :
368
471
logging .exception ("Failed to set file permissions" )
369
472
return ""
370
473
371
474
try :
372
475
cmd_result = device .shell (
373
- DEVICE_PUSH_DIR + binary_file + ECHO_EXIT_CODE , read_timeout_s = 60
476
+ DEVICE_PUSH_DIR + binary_file + ECHO_EXIT_CODE ,
477
+ read_timeout_s = Config .get ("adb_shell_default_read_timeout_s" ),
374
478
)
375
479
except Exception :
376
480
logging .exception ("Failed to exec file" )
@@ -400,7 +504,9 @@ def push_files(device: AdbDeviceTcp, filepath: Path) -> bool:
400
504
for file in file_list :
401
505
try :
402
506
device .push (
403
- local_path = file , device_path = DEVICE_PUSH_DIR + os .path .basename (file )
507
+ local_path = file ,
508
+ device_path = DEVICE_PUSH_DIR + os .path .basename (file ),
509
+ read_timeout_s = Config .get ("adb_shell_default_read_timeout_s" ),
404
510
)
405
511
except Exception :
406
512
logging .exception ("Failed to push files at path %s to device" , filepath )
@@ -587,7 +693,10 @@ def process_command(
587
693
stf_device .get ("serial" ),
588
694
)
589
695
try :
590
- result = adb_device .shell (command = command + ECHO_EXIT_CODE )
696
+ result = adb_device .shell (
697
+ command = command + ECHO_EXIT_CODE ,
698
+ read_timeout_s = Config .get ("adb_shell_default_read_timeout_s" ),
699
+ )
591
700
return result_to_dict (str (result ))
592
701
except Exception as adb_exception :
593
702
logging .warning ("Failed to run shell command %s: %s" , command , adb_exception )
@@ -711,7 +820,9 @@ def process_device(
711
820
)
712
821
if args .command :
713
822
device_result = process_command (
714
- adb_device , args .command , stf_device
823
+ adb_device ,
824
+ args .command ,
825
+ stf_device ,
715
826
)
716
827
elif args .exec_file :
717
828
logging .info (
@@ -737,30 +848,22 @@ def main() -> int:
737
848
738
849
args = parse_args ()
739
850
740
- config = load_config (config_file = args .config )
741
- if not config :
742
- return 1
743
-
744
- device_farmer_url = config .get ("device_farmer_url" )
745
- if not device_farmer_url :
746
- logging .error ("Missing required config value 'device_farmer_url'" )
747
- return 1
748
-
749
- access_token = config .get ("access_token" )
750
- if not access_token :
751
- logging .error ("Missing required config value 'access_token'" )
851
+ try :
852
+ Config .init_config (args )
853
+ except Exception as e :
854
+ logging .error ("Failed to initialize config: %s" , e )
752
855
return 1
753
856
754
- adb_private_key_path = config .get ("adb_private_key_path" , DEFAULT_ADB_PRI_KEY )
755
-
756
857
try :
757
858
swagger_client , request_options = api_connect (
758
- api_url = device_farmer_url , api_token = access_token
859
+ api_url = Config .get ("device_farmer_url" ),
860
+ api_token = Config .get ("access_token" ),
759
861
)
760
862
761
863
except Exception :
762
864
logging .error (
763
- "Failed to connect to device farmer instance at %s" , device_farmer_url
865
+ "Failed to connect to device farmer instance at %s" ,
866
+ Config .get ("device_farmer_url" ),
764
867
)
765
868
return 1
766
869
@@ -794,7 +897,7 @@ def main() -> int:
794
897
795
898
try :
796
899
key_signer = PythonRSASigner .FromRSAKeyPath (
797
- rsa_key_path = (os .path .abspath (adb_private_key_path ))
900
+ rsa_key_path = (os .path .abspath (Config . get ( " adb_private_key_path" ) ))
798
901
)
799
902
# Use the key_signer object for authentication
800
903
except (IOError , ValueError ):
@@ -805,7 +908,11 @@ def main() -> int:
805
908
806
909
for stf_device in stf_devices :
807
910
device_serial , result = process_device (
808
- stf_device , swagger_client , request_options , args , key_signer
911
+ stf_device ,
912
+ swagger_client ,
913
+ request_options ,
914
+ args ,
915
+ key_signer ,
809
916
)
810
917
results [device_serial ] = result
811
918
0 commit comments