diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
commit | 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch) | |
tree | d2e54377d14d604356c86862a326f64ae64dadd6 /src/DirectoryMonitor.vala |
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/DirectoryMonitor.vala')
-rw-r--r-- | src/DirectoryMonitor.vala | 1454 |
1 files changed, 1454 insertions, 0 deletions
diff --git a/src/DirectoryMonitor.vala b/src/DirectoryMonitor.vala new file mode 100644 index 0000000..1778fe4 --- /dev/null +++ b/src/DirectoryMonitor.vala @@ -0,0 +1,1454 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +// +// DirectoryMonitor will monitor an entire directory for changes to all files and directories +// within it. It uses FileMonitor to monitor all directories it discovers at initialization +// and reports changes to the files and directories just as FileMonitor reports them. Subclasses +// can override the notify_* methods to filter or monitor events before the signal is fired, +// or can override the signals themselves to be notified afterwards. +// +// start_discovery() must be called to initiate monitoring. Directories and files will be reported +// as they're discovered. Directories will be monitored as they're discovered as well. Discovery +// can only be initiated once. +// +// All signals are virtual and have a corresponding notify_* protected virtual function. +// Subclasses can either override the notify or the signal to decide when they want to process +// the event. +// +// DirectoryMonitor also adds a level of intelligence to GLib's monitoring API.Because certain +// file/directory events are decomposed by FileMonitor into more atomic events, it's difficult +// to know when these "composed" events have occurred. (For example, a file move is reported +// as a DELETED event followed by a CREATED event, with no clue that the two are related.) Later +// versions added the MOVE event, but we can't rely on those being installed. Also, documentation +// suggests it's only available with certain back-ends. +// +// DirectoryMonitor attempts to solve this by deducing when a set of events actually equals +// a composite event. It requires more memory in order to do this (i.e. it stores all files and +// their information), but the trade-off is easier file/directory monitoring via familiar +// semantics. +// +// DirectoryMonitor also will synthesize events when normal monitor events don't produce expected +// results. For example, if a directory is moved out of DirectoryMonitor's root, it is reported +// as a delete event, but none of its children are reported as deleted. Similarly, a directory +// rename can be captured as a move, but notifications for all its children are not fired and +// are synthesized by DirectoryMonitor. DirectoryMonitor will fire delete and move notifications +// for all the directory's children in depth-first order. +// +// In general, DirectoryMonitor attempts to preserve ordering of events, so that (for example) a +// file-altered event doesn't fire before a file-created, and so on. +// +// Because of these requirements, DirectoryMonitor maintains a FileInfo struct on all directories +// and files being monitored. (It maintains the attributes gather during the discovery phase, i.e. +// SUPPLIED_ATTRIBUTES.) This information can be retrieved via get_info(), get_file_id(), and +// get_etag(). These calls can be made at any time; the information is stored before any signal +// is fired. +// +// Note that DirectoryMonitor currently only supports files and directories. Other file types +// (special, symbolic links, shortcuts, and mount points) are not supported. It has been seen +// when a temporary file is created for its file type to be reported as "unknown" and when it's +// altered/deleted to be reported as a regular file. This means it's possible for a file not to +// be reported as discovered or created but to be reported as altered and/or deleted. +// +// DirectoryMonitor can be configured to not recurse (in which case it only discovers/monitors +// the root directory) and to not monitor (in which case only discovery occurs). +// + +public class DirectoryMonitor : Object { + public const int DEFAULT_PRIORITY = Priority.LOW; + public const FileQueryInfoFlags DIR_INFO_FLAGS = FileQueryInfoFlags.NONE; + public const FileQueryInfoFlags FILE_INFO_FLAGS = FileQueryInfoFlags.NOFOLLOW_SYMLINKS; + + // when using UNKNOWN_FILE_FLAGS, check if the resulting FileInfo's symlink status matches + // symlink support for files and directories by calling is_file_symlink_supported(). + public const FileQueryInfoFlags UNKNOWN_INFO_FLAGS = FileQueryInfoFlags.NONE; + public const bool SUPPORT_DIR_SYMLINKS = true; + public const bool SUPPORT_FILE_SYMLINKS = false; + + public const string SUPPLIED_ATTRIBUTES = Util.FILE_ATTRIBUTES; + + private const FileMonitorFlags FILE_MONITOR_FLAGS = FileMonitorFlags.SEND_MOVED; + private const uint DELETED_EXPIRATION_MSEC = 500; + private const int MAX_EXPLORATION_DIRS = 5; + + private enum FType { + FILE, + DIRECTORY, + UNSUPPORTED + } + + private class QueryInfoQueueElement { + private static uint current = 0; + + public DirectoryMonitor owner; + public File file; + public File? other_file; + public FileMonitorEvent event; + public uint position; + public ulong time_created_msec; + public FileInfo? info = null; + public Error? err = null; + public bool completed = false; + + public QueryInfoQueueElement(DirectoryMonitor owner, File file, File? other_file, + FileMonitorEvent event) { + this.owner = owner; + this.file = file; + this.other_file = other_file; + this.event = event; + this.position = current++; + this.time_created_msec = now_ms(); + } + + public void on_completed(Object? source, AsyncResult aresult) { + File source_file = (File) source; + + // finish the async operation to get the result + try { + info = source_file.query_info_async.end(aresult); + } catch (Error err) { + this.err = err; + } + + // mark as completed + completed = true; + + // notify owner this job is finished, to process the queue + owner.process_query_queue(this); + } + } + + // The FileInfoMap solves several related problems while maintaining FileInfo's in memory + // so they're available to users of DirectoryMonitor as well as DirectoryMonitor itself, + // which uses them to detect certain conditions. FileInfoMap uses a File ID to maintain + // only unique references to each File (and thus can be used to detect symlinked files). + private class FileInfoMap { + private Gee.HashMap<File, FileInfo> map = new Gee.HashMap<File, FileInfo>(file_hash, + file_equal); + private Gee.HashMap<string, File> id_map = new Gee.HashMap<string, File>(null, null, + file_equal); + + public FileInfoMap() { + } + + protected bool normalize_file(File file, FileInfo? info, out File normalized, out string id) { + // if no info is supplied, see if file straight-up corresponds .. if not, we're out of + // luck + FileInfo? local_info = info; + if (local_info == null) { + local_info = map.get(file); + if (local_info == null) { + normalized = null; + id = null; + + return false; + } + } + + string? file_id = get_file_info_id(local_info); + if (file_id == null) { + normalized = null; + id = null; + + return false; + } + + File? known_file = id_map.get(file_id); + + id = (string) file_id; + normalized = (known_file != null) ? known_file : file; + + return true; + } + + public bool update(File file, FileInfo info) { + // if the file out-and-out exists, remove it now + if (map.has_key(file)) { + bool removed = map.unset(file); + assert(removed); + } + + // if the id exists, remove it too + string? existing_id = get_file_info_id(info); + if (existing_id != null && id_map.has_key(existing_id)) { + bool removed = id_map.unset(existing_id); + assert(removed); + } + + string id; + File normalized; + if (!normalize_file(file, info, out normalized, out id)) + return false; + + map.set(normalized, info); + id_map.set(id, normalized); + + return true; + } + + public bool remove(File file, FileInfo? info) { + string id; + File normalized; + if (!normalize_file(file, info, out normalized, out id)) + return false; + + map.unset(normalized); + id_map.unset(id); + + return true; + } + + // This calls the virtual function remove() for all files, so overriding it is sufficient + // (but not necessarily most efficient) + public void remove_all(Gee.Collection<File> files) { + foreach (File file in files) + remove(file, null); + } + + public bool contains(File file, FileInfo? info) { + string id; + File normalized; + if (!normalize_file(file, info, out normalized, out id)) + return false; + + return id_map.has_key(id); + } + + public string? get_id(File file, FileInfo? info) { + // if FileInfo is valid, easy pickings + if (info != null) + return get_file_info_id(info); + + string id; + File normalized; + if (!normalize_file(file, null, out normalized, out id)) + return null; + + return id; + } + + public Gee.Collection<File> get_all() { + return map.keys; + } + + public FileInfo? get_info(File file) { + // if file is known as-is, use that + FileInfo? info = map.get(file); + if (info != null) + return info; + + string id; + File normalized; + if (!normalize_file(file, null, out normalized, out id)) + return null; + + return map.get(normalized); + } + + public FileInfo? query_info(File file, Cancellable? cancellable) { + FileInfo? info = get_info(file); + if (info != null) + return info; + + // This *only* retrieves the file ID, which is then used to obtain the in-memory file + // information. + try { + info = file.query_info(FileAttribute.ID_FILE, UNKNOWN_INFO_FLAGS, cancellable); + } catch (Error err) { + warning("Unable to query file ID of %s: %s", file.get_path(), err.message); + + return null; + } + + if (!is_file_symlink_supported(info)) + return null; + + string? id = info.get_attribute_string(FileAttribute.ID_FILE); + if (id == null) + return null; + + File? normalized = id_map.get(id); + if (normalized == null) + return null; + + return map.get(file); + } + + public File? find_match(FileInfo match) { + string? match_id = get_file_info_id(match); + if (match_id == null) + return null; + + // get all the interesting matchable items from the supplied FileInfo + int64 match_size = match.get_size(); + TimeVal match_time = match.get_modification_time(); + + foreach (File file in map.keys) { + FileInfo info = map.get(file); + + // file id match is instant match + if (get_file_info_id(info) == match_id) + return file; + + // if file size *and* modification time match, stop + if (match_size != info.get_size()) + continue; + + TimeVal time = info.get_modification_time(); + + if (time.tv_sec != match_time.tv_sec) + continue; + + return file; + } + + return null; + } + + public void remove_descendents(File root, FileInfoMap descendents) { + Gee.ArrayList<File> pruned = null; + foreach (File file in map.keys) { + File? parent = file.get_parent(); + while (parent != null) { + if (parent.equal(root)) { + if (pruned == null) + pruned = new Gee.ArrayList<File>(); + + pruned.add(file); + descendents.update(file, map.get(file)); + + break; + } + + parent = parent.get_parent(); + } + } + + if (pruned != null) + remove_all(pruned); + } + + // This returns only the immediate descendents of the root sorted by type. Returns the + // total number of children located. + public int get_children(File root, Gee.Collection<File> files, Gee.Collection<File> dirs) { + int count = 0; + foreach (File file in map.keys) { + File? parent = file.get_parent(); + if (parent == null || !parent.equal(root)) + continue; + + FType ftype = get_ftype(map.get(file)); + switch (ftype) { + case FType.FILE: + files.add(file); + count++; + break; + + case FType.DIRECTORY: + dirs.add(file); + count++; + break; + + default: + assert(ftype == FType.UNSUPPORTED); + break; + } + } + + return count; + } + } + + private File root; + private bool recurse; + private bool monitoring; + private Gee.HashMap<string, FileMonitor> monitors = new Gee.HashMap<string, FileMonitor>(); + private Gee.Queue<QueryInfoQueueElement> query_info_queue = new Gee.LinkedList< + QueryInfoQueueElement>(); + private FileInfoMap files = new FileInfoMap(); + private FileInfoMap parent_moved = new FileInfoMap(); + private Cancellable cancellable = new Cancellable(); + private int outstanding_exploration_dirs = 0; + private bool started = false; + private bool has_discovery_started = false; + private uint delete_timer_id = 0; + + // This signal will be fired *after* directory-moved has been fired. + public virtual signal void root_moved(File old_root, File new_root, FileInfo new_root_info) { + } + + // If the root is deleted, then the DirectoryMonitor is essentially dead; it has no monitor + // to wait for the root to be re-created, and everything beneath the root is obviously blown + // away as well. + // + // This signal will be fired *after* directory-deleted has been fired. + public virtual signal void root_deleted(File root) { + } + + public virtual signal void discovery_started() { + } + + public virtual signal void file_discovered(File file, FileInfo info) { + } + + public virtual signal void directory_discovered(File file, FileInfo info) { + } + + // reason is a user-visible string. May be called more than once during discovery. + // Discovery always completes with discovery-completed. + public virtual signal void discovery_failed(string reason) { + } + + public virtual signal void discovery_completed() { + has_discovery_started = false; + mdbg("discovery completed"); + } + + public virtual signal void file_created(File file, FileInfo info) { + } + + public virtual signal void file_moved(File old_file, File new_file, FileInfo new_file_info) { + } + + // FileInfo is not updated for each file-altered signal. + public virtual signal void file_altered(File file) { + } + + // This is called when the monitor detects that alteration (attributes or otherwise) to the + // file has completed. + public virtual signal void file_alteration_completed(File file, FileInfo info) { + } + + public virtual signal void file_attributes_altered(File file) { + } + + public virtual signal void file_deleted(File file) { + } + + // This implies that the directory is now being monitored. + public virtual signal void directory_created(File dir, FileInfo info) { + } + + // This implies that the old directory is no longer being monitored and the new one is. + public virtual signal void directory_moved(File old_dir, File new_dir, FileInfo new_dir_info) { + } + + // FileInfo is not updated for each directory-altered signal. + public virtual signal void directory_altered(File dir) { + } + + // This is called when the monitor detects that alteration (attributes or otherwise) to the + // directory has completed. + public virtual signal void directory_alteration_completed(File dir, FileInfo info) { + } + + public virtual signal void directory_attributes_altered(File dir) { + } + + // This implies that the directory is now no longer be monitored (unsurprisingly). + public virtual signal void directory_deleted(File dir) { + } + + public virtual signal void closed() { + } + + public DirectoryMonitor(File root, bool recurse, bool monitoring) { + this.root = root; + this.recurse = recurse; + this.monitoring = monitoring; + } + + protected static void mdbg(string msg) { +#if TRACE_MONITORING + debug("%s", msg); +#endif + } + + public bool is_recursive() { + return recurse; + } + + public bool is_monitoring() { + return monitoring; + } + + protected virtual void notify_root_deleted(File root) { + assert(this.root.equal(root)); + + mdbg("root deleted"); + root_deleted(root); + } + + private void internal_notify_root_moved(File old_root, File new_root, FileInfo new_root_info) { + bool removed = files.remove(old_root, null); + assert(removed); + + bool updated = files.update(new_root, new_root_info); + assert(updated); + + root = new_root; + + notify_root_moved(old_root, new_root, new_root_info); + } + + protected virtual void notify_root_moved(File old_root, File new_root, FileInfo new_root_info) { + assert(this.root.equal(old_root)); + + mdbg("root moved: %s -> %s".printf(old_root.get_path(), new_root.get_path())); + root_moved(old_root, new_root, new_root_info); + } + + protected virtual void notify_discovery_started() { + mdbg("discovery started"); + discovery_started(); + } + + protected virtual void internal_notify_file_discovered(File file, FileInfo info) { + if (!files.update(file, info)) { + debug("DirectoryMonitor.internal_notify_file_discovered: %s discovered but not added to file map", + file.get_path()); + + return; + } + + notify_file_discovered(file, info); + } + + protected virtual void notify_file_discovered(File file, FileInfo info) { + mdbg("file discovered: %s".printf(file.get_path())); + file_discovered(file, info); + } + + protected virtual void internal_notify_directory_discovered(File dir, FileInfo info) { + bool updated = files.update(dir, info); + assert(updated); + + notify_directory_discovered(dir, info); + } + + protected virtual void notify_directory_discovered(File dir, FileInfo info) { + mdbg("directory discovered: %s".printf(dir.get_path())); + directory_discovered(dir, info); + } + + protected virtual void notify_discovery_failed(string reason) { + warning("discovery failed: %s", reason); + discovery_failed(reason); + } + + protected virtual void notify_discovery_completed() { + discovery_completed(); + } + + private void internal_notify_file_created(File file, FileInfo info) { + File old_file; + FileInfo old_file_info; + if (is_file_create_move(file, info, out old_file, out old_file_info)) { + internal_notify_file_moved(old_file, file, info); + } else { + bool updated = files.update(file, info); + assert(updated); + + notify_file_created(file, info); + } + } + + protected virtual void notify_file_created(File file, FileInfo info) { + mdbg("file created: %s".printf(file.get_path())); + file_created(file, info); + } + + private void internal_notify_file_moved(File old_file, File new_file, FileInfo new_file_info) { + // don't assert because it's possible this call was generated via a deleted-created + // sequence, in which case the old_file won't be in files + files.remove(old_file, null); + + bool updated = files.update(new_file, new_file_info); + assert(updated); + + notify_file_moved(old_file, new_file, new_file_info); + } + + protected virtual void notify_file_moved(File old_file, File new_file, FileInfo new_file_info) { + mdbg("file moved: %s -> %s".printf(old_file.get_path(), new_file.get_path())); + file_moved(old_file, new_file, new_file_info); + } + + protected virtual void notify_file_altered(File file) { + mdbg("file altered: %s".printf(file.get_path())); + file_altered(file); + } + + private void internal_notify_file_alteration_completed(File file, FileInfo info) { + bool updated = files.update(file, info); + assert(updated); + + notify_file_alteration_completed(file, info); + } + + protected virtual void notify_file_alteration_completed(File file, FileInfo info) { + mdbg("file alteration completed: %s".printf(file.get_path())); + file_alteration_completed(file, info); + } + + protected virtual void notify_file_attributes_altered(File file) { + mdbg("file attributes altered: %s".printf(file.get_path())); + file_attributes_altered(file); + } + + private void internal_notify_file_deleted(File file) { + bool removed = files.remove(file, null); + assert(removed); + + notify_file_deleted(file); + } + + protected virtual void notify_file_deleted(File file) { + mdbg("file deleted: %s".printf(file.get_path())); + file_deleted(file); + } + + private void internal_notify_directory_created(File dir, FileInfo info) { + File old_dir; + FileInfo old_dir_info; + if (is_file_create_move(dir, info, out old_dir, out old_dir_info)) { + // A directory move, like a file move, is actually a directory-deleted followed + // by a directory-created. Unlike a file move, what follows directory-created + // is a file/directory-created for each file and directory inside the folder + // (although the matching deletes are never fired). We want to issue moves for + // all those files as well and suppress the create calls. + files.remove_descendents(old_dir, parent_moved); + + internal_notify_directory_moved(old_dir, old_dir_info, dir, info); + } else { + bool updated = files.update(dir, info); + assert(updated); + + notify_directory_created(dir, info); + } + } + + protected virtual void notify_directory_created(File dir, FileInfo info) { + mdbg("directory created: %s".printf(dir.get_path())); + directory_created(dir, info); + } + + private void internal_notify_directory_moved(File old_dir, FileInfo old_dir_info, File new_dir, + FileInfo new_dir_info) { + async_internal_notify_directory_moved.begin(old_dir, old_dir_info, new_dir, new_dir_info); + } + + private async void async_internal_notify_directory_moved(File old_dir, FileInfo old_dir_info, + File new_dir, FileInfo new_dir_info) { + Gee.ArrayList<File> file_children = new Gee.ArrayList<File>(file_equal); + Gee.ArrayList<File> dir_children = new Gee.ArrayList<File>(file_equal); + int count = files.get_children(old_dir, file_children, dir_children); + if (count > 0) { + // descend into directories and let them notify their children on the way up + // (if files.get_info() returns null, that indicates the directory/file was already + // deleted in recurse) + foreach (File dir_child in dir_children) { + FileInfo? dir_info = files.get_info(dir_child); + if (dir_info == null) { + warning("Unable to retrieve directory-moved info for %s", dir_child.get_path()); + + continue; + } + + yield async_internal_notify_directory_moved(dir_child, dir_info, + new_dir.get_child(dir_child.get_basename()), dir_info); + } + + // then move the children + foreach (File file_child in file_children) { + FileInfo? file_info = files.get_info(file_child); + if (file_info == null) { + warning("Unable to retrieve directory-moved info for %s", file_child.get_path()); + + continue; + } + + internal_notify_file_moved(file_child, new_dir.get_child(file_child.get_basename()), + file_info); + + Idle.add(async_internal_notify_directory_moved.callback, DEFAULT_PRIORITY); + yield; + } + } + + // Don't assert here because it's possible this call was made due to a deleted-created + // sequence, in which case the directory has already been removed from files + files.remove(old_dir, null); + + bool updated = files.update(new_dir, new_dir_info); + assert(updated); + + // remove the old monitor and add the new one + remove_monitor(old_dir, old_dir_info); + add_monitor(new_dir, new_dir_info); + + notify_directory_moved(old_dir, new_dir, new_dir_info); + } + + protected virtual void notify_directory_moved(File old_dir, File new_dir, FileInfo new_dir_info) { + mdbg("directory moved: %s -> %s".printf(old_dir.get_path(), new_dir.get_path())); + directory_moved(old_dir, new_dir, new_dir_info); + + if (old_dir.equal(root)) + internal_notify_root_moved(old_dir, new_dir, new_dir_info); + } + + protected virtual void notify_directory_altered(File dir) { + mdbg("directory altered: %s".printf(dir.get_path())); + directory_altered(dir); + } + + private void internal_notify_directory_alteration_completed(File dir, FileInfo info) { + bool updated = files.update(dir, info); + assert(updated); + + notify_directory_alteration_completed(dir, info); + } + + protected virtual void notify_directory_alteration_completed(File dir, FileInfo info) { + mdbg("directory alteration completed: %s".printf(dir.get_path())); + directory_alteration_completed(dir, info); + } + + protected virtual void notify_directory_attributes_altered(File dir) { + mdbg("directory attributes altered: %s".printf(dir.get_path())); + directory_attributes_altered(dir); + } + + private void internal_notify_directory_deleted(File dir) { + FileInfo? info = files.get_info(dir); + assert(info != null); + + // stop monitoring this directory + remove_monitor(dir, info); + + async_notify_directory_deleted.begin(dir, false); + } + + private async void async_notify_directory_deleted(File dir, bool already_removed) { + // Note that in this function no assertion checking is done ... there are many + // reasons a deleted file may not be known to the internal bookkeeping; if + // the file is gone and we didn't know about it, then it's no problem. + + // because a directory can be deleted without its children being deleted first (probably + // means it has been moved to a location outside of the monitored root), need to + // synthesize notifications for all its children + Gee.ArrayList<File> file_children = new Gee.ArrayList<File>(file_equal); + Gee.ArrayList<File> dir_children = new Gee.ArrayList<File>(file_equal); + int count = files.get_children(dir, file_children, dir_children); + if (count > 0) { + // don't use internal_* variants as they deal with "real" and not synthesized + // notifications. also note that files.get_info() can return null because items are + // being deleted on the way back up the tree ... when files.get_info() returns null, + // it means the file/directory was already deleted in a recursed method, and so no + // assertions on them + + // descend first into directories, deleting files and directories on the way up + foreach (File dir_child in dir_children) + yield async_notify_directory_deleted(dir_child, false); + + // now notify deletions on all immediate children files ... don't notify directory + // deletion because that's handled right before exiting this method + foreach (File file_child in file_children) { + files.remove(file_child, null); + + notify_file_deleted(file_child); + + Idle.add(async_notify_directory_deleted.callback, DEFAULT_PRIORITY); + yield; + } + } + + if (!already_removed) + files.remove(dir, null); + + notify_directory_deleted(dir); + } + + protected virtual void notify_directory_deleted(File dir) { + mdbg("directory deleted: %s".printf(dir.get_path())); + directory_deleted(dir); + + if (dir.equal(root)) + notify_root_deleted(dir); + } + + protected virtual void notify_closed() { + mdbg("monitoring of %s closed".printf(root.get_path())); + closed(); + } + + public File get_root() { + return root; + } + + public bool is_in_root(File file) { + return file.has_prefix(root); + } + + public bool has_started() { + return started; + } + + public void start_discovery() { + assert(!started); + + has_discovery_started = true; + started = true; + + notify_discovery_started(); + + // start exploring the directory, adding monitors as the directories are discovered + outstanding_exploration_dirs = 1; + explore_async.begin(root, null, true); + } + + // This should be called when a DirectoryMonitor needs to be destroyed or released. This + // will halt background exploration and close all resources. + public virtual void close() { + // cancel any outstanding async I/O + cancellable.cancel(); + + // cancel all monitors + foreach (FileMonitor monitor in monitors.values) + cancel_monitor(monitor); + + monitors.clear(); + + notify_closed(); + } + + private static FType get_ftype(FileInfo info) { + FileType file_type = info.get_file_type(); + switch (file_type) { + case FileType.REGULAR: + return FType.FILE; + + case FileType.DIRECTORY: + return FType.DIRECTORY; + + default: + mdbg("query_ftype: Unknown file type %s".printf(file_type.to_string())); + return FType.UNSUPPORTED; + } + } + + private async void explore_async(File dir, FileInfo? dir_info, bool in_discovery) { + if (files.contains(dir, dir_info)) { + warning("Directory loop detected at %s, not exploring", dir.get_path()); + + explore_directory_completed(in_discovery); + + return; + } + + // if FileInfo wasn't supplied by caller, fetch it now + FileInfo? local_dir_info = dir_info; + if (local_dir_info == null) { + try { + local_dir_info = yield dir.query_info_async(SUPPLIED_ATTRIBUTES, DIR_INFO_FLAGS, + DEFAULT_PRIORITY, cancellable); + } catch (Error err) { + warning("Unable to retrieve info on %s: %s", dir.get_path(), err.message); + + explore_directory_completed(in_discovery); + + return; + } + } + + if (local_dir_info.get_is_hidden()) { + warning("Ignoring hidden directory %s", dir.get_path()); + + explore_directory_completed(in_discovery); + + return; + } + + // File ID is required for directory monitoring. No ID, no ride! + // TODO: Replace the warning with notify_discovery_failed() and provide a user-visible + // string. + if (get_file_info_id(local_dir_info) == null) { + warning("Unable to retrieve file ID on %s: skipping", dir.get_path()); + + explore_directory_completed(in_discovery); + + return; + } + + // verify this is a directory + if (local_dir_info.get_file_type() != FileType.DIRECTORY) { + notify_discovery_failed(_("Unable to monitor %s: Not a directory (%s)").printf( + dir.get_path(), local_dir_info.get_file_type().to_string())); + + explore_directory_completed(in_discovery); + + return; + } + + // collect all directories and files in the directory, to consolidate reporting them as + // well as traversing the subdirectories -- but to avoid a lot of unnecessary resource + // allocations (think empty directories, or leaf nodes with only files), only allocate + // the maps when necessary + Gee.HashMap<File, FileInfo> dir_map = null; + Gee.HashMap<File, FileInfo> file_map = null; + + try { + FileEnumerator enumerator = yield dir.enumerate_children_async(SUPPLIED_ATTRIBUTES, + UNKNOWN_INFO_FLAGS, DEFAULT_PRIORITY, cancellable); + for (;;) { + List<FileInfo>? infos = yield enumerator.next_files_async(10, DEFAULT_PRIORITY, + cancellable); + if (infos == null) + break; + + foreach (FileInfo info in infos) { + // we don't deal with hidden files or directories + if (info.get_is_hidden()) { + warning("Skipping hidden file/directory %s", + dir.get_child(info.get_name()).get_path()); + + continue; + } + + // check for symlink support + if (!is_file_symlink_supported(info)) + continue; + + switch (info.get_file_type()) { + case FileType.REGULAR: + if (file_map == null) + file_map = new Gee.HashMap<File, FileInfo>(file_hash, file_equal); + + file_map.set(dir.get_child(info.get_name()), info); + break; + + case FileType.DIRECTORY: + if (dir_map == null) + dir_map = new Gee.HashMap<File, FileInfo>(file_hash, file_equal); + + dir_map.set(dir.get_child(info.get_name()), info); + break; + + default: + // ignored + break; + } + } + } + } catch (Error err2) { + warning("Aborted directory traversal of %s: %s", dir.get_path(), err2.message); + + explore_directory_completed(in_discovery); + + return; + } + + // report the local (caller-supplied) directory as discovered *before* reporting its files + if (in_discovery) + internal_notify_directory_discovered(dir, local_dir_info); + else + internal_notify_directory_created(dir, local_dir_info); + + // now with everything snarfed up and the directory reported as discovered, begin + // monitoring the directory + add_monitor(dir, local_dir_info); + + // report files in local directory + if (file_map != null) + yield notify_directory_files(file_map, in_discovery); + + // post all the subdirectory traversals, allowing them to report themselves as discovered + if (recurse && dir_map != null) { + foreach (File subdir in dir_map.keys) { + if (++outstanding_exploration_dirs > MAX_EXPLORATION_DIRS) + yield explore_async(subdir, dir_map.get(subdir), in_discovery); + else + explore_async.begin(subdir, dir_map.get(subdir), in_discovery); + } + } + + explore_directory_completed(in_discovery); + } + + private async void notify_directory_files(Gee.Map<File, FileInfo> map, bool in_discovery) { + Gee.MapIterator<File, FileInfo> iter = map.map_iterator(); + while (iter.next()) { + if (in_discovery) + internal_notify_file_discovered(iter.get_key(), iter.get_value()); + else + internal_notify_file_created(iter.get_key(), iter.get_value()); + + Idle.add(notify_directory_files.callback, DEFAULT_PRIORITY); + yield; + } + } + + // called whenever exploration of a directory is completed, to know when to signal that + // discovery has ended + private void explore_directory_completed(bool in_discovery) { + assert(outstanding_exploration_dirs > 0); + outstanding_exploration_dirs--; + + if (in_discovery && outstanding_exploration_dirs == 0) + notify_discovery_completed(); + } + + // Only submit directories ... file monitoring is wasteful when a single directory monitor can + // do all the work. Returns true if monitor added, false if already monitored (or not + // monitoring, or unable to monitor due to error). + private bool add_monitor(File dir, FileInfo info) { + if (!monitoring) + return false; + + string? id = files.get_id(dir, info); + if (id == null) + return false; + + // if one already exists, nop + if (monitors.has_key(id)) + return false; + + FileMonitor monitor = null; + try { + monitor = dir.monitor_directory(FILE_MONITOR_FLAGS, null); + } catch (Error err) { + warning("Unable to monitor %s: %s", dir.get_path(), err.message); + + return false; + } + + monitors.set(id, monitor); + monitor.changed.connect(on_monitor_notification); + + mdbg("Added monitor for %s".printf(dir.get_path())); + + return true; + } + + // Returns true if the directory is removed (i.e. was being monitored). + private bool remove_monitor(File dir, FileInfo info) { + if (!monitoring) + return false; + + string? id = files.get_id(dir, info); + if (id == null) + return false; + + FileMonitor? monitor = monitors.get(id); + if (monitor == null) + return false; + + bool removed = monitors.unset(id); + assert(removed); + + cancel_monitor(monitor); + + mdbg("Removed monitor for %s".printf(dir.get_path())); + + return true; + } + + private void cancel_monitor(FileMonitor monitor) { + monitor.changed.disconnect(on_monitor_notification); + monitor.cancel(); + } + + private void on_monitor_notification(File file, File? other_file, FileMonitorEvent event) { + mdbg("NOTIFY %s: file=%s other_file=%s".printf(event.to_string(), file.get_path(), + other_file != null ? other_file.get_path() : "(none)")); + + // The problem: Having basic file information about each file is valuable (and necessary + // in certain situations), but it is a blocking operation, no matter how "quick" it + // may seem. Async I/O is perfect to handle this, but it can complete out of order, and + // it's highly desirous to report events in the same order they're received. FileInfo + // queries are queued up then and processed in order as they're completed. + + // Every event needs to be queued, but not all events generates query I/O + QueryInfoQueueElement query_info = new QueryInfoQueueElement(this, file, other_file, event); + query_info_queue.offer(query_info); + + switch (event) { + case FileMonitorEvent.CREATED: + case FileMonitorEvent.CHANGES_DONE_HINT: + file.query_info_async.begin(SUPPLIED_ATTRIBUTES, UNKNOWN_INFO_FLAGS, + DEFAULT_PRIORITY, cancellable, query_info.on_completed); + break; + + case FileMonitorEvent.DELETED: + // don't complete it yet, it might be followed by a CREATED event indicating a + // move ... instead, let it sit on the queue and allow the timer (or a coming + // CREATED event) complete it + if (delete_timer_id == 0) + delete_timer_id = Timeout.add(DELETED_EXPIRATION_MSEC / 2, check_for_expired_delete_events); + break; + + case FileMonitorEvent.MOVED: + // unlike the others, other_file is the destination of the move, and therefore the + // one we need to get info on + if (other_file != null) { + other_file.query_info_async.begin(SUPPLIED_ATTRIBUTES, UNKNOWN_INFO_FLAGS, + DEFAULT_PRIORITY, cancellable, query_info.on_completed); + } else { + warning("Unable to process MOVED event: no other_file"); + query_info_queue.remove(query_info); + } + break; + + default: + // artificially complete it + query_info.completed = true; + process_query_queue(query_info); + break; + } + } + + private void process_query_queue(QueryInfoQueueElement? query_info) { + // if the completed element was a CREATE event, attempt to match it to a DELETE (which + // then converts into a MOVED) and remove the CREATE event + if (query_info != null && query_info.info != null && query_info.event == FileMonitorEvent.CREATED) { + // if there's no match in the files table for this created file, then it can't be + // matched to a previously deleted file + File? match = files.find_match(query_info.info); + if (match != null) { + bool matched = false; + foreach (QueryInfoQueueElement enqueued in query_info_queue) { + if (enqueued.event != FileMonitorEvent.DELETED + || enqueued.completed + || !match.equal(enqueued.file)) { + continue; + } + + mdbg("Matching CREATED %s to DELETED %s for MOVED".printf(query_info.file.get_path(), + enqueued.file.get_path())); + + enqueued.event = FileMonitorEvent.MOVED; + enqueued.other_file = query_info.file; + enqueued.info = query_info.info; + enqueued.completed = true; + + matched = true; + + break; + } + + if (matched) + query_info_queue.remove(query_info); + } + } + + // peel off completed events from the queue in order + for (;;) { + // check if empty or waiting for completion on the next event + QueryInfoQueueElement? next = query_info_queue.peek(); + if (next == null || !next.completed) + break; + + // remove + QueryInfoQueueElement? n = query_info_queue.poll(); + assert(next == n); + + mdbg("Completed info query %u for %s on %s".printf(next.position, next.event.to_string(), + next.file.get_path())); + + if (next.err != null) { + mdbg("Unable to retrieve file information for %s, dropping %s: %s".printf( + next.file.get_path(), next.event.to_string(), next.err.message)); + + continue; + } + + // Directory monitoring requires file ID. No ID, no ride! + if (next.info != null && get_file_info_id(next.info) == null) { + mdbg("Unable to retrieve file ID for %s, dropping %s".printf(next.file.get_path(), + next.event.to_string())); + + continue; + } + + // watch for symlink support + if (next.info != null && !is_file_symlink_supported(next.info)) { + mdbg("No symlink support for %s, dropping %s".printf(next.file.get_path(), + next.event.to_string())); + + continue; + } + + on_monitor_notification_ready(next.file, next.other_file, next.info, next.event); + } + } + + private void on_monitor_notification_ready(File file, File? other_file, FileInfo? info, + FileMonitorEvent event) { + mdbg("READY %s: file=%s other_file=%s".printf(event.to_string(), file.get_path(), + other_file != null ? other_file.get_path() : "(null)")); + + // Nasty, nasty switches-in-a-switch construct, but this demuxes the possibilities into + // easily digestible upcalls and signals + switch (event) { + case FileMonitorEvent.CREATED: + assert(info != null); + + FType ftype = get_ftype(info); + switch (ftype) { + case FType.FILE: + internal_notify_file_created(file, info); + break; + + case FType.DIRECTORY: + // other files may have been created under this new directory before we have + // a chance to register a monitor, so scan it now looking for new additions + // (this call will notify of creation and monitor this new directory once + // it's been scanned) + outstanding_exploration_dirs++; + explore_async.begin(file, info, false); + break; + + default: + assert(ftype == FType.UNSUPPORTED); + break; + } + break; + + case FileMonitorEvent.CHANGED: + // don't query info for each change, but only when done hint comes down the pipe + assert(info == null); + + FileInfo local_info = get_file_info(file); + if (local_info == null) { + mdbg("Changed event for unknown file %s".printf(file.get_path())); + + break; + } + + FType ftype = get_ftype(local_info); + switch (ftype) { + case FType.FILE: + notify_file_altered(file); + break; + + case FType.DIRECTORY: + notify_directory_altered(file); + break; + + default: + assert(ftype == FType.UNSUPPORTED); + break; + } + break; + + case FileMonitorEvent.CHANGES_DONE_HINT: + assert(info != null); + + FType ftype = get_ftype(info); + switch (ftype) { + case FType.FILE: + internal_notify_file_alteration_completed(file, info); + break; + + case FType.DIRECTORY: + internal_notify_directory_alteration_completed(file, info); + break; + + default: + assert(ftype == FType.UNSUPPORTED); + break; + } + break; + + case FileMonitorEvent.MOVED: + assert(info != null); + assert(other_file != null); + + // in the moved case, file info is for other file (the destination in the move + // operation) + FType ftype = get_ftype(info); + switch (ftype) { + case FType.FILE: + internal_notify_file_moved(file, other_file, info); + break; + + case FType.DIRECTORY: + // get the old FileInfo (contained in files) + FileInfo? old_dir_info = files.get_info(file); + if (old_dir_info == null) { + warning("Directory moved event for unknown file %s", file.get_path()); + + break; + } + + internal_notify_directory_moved(file, old_dir_info, other_file, info); + break; + + default: + assert(ftype == FType.UNSUPPORTED); + break; + } + break; + + case FileMonitorEvent.DELETED: + assert(info == null); + + FileInfo local_info = get_file_info(file); + if (local_info == null) { + warning("Deleted event for unknown file %s", file.get_path()); + + break; + } + + FType ftype = get_ftype(local_info); + switch (ftype) { + case FType.FILE: + internal_notify_file_deleted(file); + break; + + case FType.DIRECTORY: + internal_notify_directory_deleted(file); + break; + + default: + assert(ftype == FType.UNSUPPORTED); + break; + } + break; + + case FileMonitorEvent.ATTRIBUTE_CHANGED: + // doesn't fetch attributes until CHANGES_DONE_HINT comes down the pipe + assert(info == null); + + FileInfo local_info = get_file_info(file); + if (local_info == null) { + warning("Attribute changed event for unknown file %s", file.get_path()); + + break; + } + + FType ftype = get_ftype(local_info); + switch (ftype) { + case FType.FILE: + notify_file_attributes_altered(file); + break; + + case FType.DIRECTORY: + notify_directory_attributes_altered(file); + break; + + default: + assert(ftype == FType.UNSUPPORTED); + break; + } + break; + + case FileMonitorEvent.PRE_UNMOUNT: + case FileMonitorEvent.UNMOUNTED: + // not currently handling these events + break; + + default: + warning("Unknown directory monitor event %s", event.to_string()); + break; + } + } + + // Returns true if a move occurred. Internal state is modified to recognize the + // situation (i.e. the move should be reported). + private bool is_file_create_move(File file, FileInfo info, out File old_file, + out FileInfo old_file_info) { + // look for created file whose parent was actually moved + File? match = parent_moved.find_match(info); + if (match != null) { + old_file = match; + old_file_info = parent_moved.get_info(match); + + parent_moved.remove(match, info); + + return true; + } + + old_file = null; + old_file_info = null; + + return false; + } + + private bool check_for_expired_delete_events() { + ulong expiration = now_ms() - DELETED_EXPIRATION_MSEC; + + bool any_deleted = false; + bool any_expired = false; + foreach (QueryInfoQueueElement element in query_info_queue) { + if (element.event != FileMonitorEvent.DELETED) + continue; + + any_deleted = true; + + if (element.time_created_msec > expiration) + continue; + + // synthesize the completion + element.completed = true; + any_expired = true; + } + + if (any_expired) + process_query_queue(null); + + if (!any_deleted) + delete_timer_id = 0; + + return any_deleted; + } + + // This method does its best to return FileInfo for the file. It performs no I/O. + public FileInfo? get_file_info(File file) { + return files.get_info(file); + } + + // This method returns all files and directories that the DirectoryMonitor knows of. This + // call is only useful when runtime monitoring is enabled. It performs no I/O. + public Gee.Collection<File> get_files() { + return files.get_all(); + } + + // This method will attempt to find the in-memory FileInfo for the file, but if it cannot + // be found it will query the file for it's ID and obtain in-memory file information from + // there. + public FileInfo? query_file_info(File file) { + return files.query_info(file, cancellable); + } + + // This checks if the FileInfo is for a symlinked file/directory and if symlinks for the file + // type are supported by DirectoryMonitor. Note that this requires the FileInfo have support + // for the "standard::is-symlink" and "standard::type" file attributes, which SUPPLIED_ATTRIBUTES + // provides. + // + // Returns true if the file is not a symlink or if symlinks are supported for the file type, + // false otherwise. If an unsupported file type, returns false. + public static bool is_file_symlink_supported(FileInfo info) { + if (!info.get_is_symlink()) + return true; + + FType ftype = get_ftype(info); + switch (ftype) { + case FType.DIRECTORY: + return SUPPORT_DIR_SYMLINKS; + + case FType.FILE: + return SUPPORT_FILE_SYMLINKS; + + default: + assert(ftype == FType.UNSUPPORTED); + + return false; + } + } +} + |