/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * glogg is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with glogg. If not, see . */ #ifndef WATCHTOWER_H #define WATCHTOWER_H #include "config.h" #include #include #include #include #include "watchtowerlist.h" // Allow the client to register for notification on an arbitrary number // of files. It will be notified to any change (creation/deletion/modification) // on those files. // It is passed a platform specific driver. // FIXME: Where to put that so it is not dependant // Registration object to implement RAII #ifdef HAS_TEMPLATE_ALIASES using Registration = std::shared_ptr; #else typedef std::shared_ptr Registration; #endif template class WatchTower { public: // Create an empty watchtower WatchTower(); // Destroy the object ~WatchTower(); // Set the polling interval (in ms) // 0 disables polling and is the default void setPollingInterval( uint32_t interval_ms ); // Add a file to the notification list. notification will be called when // the file is modified, moved or deleted. // Lifetime of the notification is tied to the Registration object returned. // Note the notification function is called with a mutex held and in a // third party thread, beware of races! Registration addFile( const std::string& file_name, std::function notification ); // Number of watched directories (for tests) unsigned int numberWatchedDirectories() const; private: // The driver (parametrised) Driver driver_; // List of files/dirs observed ObservedFileList observed_file_list_; // Protects the observed_file_list_ std::mutex observers_mutex_; // Polling interval (0 disables polling) uint32_t polling_interval_ms_ = 0; // Thread std::atomic_bool running_; std::thread thread_; // Exist as long as the onject exists, to ensure observers won't try to // call us if we are dead. std::shared_ptr heartBeat_; // Private member functions std::tuple addFileToDriver( const std::string& ); static void removeNotification( WatchTower* watch_tower, std::shared_ptr notification ); void run(); }; // Class template implementation #include #include #include #include #include "log.h" namespace { bool isSymLink( const std::string& file_name ); std::string directory_path( const std::string& path ); }; template WatchTower::WatchTower() : driver_(), thread_(), heartBeat_(std::shared_ptr((void*) 0xDEADC0DE, [] (void*) {})) { running_ = true; thread_ = std::thread( &WatchTower::run, this ); } template WatchTower::~WatchTower() { running_ = false; driver_.interruptWait(); thread_.join(); } template void WatchTower::setPollingInterval( uint32_t interval_ms ) { if ( polling_interval_ms_ != interval_ms ) { polling_interval_ms_ = interval_ms; // Break out of the wait if ( polling_interval_ms_ > 0 ) driver_.interruptWait(); } } template Registration WatchTower::addFile( const std::string& file_name, std::function notification ) { // LOG(logDEBUG) << "WatchTower::addFile " << file_name; std::weak_ptr weakHeartBeat(heartBeat_); std::lock_guard lock( observers_mutex_ ); auto existing_observed_file = observed_file_list_.searchByName( file_name ); std::shared_ptr> ptr( new std::function(std::move( notification )) ); if ( ! existing_observed_file ) { typename Driver::FileId file_id; typename Driver::SymlinkId symlink_id; std::tie( file_id, symlink_id ) = addFileToDriver( file_name ); auto new_file = observed_file_list_.addNewObservedFile( ObservedFile( file_name, ptr, file_id, symlink_id ) ); auto dir = observed_file_list_.watchedDirectoryForFile( file_name ); if ( ! dir ) { LOG(logDEBUG) << "WatchTower::addFile dir for " << file_name << " not watched, adding..."; dir = observed_file_list_.addWatchedDirectoryForFile( file_name, [this, weakHeartBeat] (ObservedDir* dir) { if ( auto heart_beat = weakHeartBeat.lock() ) { driver_.removeDir( dir->dir_id_ ); } } ); dir->dir_id_ = driver_.addDir( dir->path ); if ( ! dir->dir_id_.valid() ) { LOG(logWARNING) << "WatchTower::addFile driver failed to add dir"; dir = nullptr; } } else { LOG(logDEBUG) << "WatchTower::addFile Found exisiting watch for dir " << file_name; } // Associate the dir to the file if ( dir ) new_file->dir_ = dir; } else { existing_observed_file->addCallback( ptr ); } // Returns a shared pointer that removes its own entry // from the list of watched stuff when it goes out of scope! // Uses a custom deleter to do the work. return std::shared_ptr( 0x0, [this, ptr, weakHeartBeat] (void*) { if ( auto heart_beat = weakHeartBeat.lock() ) WatchTower::removeNotification( this, ptr ); } ); } template unsigned int WatchTower::numberWatchedDirectories() const { return observed_file_list_.numberWatchedDirectories(); } // // Private functions // // Add the passed file name to the driver, returning the file and symlink id template std::tuple WatchTower::addFileToDriver( const std::string& file_name ) { typename Driver::SymlinkId symlink_id; auto file_id = driver_.addFile( file_name ); if ( isSymLink( file_name ) ) { // We want to follow the name (as opposed to the inode) // so we watch the symlink as well. symlink_id = driver_.addSymlink( file_name ); } return std::make_tuple( file_id, symlink_id ); } // Called by the dtor for a registration object template void WatchTower::removeNotification( WatchTower* watch_tower, std::shared_ptr notification ) { LOG(logDEBUG) << "WatchTower::removeNotification"; std::lock_guard lock( watch_tower->observers_mutex_ ); auto file = watch_tower->observed_file_list_.removeCallback( notification ); if ( file ) { watch_tower->driver_.removeFile( file->file_id_ ); watch_tower->driver_.removeSymlink( file->symlink_id_ ); } } // Run in its own thread template void WatchTower::run() { while ( running_ ) { std::unique_lock lock( observers_mutex_ ); std::vector*> files_needing_readding; auto files = driver_.waitAndProcessEvents( &observed_file_list_, &lock, &files_needing_readding, polling_interval_ms_ ); LOG(logDEBUG) << "WatchTower::run: waitAndProcessEvents returned " << files.size() << " files, " << files_needing_readding.size() << " needing re-adding"; for ( auto file: files_needing_readding ) { // A file 'needing readding' has the same name, // but probably a different inode, so it needs // to be readded for some drivers that rely on the // inode (e.g. inotify) driver_.removeFile( file->file_id_ ); driver_.removeSymlink( file->symlink_id_ ); std::tie( file->file_id_, file->symlink_id_ ) = addFileToDriver( file->file_name_ ); } for ( auto file: files ) { for ( auto observer: file->callbacks ) { LOG(logDEBUG) << "WatchTower::run: notifying the client!"; // Here we have to cast our generic pointer back to // the function pointer in order to perform the call const std::shared_ptr> fptr = std::static_pointer_cast>( observer ); // The observer is called with the mutex held, // Let's hope it doesn't do anything too funky. (*fptr)(); file->markAsChanged(); } } if ( polling_interval_ms_ > 0 ) { // Also call files that have not been called for a while for ( auto file: observed_file_list_ ) { uint32_t ms_since_last_check = std::chrono::duration_cast( std::chrono::steady_clock::now() - file->timeForLastCheck() ).count(); if ( ( ms_since_last_check > polling_interval_ms_ ) && file->hasChanged() ) { LOG(logDEBUG) << "WatchTower::run: " << file->file_name_; for ( auto observer: file->callbacks ) { LOG(logDEBUG) << "WatchTower::run: notifying the client because of a timeout!"; // Here we have to cast our generic pointer back to // the function pointer in order to perform the call const std::shared_ptr> fptr = std::static_pointer_cast>( observer ); // The observer is called with the mutex held, // Let's hope it doesn't do anything too funky. (*fptr)(); } file->markAsChanged(); } } } } } namespace { bool isSymLink( const std::string& file_name ) { #ifdef HAVE_SYMLINK struct stat buf; lstat( file_name.c_str(), &buf ); return ( S_ISLNK(buf.st_mode) ); #else return false; #endif } }; #endif