Skip to content

Commit e2747ac

Browse files
authored
Merge pull request #27 from Zetier/25-fix-read-timeout
Fix Read Timeout
2 parents abab826 + dd144bb commit e2747ac

File tree

2 files changed

+174
-56
lines changed

2 files changed

+174
-56
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,24 @@ following:
4646
- `adb_private_key_path`: (Optional) Path to a non-default adb private key.
4747
Defaults to `~/.android/adbkey` if not specified
4848

49+
- `adb_shell_default_read_timeout_s`: (Optional) Default total timeout (in
50+
seconds) for reading data from a device. This value may need to be increased
51+
from the default value of 10s for certain device operations.
52+
4953
```json
5054
{
5155
"access_token": "ef02da4fb3884395af4cf011061a2318ca5e9a04abd04de59c5c99afcce0b7fz",
5256
"device_farmer_url": "https://my.device-farmer-instance.com/",
53-
"adb_private_key_path": "/custom/path/adb.key"
57+
"adb_private_key_path": "/custom/path/adb.key",
58+
"adb_shell_default_read_timeout_s": 32.5
5459
}
5560
```
5661

62+
All config options can be specified on the command line to override any
63+
defaults set in the config file. For example, the command line option
64+
`--device_farmer_url` can be used to override the DeviceFarmer URL set in the
65+
config file.
66+
5767
## Usage
5868

5969
```sh
@@ -78,6 +88,7 @@ optional arguments:
7888
regex matched
7989
-p PUSH_FILES, --push-files PUSH_FILES
8090
Specify the path to the file or directory to be pushed to the device. Pushes to /data/local/tmp/.
91+
-v, --verbose Increase log level. Can be supplied multiple times to further increase log verbosity (e.g. -vv)
8192
```
8293
8394
### Device Fields
@@ -258,7 +269,7 @@ android-range-test:
258269
be unlocked.
259270
260271
- Make sure to provide the correct path to the ADB private key file via
261-
`lariat_config.json` if it is different from the default location
272+
`~/.lariat/config.json` if it is different from the default location
262273
(`~/.android/adbkey`)
263274
264275
- By default, Lariat will enumerate ~every~ device on the DeviceFarmer range,

src/lariat/lariat.py

Lines changed: 161 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414
import urllib3
1515
from adb_shell.adb_device import AdbDeviceTcp
1616
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
17+
from adb_shell.constants import DEFAULT_READ_TIMEOUT_S
1718
from bravado.client import SwaggerClient
1819
from bravado.exception import HTTPError
1920
from bravado.requests_client import RequestsClient
2021

21-
logging.basicConfig(level=logging.INFO)
22-
2322
# Ignore cert warnings
2423
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
2524

@@ -36,6 +35,103 @@
3635
DEFAULT_CFG_FILE = Path.home() / ".lariat/config.json"
3736

3837

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+
39135
def parse_args() -> argparse.Namespace:
40136
"""Parse command-line arguments for the DeviceFarmer automation tool.
41137
@@ -95,6 +191,21 @@ def parse_args() -> argparse.Namespace:
95191
+ ".",
96192
)
97193

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+
98209
parsed_args = parser.parse_args()
99210

100211
if not (
@@ -120,33 +231,14 @@ def parse_args() -> argparse.Namespace:
120231
"--exec-file argument does not exist: " + str(parsed_args.exec_file)
121232
)
122233

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)
137240

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
150242

151243

152244
def api_connect(
@@ -168,7 +260,7 @@ def api_connect(
168260
# The user can provide either the base URL of their Device Farmer instance (e.g myorg.devicefarmer.com)
169261
# or a full path to their Swagger 2.0 spec file (e.g myorg.devicefarmer.com/custom/path/swagger.json)
170262
# 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"):
172264
spec_url = api_url
173265
else:
174266
spec_url = api_url + "/api/v1/swagger.json"
@@ -333,7 +425,11 @@ def adb_connect_device(
333425

334426
try:
335427
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+
)
337433
except Exception:
338434
logging.exception("Failed to connect to adb device %s", device_url)
339435
return None
@@ -357,20 +453,28 @@ def push_and_exec_file(device: AdbDeviceTcp, bin_path: Path) -> str:
357453
binary_file = os.path.basename(bin_path)
358454

359455
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+
)
361461
except Exception:
362462
logging.exception("Failed to push file")
363463
return ""
364464

365465
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+
)
367470
except Exception:
368471
logging.exception("Failed to set file permissions")
369472
return ""
370473

371474
try:
372475
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"),
374478
)
375479
except Exception:
376480
logging.exception("Failed to exec file")
@@ -400,7 +504,9 @@ def push_files(device: AdbDeviceTcp, filepath: Path) -> bool:
400504
for file in file_list:
401505
try:
402506
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"),
404510
)
405511
except Exception:
406512
logging.exception("Failed to push files at path %s to device", filepath)
@@ -587,7 +693,10 @@ def process_command(
587693
stf_device.get("serial"),
588694
)
589695
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+
)
591700
return result_to_dict(str(result))
592701
except Exception as adb_exception:
593702
logging.warning("Failed to run shell command %s: %s", command, adb_exception)
@@ -711,7 +820,9 @@ def process_device(
711820
)
712821
if args.command:
713822
device_result = process_command(
714-
adb_device, args.command, stf_device
823+
adb_device,
824+
args.command,
825+
stf_device,
715826
)
716827
elif args.exec_file:
717828
logging.info(
@@ -737,30 +848,22 @@ def main() -> int:
737848

738849
args = parse_args()
739850

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)
752855
return 1
753856

754-
adb_private_key_path = config.get("adb_private_key_path", DEFAULT_ADB_PRI_KEY)
755-
756857
try:
757858
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"),
759861
)
760862

761863
except Exception:
762864
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"),
764867
)
765868
return 1
766869

@@ -794,7 +897,7 @@ def main() -> int:
794897

795898
try:
796899
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")))
798901
)
799902
# Use the key_signer object for authentication
800903
except (IOError, ValueError):
@@ -805,7 +908,11 @@ def main() -> int:
805908

806909
for stf_device in stf_devices:
807910
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,
809916
)
810917
results[device_serial] = result
811918

0 commit comments

Comments
 (0)