First working version
parent
d8bf3d64cb
commit
439a53940c
|
@ -1,6 +1,8 @@
|
|||
bin_PROGRAMS = onchanged
|
||||
|
||||
onchanged_VALASOURCES = \
|
||||
src/watchedpath.vala \
|
||||
src/watchlist.vala \
|
||||
src/onchanged.vala
|
||||
|
||||
onchanged_SOURCES = $(onchanged_VALASOURCES)
|
||||
|
|
|
@ -1,3 +1,62 @@
|
|||
public static int main() {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<string, FileMonitor> _monitors;
|
||||
|
||||
construct {
|
||||
_monitors =
|
||||
new HashTable<string, FileMonitor>(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
namespace onchanged {
|
||||
|
||||
public class WatchList : Object {
|
||||
private List<WatchedPath> watches;
|
||||
|
||||
construct {
|
||||
watches = new List<WatchedPath>();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue