xref: /glogg/src/watchtower.h (revision f869e41d2c129cd0f2f3eccb5e9d0d80a5998201)
1 /*
2  * Copyright (C) 2015 Nicolas Bonnefon and other contributors
3  *
4  * This file is part of glogg.
5  *
6  * glogg is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * glogg is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with glogg.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #ifndef WATCHTOWER_H
21 #define WATCHTOWER_H
22 
23 #include "config.h"
24 
25 #include <memory>
26 #include <atomic>
27 #include <thread>
28 #include <mutex>
29 
30 #include "watchtowerlist.h"
31 
32 // Allow the client to register for notification on an arbitrary number
33 // of files. It will be notified to any change (creation/deletion/modification)
34 // on those files.
35 // It is passed a platform specific driver.
36 
37 // FIXME: Where to put that so it is not dependant
38 // Registration object to implement RAII
39 #ifdef HAS_TEMPLATE_ALIASES
40 using Registration = std::shared_ptr<void>;
41 #else
42 typedef std::shared_ptr<void> Registration;
43 #endif
44 
45 template<typename Driver>
46 class WatchTower {
47   public:
48     // Create an empty watchtower
49     WatchTower();
50     // Destroy the object
51     ~WatchTower();
52 
53     // Set the polling interval (in ms)
54     // 0 disables polling and is the default
55     void setPollingInterval( uint32_t interval_ms );
56 
57     // Add a file to the notification list. notification will be called when
58     // the file is modified, moved or deleted.
59     // Lifetime of the notification is tied to the Registration object returned.
60     // Note the notification function is called with a mutex held and in a
61     // third party thread, beware of races!
62     Registration addFile( const std::string& file_name,
63             std::function<void()> notification );
64 
65     // Number of watched directories (for tests)
66     unsigned int numberWatchedDirectories() const;
67 
68   private:
69     // The driver (parametrised)
70     Driver driver_;
71 
72     // List of files/dirs observed
73     ObservedFileList<Driver> observed_file_list_;
74 
75     // Protects the observed_file_list_
76     std::mutex observers_mutex_;
77 
78     // Polling interval (0 disables polling)
79     uint32_t polling_interval_ms_ = 0;
80 
81     // Thread
82     std::atomic_bool running_;
83     std::thread thread_;
84 
85     // Exist as long as the onject exists, to ensure observers won't try to
86     // call us if we are dead.
87     std::shared_ptr<void> heartBeat_;
88 
89     // Private member functions
90     std::tuple<typename Driver::FileId, typename Driver::SymlinkId>
91         addFileToDriver( const std::string& );
92     static void removeNotification( WatchTower* watch_tower,
93             std::shared_ptr<void> notification );
94     void run();
95 };
96 
97 // Class template implementation
98 
99 #include <algorithm>
100 
101 #include <sys/types.h>
102 #include <sys/stat.h>
103 #include <unistd.h>
104 
105 #include "log.h"
106 
107 namespace {
108     bool isSymLink( const std::string& file_name );
109     std::string directory_path( const std::string& path );
110 };
111 
112 template <typename Driver>
WatchTower()113 WatchTower<Driver>::WatchTower()
114     : driver_(), thread_(),
115     heartBeat_(std::shared_ptr<void>((void*) 0xDEADC0DE, [] (void*) {}))
116 {
117     running_ = true;
118     thread_ = std::thread( &WatchTower::run, this );
119 }
120 
121 template <typename Driver>
~WatchTower()122 WatchTower<Driver>::~WatchTower()
123 {
124     running_ = false;
125     driver_.interruptWait();
126     thread_.join();
127 }
128 
129 template <typename Driver>
setPollingInterval(uint32_t interval_ms)130 void WatchTower<Driver>::setPollingInterval( uint32_t interval_ms )
131 {
132     if ( polling_interval_ms_ != interval_ms ) {
133         polling_interval_ms_ = interval_ms;
134         // Break out of the wait
135         if ( polling_interval_ms_ > 0 )
136             driver_.interruptWait();
137     }
138 }
139 
140 template <typename Driver>
addFile(const std::string & file_name,std::function<void ()> notification)141 Registration WatchTower<Driver>::addFile(
142         const std::string& file_name,
143         std::function<void()> notification )
144 {
145     LOG(logDEBUG) << "WatchTower::addFile " << file_name;
146 
147     std::weak_ptr<void> weakHeartBeat(heartBeat_);
148 
149     std::lock_guard<std::mutex> lock( observers_mutex_ );
150 
151     auto existing_observed_file =
152         observed_file_list_.searchByName( file_name );
153 
154     std::shared_ptr<std::function<void()>> ptr( new std::function<void()>(std::move( notification )) );
155 
156     if ( ! existing_observed_file )
157     {
158         typename Driver::FileId file_id;
159         typename Driver::SymlinkId symlink_id;
160 
161         std::tie( file_id, symlink_id ) = addFileToDriver( file_name );
162         auto new_file = observed_file_list_.addNewObservedFile(
163                 ObservedFile<Driver>( file_name, ptr, file_id, symlink_id ) );
164 
165         auto dir = observed_file_list_.watchedDirectoryForFile( file_name );
166         if ( ! dir ) {
167             LOG(logDEBUG) << "WatchTower::addFile dir for " << file_name
168                 << " not watched, adding...";
169 
170             dir = observed_file_list_.addWatchedDirectoryForFile( file_name,
171                     [this, weakHeartBeat] (ObservedDir<Driver>* dir) {
172                         if ( auto heart_beat = weakHeartBeat.lock() ) {
173                             driver_.removeDir( dir->dir_id_ );
174                         } } );
175 
176             dir->dir_id_ = driver_.addDir( dir->path );
177 
178             if ( ! dir->dir_id_.valid() ) {
179                 LOG(logWARNING) << "WatchTower::addFile driver failed to add dir";
180                 dir = nullptr;
181             }
182         }
183         else {
184             LOG(logDEBUG) << "WatchTower::addFile Found exisiting watch for dir " << file_name;
185         }
186 
187         // Associate the dir to the file
188         if ( dir )
189             new_file->dir_ = dir;
190     }
191     else
192     {
193         LOG(logDEBUG) << "WatchTower::addFile add extra callback for already monitored " << file_name;
194         existing_observed_file->addCallback( ptr );
195     }
196 
197     // Returns a shared pointer that removes its own entry
198     // from the list of watched stuff when it goes out of scope!
199     // Uses a custom deleter to do the work.
200     return std::shared_ptr<void>( 0x0, [this, ptr, weakHeartBeat] (void*) {
201             if ( auto heart_beat = weakHeartBeat.lock() )
202                 WatchTower<Driver>::removeNotification( this, ptr );
203             } );
204 }
205 
206 template <typename Driver>
numberWatchedDirectories()207 unsigned int WatchTower<Driver>::numberWatchedDirectories() const
208 {
209     return observed_file_list_.numberWatchedDirectories();
210 }
211 
212 //
213 // Private functions
214 //
215 
216 // Add the passed file name to the driver, returning the file and symlink id
217 template <typename Driver>
218 std::tuple<typename Driver::FileId, typename Driver::SymlinkId>
addFileToDriver(const std::string & file_name)219 WatchTower<Driver>::addFileToDriver( const std::string& file_name )
220 {
221     typename Driver::SymlinkId symlink_id;
222     auto file_id = driver_.addFile( file_name );
223 
224     if ( isSymLink( file_name ) )
225     {
226         // We want to follow the name (as opposed to the inode)
227         // so we watch the symlink as well.
228         symlink_id = driver_.addSymlink( file_name );
229     }
230 
231     return std::make_tuple( file_id, symlink_id );
232 }
233 
234 // Called by the dtor for a registration object
235 template <typename Driver>
removeNotification(WatchTower * watch_tower,std::shared_ptr<void> notification)236 void WatchTower<Driver>::removeNotification(
237         WatchTower* watch_tower, std::shared_ptr<void> notification )
238 {
239     LOG(logDEBUG) << "WatchTower::removeNotification";
240 
241     std::lock_guard<std::mutex> lock( watch_tower->observers_mutex_ );
242 
243     auto file =
244         watch_tower->observed_file_list_.removeCallback( notification );
245 
246     if ( file )
247     {
248         LOG(logDEBUG) << "WatchTower::removeNotification - remove the file";
249         watch_tower->driver_.removeFile( file->file_id_ );
250         watch_tower->driver_.removeSymlink( file->symlink_id_ );
251     }
252 }
253 
254 // Run in its own thread
255 template <typename Driver>
run()256 void WatchTower<Driver>::run()
257 {
258     while ( running_ ) {
259         std::unique_lock<std::mutex> lock( observers_mutex_ );
260 
261         std::vector<ObservedFile<Driver>*> files_needing_readding;
262 
263         auto files = driver_.waitAndProcessEvents(
264                 &observed_file_list_, &lock, &files_needing_readding, polling_interval_ms_ );
265         LOG(logDEBUG) << "WatchTower::run: waitAndProcessEvents returned "
266             << files.size() << " files, " << files_needing_readding.size()
267             << " needing re-adding";
268 
269         for ( auto file: files_needing_readding ) {
270             // A file 'needing readding' has the same name,
271             // but probably a different inode, so it needs
272             // to be readded for some drivers that rely on the
273             // inode (e.g. inotify)
274             driver_.removeFile( file->file_id_ );
275             driver_.removeSymlink( file->symlink_id_ );
276 
277             std::tie( file->file_id_, file->symlink_id_ ) =
278                 addFileToDriver( file->file_name_ );
279         }
280 
281         for ( auto file: files ) {
282             for ( auto observer: file->callbacks ) {
283                 LOG(logDEBUG) << "WatchTower::run: notifying the client!";
284                 // Here we have to cast our generic pointer back to
285                 // the function pointer in order to perform the call
286                 const std::shared_ptr<std::function<void()>> fptr =
287                     std::static_pointer_cast<std::function<void()>>( observer );
288                 // The observer is called with the mutex held,
289                 // Let's hope it doesn't do anything too funky.
290                 (*fptr)();
291 
292                 file->markAsChanged();
293             }
294         }
295 
296         if ( polling_interval_ms_ > 0 ) {
297             // Also call files that have not been called for a while
298             for ( auto file: observed_file_list_ ) {
299                 uint32_t ms_since_last_check =
300                     std::chrono::duration_cast<std::chrono::milliseconds>(
301                             std::chrono::steady_clock::now() - file->timeForLastCheck() ).count();
302                 if ( ( ms_since_last_check > polling_interval_ms_ ) && file->hasChanged() ) {
303                     LOG(logDEBUG) << "WatchTower::run: " << file->file_name_;
304                     for ( auto observer: file->callbacks ) {
305                         LOG(logDEBUG) << "WatchTower::run: notifying the client because of a timeout!";
306                         // Here we have to cast our generic pointer back to
307                         // the function pointer in order to perform the call
308                         const std::shared_ptr<std::function<void()>> fptr =
309                             std::static_pointer_cast<std::function<void()>>( observer );
310                         // The observer is called with the mutex held,
311                         // Let's hope it doesn't do anything too funky.
312                         (*fptr)();
313                     }
314                     file->markAsChanged();
315                 }
316             }
317         }
318     }
319 }
320 
321 namespace {
isSymLink(const std::string & file_name)322     bool isSymLink( const std::string& file_name )
323     {
324 #ifdef HAVE_SYMLINK
325         struct stat buf;
326 
327         lstat( file_name.c_str(), &buf );
328         return ( S_ISLNK(buf.st_mode) );
329 #else
330         return false;
331 #endif
332     }
333 };
334 
335 #endif
336