diff --git a/doc/source/revent.rst b/doc/source/revent.rst index b3e6e6cf..39d80b3b 100644 --- a/doc/source/revent.rst +++ b/doc/source/revent.rst @@ -106,3 +106,107 @@ where as UI Automator only works for Android UI elements (such as text boxes or radio buttons), which makes the latter useless for things like games. Recording 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 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ diff --git a/wlauto/commands/record.py b/wlauto/commands/record.py index ae6e2ab1..21f80399 100644 --- a/wlauto/commands/record.py +++ b/wlauto/commands/record.py @@ -15,6 +15,8 @@ import os import sys +import signal +from math import ceil from wlauto import ExtensionLoader, Command, settings from wlauto.common.resources import Executable @@ -22,6 +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 class RecordCommand(Command): @@ -114,13 +117,15 @@ class RecordCommand(Command): self.logger.info("Press Enter when you are ready to record...") raw_input("") - command = "{} record -t 100000 -s {}".format(self.target_binary, revent_file) + command = "{} record -s {}".format(self.target_binary, revent_file) self.device.kick_off(command) self.logger.info("Press Enter when you have finished recording...") raw_input("") - self.device.killall("revent") - + self.device.killall("revent", signal.SIGTERM) + self.logger.info("Waiting for revent to finish") + while self.device.get_pids_of("revent"): + pass self.logger.info("Pulling files from device") self.device.pull_file(revent_file, args.output or os.getcwdu()) @@ -154,8 +159,10 @@ class ReplayCommand(RecordCommand): self.logger.info("Starting {}".format(args.package)) self.device.execute('monkey -p {} -c android.intent.category.LAUNCHER 1'.format(args.package)) + self.logger.info("Replaying recording") command = "{} replay {}".format(self.target_binary, revent_file) - self.device.execute(command) + timeout = ceil(ReventParser.get_revent_duration(args.revent)) + 30 + self.device.execute(command, timeout=timeout) self.logger.info("Finished replay") diff --git a/wlauto/common/android/workload.py b/wlauto/common/android/workload.py index 3c5268c9..f784946a 100644 --- a/wlauto/common/android/workload.py +++ b/wlauto/common/android/workload.py @@ -16,6 +16,7 @@ import os import sys import time +from math import ceil from wlauto.core.extension import Parameter from wlauto.core.workload import Workload @@ -25,6 +26,7 @@ from wlauto.common.resources import ExtensionAsset, Executable from wlauto.exceptions import WorkloadError, ResourceError, ConfigError from wlauto.utils.android import ApkInfo, ANDROID_NORMAL_PERMISSIONS from wlauto.utils.types import boolean +from wlauto.utils.revent import ReventParser import wlauto.common.android.resources @@ -322,16 +324,13 @@ AndroidBenchmark = ApkWorkload # backward compatibility class ReventWorkload(Workload): - default_setup_timeout = 5 * 60 # in seconds - default_run_timeout = 10 * 60 # in seconds - def __init__(self, device, _call_super=True, **kwargs): if _call_super: super(ReventWorkload, self).__init__(device, **kwargs) devpath = self.device.path self.on_device_revent_binary = devpath.join(self.device.binaries_directory, 'revent') - self.setup_timeout = kwargs.get('setup_timeout', self.default_setup_timeout) - self.run_timeout = kwargs.get('run_timeout', self.default_run_timeout) + self.setup_timeout = kwargs.get('setup_timeout', None) + self.run_timeout = kwargs.get('run_timeout', None) self.revent_setup_file = None self.revent_run_file = None self.on_device_setup_revent = None @@ -346,6 +345,10 @@ 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 + self.setup_timeout = self.setup_timeout or default_setup_timeout + self.run_timeout = self.run_timeout or default_run_timeout def setup(self, context): self.device.killall('revent') diff --git a/wlauto/common/bin/arm64/revent b/wlauto/common/bin/arm64/revent index 1b1b5e8c..e18d7c36 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 f908b4d3..0744cf58 100755 Binary files a/wlauto/common/bin/armeabi/revent and b/wlauto/common/bin/armeabi/revent differ diff --git a/wlauto/external/revent/revent.c b/wlauto/external/revent/revent.c index 667bb51c..b065335b 100644 --- a/wlauto/external/revent/revent.c +++ b/wlauto/external/revent/revent.c @@ -42,7 +42,10 @@ #define INPDEV_MAX_DEVICES 16 #define INPDEV_MAX_PATH 30 +const char magic[] = "REVENT"; +//This should be incremented if any changes are made to the file format +uint16_t file_version = 1; #ifndef ANDROID int strlcpy(char *dest, char *source, size_t size) @@ -110,6 +113,56 @@ bool_t is_numeric(char *string) 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) +{ + 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; +} + +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; @@ -191,41 +244,52 @@ void dump(const char *logfile) int fdin = open(logfile, O_RDONLY); if (fdin < 0) die("Could not open eventlog %s\n", logfile); - int nfds; - size_t 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"); - int32_t len; int32_t i; char buf[INPDEV_MAX_PATH]; + //Read magic + len = strlen(magic); + size_t rb = read(fdin, &buf[0], len); + 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 = 0; + 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); - inpdev->id_pathv[inpdev->id_pathc][INPDEV_MAX_PATH-1] = '\0'; - inpdev->id_pathc++; } + replay_event_t rep_ev; struct input_event ev; int count = 0; while(1) { - int32_t idx; - rb = read(fdin, &idx, sizeof(idx)); - if (rb != sizeof(idx)) break; - rb = read(fdin, &ev, sizeof(ev)); - if (rb < (int)sizeof(ev)) break; + 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[idx], ev.type, ev.code, ev.value); + inpdev->id_pathv[rep_ev.dev_idx], ev.type, ev.code, ev.value); count++; } @@ -247,7 +311,22 @@ int replay_buffer_init(replay_buffer_t **buffer, const char *logfile) if (fdin < 0) die("Could not open eventlog %s\n", logfile); - size_t rb = read(fdin, &(buff->num_fds), sizeof(buff->num_fds)); + int32_t len, i; + + //Read magic + char buf[7]; + len = strlen(magic); + size_t rb = read(fdin, &buf[0], len); + 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"); @@ -255,13 +334,15 @@ int replay_buffer_init(replay_buffer_t **buffer, const char *logfile) if (!buff->fds) die("out of memory\n"); - int32_t len, i; - char path_buff[256]; // should be more than enough + + 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"); @@ -275,8 +356,7 @@ int replay_buffer_init(replay_buffer_t **buffer, const char *logfile) replay_event_t rep_ev; i = 0; while(1) { - rb = read(fdin, &rep_ev, sizeof(rep_ev)); - if (rb < (int)sizeof(rep_ev)) + if (read_replay_event(fdin, &rep_ev) == -1) break; if (i == 0) { @@ -369,6 +449,8 @@ void usage() " -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" @@ -488,19 +570,7 @@ int count; void term_handler(int signum) { - int32_t i; - for (i=0; i < inpdev->id_pathc; i++) - { - close(fds[i]); - } - - fclose(fdout); - free(fds); - dprintf("Recorded %d events\n", count); - - inpdev_close(inpdev); - revent_args_close(rargs); - exit(0); + (void)signum; } void record(inpdev_t *inpdev, int delay, const char *logfile) @@ -524,6 +594,11 @@ void record(inpdev_t *inpdev, int delay, const char *logfile) 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]); @@ -536,11 +611,17 @@ void record(inpdev_t *inpdev, int delay, const char *logfile) 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]); + 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 timeval tout; + struct timespec tout; while(1) { FD_ZERO(&readfds); @@ -552,10 +633,13 @@ void record(inpdev_t *inpdev, int delay, const char *logfile) FD_SET(fds[i], &readfds); /* wait for input */ tout.tv_sec = delay; - tout.tv_usec = 0; - int32_t r = select(maxfd+1, &readfds, NULL, NULL, &tout); + 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 (!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 @@ -566,12 +650,12 @@ void record(inpdev_t *inpdev, int delay, const char *logfile) gettimeofday(&ev.time, NULL); fwrite(&keydev, sizeof(keydev), 1, fdout); fwrite(&_padding, sizeof(_padding), 1, fdout); - fwrite(&ev, sizeof(ev), 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); - fwrite(&ev, sizeof(ev), 1, fdout); + write_input_event(fdout, &ev); dprintf("added fake return exiting...\n"); break; } @@ -589,7 +673,7 @@ void record(inpdev_t *inpdev, int delay, const char *logfile) keydev = i; fwrite(&i, sizeof(i), 1, fdout); fwrite(&_padding, sizeof(_padding), 1, fdout); - fwrite(&ev, sizeof(ev), 1, fdout); + write_input_event(fdout, &ev); count++; } } diff --git a/wlauto/resource_getters/standard.py b/wlauto/resource_getters/standard.py index 1aedcb77..7d9c22a9 100644 --- a/wlauto/resource_getters/standard.py +++ b/wlauto/resource_getters/standard.py @@ -33,6 +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 logging.getLogger("requests").setLevel(logging.WARNING) @@ -98,7 +99,12 @@ class ReventGetter(ResourceGetter): location = _d(os.path.join(self.get_base_location(resource), 'revent_files')) for candidate in os.listdir(location): if candidate.lower() == filename.lower(): - return os.path.join(location, candidate) + path = os.path.join(location, candidate) + try: + ReventParser.check_revent_file(path) + return path + except ValueError as e: + self.logger.warning(e.message) class PackageApkGetter(PackageFileGetter): @@ -430,7 +436,11 @@ class HttpGetter(ResourceGetter): for asset in assets: pathname = os.path.basename(asset['path']).lower() if pathname == filename: - return asset + try: + ReventParser.check_revent_file(asset['path']) + return asset + except ValueError as e: + self.logger.warning(e.message) else: # file for asset in assets: if asset['path'].lower() == resource.path.lower(): @@ -514,14 +524,22 @@ class RemoteFilerGetter(ResourceGetter): # There tends to be some confusion as to where revent files should # be placed. This looks both in the extension's directory, and in # 'revent_files' subdirectory under it, if it exists. + path = None if os.path.isdir(alternate_location): for candidate in os.listdir(alternate_location): if candidate.lower() == filename.lower(): - return os.path.join(alternate_location, candidate) + path = os.path.join(alternate_location, candidate) if os.path.isdir(location): for candidate in os.listdir(location): if candidate.lower() == filename.lower(): - return os.path.join(location, candidate) + path = os.path.join(location, candidate) + if path: + try: + ReventParser.check_revent_file(path) + return path + except ValueError as e: + self.logger.warning(e.message) + else: raise ValueError('Unexpected resource type: {}'.format(resource.name)) diff --git a/wlauto/utils/revent.py b/wlauto/utils/revent.py new file mode 100644 index 00000000..31ea5043 --- /dev/null +++ b/wlauto/utils/revent.py @@ -0,0 +1,78 @@ +# Copyright 2016 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. +# + +import struct +import datetime +import os + + +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() + + +def _read_struct(f, struct_spec): + data = f.read(struct_spec.size) + return struct_spec.unpack(data)