Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add umockdev_testbed_wait_script(), add API to add scripts/evemu as strings #259

Merged
merged 6 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 167 additions & 59 deletions src/umockdev.vala
Original file line number Diff line number Diff line change
Expand Up @@ -1033,23 +1033,53 @@ public class Testbed: GLib.Object {
*/
public bool load_script (string? dev, string recordfile)
throws GLib.Error, FileError, IOError, RegexError
{
return this.load_script_from_stream(dev,
new DataInputStream(File.new_for_path(recordfile).read()),
recordfile);
}

/**
* umockdev_testbed_load_script_from_string:
* @self: A #UMockdevTestbed.
* @dev: Device path (/dev/...) for which to load the script record.
* %NULL is valid; in this case the script is associated with
* the device node it was recorded from.
* @script: script string
* @error: return location for a GError, or %NULL
*
* Load a script record file for a particular device into the testbed.
* script records can be created with umockdev-record --script.
*
* Returns: %TRUE on success, %FALSE if @recordfile is invalid and an error
* occurred.
*/
public bool load_script_from_string (string? dev, string script)
throws GLib.Error, IOError, RegexError
{
return this.load_script_from_stream(dev,
new DataInputStream(new MemoryInputStream.from_data(script.data)),
"<string>");
}

private bool load_script_from_stream (string? dev, DataInputStream script, string scriptname)
throws GLib.Error, IOError, RegexError
{
string? owned_dev = dev;
if (owned_dev == null) {
var recording = new DataInputStream(File.new_for_path(recordfile).read());

// Ignore any leading comments
string line = recording.read_line();
string line = script.read_line();
while (line != null && line.has_prefix("#"))
line = recording.read_line();
line = script.read_line();

// Next must be our d 0 <devicenode> header
if (line == null)
error("script recording %s has no non-comment content", recordfile);
error("script %s has no non-comment content", scriptname);

MatchInfo header_matcher;
if (!(new Regex("^d 0 (.*)(\n|$)")).match(line, 0, out header_matcher))
error("null passed for device node, but recording %s has no d 0 header", recordfile);
error("null passed for device node, but script %s has no d 0 header", scriptname);
owned_dev = header_matcher.fetch(1);
}

Expand All @@ -1059,7 +1089,29 @@ public class Testbed: GLib.Object {
if (fd < 0)
throw new FileError.INVAL (owned_dev + " is not a device suitable for scripts");

this.dev_script_runner.insert (owned_dev, new ScriptRunner (owned_dev, recordfile, fd));
this.dev_script_runner.insert (owned_dev, new ScriptRunner (fd, owned_dev, script, scriptname));
return true;
}

/**
* umockdev_testbed_wait_script:
* @self: A #UMockdevTestbed.
* @dev: Device path (/dev/...) to wait for
* @error: return location for a GError, or %NULL
*
* Wait for a previously loaded script or evemu record to finish.
*
* Returns: %TRUE on success, %FALSE if @dev is invalid and an error
* occurred.
*/
public bool wait_script (string dev)
throws FileError
{
unowned var runner = this.dev_script_runner.lookup (dev);
if (runner == null)
throw new FileError.NOENT (dev + " has no active script runner");
runner.wait ();
this.dev_script_runner.remove (dev);
return true;
}

Expand Down Expand Up @@ -1122,8 +1174,22 @@ public class Testbed: GLib.Object {
public bool load_evemu_events (string? dev, string eventsfile)
throws GLib.Error, FileError, IOError, RegexError
{
File f_ev = File.new_for_path(eventsfile);
var s_ev = new DataInputStream(f_ev.read());
return this.load_evemu_events_from_stream(dev,
new DataInputStream(File.new_for_path(eventsfile).read()),
eventsfile);
}

public bool load_evemu_events_from_string (string? dev, string events)
throws GLib.Error, FileError, IOError, RegexError
{
return this.load_evemu_events_from_stream(dev,
new DataInputStream(new MemoryInputStream.from_data(events.data)),
"<string>");
}

private bool load_evemu_events_from_stream (string? dev, DataInputStream events, string eventsname)
throws GLib.Error, IOError, RegexError
{
string line;
string? recorded_dev = null;
size_t len;
Expand All @@ -1132,20 +1198,19 @@ public class Testbed: GLib.Object {
var default_dev_re = new Regex("^# device (.*)$");
var event_re = new Regex("^E: ([0-9]+)\\.([0-9]+) +([0-9a-fA-F]+) +([0-9a-fA-F]+) +(-?[0-9]+) *#?");

string script_file;
int script_fd = FileUtils.open_tmp ("evemu.XXXXXX.script", out script_file);
string script = "";
int delay = 0;
bool first = true;

while ((line = s_ev.read_line(out len)) != null) {
while ((line = events.read_line(out len)) != null) {
if (default_dev_re.match(line, 0, out match)) {
recorded_dev = match.fetch(1);
continue;
}

if (!event_re.match(line, 0, out match)) {
if (!line.has_prefix("#"))
warning("Ignoring invalid line in %s: %s", eventsfile, line);
warning("Ignoring invalid line in %s: %s", eventsname, line);
continue;
}
time_t ev_sec = (time_t) uint64.parse(match.fetch(1));
Expand All @@ -1166,22 +1231,17 @@ public class Testbed: GLib.Object {

uint8[] ev_data = new uint8[sizeof(LinuxFixes.Input.Event)];
Posix.memcpy(ev_data, &ev, ev_data.length);
string script_line = "r " + delay.to_string() + " " + ScriptRunner.encode(ev_data) + "\n";
assert (Posix.write(script_fd, script_line, script_line.length) == script_line.length);
script += "r " + delay.to_string() + " " + ScriptRunner.encode(ev_data) + "\n";
}

Posix.close (script_fd);

string? owned_dev = dev;
if (owned_dev == null) {
if (recorded_dev == null)
error("null passed for device node, but recording %s has no '# device' header", eventsfile);
error("null passed for device node, but recording %s has no '# device' header", eventsname);
owned_dev = recorded_dev;
}

bool ret = load_script(owned_dev, script_file);
FileUtils.unlink(script_file);
return ret;
return load_script_from_string(owned_dev, script);
}

private static HashTable<string, string> bus_lookup_table;
Expand Down Expand Up @@ -1743,15 +1803,21 @@ find_devnode (string devpath)

private class ScriptRunner {

public ScriptRunner (string device, string script_file, int fd) throws FileError
/**
* ScriptRunner:
* @device_fd: File descriptor of the emulated device node
* @device: Name of the emulated device node, only for debug/error messages
* @script: Script to load and run
* @script_file: Description (e.g. file name) of @script, only for debug/error messages
*
* Create a new script runner for running @script.
*/
public ScriptRunner (int device_fd, string device, DataInputStream script, string script_file) throws FileError
{
this.script = FileStream.open (script_file, "r");
if (this.script == null)
throw new FileError.FAILED ("Cannot open script record file " + script_file);

this.fd = device_fd;
this.device = device;
this.script = script;
this.script_file = script_file;
this.fd = fd;
this.running = true;

this.thread = new Thread<void*> (device, this.run);
Expand All @@ -1772,6 +1838,18 @@ private class ScriptRunner {
this.thread.join ();
}

public void wait ()
{
if (!this.running) {
debug ("Script runner for %s already finished, nothing to wait for", this.device);
return;
}

debug ("Waiting for script runner for %s to finish", this.device);
this.thread.join ();
assert (this.running == false);
}

private void* run ()
{
char op;
Expand Down Expand Up @@ -1828,37 +1906,58 @@ private class ScriptRunner {

private uint8[] next_line (out char op, out uint32 delta)
{
// read operation code; skip empty lines and comments
int c;
for (;;) {
c = this.script.getc ();
if (c == FileStream.EOF) {
debug ("ScriptRunner[%s]: end of script %s, closing", this.device, this.script_file);
op = 'Q';
delta = 0;
return {};
} else if (c == '#') {
assert (this.script.read_line () != null);
} else if (c != '\n') {
op = (char) c;
break;
try {
// read operation code; skip empty lines and comments
for (;;) {
int c;
try {
c = this.script.read_byte ();
} catch (GLib.IOError e) {
if (e.code == GLib.IOError.FAILED) {
debug ("ScriptRunner[%s]: end of script %s, closing: %s", this.device, this.script_file, e.message);
op = 'Q';
delta = 0;
return {};
}
error ("ScriptRunner[%s]: failed to read script %s: %s", this.device, this.script_file, e.message);
}
if (c == '#') {
assert (this.script.read_line () != null);
} else if (c != '\n') {
op = (char) c;
break;
}
}
}

var cur_pos = this.script.tell ();
if (this.script.getc () != ' ')
error ("Missing space after operation code in %s at position %li", this.script_file, cur_pos);
var cur_pos = this.script.tell ();
if (this.script.read_byte () != ' ')
error ("Missing space after operation code in %s at position %" + uint64.FORMAT, this.script_file, cur_pos);

// read time delta
cur_pos = this.script.tell ();
if (this.script.scanf ("%" + uint32.FORMAT + " ", out delta) != 1)
error ("Cannot parse time in %s at position %li", this.script_file, cur_pos);
// read time delta
cur_pos = this.script.tell ();
string delta_str;
try {
size_t len;
delta_str = this.script.read_upto (" ", -1, out len, null);
} catch (GLib.IOError e) {
error ("No time value in %s at position %" + uint64.FORMAT + ": %s", this.script_file, cur_pos, e.message);
}
uint8 b = this.script.read_byte();
assert(b == ' ');

// remainder of the line is the data
string? line = this.script.read_line ();
assert (line != null);
int d;
if (!int.try_parse (delta_str, out d) || d < 0)
error ("Invalid time value '%s' in %s at position %" + uint64.FORMAT, delta_str, this.script_file, cur_pos);
delta = d;

return decode (line);
// remainder of the line is the data
string? line = this.script.read_line ();
assert (line != null);

return decode (line);
} catch (GLib.IOError e) {
error ("ScriptRunner[%s]: failed to read script %s: %s", this.device, this.script_file, e.message);
}
}

private void op_write (uint8[] data, uint32 delta)
Expand Down Expand Up @@ -1891,7 +1990,11 @@ private class ScriptRunner {
if (ret <= 0) {
debug ("ScriptRunner[%s]: got failure or EOF on read operation on expected block '%s', resetting",
this.device, encode(data[len:data.length]));
this.script.seek (0, FileSeek.SET);
try {
this.script.seek (0, SeekType.SET);
} catch (GLib.Error e) {
error ("ScriptRunner[%s]: failed to reset script %s: %s", this.device, this.script_file, e.message);
}
return;
}

Expand Down Expand Up @@ -1929,8 +2032,12 @@ private class ScriptRunner {

private static uint8[] decode (string quoted)
{
int i;
// skip over initial whitespace
for (i = 0; quoted.data[i] == ' ' && i < quoted.length; ++i);

uint8[] data = {};
for (int i = 0; i < quoted.length; ++i) {
for (; i < quoted.length; ++i) {
if (quoted.data[i] == '^') {
assert (i + 1 < quoted.length);
data += (quoted.data[i+1] == '`') ? '^' : (quoted.data[i+1] - 64);
Expand Down Expand Up @@ -1974,11 +2081,11 @@ private class ScriptRunner {
return d;
}

private int fd;
public string device { get; private set; }
private DataInputStream script;
private string script_file;
private Thread<void*> thread;
private FileStream script;
private int fd;
private bool running;
private uint fuzz = 0;
}
Expand Down Expand Up @@ -2098,11 +2205,12 @@ private class SocketServer {
string sock_path = null;
try {
sock_path = ((UnixSocketAddress) s.get_local_address()).path;
string script = this.socket_scriptfile.get (sock_path);
string scriptfile = this.socket_scriptfile.get (sock_path);
debug ("socket server thread: accepted request on server socket fd %i, path %s, script %s",
s.fd, sock_path, script);
s.fd, sock_path, scriptfile);
string key = "%s%i".printf (sock_path, fd);
this.script_runners.insert (key, new ScriptRunner (key, script, fd));
var script = new DataInputStream(File.new_for_path(scriptfile).read());
this.script_runners.insert (key, new ScriptRunner (fd, key, script, scriptfile));
} catch (GLib.Error e) {
error ("socket server thread: cannot launch ScriptRunner: %s", e.message);
}
Expand Down
Loading