diff --git a/doc/source/invocation.rst b/doc/source/invocation.rst index a322b324..8ae30d80 100644 --- a/doc/source/invocation.rst +++ b/doc/source/invocation.rst @@ -148,7 +148,7 @@ either ``setup`` or ``run``. This should be specified with the ``-s`` argument. The full set of options for this command are:: usage: wa record [-h] [-c CONFIG] [-v] [--debug] [--version] [-d DEVICE] - [-s SUFFIX] [-o OUTPUT] [-p PACKAGE] [-C] + [-s SUFFIX] [-o OUTPUT] [-p PACKAGE] [-g] [-C] optional arguments: -h, --help show this help message and exit @@ -165,6 +165,7 @@ argument. The full set of options for this command are:: Directory to save the recording in -p PACKAGE, --package PACKAGE Package to launch before recording + -g, --gamepad Record from a gamepad rather than all devices. -C, --clear Clear app cache before launching it .. _replay-command: diff --git a/doc/source/revent.rst b/doc/source/revent.rst index 8c843a74..36b8e4d7 100644 --- a/doc/source/revent.rst +++ b/doc/source/revent.rst @@ -1,7 +1,10 @@ .. _revent_files_creation: revent -====== +++++++ + +Overview and Usage +================== revent utility can be used to record and later play back a sequence of user input events, such as key presses and touch screen taps. This is an alternative @@ -42,6 +45,10 @@ to the current directory. It will be named ``{device_model}.revent``. When recording revent files for a ``GameWorkload`` you can use the ``-s`` option to add ``run`` or ``setup`` suffixes. +From version 2.6 of WA onwards, a "gamepad" recording mode is also supported. +This mode requires a gamepad to be connected to the device when recoridng, but +the recordings produced in this mode should be portable across devices. + For more information run please read :ref:`record-command` @@ -55,6 +62,7 @@ replay:: For more information run please read :ref:`replay-command` + Using revent With Workloads --------------------------- @@ -108,110 +116,6 @@ revent sequence is also faster than writing automation code (on the other hand, one would need maintain a different revent log for each screen resolution). -File format of revent -===================== - -.. note:: All values below are little endian - -Recording structure of revent ------------------------------ - -revent recordings are made of of five parts: - - * A "magic" string of `REVENT` to help identify revent recordings. - * A unsigned integer representing the revent file format version. - * A signed integer that gives the number of devices in this recording. - * A series of device paths, the number of which is given in the previous field. - For more detail see `Device path structure`_ below. - * An unlimited number of recorded events. For more detail see `Event Structure`_ - below. - -:: - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | MAGIC | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | MAGIC cont. | Version | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Number of devices | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - | Device paths +-+-+-+-+-+-+-+-+-+-+-+-+ - | | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | - | | - | Events | - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -Device path structure ----------------------- - -This part of an revent recording is used to store the paths to input devices used in the -recording. It consists of: - - * A signed integer giving the size of the following string. - * A string, with a maximum length of 30, containing the path of an input device. - -:: - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Length of device path | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - | Device path | - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -Event structure ---------------- - -The majority of an revent recording will be made up of the input events that were -recorded. There and be an unlimited number of these events in an revent file and they -are structured as follows: - - * A signed integer representing which device from the list of device paths - this event is for (zero indexed). E.g. Device ID = 3 would be the 4th - device in the list of device paths. - * 32 bits of padding - * A signed integer representing the number of seconds since "epoch" when the - event was recorded. - * A signed integer representing the microseconds part of the timestamp. - * An unsigned integer representing the event type - * An unsigned integer representing the event code - * An unsigned integer representing the event value - -For more information about the event type, code and value please read: -https://www.kernel.org/doc/Documentation/input/event-codes.txt - -:: - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Device ID | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | PADDING | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Timestamp Seconds | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Timestamp Seconds cont. | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Timestamp Micoseconds | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Timestamp Micoseconds cont. | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Event Type | Event Code | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Event Value | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Using state detection with revent ================================= @@ -231,6 +135,8 @@ State and phase definitions should be placed in a directory of the following structure inside the dependencies directory of each workload (along with revent files etc): +:: + dependencies/ / state_definitions/ @@ -307,3 +213,261 @@ case an unexpected state is encountered. expected_state: gameplay - phase_name: run_complete expected_state: level_cleared_screen + + +File format of revent recordings +================================ + +You do not need to understand recording format in order to use revent. This +section is intended for those looking to extend revent in some way, or to +utilize revent recordings for other purposes. + +Format Overview +--------------- + +Recordings are stored in a binary format. A recording consists of three +sections:: + + +-+-+-+-+-+-+-+-+-+-+-+ + | Header | + +-+-+-+-+-+-+-+-+-+-+-+ + | | + | Device Description | + | | + +-+-+-+-+-+-+-+-+-+-+-+ + | | + | | + | Event Stream | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+ + +The header contains metadata describing the recording. The device description +contains information about input devices involved in this recording. Finally, +the event stream contains the recorded input events. + +All fields are either fixed size or prefixed with their length or the number of +(fixed-sized) elements. + +.. note:: All values below are little endian + + +Recording Header +---------------- + +An revent recoding header has the following structure + + * It starts with the "magic" string ``REVENT`` to indicate that this is an + revent recording. + * The magic is followed by a 16 bit version number. This indicates the format + version of the recording that follows. Current version is ``2``. + * The next 16 bits indicate the type of the recording. This dictates the + structure of the Device Description section. Valid values are: + + ``0`` + This is a general input event recording. The device description + contains a list of paths from which the events where recorded. + ``1`` + This a gamepad recording. The device description contains the + description of the gamepad used to create the recording. + + * The header is zero-padded to 128 bits. + +:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 'R' | 'E' | 'V' | 'E' | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 'N' | 'T' | Version | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Mode | PADDING | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | PADDING | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Device Description +------------------ + +This section describes the input devices used in the recording. Its structure is +determined by the value of ``Mode`` field in the header. + +general recording +~~~~~~~~~~~~~~~~~ + +.. note:: This is the only format supported prior to version ``2``. + +The recording has been made from all available input devices. This section +contains the list of ``/dev/input`` paths for the devices, prefixed with total +number of the devices recorded. + +:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Number of devices | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Device paths +-+-+-+-+-+-+-+-+-+-+-+-+ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Similarly, each device path is a length-prefixed string. Unlike C strings, the +path is *not* NULL-terminated. + +:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Length of device path | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Device path | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +gamepad recording +~~~~~~~~~~~~~~~~~ + +The recording has been made from a specific gamepad. All events in the stream +will be for that device only. The section describes the device properties that +will be used to create a virtual input device using ``/dev/uinput``. Please +see ``linux/input.h`` header in the Linux kernel source for more information +about the fields in this section. + +:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | bustype | vendor | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | product | version | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | name_length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | name | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ev_bits | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | | + | key_bits (96 bytes) | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | | + | rel_bits (96 bytes) | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | | + | abs_bits (96 bytes) | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | num_absinfo | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | | + | | + | | + | absinfo entries | + | | + | | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Each ``absinfo`` entry consists of six 32 bit values. The number of entries is +determined by the ``abs_bits`` field. + + +:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | minimum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | maximum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | fuzz | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | flat | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | resolution | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Event structure +--------------- + +The majority of an revent recording will be made up of the input events that were +recorded. The event stream is prefixed with the number of events in the stream. + +Each event entry structured as follows: + + * A signed integer representing which device from the list of device paths + this event is for (zero indexed). E.g. Device ID = 3 would be the 4th + device in the list of device paths. + * 32 bits of padding + * A signed integer representing the number of seconds since "epoch" when the + event was recorded. + * A signed integer representing the microseconds part of the timestamp. + * An unsigned integer representing the event type + * An unsigned integer representing the event code + * An unsigned integer representing the event value + +For more information about the event type, code and value please read: +https://www.kernel.org/doc/Documentation/input/event-codes.txt + +:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Device ID | Timestamp Seconds | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Timestamp Seconds (cont.) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Timestamp Seconds (cont.) | stamp Micoseconds | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Timestamp Micoseconds (cont.) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Timestamp Micoseconds (cont.) | Event Type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Event Code | Event Value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Event Value (cont.) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Parser +------ + +WA has a parser for revent recordings. This can be used to work with revent +recordings in scripts. Here is an example: + +.. code:: python + + from wlauto.utils.revent import ReventRecording + + with ReventRecording('/path/to/recording.revent') as recording: + print "Recording: {}".format(recording.filepath) + print "There are {} input events".format(recording.num_events) + print "Over a total of {} seconds".format(recording.duration) 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/common/bin/arm64/revent b/wlauto/common/bin/arm64/revent index 4883292e..68571ec2 100755 Binary files a/wlauto/common/bin/arm64/revent and b/wlauto/common/bin/arm64/revent differ diff --git a/wlauto/common/bin/armeabi/revent b/wlauto/common/bin/armeabi/revent index 1019c44a..651cd1cc 100755 Binary files a/wlauto/common/bin/armeabi/revent and b/wlauto/common/bin/armeabi/revent differ diff --git a/wlauto/external/revent/Makefile b/wlauto/external/revent/Makefile index dbbfea75..c5ff3dc1 100644 --- a/wlauto/external/revent/Makefile +++ b/wlauto/external/revent/Makefile @@ -1,7 +1,12 @@ # CROSS_COMPILE=aarch64-linux-gnu- make # CC=gcc -CFLAGS=-static -lc + +ifdef DEBUG + CFLAGS=-static -lc -g +else + CFLAGS=-static -lc -O2 +endif revent: revent.c $(CROSS_COMPILE)$(CC) $(CFLAGS) revent.c -o revent diff --git a/wlauto/external/revent/revent.c b/wlauto/external/revent/revent.c index efc227f8..5543a2c8 100644 --- a/wlauto/external/revent/revent.c +++ b/wlauto/external/revent/revent.c @@ -11,700 +11,1453 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. -*/ + */ -#include +#include +#include +#include +#include +#include #include +#include #include #include #include -#include -#include -#include -#include +#include #include -#include -#include - +#include +#include #define die(args...) do { \ - fprintf(stderr, "ERROR: "); \ - fprintf(stderr, args); \ - exit(EXIT_FAILURE); \ + fprintf(stderr, "ERROR: "); \ + fprintf(stderr, args); \ + fprintf(stderr, "\n"); \ + exit(EXIT_FAILURE); \ } while(0) #define dprintf(args...) if (verbose) printf(args) - #define INPDEV_MAX_DEVICES 16 #define INPDEV_MAX_PATH 30 -const char magic[] = "REVENT"; +#define MAX_NAME_LEN 255 +#define EV_BITS_SIZE (EV_MAX / 8 + 1) +#define KEY_BITS_SIZE (KEY_MAX / 8 + 1) -//This should be incremented if any changes are made to the file format -uint16_t file_version = 1; -int strlcpy(char *dest, char *source, size_t size) -{ - strncpy(dest, source, size-1); - dest[size-1] = '\0'; - return size; -} +#define HEADER_PADDING_SIZE 6 +#define EVENT_PADDING_SIZE 4 + +const char MAGIC[] = "REVENT"; + +// NOTE: This should be incremented if any changes are made to the file format +uint16_t FORMAT_VERSION = 2; typedef enum { - FALSE=0, - TRUE + FALSE=0, + TRUE } bool_t; -typedef enum { - RECORD=0, - REPLAY, - DUMP, - INFO, - INVALID -} revent_mode_t; +typedef enum { + GENERAL_MODE=0, + GAMEPAD_MODE, + INVALID_MODE // should be last +} recording_mode_t; + +typedef enum { + RECORD_COMMAND=0, + REPLAY_COMMAND, + DUMP_COMMAND, + INFO_COMMAND, + INVALID_COMMAND +} revent_command_t; typedef struct { - revent_mode_t mode; - int32_t record_time; - int32_t device_number; - char *file; + struct input_absinfo absinfo; + int ev_code; +} absinfo_t; + +typedef struct { + struct input_id id; + char name[MAX_NAME_LEN]; + char ev_bits[EV_BITS_SIZE]; + char abs_bits[KEY_BITS_SIZE]; + char rel_bits[KEY_BITS_SIZE]; + char key_bits[KEY_BITS_SIZE]; + uint32_t num_absinfo; + absinfo_t absinfo[ABS_CNT]; +} device_info_t; + +typedef struct { + revent_command_t command; + recording_mode_t mode; + int32_t record_time; + int32_t device_number; + char *file; } revent_args_t; typedef struct { - int32_t id_pathc; /* Count of total paths so far. */ - char id_pathv[INPDEV_MAX_DEVICES][INPDEV_MAX_PATH]; /* List of paths matching pattern. */ -} inpdev_t; + int32_t num; + char **paths; + int *fds; + int max_fd; +} input_devices_t; typedef struct { - int32_t dev_idx; - int32_t _padding; - struct input_event event; + int16_t dev_idx; + struct input_event event; } replay_event_t; typedef struct { - int32_t num_fds; - int32_t num_events; - int *fds; - replay_event_t *events; -} replay_buffer_t; + uint16_t version; + recording_mode_t mode; +} revent_record_desc_t; +typedef struct { + revent_record_desc_t desc; + input_devices_t devices; + device_info_t *gamepad_info; + uint64_t num_events; + replay_event_t *events; +} revent_recording_t; bool_t verbose = FALSE; bool_t wait_for_stdin = TRUE; bool_t is_numeric(char *string) { - int len = strlen(string); + int len = strlen(string); - int i = 0; - while(i < len) - { - if(!isdigit(string[i])) - return FALSE; - i++; - } + int i = 0; + while(i < len) + { + if(!isdigit(string[i])) + return FALSE; + i++; + } - return TRUE; + return TRUE; } -// Has to be done this explicitly to maintain compatibility between -// 32-bit and 64-bit devices -int read_replay_event(int fdin, replay_event_t* ev) +int test_bit(const char *mask, int bit) { + return mask[bit / 8] & (1 << (bit % 8)); +} + +int count_bits(const char *mask) { + int count = 0, i; + static const uint8_t nybble_lookup[16] = { + 0, 1, 1, 2, 1, 2, 2, 3, + 1, 2, 2, 3, 2, 3, 3, 4 + }; + + for (i = 0; i < KEY_MAX/8 + 1; i++) { + char byte = mask[i]; + count += nybble_lookup[byte & 0x0F] + nybble_lookup[byte >> 4]; + } + + return count; +} + +/* + * An input device is considered to be a gamepad if it supports + * ABS x and Y axes and the four gamepad buttons (variously known as + * square/triangle/circle/X, A/B/X/Y, or north/south/east/west). + */ +bool_t is_gamepad(device_info_t *dev) { - size_t rb; - - rb = read(fdin, &(ev->dev_idx), sizeof(int32_t)); - if (rb < (int)sizeof(int32_t)) return -1; - - rb = read(fdin, &(ev->_padding), sizeof(int32_t)); - if (rb < (int)sizeof(int32_t)) return -1; - - struct timeval time; - uint64_t temp_time; - rb = read(fdin, &temp_time, sizeof(uint64_t)); - if (rb < (int)sizeof(uint64_t)) return -1; - time.tv_sec = (time_t)temp_time; - - rb = read(fdin, &temp_time, sizeof(uint64_t)); - if (rb < (int)sizeof(uint64_t)) return -1; - time.tv_usec = (suseconds_t)temp_time; - - ev->event.time = time; - - rb = read(fdin, &(ev->event.type), sizeof(uint16_t)); - if (rb < (int)sizeof(uint16_t)) return -1; - - rb = read(fdin, &(ev->event.code), sizeof(uint16_t)); - if (rb < (int)sizeof(uint16_t)) return -1; - - rb = read(fdin, &(ev->event.value), sizeof(int32_t)); - if (rb < (int)sizeof(int32_t)) return -1; - - return 0; + if (!test_bit(dev->abs_bits, ABS_X)) + return FALSE; + if (!test_bit(dev->abs_bits, ABS_Y)) + return FALSE; + if (!test_bit(dev->key_bits, BTN_GAMEPAD)) + return FALSE; + return TRUE; } -void write_input_event(FILE * fdout, struct input_event* ev) -{ - uint64_t time; - time = (uint64_t)ev->time.tv_sec; - fwrite(&time, sizeof(uint64_t), 1, fdout); - time = (uint64_t)ev->time.tv_usec; - fwrite(&time, sizeof(uint64_t), 1, fdout); - fwrite(&(ev->type), sizeof(uint16_t), 1, fdout); - fwrite(&(ev->code), sizeof(uint16_t), 1, fdout); - fwrite(&(ev->value), sizeof(int32_t), 1, fdout); - -} - - off_t get_file_size(const char *filename) { - struct stat st; + struct stat st; - if (stat(filename, &st) == 0) - return st.st_size; + if (stat(filename, &st) == 0) + return st.st_size; - die("Cannot determine size of %s: %s\n", filename, strerror(errno)); + die("Cannot determine size of %s: %s", filename, strerror(errno)); } -int inpdev_init(inpdev_t **inpdev, int devid) -{ - int32_t i; - int fd; - int32_t num_devices; +int get_device_info(int fd, device_info_t *info) { + bzero(info, sizeof(device_info_t)); - *inpdev = malloc(sizeof(inpdev_t)); - (*inpdev)->id_pathc = 0; + if (ioctl(fd, EVIOCGID, &info->id) < 0) + return errno; - if (devid == -1) { - // device id was not specified so we want to record from all available input devices. - for(i = 0; i < INPDEV_MAX_DEVICES; ++i) - { - sprintf((*inpdev)->id_pathv[(*inpdev)->id_pathc], "/dev/input/event%d", i); - fd = open((*inpdev)->id_pathv[(*inpdev)->id_pathc], O_RDONLY); - if(fd > 0) - { - close(fd); - dprintf("opened %s\n", (*inpdev)->id_pathv[(*inpdev)->id_pathc]); - (*inpdev)->id_pathc++; - } - else - { - dprintf("could not open %s\n", (*inpdev)->id_pathv[(*inpdev)->id_pathc]); - } - } - } - else { - // device id was specified so record just that device. - sprintf((*inpdev)->id_pathv[0], "/dev/input/event%d", devid); - fd = open((*inpdev)->id_pathv[0], O_RDONLY); - if(fd > 0) - { - close(fd); - dprintf("opened %s\n", (*inpdev)->id_pathv[0]); - (*inpdev)->id_pathc++; - } - else - { - die("could not open %s\n", (*inpdev)->id_pathv[0]); - } - } + if (ioctl(fd, EVIOCGNAME(MAX_NAME_LEN * sizeof(char)), &info->name) < 0) + return errno; - return 0; + if (ioctl(fd, EVIOCGBIT(0, sizeof(info->ev_bits)), &info->ev_bits) < 0) + return errno; + + int ev_type; + for (ev_type = 0 ; ev_type < EV_MAX; ev_type++) { + if (test_bit(info->ev_bits, ev_type)) { + + if (ev_type == EV_ABS) { + ioctl(fd, EVIOCGBIT(ev_type, sizeof(info->abs_bits)), &info->abs_bits); + + int ev_code; + for (ev_code = 0; ev_code < KEY_MAX; ev_code++) { + if (test_bit(info->abs_bits, ev_code)) { + absinfo_t *inf = &info->absinfo[info->num_absinfo++]; + inf->ev_code = ev_code; + ioctl(fd, EVIOCGABS(ev_code), &inf->absinfo); + } + } + } else if (ev_type == EV_REL) { + ioctl(fd, EVIOCGBIT(ev_type, sizeof(info->rel_bits)), &info->rel_bits); + } else if (ev_type == EV_KEY) { + ioctl(fd, EVIOCGBIT(ev_type, sizeof(info->key_bits)), &info->key_bits); + } + } + } + + return 0; } -int inpdev_close(inpdev_t *inpdev) +void destroy_replay_device(int fd) { - free(inpdev); - return 0; + if(ioctl(fd, UI_DEV_DESTROY) < 0) + die("Could not destroy replay device"); } -void printDevProperties(const char* aDev) +inline void set_evbit(fd, bit) { - int fd = -1; - char name[256]= "Unknown"; - if ((fd = open(aDev, O_RDONLY)) < 0) - die("could not open %s\n", aDev); - - if(ioctl(fd, EVIOCGNAME(sizeof(name)), name) < 0) - die("evdev ioctl failed on %s\n", aDev); - - printf("The device on %s says its name is %s\n", - aDev, name); - close(fd); + if(ioctl(fd, UI_SET_EVBIT, bit) < 0) + die("Could not set EVBIT %i", bit); } -void dump(const char *logfile) +inline void set_keybit(fd, bit) { - int fdin = open(logfile, O_RDONLY); - if (fdin < 0) die("Could not open eventlog %s\n", logfile); - - int32_t len; - int32_t i; - char buf[INPDEV_MAX_PATH]; - - //Read magic - len = strlen(magic); - size_t rb = read(fdin, &buf[0], len); - buf[len] = '\0'; - if (rb != len) die("problems reading eventlog\n"); - if(strcmp(magic, buf) != 0) - die("File is not an revent recording, are you using an old recording?"); - - //Read file format version - uint16_t version; - rb = read(fdin, &version, sizeof(version)); - if (rb != sizeof(version)) die("problems reading eventlog\n"); - printf("File format version: %i\n", version); - - int32_t nfds; - rb = read(fdin, &nfds, sizeof(nfds)); - if (rb != sizeof(nfds)) die("problems reading eventlog\n"); - int *fds = malloc(sizeof(int)*nfds); - if (!fds) die("out of memory\n"); - - inpdev_t *inpdev = malloc(sizeof(inpdev_t)); - inpdev->id_pathc = nfds; - for (i=0; i= INPDEV_MAX_PATH) die("path length too long, file corrupt"); - rb = read(fdin, &buf[0], len); - if (rb != len) die("problems reading eventlog\n"); - strlcpy(inpdev->id_pathv[inpdev->id_pathc], buf, INPDEV_MAX_PATH); - } - - replay_event_t rep_ev; - struct input_event ev; - int count = 0; - while(1) { - if (read_replay_event(fdin, &rep_ev) == -1) - break; - ev = rep_ev.event; - - printf("%10u.%-6u %30s type %2d code %3d value %4d\n", - (unsigned int)ev.time.tv_sec, (unsigned int)ev.time.tv_usec, - inpdev->id_pathv[rep_ev.dev_idx], ev.type, ev.code, ev.value); - count++; - } - - printf("\nTotal: %d events\n", count); - close(fdin); - free(inpdev); + if(ioctl(fd, UI_SET_KEYBIT, bit) < 0) + die("Could not set KEYBIT %i", bit); } -int replay_buffer_init(replay_buffer_t **buffer, const char *logfile) +inline void set_absbit(fd, bit) { - *buffer = malloc(sizeof(replay_buffer_t)); - replay_buffer_t *buff = *buffer; - off_t fsize = get_file_size(logfile); - buff->events = (replay_event_t *)malloc((size_t)fsize); - if (!buff->events) - die("out of memory\n"); - - int fdin = open(logfile, O_RDONLY); - if (fdin < 0) - die("Could not open eventlog %s\n", logfile); - - int32_t len, i; - - //Read magic - char buf[7]; - len = strlen(magic); - size_t rb = read(fdin, &buf[0], len); - buf[len] = '\0'; - if (rb != len) die("problems reading eventlog\n"); - if(strcmp(magic, buf) != 0) - die("File is not an revent recording, are you using an old recording?"); - - //Read file format version - uint16_t version; - rb = read(fdin, &version, sizeof(version)); - if (rb != sizeof(version)) die("problems reading eventlog\n"); - - rb = read(fdin, &(buff->num_fds), sizeof(buff->num_fds)); - if (rb!=sizeof(buff->num_fds)) - die("problems reading eventlog\n"); - - buff->fds = malloc(sizeof(int) * buff->num_fds); - if (!buff->fds) - die("out of memory\n"); - - - char path_buff[INPDEV_MAX_PATH]; - for (i = 0; i < buff->num_fds; i++) { - memset(path_buff, 0, sizeof(path_buff)); - rb = read(fdin, &len, sizeof(len)); - if (rb!=sizeof(len)) - die("problems reading eventlog\n"); - if (len >= INPDEV_MAX_PATH) - die("path length too long, file corrupt"); - rb = read(fdin, &path_buff[0], len); - if (rb != len) - die("problems reading eventlog\n"); - - buff->fds[i] = open(path_buff, O_WRONLY | O_NDELAY); - if (buff->fds[i] < 0) - die("could not open device file %s\n", path_buff); - } - - struct timeval start_time; - replay_event_t rep_ev; - i = 0; - while(1) { - if (read_replay_event(fdin, &rep_ev) == -1) - break; - - if (i == 0) { - start_time = rep_ev.event.time; - } - timersub(&(rep_ev.event.time), &start_time, &(rep_ev.event.time)); - memcpy(&(buff->events[i]), &rep_ev, sizeof(rep_ev)); - i++; - } - buff->num_events = i - 1; - close(fdin); - return 0; + if(ioctl(fd, UI_SET_ABSBIT, bit) < 0) + die("Could not set ABSBIT %i", bit); } -int replay_buffer_close(replay_buffer_t *buff) +inline void set_relbit(fd, bit) { - free(buff->fds); - free(buff->events); - free(buff); - return 0; + if(ioctl(fd, UI_SET_RELBIT, bit) < 0) + die("Could not set RELBIT %i", bit); } -int replay_buffer_play(replay_buffer_t *buff) +inline void block_sigterm(sigset_t *oldset) { - int32_t i = 0, rb; - struct timeval start_time, now, desired_time, last_event_delta, delta; - memset(&last_event_delta, 0, sizeof(struct timeval)); - gettimeofday(&start_time, NULL); - - while (i < buff->num_events) { - gettimeofday(&now, NULL); - timeradd(&start_time, &last_event_delta, &desired_time); - - if (timercmp(&desired_time, &now, >)) { - timersub(&desired_time, &now, &delta); - useconds_t d = (useconds_t)delta.tv_sec * 1000000 + delta.tv_usec; - dprintf("now %u.%u desiredtime %u.%u sleeping %u uS\n", - (unsigned int)now.tv_sec, (unsigned int)now.tv_usec, - (unsigned int)desired_time.tv_sec, (unsigned int)desired_time.tv_usec, d); - usleep(d); - } - - int32_t idx = (buff->events[i]).dev_idx; - struct input_event ev = (buff->events[i]).event; - while((i < buff->num_events) && !timercmp(&ev.time, &last_event_delta, !=)) { - rb = write(buff->fds[idx], &ev, sizeof(ev)); - if (rb!=sizeof(ev)) - die("problems writing\n"); - dprintf("replayed event: type %d code %d value %d\n", ev.type, ev.code, ev.value); - - i++; - idx = (buff->events[i]).dev_idx; - ev = (buff->events[i]).event; - } - last_event_delta = ev.time; - } + sigset_t sigset; + sigemptyset(&sigset); + sigaddset(&sigset, SIGTERM); + sigprocmask(SIG_BLOCK, &sigset, oldset); } -void replay(const char *logfile) +// Events are recorded with their original timestamps, but for playback, we +// want to treat timestamps as deltas from event zero. +void adjust_event_times(revent_recording_t *recording) { - replay_buffer_t *replay_buffer; - replay_buffer_init(&replay_buffer, logfile); - replay_buffer_play(replay_buffer); - replay_buffer_close(replay_buffer); + uint64_t i; + struct timeval time_zero, time_delta; + + if (recording->num_events == 0) + return; + + time_zero.tv_sec = recording->events[0].event.time.tv_sec; + time_zero.tv_usec = recording->events[0].event.time.tv_usec; + + for(i = 0; i < recording->num_events; i++) { + timersub(&recording->events[i].event.time, &time_zero, &time_delta); + recording->events[i].event.time.tv_sec = time_delta.tv_sec; + recording->events[i].event.time.tv_usec = time_delta.tv_usec; + } +} + +int write_record_header(int fd, const revent_record_desc_t *desc) +{ + ssize_t ret; + char padding[HEADER_PADDING_SIZE]; + + ret = write(fd, MAGIC, 6); + if (ret < 6) + return errno; + + ret = write(fd, &desc->version, sizeof(desc->version)); + if (ret < sizeof(desc->version)) + return errno; + + ret = write(fd, (uint16_t *)&desc->mode, sizeof(uint16_t)); + if (ret < sizeof(uint16_t)) + return errno; + + bzero(padding, HEADER_PADDING_SIZE); + ret = write(fd, padding, HEADER_PADDING_SIZE); + if (ret < HEADER_PADDING_SIZE) + return errno; + + return 0; +} + +int read_record_header(int fd, revent_record_desc_t *desc) +{ + char start[7], padding[HEADER_PADDING_SIZE]; + ssize_t ret; + + ret = read(fd, start, 6); + if (ret < 6) + return errno; + + start[6] = '\0'; + if (strcmp(start, MAGIC)) + return EINVAL; + + ret = read(fd, &desc->version, sizeof(desc->version)); + if (ret < sizeof(desc->version)) + return errno; + + if (desc->version >= 2) { + ret = read(fd, &desc->mode, sizeof(uint16_t)); + if (ret < sizeof(uint16_t)) + return errno; + + ret = read(fd, padding, HEADER_PADDING_SIZE); + if (ret < HEADER_PADDING_SIZE) + return errno; + } else { + /* Version 1 supports only general recordings (mode 0) and + * does not have padding + */ + desc->mode = GENERAL_MODE; + } + + return 0; +} + +int write_general_input_devices(const input_devices_t *devices, FILE *fout) +{ + size_t ret; + uint32_t path_len; + int i; + + ret = fwrite(&devices->num, sizeof(uint32_t), 1, fout); + if (ret < 1) { + return errno; + } + + for (i = 0; i < devices->num; i++) { + path_len = (uint32_t)strlen(devices->paths[i]); + ret = fwrite(&path_len, sizeof(uint32_t), 1, fout); + if (ret < 1) { + return errno; + } + + ret = fwrite(devices->paths[i], sizeof(char), path_len, fout); + if (ret < path_len) { + return errno; + } + } + + return 0; +} + +int read_general_input_devices(input_devices_t *devices, FILE *fin) +{ + size_t ret; + uint32_t path_len; + int i; + + ret = fread(&devices->num, sizeof(uint32_t), 1, fin); + if (ret < 1) { + return EIO; + } + + devices->paths = malloc(sizeof(char *) * devices->num); + if (devices->paths == NULL) { + return ENOMEM; + } + + for (i = 0; i < devices->num; i++) { + ret = fread(&path_len, sizeof(uint32_t), 1, fin); + if (ret < 1) { + return EIO; + } + + devices->paths[i] = malloc(sizeof(char) * path_len + 1); + if (devices->paths[i] == NULL) { + return ENOMEM; + } + + ret = fread(devices->paths[i], sizeof(char), path_len, fin); + if (ret < path_len) { + return EIO; + } + devices->paths[i][path_len] = '\0'; + } + + return 0; +} + +int write_input_id(FILE *fout, const struct input_id *id) +{ + int ret = 0; + ret += fwrite(&id->bustype, sizeof(uint16_t), 1, fout); + ret += fwrite(&id->vendor, sizeof(uint16_t), 1, fout); + ret += fwrite(&id->product, sizeof(uint16_t), 1, fout); + ret += fwrite(&id->version, sizeof(uint16_t), 1, fout); + if (ret < 4) + return errno; + return 0; +} + +int read_input_id(FILE *fin, struct input_id *id) +{ + int ret = 0; + ret += fread(&id->bustype, sizeof(uint16_t), 1, fin); + ret += fread(&id->vendor, sizeof(uint16_t), 1, fin); + ret += fread(&id->product, sizeof(uint16_t), 1, fin); + ret += fread(&id->version, sizeof(uint16_t), 1, fin); + if (ret < 4) + return errno; + return 0; +} + +int write_absinfo(FILE *fout, const absinfo_t *info) +{ + int ret = 0; + ret += fwrite(&info->ev_code, sizeof(int32_t), 1, fout); + ret += fwrite(&info->absinfo.value, sizeof(int32_t), 1, fout); + ret += fwrite(&info->absinfo.minimum, sizeof(int32_t), 1, fout); + ret += fwrite(&info->absinfo.maximum, sizeof(int32_t), 1, fout); + ret += fwrite(&info->absinfo.fuzz, sizeof(int32_t), 1, fout); + ret += fwrite(&info->absinfo.flat, sizeof(int32_t), 1, fout); + ret += fwrite(&info->absinfo.resolution, sizeof(int32_t), 1, fout); + if (ret < 7) + return errno; + return 0; +} + +int read_absinfo(FILE *fin, absinfo_t *info) +{ + int ret = 0; + ret += fread(&info->ev_code, sizeof(int32_t), 1, fin); + ret += fread(&info->absinfo.value, sizeof(int32_t), 1, fin); + ret += fread(&info->absinfo.minimum, sizeof(int32_t), 1, fin); + ret += fread(&info->absinfo.maximum, sizeof(int32_t), 1, fin); + ret += fread(&info->absinfo.fuzz, sizeof(int32_t), 1, fin); + ret += fread(&info->absinfo.flat, sizeof(int32_t), 1, fin); + ret += fread(&info->absinfo.resolution, sizeof(int32_t), 1, fin); + if (ret < 7) + return errno; + return 0; +} + +int write_device_info(FILE *fout, const device_info_t *info) +{ + int ret = write_input_id(fout, &info->id); + if (ret) + return ret; + + uint32_t name_len = (uint32_t)strlen(info->name); + ret = fwrite(&name_len, sizeof(uint32_t), 1, fout); + ret += fwrite(info->name, sizeof(char), name_len, fout); + if (ret < (name_len + 1)) + return EIO; + + ret = fwrite(info->ev_bits, sizeof(char), EV_BITS_SIZE, fout); + ret += fwrite(info->abs_bits, sizeof(char), KEY_BITS_SIZE, fout); + ret += fwrite(info->rel_bits, sizeof(char), KEY_BITS_SIZE, fout); + ret += fwrite(info->key_bits, sizeof(char), KEY_BITS_SIZE, fout); + if (ret < (EV_BITS_SIZE + KEY_BITS_SIZE * 3)) + return EIO; + printf("EV_BITS_SIZE: %d\n", EV_BITS_SIZE); + printf("KEY_BITS_SIZE: %d\n", KEY_BITS_SIZE); + + ret = fwrite(&info->num_absinfo, sizeof(uint32_t), 1, fout); + if (ret < 1) + return errno; + + int i; + for (i = 0; i < info->num_absinfo; i++) { + ret = write_absinfo(fout, &info->absinfo[i]); + if (ret) + return ret; + } + + return 0; +} + +int read_device_info(FILE *fin, device_info_t *info) +{ + int ret = read_input_id(fin, &info->id); + if (ret) + return ret; + + uint32_t name_len = 0; + fread(&name_len, sizeof(uint32_t), 1, fin); + if (!name_len) + return EIO; + + ret += fread(info->name, sizeof(char), name_len, fin); + if (ret < name_len) + return EIO; + info->name[name_len] = '\0'; + + ret = fread(info->ev_bits, sizeof(char), EV_BITS_SIZE, fin); + ret += fread(info->abs_bits, sizeof(char), KEY_BITS_SIZE, fin); + ret += fread(info->rel_bits, sizeof(char), KEY_BITS_SIZE, fin); + ret += fread(info->key_bits, sizeof(char), KEY_BITS_SIZE, fin); + if (ret < (EV_BITS_SIZE + KEY_BITS_SIZE * 3)) + return EIO; + + ret = fread(&info->num_absinfo, sizeof(uint32_t), 1, fin); + if (ret < 1) + return errno; + + int i; + for (i = 0; i < info->num_absinfo; i++) { + ret = read_absinfo(fin, &info->absinfo[i]); + if (ret) + return ret; + } + + return 0; +} + +void print_device_info(device_info_t *info) +{ + printf("device name: %s\n", info->name); + printf("bustype: 0x%x vendor: 0x%x product: 0x%x version: 0x%x\n", + info->id.bustype, info->id.vendor, info->id.product, info->id.version); + printf("abs_bits: %d\n", count_bits(info->abs_bits)); + printf("rel_bits: %d\n", count_bits(info->rel_bits)); + printf("key_bits: %d\n", count_bits(info->key_bits)); + printf("num_absinfo: %ld\n", info->num_absinfo); + + int i; + printf("KEY: "); + for (i = 0; i < KEY_MAX; i++) { + if (test_bit(info->key_bits, i)) { + printf("%04x ", i); + } + } + printf("\n"); + + struct input_absinfo *inf; + int ev_code; + printf("ABS:\n"); + for (i = 0; i < info->num_absinfo; i++) { + ev_code = info->absinfo[i].ev_code; + inf = &info->absinfo[i].absinfo; + printf("%04x : min %i, max %i, fuzz %0i, flat %i, res %i\n", ev_code, + inf->minimum, inf->maximum, inf->fuzz, inf->flat, + inf->resolution); + } +} + +int write_replay_event(FILE *fout, const replay_event_t *ev) +{ + size_t ret; + + ret = fwrite(&ev->dev_idx, sizeof(uint16_t), 1, fout); + if (ret < 1) + return errno; + + ret = fwrite(&ev->event.time.tv_sec, sizeof(uint64_t), 1, fout); + if (ret < 1) + return errno; + + ret = fwrite(&ev->event.time.tv_usec, sizeof(uint64_t), 1, fout); + if (ret < 1) + return errno; + + ret = fwrite(&ev->event.type, sizeof(uint16_t), 1, fout); + if (ret < 1) + return errno; + + ret = fwrite(&ev->event.code, sizeof(uint16_t), 1, fout); + if (ret < 1) + return errno; + + ret = fwrite(&ev->event.value, sizeof(uint32_t), 1, fout); + if (ret < 1) + return errno; + + return 0; +} + +int read_replay_event(FILE *fin, replay_event_t *ev) +{ + size_t ret; + + ret = fread(&ev->dev_idx, sizeof(uint16_t), 1, fin); + if (ret < 1) + return errno; + + ret = fread(&ev->event.time.tv_sec, sizeof(uint64_t), 1, fin); + if (ret < 1) + return errno; + + ret = fread(&ev->event.time.tv_usec, sizeof(uint64_t), 1, fin); + if (ret < 1) + return errno; + + ret = fread(&ev->event.type, sizeof(uint16_t), 1, fin); + if (ret < 1) + return errno; + + ret = fread(&ev->event.code, sizeof(uint16_t), 1, fin); + if (ret < 1) + return errno; + + ret = fread(&ev->event.value, sizeof(uint32_t), 1, fin); + if (ret < 1) + return errno; + + return 0; +} + +int read_legacy_replay_event(int fdin, replay_event_t* ev) +{ + size_t rb; + char padding[EVENT_PADDING_SIZE]; + + rb = read(fdin, &(ev->dev_idx), sizeof(int32_t)); + if (rb < (int)sizeof(int32_t)) + return errno; + + rb = read(fdin, &padding, EVENT_PADDING_SIZE); + if (rb < (int)sizeof(int32_t)) + return errno; + + struct timeval time; + uint64_t temp_time; + rb = read(fdin, &temp_time, sizeof(uint64_t)); + if (rb < (int)sizeof(uint64_t)) + return errno; + time.tv_sec = (time_t)temp_time; + + rb = read(fdin, &temp_time, sizeof(uint64_t)); + if (rb < (int)sizeof(uint64_t)) + return errno; + time.tv_usec = (suseconds_t)temp_time; + + ev->event.time = time; + + rb = read(fdin, &(ev->event.type), sizeof(uint16_t)); + if (rb < (int)sizeof(uint16_t)) + return errno; + + rb = read(fdin, &(ev->event.code), sizeof(uint16_t)); + if (rb < (int)sizeof(uint16_t)) + return errno; + + rb = read(fdin, &(ev->event.value), sizeof(int32_t)); + if (rb < (int)sizeof(int32_t)) + return errno; + + return 0; +} + +int open_revent_recording(const char *filepath, revent_record_desc_t *desc, FILE **fin) +{ + *fin = fopen(filepath, "r"); + if (*fin == NULL) + return errno; + + int ret = read_record_header(fileno(*fin), desc); + if (ret) + return ret; + + if (desc->version < 0 || desc->version > FORMAT_VERSION) + return EPROTO; + + return 0; +} + +FILE *init_recording(const char *pathname, recording_mode_t mode) +{ + revent_record_desc_t desc = { .mode = mode, .version = FORMAT_VERSION }; + + FILE *fh = fopen(pathname, "w"); + if (fh == NULL) + return fh; + + write_record_header(fileno(fh), &desc); + + return fh; +} + +void init_input_devices(input_devices_t *devices) +{ + devices->num = 0; + devices->max_fd = -1; + devices->paths = NULL; + devices->fds = NULL; +} + +int init_general_input_devices(input_devices_t *devices) +{ + uint32_t num, i, path_len; + char paths[INPDEV_MAX_DEVICES][INPDEV_MAX_PATH]; + int fds[INPDEV_MAX_DEVICES]; + int max_fd; + + num = 0; + for(i = 0; i < INPDEV_MAX_DEVICES; ++i) { + sprintf(paths[num], "/dev/input/event%d", i); + fds[num] = open(paths[num], O_RDONLY); + if(fds[num] > 0) { + if (fds[num] > max_fd) + max_fd = fds[num]; + dprintf("opened %s\n", paths[num]); + num++; + } + else { + dprintf("could not open %s\n", paths[num]); + } + } + + if (num == 0) + return EACCES; + + devices->num = num; + devices->max_fd = max_fd; + + devices->paths = malloc(sizeof(char *) * num); + if (devices->paths == NULL) { + return ENOMEM; + } + for (i = 0; i < num; i ++) { + path_len = strlen(paths[i]); + devices->paths[i] = malloc(sizeof(char) * (path_len + 1)); + if (devices->paths[i] == NULL) + return ENOMEM; + strncpy(devices->paths[i], paths[i], path_len + 1); + } + + devices->fds = malloc(sizeof(int) * num); + if (devices->fds == NULL) { + return ENOMEM; + } + for (i = 0; i < num; i ++) + devices->fds[i] = fds[i]; + + return 0; +} + +void fini_general_input_devices(input_devices_t *devices) +{ + int i; + for (i = 0; i < devices->num; i++) { + if (devices->fds != NULL) + close(devices->fds[i]); + if (devices->paths != NULL) + free(devices->paths[i]); + } + free(devices->fds); + devices->num = 0; +} + + +int init_gamepad_input_devices(input_devices_t *devices, device_info_t *gamepad_info) +{ + int i; + char *gamepad_path = NULL; + input_devices_t all_devices; + device_info_t info; + + int ret = init_general_input_devices(&all_devices); + if (ret) { + return ret; + } + + for (i = 0; i < all_devices.num; i++) { + ret = get_device_info(all_devices.fds[i], &info); + if (ret) { + dprintf("Could not get info for %s: %s\n", all_devices.paths[i], strerror(errno)); + continue; + } + + if (!is_gamepad(&info)) { + dprintf("not a gamepad: %s\n", all_devices.paths[i]); + continue; + } + + if (gamepad_path != NULL) { + die("More than one device identified as a gamepad (run \"reven info\" to see which)"); + } + + gamepad_path = malloc(sizeof(char) * INPDEV_MAX_PATH); + if (gamepad_path == NULL) + die("Could not create replay device: %s", strerror(ENOMEM)); + strncpy(gamepad_path, all_devices.paths[i], INPDEV_MAX_PATH); + memcpy(gamepad_info, &info, sizeof(device_info_t)); + } + + fini_general_input_devices(&all_devices); + + if (gamepad_path == NULL) { + return ENOMEDIUM; + } + + dprintf("Found gamepad: %s\n", gamepad_path); + devices->num = 1; + + devices->paths = malloc(sizeof(char *)); + devices->paths[0] = gamepad_path; + + devices->fds = malloc(sizeof(int *)); + if (devices->fds == NULL) + return ENOMEM; + devices->fds[0] = open(gamepad_path, O_RDONLY); + if (devices->fds[0] < 0) { + return errno; + } + devices->max_fd = devices->fds[0]; + + return 0; +} + +void fini_gamepad_input_devices(input_devices_t *devices) +{ + fini_general_input_devices(devices); +} + +void init_revent_recording(revent_recording_t *recording) +{ + recording->num_events = 0; + recording->desc.version = 0; + recording->desc.mode = INVALID_MODE; + recording->events = NULL; + recording->gamepad_info = NULL; + init_input_devices(&recording->devices); +} + +void fini_revent_recording(revent_recording_t *recording) +{ + if (recording->desc.mode == GENERAL_MODE) { + fini_general_input_devices(&recording->devices); + } else if (recording->desc.mode == GAMEPAD_MODE) { + fini_gamepad_input_devices(&recording->devices); + free(recording->gamepad_info); + } else { + // We're finalizing the recording so at this point, + // we don't care. + } + free(recording->events); + recording->num_events = 0; + recording->desc.version = 0; + recording->desc.mode = INVALID_MODE; +} + +void open_general_input_devices_for_playback_or_die(input_devices_t *devices) +{ + int i, ret; + devices->fds = malloc(sizeof(int) * devices->num); + if (devices->fds == NULL) + die("Could not allocate file descriptor array: %s", strerror(ENOMEM)); + + for (i = 0; i < devices->num; i++) + { + ret = open(devices->paths[i], O_WRONLY | O_NDELAY); + if (ret < 0) { + die("Could not open \"%s\" for writing: %s", + devices->paths[i], strerror(errno)); + } + devices->fds[i] = ret; + if (devices->fds[i] > devices->max_fd) + devices->max_fd = devices->fds[i]; + dprintf("Opened %s\n", devices->paths[i]); + } +} + +int create_replay_device_or_die(const device_info_t *info) +{ + int i; + + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); + if (fd < 0) { + if (errno == ENOENT) { + die("uinput not supported by the kernel (is the module installed?)"); + } else if (errno == EACCES) { + die("Cannot access \"/dev/uinput\" (try re-running as root)"); + } else { + die("Could not open \"/dev/uinput\" for writing: %s", strerror(errno)); + } + } + + struct uinput_user_dev uidev; + memset(&uidev, 0, sizeof(uidev)); + snprintf(uidev.name, UINPUT_MAX_NAME_SIZE, "revent-replay %s", info->name); + uidev.id.bustype = BUS_USB; + uidev.id.vendor = info->id.vendor; + uidev.id.product = info->id.product; + uidev.id.version = info->id.version; + + set_evbit(fd, EV_SYN); + + set_evbit(fd, EV_KEY); + for (i = 0; i < KEY_MAX; i++) { + if (test_bit(info->key_bits, i)) + set_keybit(fd, i); + } + + set_evbit(fd, EV_REL); + for (i = 0; i < REL_MAX; i++) { + if (test_bit(info->rel_bits, i)) + set_relbit(fd, i); + } + + set_evbit(fd, EV_ABS); + for (i = 0; i < info->num_absinfo; i++) { + int ev_code = info->absinfo[i].ev_code; + set_absbit(fd, ev_code); + uidev.absmin[ev_code] = info->absinfo[i].absinfo.minimum; + uidev.absmax[ev_code] = info->absinfo[i].absinfo.maximum; + uidev.absfuzz[ev_code] = info->absinfo[i].absinfo.fuzz; + uidev.absflat[ev_code] = info->absinfo[i].absinfo.flat; + } + if (write(fd, &uidev, sizeof(uidev)) < sizeof(uidev)) { + die("Could not write absinfo:", strerror(errno)); + } + + if(ioctl(fd, UI_DEV_CREATE) < 0) + die("Could not create replay device:", strerror(errno)); + + // wait for the new device to be recognised by the system + sleep(3); + + return fd; +} + +inline void read_revent_recording_or_die(const char *filepath, revent_recording_t *recording) +{ + int ret; + FILE *fin; + uint64_t i; + off_t fsize; + + ret = open_revent_recording(filepath, &recording->desc, &fin); + if (ret) { + if (ret == EINVAL) { + die("%s does not appear to be an revent recording", filepath); + } else if (ret == EPROTO) { + die("%s contains recording for unsupported version \"%u\"; max supported version is \"%u\"", + filepath, recording->desc.version, FORMAT_VERSION); + } else { + die("%s revent recording appears to be corrupted", filepath); + } + } + + if (recording->desc.mode == GENERAL_MODE) { + ret = read_general_input_devices(&recording->devices, fin); + if (ret) { + die("Could not read devices: %s", strerror(ret)); + } + recording->gamepad_info = NULL; + } else if (recording->desc.mode == GAMEPAD_MODE) { + recording->gamepad_info = malloc(sizeof(device_info_t)); + if (recording->gamepad_info == NULL) + die("Could not allocate gamepad info buffer: %s", strerror(ENOMEM)); + ret = read_device_info(fin, recording->gamepad_info); + if (ret) + die("Could not read gamepad info: %s", strerror(ret)); + } else { + die("Unexpected recording mode: %d", recording->desc.mode); + } + + if (recording->desc.version > 1) { + ret = fread(&recording->num_events, sizeof(uint64_t), 1, fin); + if (ret < 1) + die("Could not read the number of recorded events"); + + recording->events = malloc(sizeof(replay_event_t) * recording->num_events); + if (recording->events == NULL) + die("Not enough memory to allocate replay buffer"); + + for(i=0; i < recording->num_events; i++) { + read_replay_event(fin, &recording->events[i]); + } + } else { // backwards compatibility + /* Prior to verion 2, the total number of recorded events was not being + * written as part of the recording. We will use the size of the file on + * disk to estimate the recording buffer size and keep reading the events + * untils EOF, keeping track of how many we read so that the total can + * then be updated. The format of the events is also different -- it + * featured larger device ID an unnecessary padding. + */ + fsize = get_file_size(filepath); + recording->events = malloc((size_t)fsize); + i = 0; + while (1) { + ret = read_legacy_replay_event(fileno(fin), &recording->events[i]); + if (ret == EOF) { + break; + } else if (ret) { + die("error reading events: %s", strerror(ret)); + } + i++; + } + recording->num_events = i; + } + + fclose(fin); +} + +void open_gamepad_input_devices_for_playback_or_die(input_devices_t *devices, const device_info_t *info) +{ + int fd = create_replay_device_or_die(info); + devices->num = 1; + devices->fds = malloc(sizeof(int)); + if (devices->fds == NULL) + die("Could not create replay devices: %s", strerror(ENOMEM)); + devices->fds[0] = fd; + devices->max_fd = fd; +} + +void record(const char *filepath, int delay, recording_mode_t mode) +{ + int ret; + FILE *fout = init_recording(filepath, mode); + if (fout == NULL) + die("Could not create recording \"%s\": %s", filepath, strerror(errno)); + + input_devices_t devices; + init_input_devices(&devices); + + if (mode == GENERAL_MODE) { + ret = init_general_input_devices(&devices); + if (ret) + die("Could not initialize input devices: %s", strerror(ret)); + ret = write_general_input_devices(&devices, fout); + if (ret) + die("Could not record input devices: %s", strerror(ret)); + } else if (mode == GAMEPAD_MODE) { + device_info_t info; + ret = init_gamepad_input_devices(&devices, &info); + if (ret == ENOMEDIUM) { + die("There does not appear to be a gamepad connected"); + } else if (ret) { + die("Problem initializing gamepad device: %s", strerror(ret)); + } + ret = write_device_info(fout, &info); + if (ret) + die("Problem writing gamepad info: %s", strerror(ret)); + } else { + fclose(fout); + die("Invalid recording mode specified"); + } + + sigset_t old_sigset; + block_sigterm(&old_sigset); + + // Write the zero size as a place holder and remember the position in the + // file stream, so that it may be updated at the end with the actual event + // count. + uint64_t event_count = 0; + long size_pos = ftell(fout); + ret = fwrite(&event_count, sizeof(uint64_t), 1, fout); + if (ret < 1) + die("Could not initialise event count: %s", strerror(errno)); + + char padding[EVENT_PADDING_SIZE]; + bzero(padding, EVENT_PADDING_SIZE); + + fd_set readfds; + struct timespec tout; + replay_event_t rev; + int32_t maxfd = 0; + int32_t keydev = 0; + int i; + printf("recording...\n"); + while(1) + { + FD_ZERO(&readfds); + if (wait_for_stdin) + FD_SET(STDIN_FILENO, &readfds); + for (i=0; i < devices.num; i++) + FD_SET(devices.fds[i], &readfds); + + /* wait for input */ + tout.tv_sec = delay; + tout.tv_nsec = 0; + ret = pselect(devices.max_fd + 1, &readfds, NULL, NULL, &tout, &old_sigset); + if (errno == EINTR) + break; + if (!ret) + break; + + if (wait_for_stdin && FD_ISSET(STDIN_FILENO, &readfds)) { + // in this case the key down for the return key will be recorded + // so we need to up the key up + memset(&rev, 0, sizeof(rev)); + rev.dev_idx = keydev; + rev.event.type = EV_KEY; + rev.event.code = KEY_ENTER; + rev.event.value = 0; + gettimeofday(&rev.event.time, NULL); + write_replay_event(fout, &rev); + + // syn + memset(&rev, 0, sizeof(rev)); + rev.dev_idx = keydev; + rev.event.type = EV_SYN; + rev.event.code = 0; + rev.event.value = 0; + gettimeofday(&rev.event.time, NULL); + write_replay_event(fout, &rev); + + dprintf("added fake return exiting...\n"); + break; + } + + for (i = 0; i < devices.num; i++) + { + if (FD_ISSET(devices.fds[i], &readfds)) + { + dprintf("got event from %s\n", devices.paths[i]); + memset(&rev, 0, sizeof(rev)); + rev.dev_idx = i; + ret = read(devices.fds[i], (void *)&rev.event, sizeof(rev.event)); + dprintf("%d event: type %d code %d value %d\n", + (unsigned int)ret, rev.event.type, rev.event.code, rev.event.value); + if (rev.event.type == EV_KEY && rev.event.code == KEY_ENTER && rev.event.value == 1) + keydev = i; + write_replay_event(fout, &rev); + event_count++; + } + } + } + + dprintf("Writing event count..."); + if ((ret = fseek(fout, size_pos, SEEK_SET)) == -1) + die("Could not write event count: %s", strerror(errno)); + ret = fwrite(&event_count, sizeof(uint64_t), 1, fout); + if (ret < 1) + die("Could not write event count: %s", strerror(errno)); + + fclose(fout); + + if (mode == GENERAL_MODE) { + fini_general_input_devices(&devices); + } else if (mode == GAMEPAD_MODE) { + fini_gamepad_input_devices(&devices); + } else { + // Should never get here, as would have failed at the beginning + die("Unexpected mode on finish"); + } +} + +void dump(const char *filepath) +{ + int i, ret = 0; + revent_recording_t recording; + init_revent_recording(&recording); + + read_revent_recording_or_die(filepath, &recording); + printf("recording version: %u\n", recording.desc.version); + printf("recording type: %i\n", recording.desc.mode); + printf("number of recorded events: %lu\n", recording.num_events); + + printf("\n"); + if (recording.desc.mode == GENERAL_MODE) { + printf("devices:\n"); + for (i = 0; i < recording.devices.num; i++) { + printf("%2i: %s\n", i, recording.devices.paths[i]); + } + } else if (recording.desc.mode == GAMEPAD_MODE) { + print_device_info(recording.gamepad_info); + } else { + die("Unexpected recording type: %d", recording.desc.mode); + } + + printf("\nevents:\n"); + for (i =0; i < recording.num_events; i++) { + printf("%ld.%06ld dev: %d type: %d code: %d value %d\n", + recording.events[i].event.time.tv_sec, + recording.events[i].event.time.tv_usec, + recording.events[i].dev_idx, + recording.events[i].event.type, + recording.events[i].event.code, + recording.events[i].event.value + ); + } + + fini_revent_recording(&recording); +} + +void replay(const char *filepath) +{ + revent_recording_t recording; + init_revent_recording(&recording); + + read_revent_recording_or_die(filepath, &recording); + switch (recording.desc.mode) { + case GENERAL_MODE: + dprintf("Opening input devices for playback\n"); + open_general_input_devices_for_playback_or_die(&recording.devices); + break; + case GAMEPAD_MODE: + dprintf("Creating gamepad playback device\n"); + open_gamepad_input_devices_for_playback_or_die(&recording.devices, recording.gamepad_info); + break; + default: + die("Unexpected recording mod: %d", recording.desc.mode); + } + dprintf("Adjusting event timestamps\n"); + adjust_event_times(&recording); + + struct timeval start_time, now, desired_time, last_event_delta, delta; + bzero(&last_event_delta, sizeof(struct timeval)); + gettimeofday(&start_time, NULL); + + int ret; + uint64_t i = 0; + dprintf("Starting payback\n"); + while (i < recording.num_events) { + gettimeofday(&now, NULL); + timeradd(&start_time, &last_event_delta, &desired_time); + + if (timercmp(&desired_time, &now, >)) { + timersub(&desired_time, &now, &delta); + useconds_t d = (useconds_t)delta.tv_sec * 1000000 + delta.tv_usec; + dprintf("now %u.%u desiredtime %u.%u sleeping %u uS\n", + (unsigned int)now.tv_sec, + (unsigned int)now.tv_usec, + (unsigned int)desired_time.tv_sec, + (unsigned int)desired_time.tv_usec, + d); + usleep(d); + } + + int32_t idx = (recording.events[i]).dev_idx; + struct input_event ev = (recording.events[i]).event; + while((i < recording.num_events) && !timercmp(&ev.time, &last_event_delta, !=)) { + ret = write(recording.devices.fds[idx], &ev, sizeof(ev)); + if (ret != sizeof(ev)) + die("Could not replay event"); + dprintf("replayed event: type %d code %d value %d\n", ev.type, ev.code, ev.value); + + i++; + idx = recording.events[i].dev_idx; + ev = recording.events[i].event; + } + last_event_delta = ev.time; + } + + if (recording.desc.mode == GAMEPAD_MODE) + destroy_replay_device(recording.devices.fds[0]); + fini_revent_recording(&recording); +} + +void info(void) +{ + input_devices_t devices; + init_input_devices(&devices); + + int ret = init_general_input_devices(&devices); + if (ret) { + die("Could not read input devices: %s", strerror(errno)); + } + + int i; + device_info_t info; + for (i = 0; i < devices.num; i++) { + ret = get_device_info(devices.fds[i], &info); + if (ret) { + printf("Could not get info for %s: %s\n", devices.paths[i], strerror(errno)); + continue; + } + + printf("DEVICE %d\n", i); + printf("device path: %s\n", devices.paths[i]); + printf("is gamepad: %s\n", is_gamepad(&info) ? "yes" : "no"); + print_device_info(&info); + printf("\n"); + } + + fini_general_input_devices(&devices); } void usage() { - printf("usage:\n revent [-h] [-v] COMMAND [OPTIONS] \n" - "\n" - " Options:\n" - " -h print this help message and quit.\n" - " -v enable verbose output.\n" - "\n" - " Commands:\n" - " record [-t SECONDS] [-d DEVICE] FILE\n" - " Record input event. stops after return on STDIN (or, optionally, \n" - " a fixed delay)\n" - "\n" - " FILE file into which events will be recorded.\n" - " -t SECONDS time, in seconds, for which to record events.\n" - " if not specifed, recording will continue until\n" - " return key is pressed.\n" - " -d DEVICE the number of the input device form which\n" - " events will be recoreded. If not specified, \n" - " all available inputs will be used.\n" - " -s Recording will not be stopped if there is \n" - " input on STDIN.\n" - "\n" - " replay FILE\n" - " replays previously recorded events from the specified file.\n" - "\n" - " FILE file into which events will be recorded.\n" - "\n" - " dump FILE\n" - " dumps the contents of the specified event log to STDOUT in\n" - " human-readable form.\n" - "\n" - " FILE event log which will be dumped.\n" - "\n" - " info\n" - " shows info about each event char device\n" - "\n" - ); + printf("usage:\n revent [-h] [-v] COMMAND [OPTIONS] \n" + "\n" + " Options:\n" + " -h print this help message and quit.\n" + " -v enable verbose output.\n" + "\n" + " Commands:\n" + " record [-t SECONDS] [-d DEVICE] FILE\n" + " Record input event. stops after return on STDIN (or, optionally, \n" + " a fixed delay)\n" + "\n" + " FILE file into which events will be recorded.\n" + " -t SECONDS time, in seconds, for which to record events.\n" + " if not specified, recording will continue until\n" + " return key is pressed.\n" + " -d DEVICE the number of the input device form which\n" + " events will be recorded. If not specified, \n" + " all available inputs will be used.\n" + " -s Recording will not be stopped if there is \n" + " input on STDIN.\n" + " -g Record in \"gamepad\" mode. A gamepad must be \n" + " connected to the device. The recording will only\n" + " be done for the gamepad and other input devices\n" + " will not be recorded. In addition to the input\n" + " events, the information about the gamepad will\n" + " also be stored in the recording. When this\n" + " recording is played back, revent will first\n" + " create a virtual gamepad device based on the\n" + " stored info and the event will be played back\n" + " into it. This type of recording should be more\n" + " portable across different devices.\n" + "\n" + " replay FILE\n" + " replays previously recorded events from the specified file.\n" + "\n" + " FILE file into which events will be recorded.\n" + "\n" + " dump FILE\n" + " dumps the contents of the specified event log to STDOUT in\n" + " human-readable form.\n" + "\n" + " FILE event log which will be dumped.\n" + "\n" + " info\n" + " shows info about each event char device\n" + "\n" + ); } void revent_args_init(revent_args_t **rargs, int argc, char** argv) { - *rargs = malloc(sizeof(revent_args_t)); - revent_args_t *revent_args = *rargs; - revent_args->mode = INVALID; - revent_args->record_time = INT_MAX; - revent_args->device_number = -1; - revent_args->file = NULL; + *rargs = malloc(sizeof(revent_args_t)); + revent_args_t *revent_args = *rargs; + revent_args->command = INVALID_COMMAND; + revent_args->mode = GENERAL_MODE; + revent_args->record_time = INT_MAX; + revent_args->device_number = -1; + revent_args->file = NULL; - int opt; - while ((opt = getopt(argc, argv, "ht:d:vs")) != -1) - { - switch (opt) { - case 'h': - usage(); - exit(0); - break; - case 't': - if (is_numeric(optarg)) { - revent_args->record_time = atoi(optarg); - dprintf("timeout: %d\n", revent_args->record_time); - } else { - die("-t parameter must be numeric; got %s.\n", optarg); - } - break; - case 'd': - if (is_numeric(optarg)) { - revent_args->device_number = atoi(optarg); - dprintf("device: %d\n", revent_args->device_number); - } else { - die("-d parameter must be numeric; got %s.\n", optarg); - } - break; - case 'v': - verbose = TRUE; - break; - case 's': - wait_for_stdin = FALSE; - break; + int opt; + while ((opt = getopt(argc, argv, "hgt:d:vs")) != -1) + { + switch (opt) { + case 'h': + usage(); + exit(0); + break; + case 'g': + revent_args->mode = GAMEPAD_MODE; + break; + case 't': + if (is_numeric(optarg)) { + revent_args->record_time = atoi(optarg); + dprintf("timeout: %d\n", revent_args->record_time); + } else { + die("-t parameter must be numeric; got %s.", optarg); + } + break; + case 'd': + if (is_numeric(optarg)) { + revent_args->device_number = atoi(optarg); + dprintf("device: %d\n", revent_args->device_number); + } else { + die("-d parameter must be numeric; got %s.", optarg); + } + break; + case 'v': + verbose = TRUE; + break; + case 's': + wait_for_stdin = FALSE; + break; - default: - die("Unexpected option: %c", opt); - } - } + default: + die("Unexpected option: %c", opt); + } + } - int next_arg = optind; - if (next_arg == argc) { - usage(); - die("Must specify a command.\n"); - } - if (!strcmp(argv[next_arg], "record")) - revent_args->mode = RECORD; - else if (!strcmp(argv[next_arg], "replay")) - revent_args->mode = REPLAY; - else if (!strcmp(argv[next_arg], "dump")) - revent_args->mode = DUMP; - else if (!strcmp(argv[next_arg], "info")) - revent_args->mode = INFO; - else { - usage(); - die("Unknown command -- %s\n", argv[next_arg]); - } - next_arg++; + int next_arg = optind; + if (next_arg == argc) { + usage(); + die("Must specify a command."); + } + if (!strcmp(argv[next_arg], "record")) + revent_args->command = RECORD_COMMAND; + else if (!strcmp(argv[next_arg], "replay")) + revent_args->command = REPLAY_COMMAND; + else if (!strcmp(argv[next_arg], "dump")) + revent_args->command = DUMP_COMMAND; + else if (!strcmp(argv[next_arg], "info")) + revent_args->command = INFO_COMMAND; + else { + usage(); + die("Unknown command -- %s", argv[next_arg]); + } + next_arg++; - if (next_arg != argc) { - revent_args->file = argv[next_arg]; - dprintf("file: %s\n", revent_args->file); - next_arg++; - if (next_arg != argc) { - die("Trailling arguments (use -h for help).\n"); - } - } + if (next_arg != argc) { + revent_args->file = argv[next_arg]; + dprintf("file: %s\n", revent_args->file); + next_arg++; + if (next_arg != argc) { + die("Trailling arguments (use -h for help)."); + } + } - if ((revent_args->mode != RECORD) && (revent_args->record_time != INT_MAX)) { - die("-t parameter is only valid for \"record\" command.\n"); - } - if ((revent_args->mode != RECORD) && (revent_args->device_number != -1)) { - die("-d parameter is only valid for \"record\" command.\n"); - } - if ((revent_args->mode == INFO) && (revent_args->file != NULL)) { - die("File path cannot be specified for \"info\" command.\n"); - } - if (((revent_args->mode == RECORD) || (revent_args->mode == REPLAY)) && (revent_args->file == NULL)) { - die("Must specify a file for recording/replaying (use -h for help).\n"); - } + if ((revent_args->command != RECORD_COMMAND) && (revent_args->record_time != INT_MAX)) { + die("-t parameter is only valid for \"record\" command."); + } + if ((revent_args->command != RECORD_COMMAND) && (revent_args->device_number != -1)) { + die("-d parameter is only valid for \"record\" command."); + } + if ((revent_args->command == INFO_COMMAND) && (revent_args->file != NULL)) { + die("File path cannot be specified for \"info\" command."); + } + if (((revent_args->command == RECORD_COMMAND) || (revent_args->command == REPLAY_COMMAND)) + && (revent_args->file == NULL)) { + die("Must specify a file for recording/replaying (use -h for help)."); + } } int revent_args_close(revent_args_t *rargs) { - free(rargs); - return 0; -} - -int* fds = NULL; -FILE* fdout = NULL; -revent_args_t *rargs = NULL; -inpdev_t *inpdev = NULL; -int count; - -void term_handler(int signum) -{ - (void)signum; -} - -void record(inpdev_t *inpdev, int delay, const char *logfile) -{ - fd_set readfds; - struct input_event ev; - int32_t i; - int32_t _padding = 0xdeadbeef; - int32_t maxfd = 0; - int32_t keydev=0; - - //signal handler - struct sigaction action; - memset(&action, 0, sizeof(struct sigaction)); - action.sa_handler = term_handler; - sigaction(SIGTERM, &action, NULL); - - fds = malloc(sizeof(int)*inpdev->id_pathc); - if (!fds) die("out of memory\n"); - - fdout = fopen(logfile, "wb"); - if (!fdout) die("Could not open eventlog %s\n", logfile); - - //Write magic & file format version - fwrite(&magic, strlen(magic), 1, fdout); - fwrite(&file_version, sizeof(file_version), 1, fdout); - - //Write device paths - fwrite(&inpdev->id_pathc, sizeof(inpdev->id_pathc), 1, fdout); - for (i=0; iid_pathc; i++) { - int32_t len = strlen(inpdev->id_pathv[i]); - fwrite(&len, sizeof(len), 1, fdout); - fwrite(inpdev->id_pathv[i], len, 1, fdout); - } - - for (i=0; i < inpdev->id_pathc; i++) - { - fds[i] = open(inpdev->id_pathv[i], O_RDONLY); - if (fds[i]>maxfd) maxfd = fds[i]; - dprintf("opened %s with %d\n", inpdev->id_pathv[i], fds[i]); - if (fds[i]<0) die("could not open %s\n", inpdev->id_pathv[i]); - } - - //Block SIGTERM - sigset_t sigset, oldset; - sigemptyset(&sigset); - sigaddset(&sigset, SIGTERM); - sigprocmask(SIG_BLOCK, &sigset, &oldset); - - count = 0; - struct timespec tout; - while(1) - { - FD_ZERO(&readfds); - if (wait_for_stdin) - { - FD_SET(STDIN_FILENO, &readfds); - } - for (i=0; i < inpdev->id_pathc; i++) - FD_SET(fds[i], &readfds); - /* wait for input */ - tout.tv_sec = delay; - tout.tv_nsec = 0; - int32_t r = pselect(maxfd+1, &readfds, NULL, NULL, &tout, &oldset); - if (errno == EINTR) - break; - /* dprintf("got %d (err %d)\n", r, errno); */ - if (!r) - break; - if (wait_for_stdin && FD_ISSET(STDIN_FILENO, &readfds)) { - // in this case the key down for the return key will be recorded - // so we need to up the key up - memset(&ev, 0, sizeof(ev)); - ev.type = EV_KEY; - ev.code = KEY_ENTER; - ev.value = 0; - gettimeofday(&ev.time, NULL); - fwrite(&keydev, sizeof(keydev), 1, fdout); - fwrite(&_padding, sizeof(_padding), 1, fdout); - write_input_event(fdout, &ev); - memset(&ev, 0, sizeof(ev)); // SYN - gettimeofday(&ev.time, NULL); - fwrite(&keydev, sizeof(keydev), 1, fdout); - fwrite(&_padding, sizeof(_padding), 1, fdout); - write_input_event(fdout, &ev); - dprintf("added fake return exiting...\n"); - break; - } - - for (i=0; i < inpdev->id_pathc; i++) - { - if (FD_ISSET(fds[i], &readfds)) - { - dprintf("Got event from %s\n", inpdev->id_pathv[i]); - memset(&ev, 0, sizeof(ev)); - size_t rb = read(fds[i], (void*) &ev, sizeof(ev)); - dprintf("%d event: type %d code %d value %d\n", - (unsigned int)rb, ev.type, ev.code, ev.value); - if (ev.type == EV_KEY && ev.code == KEY_ENTER && ev.value == 1) - keydev = i; - fwrite(&i, sizeof(i), 1, fdout); - fwrite(&_padding, sizeof(_padding), 1, fdout); - write_input_event(fdout, &ev); - count++; - } - } - } - - for (i=0; i < inpdev->id_pathc; i++) - { - close(fds[i]); - } - - fclose(fdout); - free(fds); - dprintf("Recorded %d events\n", count); + free(rargs); + return 0; } int main(int argc, char** argv) { - int i; - char *logfile = NULL; + int i; + char *logfile = NULL; + revent_args_t *rargs = NULL; - revent_args_init(&rargs, argc, argv); + revent_args_init(&rargs, argc, argv); - inpdev_init(&inpdev, rargs->device_number); + switch(rargs->command) { + case RECORD_COMMAND: + record(rargs->file, rargs->record_time, rargs->mode); + break; + case REPLAY_COMMAND: + replay(rargs->file); + break; + case DUMP_COMMAND: + dump(rargs->file); + break; + case INFO_COMMAND: + info(); + break; + defaut: + die("Unexpected revent command: %d", rargs->command); + }; - switch(rargs->mode) { - case RECORD: - record(inpdev, rargs->record_time, rargs->file); - break; - case REPLAY: - replay(rargs->file); - break; - case DUMP: - dump(rargs->file); - break; - case INFO: - for (i = 0; i < inpdev->id_pathc; i++) { - printDevProperties(inpdev->id_pathv[i]); - } - }; - - inpdev_close(inpdev); - revent_args_close(rargs); - return 0; + revent_args_close(rargs); + return 0; } 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()