From 439a53940c8a313fdaa8ee27bc4b2ede65597ffc Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Sun, 1 Feb 2015 18:57:21 -0600 Subject: [PATCH] First working version --- Makefile.am | 2 + src/onchanged.vala | 63 ++++++++++++++++++- src/watchedpath.vala | 144 +++++++++++++++++++++++++++++++++++++++++++ src/watchlist.vala | 98 +++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 src/watchedpath.vala create mode 100644 src/watchlist.vala diff --git a/Makefile.am b/Makefile.am index a5052c2..1631bff 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,6 +1,8 @@ bin_PROGRAMS = onchanged onchanged_VALASOURCES = \ + src/watchedpath.vala \ + src/watchlist.vala \ src/onchanged.vala onchanged_SOURCES = $(onchanged_VALASOURCES) diff --git a/src/onchanged.vala b/src/onchanged.vala index 0785621..0002db2 100644 --- a/src/onchanged.vala +++ b/src/onchanged.vala @@ -1,3 +1,62 @@ -public static int main() { - return 0; +namespace onchanged { + public bool debug = false; + + public class Main : Object { + [CCode(array_length = false, array_null_terminated = true)] + private static string[]? action_files; + + private const OptionEntry[] options = { + {"debug", 'D', 0, OptionArg.NONE, ref debug, + "Write debug information to stdout", null}, + {"", 0, 0, OptionArg.FILENAME_ARRAY, ref action_files, + "Action definition files", "[FILENAME [...]]"}, + { null }, + }; + + private static void parse_args(string[] args) throws OptionError { + action_files = { + Path.build_filename( + Environment.get_user_config_dir(), + "onchanged" + ) + }; + var ctx = new OptionContext(); + ctx.set_summary("Perform actions when files change"); + ctx.set_help_enabled(true); + ctx.add_main_entries(options, null); + ctx.parse(ref args); + } + + public static int main(string[] args) { + try { + parse_args(args); + } catch (OptionError e) { + stderr.printf("error: %s\n", e.message); + return 2; + } + + var watches = new WatchList(); + foreach (string filename in action_files) { + var f = File.new_for_commandline_arg(filename); + switch(f.query_file_type(FileQueryInfoFlags.NONE)) { + case FileType.UNKNOWN: + stderr.printf("Skipping missing file %s\n", f.get_path()); + continue; + case FileType.DIRECTORY: + watches.read_config_dir(f); + break; + default: + watches.read_config(f.get_path()); + break; + } + } + + var loop = new MainLoop(); + loop.run(); + + return 0; + } + + } + } diff --git a/src/watchedpath.vala b/src/watchedpath.vala new file mode 100644 index 0000000..9bff2f2 --- /dev/null +++ b/src/watchedpath.vala @@ -0,0 +1,144 @@ +namespace onchanged { + + public class WatchedPath : Object { + public string path { get; construct; } + public string[] events { get; construct; } + public bool recursive { get; construct; } + private string _action; + private HashTable _monitors; + + construct { + _monitors = + new HashTable(str_hash, str_equal); + + var file = File.new_for_path(this.path); + watch(file); + var ftype = file.query_file_type(FileQueryInfoFlags.NONE); + if (ftype == FileType.DIRECTORY && recursive) { + watch_children(file); + } + } + + public WatchedPath(string path, string[] events, + bool recursive = true) { + Object( + path: path, + events: events, + recursive: recursive + ); + } + + private void unwatch(File file) { + var path = file.get_path(); + if (_monitors.get(path) != null) { + if (onchanged.debug) { + stdout.printf("No longer watching path %s\n", path); + } + _monitors.remove(path); + } + } + + private void watch(File file) { + var path = file.get_path(); + if (_monitors.get(path) == null) { + if (onchanged.debug) { + stdout.printf("Watching path %s\n", path); + } + FileMonitor mon; + try { + mon = file.monitor(FileMonitorFlags.NONE); + } catch (GLib.Error e) { + stderr.printf( + "Failed to monitor %s: %s\n", path, e.message + ); + return; + } + mon.changed.connect(handle_event); + _monitors.insert(file.get_path(), mon); + } + } + + private void watch_children(File dir) { + try { + var enumerator = dir.enumerate_children( + FileAttribute.STANDARD_NAME, + FileQueryInfoFlags.NONE + ); + + FileInfo info; + while ((info = enumerator.next_file()) != null) { + var f = dir.resolve_relative_path(info.get_name()); + if (info.get_file_type() == FileType.DIRECTORY) { + watch(f); + watch_children(f); + } + } + } catch (GLib.Error e) { + stderr.printf("%s\n", e.message); + } + } + + private void handle_event(File file, File? other_file, + FileMonitorEvent event_type) { + if (onchanged.debug) { + stdout.printf("%s: ", event_type.to_string()); + if (other_file != null) { + stdout.printf( + "%s, %s\n", file.get_path(), other_file.get_path() + ); + } else { + stdout.printf("%s\n", file.get_path()); + } + } + var file_type = file.query_file_type(FileQueryInfoFlags.NONE); + switch (event_type) { + case FileMonitorEvent.CREATED: + if (file_type == FileType.DIRECTORY && recursive) { + watch(file); + watch_children(file); + } + do_action(file, "created"); + break; + case FileMonitorEvent.CHANGES_DONE_HINT: + do_action(file, "changed"); + break; + case FileMonitorEvent.DELETED: + unwatch(file); + do_action(file, "deleted"); + break; + } + } + + private void do_action(File file, string event) { + if (!(event in events)) { + return; + } + + string escaped_path = "'%s'".printf( + file.get_path().replace("'", "'\\''") + ); + string command = _action + .replace("$#", escaped_path) + .replace("$%", event); + if (onchanged.debug) { + stdout.printf("%s\n", command); + } + try { + string[] argv; + Shell.parse_argv(command, out argv); + Process.spawn_async("/", argv, null, SpawnFlags.SEARCH_PATH, + null, null); + } catch (GLib.ShellError e) { + stderr.printf("Error parsing action: %s\n", e.message); + } catch (GLib.SpawnError e) { + stderr.printf("Error running action: %s\n", e.message); + } + } + + public void set_action(string action) { + _action = action; + } + + } + +} diff --git a/src/watchlist.vala b/src/watchlist.vala new file mode 100644 index 0000000..c562b90 --- /dev/null +++ b/src/watchlist.vala @@ -0,0 +1,98 @@ +namespace onchanged { + + public class WatchList : Object { + private List watches; + + construct { + watches = new List(); + } + + public void read_config(string file) { + if (onchanged.debug) { + stdout.printf("Reading actions from %s\n", file); + stdout.flush(); + } + var conf = new KeyFile(); + conf.set_list_separator(','); + try { + conf.load_from_file(file, KeyFileFlags.NONE); + } catch (FileError e) { + stderr.printf("Failed to read %s: %s\n", file, e.message); + } catch (KeyFileError e) { + stderr.printf("Failed to parse %s: %s\n", file, e.message); + return; + } + + foreach (string path in conf.get_groups()) { + string action; + try { + action = conf.get_string(path, "action"); + } catch (KeyFileError e) { + stderr.printf( + "Error processing section %s: %s\n", path, e.message + ); + continue; + } + + string[]? events; + try { + events = conf.get_string_list(path, "events"); + } catch (KeyFileError e) { + if (!(e is KeyFileError.KEY_NOT_FOUND)) { + stderr.printf( + "Warning processing section %s: %s\n", path, + e.message + ); + } + events = {}; + } + + bool recursive; + try { + recursive = conf.get_boolean(path, "recursive"); + } catch (KeyFileError e) { + if (!(e is KeyFileError.KEY_NOT_FOUND)) { + stderr.printf( + "Warning processing section %s: %s\n", path, + e.message + ); + } + recursive = false; + } + + var watch = new WatchedPath(path, events, recursive); + watch.set_action(action); + watches.append(watch); + } + } + + public void read_config_dir(File dir) { + if (onchanged.debug) { + stdout.printf( + "Reading all action files in %s\n", dir.get_path() + ); + } + + try { + var enumerator = dir.enumerate_children( + FileAttribute.STANDARD_NAME, + FileQueryInfoFlags.NONE + ); + + FileInfo info; + while ((info = enumerator.next_file()) != null) { + var f = dir.resolve_relative_path(info.get_name()); + if (info.get_file_type() == FileType.DIRECTORY) { + read_config_dir(f); + } else { + read_config(f.get_path()); + } + } + } catch (GLib.Error e) { + stderr.printf("%s\n", e.message); + } + } + + } + +}