xref: /glogg/src/watchtower.h (revision 702af59ea138e3124b906092de415e3601c74d3e)
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( int 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>
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>
122 WatchTower<Driver>::~WatchTower()
123 {
124     running_ = false;
125     driver_.interruptWait();
126     thread_.join();
127 }
128 
129 template <typename Driver>
130 void WatchTower<Driver>::setPollingInterval( int 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>
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         existing_observed_file->addCallback( ptr );
194     }
195 
196     // Returns a shared pointer that removes its own entry
197     // from the list of watched stuff when it goes out of scope!
198     // Uses a custom deleter to do the work.
199     return std::shared_ptr<void>( 0x0, [this, ptr, weakHeartBeat] (void*) {
200             if ( auto heart_beat = weakHeartBeat.lock() )
201                 WatchTower<Driver>::removeNotification( this, ptr );
202             } );
203 }
204 
205 template <typename Driver>
206 unsigned int WatchTower<Driver>::numberWatchedDirectories() const
207 {
208     return observed_file_list_.numberWatchedDirectories();
209 }
210 
211 //
212 // Private functions
213 //
214 
215 // Add the passed file name to the driver, returning the file and symlink id
216 template <typename Driver>
217 std::tuple<typename Driver::FileId, typename Driver::SymlinkId>
218 WatchTower<Driver>::addFileToDriver( const std::string& file_name )
219 {
220     typename Driver::SymlinkId symlink_id;
221     auto file_id = driver_.addFile( file_name );
222 
223     if ( isSymLink( file_name ) )
224     {
225         // We want to follow the name (as opposed to the inode)
226         // so we watch the symlink as well.
227         symlink_id = driver_.addSymlink( file_name );
228     }
229 
230     return std::make_tuple( file_id, symlink_id );
231 }
232 
233 // Called by the dtor for a registration object
234 template <typename Driver>
235 void WatchTower<Driver>::removeNotification(
236         WatchTower* watch_tower, std::shared_ptr<void> notification )
237 {
238     LOG(logDEBUG) << "WatchTower::removeNotification";
239 
240     std::lock_guard<std::mutex> lock( watch_tower->observers_mutex_ );
241 
242     auto file =
243         watch_tower->observed_file_list_.removeCallback( notification );
244 
245     if ( file )
246     {
247         watch_tower->driver_.removeFile( file->file_id_ );
248         watch_tower->driver_.removeSymlink( file->symlink_id_ );
249     }
250 }
251 
252 // Run in its own thread
253 template <typename Driver>
254 void WatchTower<Driver>::run()
255 {
256     while ( running_ ) {
257         std::unique_lock<std::mutex> lock( observers_mutex_ );
258 
259         std::vector<ObservedFile<Driver>*> files_needing_readding;
260 
261         auto files = driver_.waitAndProcessEvents(
262                 &observed_file_list_, &lock, &files_needing_readding, polling_interval_ms_ );
263         LOG(logDEBUG) << "WatchTower::run: waitAndProcessEvents returned "
264             << files.size() << " files, " << files_needing_readding.size()
265             << " needing re-adding";
266 
267         for ( auto file: files_needing_readding ) {
268             // A file 'needing readding' has the same name,
269             // but probably a different inode, so it needs
270             // to be readded for some drivers that rely on the
271             // inode (e.g. inotify)
272             driver_.removeFile( file->file_id_ );
273             driver_.removeSymlink( file->symlink_id_ );
274 
275             std::tie( file->file_id_, file->symlink_id_ ) =
276                 addFileToDriver( file->file_name_ );
277         }
278 
279         for ( auto file: files ) {
280             for ( auto observer: file->callbacks ) {
281                 LOG(logDEBUG) << "WatchTower::run: notifying the client!";
282                 // Here we have to cast our generic pointer back to
283                 // the function pointer in order to perform the call
284                 const std::shared_ptr<std::function<void()>> fptr =
285                     std::static_pointer_cast<std::function<void()>>( observer );
286                 // The observer is called with the mutex held,
287                 // Let's hope it doesn't do anything too funky.
288                 (*fptr)();
289 
290                 file->markAsChanged();
291             }
292         }
293 
294         if ( polling_interval_ms_ > 0 ) {
295             // Also call files that have not been called for a while
296             for ( auto file: observed_file_list_ ) {
297                 uint32_t ms_since_last_check =
298                     std::chrono::duration_cast<std::chrono::milliseconds>(
299                             std::chrono::steady_clock::now() - file->timeForLastCheck() ).count();
300                 if ( ( ms_since_last_check > polling_interval_ms_ ) && file->hasChanged() ) {
301                     LOG(logDEBUG) << "WatchTower::run: " << file->file_name_;
302                     for ( auto observer: file->callbacks ) {
303                         LOG(logDEBUG) << "WatchTower::run: notifying the client because of a timeout!";
304                         // Here we have to cast our generic pointer back to
305                         // the function pointer in order to perform the call
306                         const std::shared_ptr<std::function<void()>> fptr =
307                             std::static_pointer_cast<std::function<void()>>( observer );
308                         // The observer is called with the mutex held,
309                         // Let's hope it doesn't do anything too funky.
310                         (*fptr)();
311                     }
312                     file->markAsChanged();
313                 }
314             }
315         }
316     }
317 }
318 
319 namespace {
320     bool isSymLink( const std::string& file_name )
321     {
322 #ifdef HAVE_SYMLINK
323         struct stat buf;
324 
325         lstat( file_name.c_str(), &buf );
326         return ( S_ISLNK(buf.st_mode) );
327 #else
328         return false;
329 #endif
330     }
331 };
332 
333 #endif
334