diff --git a/wlauto/commands/record.py b/wlauto/commands/record.py index 87b4803b..97f51311 100644 --- a/wlauto/commands/record.py +++ b/wlauto/commands/record.py @@ -24,7 +24,7 @@ from wlauto.core.resource import NO_ONE from wlauto.core.resolver import ResourceResolver from wlauto.core.configuration import RunConfiguration from wlauto.core.agenda import Agenda -from wlauto.utils.revent import ReventParser +from wlauto.utils.revent import ReventRecording, GAMEPAD_MODE class ReventCommand(Command): @@ -91,6 +91,27 @@ class RecordCommand(ReventCommand): - suffix is used by WA to determine which part of the app execution the recording is for, currently these are either ``setup`` or ``run``. This should be specified with the ``-s`` argument. + + + **gamepad recording** + + revent supports an alternative recording mode, where it will record events + from a single gamepad device. In this mode, revent will store the + description of this device as a part of the recording. When replaying such + a recording, revent will first create a virtual gamepad using the + description, and will replay the events into it, so a physical controller + does not need to be connected on replay. Unlike standard revent recordings, + recordings generated in this mode should be (to an extent) portable across + different devices. + + note: + + - The device on which a recording is being made in gamepad mode, must have + exactly one gamepad connected to it. + - The device on which a gamepad recording is being replayed must have + /dev/uinput enabled in the kernel (this interface is necessary to create + virtual gamepad). + ''' def initialize(self, context): @@ -99,6 +120,8 @@ class RecordCommand(ReventCommand): self.parser.add_argument('-s', '--suffix', help='The suffix of the revent file, e.g. ``setup``') self.parser.add_argument('-o', '--output', help='Directory to save the recording in') self.parser.add_argument('-p', '--package', help='Package to launch before recording') + self.parser.add_argument('-g', '--gamepad', help='Record from a gamepad rather than all devices.', + action="store_true") self.parser.add_argument('-C', '--clear', help='Clear app cache before launching it', action="store_true") self.parser.add_argument('-S', '--capture-screen', help='Record a screen capture after recording', @@ -125,7 +148,8 @@ class RecordCommand(ReventCommand): self.logger.info("Press Enter when you are ready to record...") raw_input("") - command = "{} record -s {}".format(self.target_binary, revent_file) + gamepad_flag = '-g ' if args.gamepad else '' + command = "{} record {}-s {}".format(self.target_binary, gamepad_flag, revent_file) self.device.kick_off(command) self.logger.info("Press Enter when you have finished recording...") @@ -172,8 +196,11 @@ class ReplayCommand(ReventCommand): self.logger.info("Replaying recording") command = "{} replay {}".format(self.target_binary, revent_file) - timeout = ceil(ReventParser.get_revent_duration(args.revent)) + 30 - self.device.execute(command, timeout=timeout) + recording = ReventRecording(args.revent) + timeout = ceil(recording.duration) + 30 + recording.close() + self.device.execute(command, timeout=timeout, + as_root=(recording.mode == GAMEPAD_MODE)) self.logger.info("Finished replay") diff --git a/wlauto/common/android/workload.py b/wlauto/common/android/workload.py index 0441ce0f..a1298e4f 100644 --- a/wlauto/common/android/workload.py +++ b/wlauto/common/android/workload.py @@ -28,7 +28,7 @@ from wlauto.common.resources import ExtensionAsset, Executable, File from wlauto.exceptions import WorkloadError, ResourceError, DeviceError from wlauto.utils.android import ApkInfo, ANDROID_NORMAL_PERMISSIONS, UNSUPPORTED_PACKAGES from wlauto.utils.types import boolean -from wlauto.utils.revent import ReventParser +from wlauto.utils.revent import ReventRecording import wlauto.utils.statedetect as state_detector import wlauto.common.android.resources @@ -466,8 +466,8 @@ class ReventWorkload(Workload): self.on_device_run_revent = devpath.join(self.device.working_directory, os.path.split(self.revent_run_file)[-1]) self._check_revent_files(context) - default_setup_timeout = ceil(ReventParser.get_revent_duration(self.revent_setup_file)) + 30 - default_run_timeout = ceil(ReventParser.get_revent_duration(self.revent_run_file)) + 30 + default_setup_timeout = ceil(ReventRecording(self.revent_setup_file).duration) + 30 + default_run_timeout = ceil(ReventRecording(self.revent_run_file).duration) + 30 self.setup_timeout = self.setup_timeout or default_setup_timeout self.run_timeout = self.run_timeout or default_run_timeout diff --git a/wlauto/resource_getters/standard.py b/wlauto/resource_getters/standard.py index 7d9c22a9..321a5922 100644 --- a/wlauto/resource_getters/standard.py +++ b/wlauto/resource_getters/standard.py @@ -33,7 +33,7 @@ from wlauto.exceptions import ResourceError from wlauto.utils.android import ApkInfo from wlauto.utils.misc import ensure_directory_exists as _d, ensure_file_directory_exists as _f, sha256, urljoin from wlauto.utils.types import boolean -from wlauto.utils.revent import ReventParser +from wlauto.utils.revent import ReventRecording logging.getLogger("requests").setLevel(logging.WARNING) @@ -101,7 +101,7 @@ class ReventGetter(ResourceGetter): if candidate.lower() == filename.lower(): path = os.path.join(location, candidate) try: - ReventParser.check_revent_file(path) + ReventRecording(path).close() # Check valid recording return path except ValueError as e: self.logger.warning(e.message) @@ -437,7 +437,7 @@ class HttpGetter(ResourceGetter): pathname = os.path.basename(asset['path']).lower() if pathname == filename: try: - ReventParser.check_revent_file(asset['path']) + ReventRecording(asset['path']).close() # Check valid recording return asset except ValueError as e: self.logger.warning(e.message) @@ -535,7 +535,7 @@ class RemoteFilerGetter(ResourceGetter): path = os.path.join(location, candidate) if path: try: - ReventParser.check_revent_file(path) + ReventRecording(path).close() # Check valid recording return path except ValueError as e: self.logger.warning(e.message) diff --git a/wlauto/utils/revent.py b/wlauto/utils/revent.py index 31ea5043..25e405a2 100644 --- a/wlauto/utils/revent.py +++ b/wlauto/utils/revent.py @@ -13,66 +13,235 @@ # limitations under the License. # -import struct -import datetime import os +import struct +from datetime import datetime +from collections import namedtuple -class ReventParser(object): - """ - Parses revent binary recording files so they can be easily read within python. - """ - - int32_struct = struct.Struct("= 30: - raise ValueError("path length too long. corrupt file") - self.device_paths.append(f.read(path_length)) - - while f.tell() < os.path.getsize(path): - device_id, sec, usec, typ, code, value = _read_struct(f, self.event_struct) - yield (device_id, datetime.datetime.fromtimestamp(sec + float(usec) / 1000000), - typ, code, value) - - @staticmethod - def check_revent_file(path): - """ - Checks whether a file starts with "REVENT" - """ - with open(path, "rb") as f: - magic, file_version = _read_struct(f, ReventParser.header_struct) - - if magic != "REVENT": - msg = "'{}' isn't an revent file, are you using an old recording?" - raise ValueError(msg.format(path)) - return file_version - - @staticmethod - def get_revent_duration(path): - """ - Takes an ReventParser and returns the duration of the revent recording in seconds. - """ - revent_parser = ReventParser().parse(path) - first = last = next(revent_parser) - for last in revent_parser: - pass - return (last[1] - first[1]).total_seconds() +GENERAL_MODE = 0 +GAMEPAD_MODE = 1 -def _read_struct(f, struct_spec): - data = f.read(struct_spec.size) +u16_struct = struct.Struct(' self.version >= 0: + self._read_devices(fh) + else: + raise ValueError('Invalid recording version: {}'.format(self.version)) + + def _read_devices(self, fh): + num_devices, = read_struct(fh, u32_struct) + for _ in xrange(num_devices): + self.device_paths.append(read_string(fh)) + + def _read_gamepad_info(self, fh): + self.gamepad_device = UinputDeviceInfo(fh) + self.device_paths.append('[GAMEPAD]') + + def _iter_events(self): + if self.fh is None: + raise RuntimeError('Attempting to iterate over events of a closed recording') + self.fh.seek(self._events_start) + if self.version >= 2: + for _ in xrange(self.num_events): + yield ReventEvent(self.fh) + else: + file_size = os.path.getsize(self.filepath) + while self.fh.tell() < file_size: + yield ReventEvent(self.fh, legacy=True) + + def __iter__(self): + for event in self.events: + yield event + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def __del__(self): + self.close()