xref: /glogg/src/watchtowerlist.h (revision 933a57448ddbe318abe6bf4495141eabeab59586)
1 #ifndef WATCHTOWERLIST_H
2 #define WATCHTOWERLIST_H
3 
4 // Utility classes for the WatchTower implementations
5 
6 #include <functional>
7 #include <string>
8 #include <vector>
9 #include <map>
10 #include <list>
11 #include <memory>
12 #include <algorithm>
13 #include <chrono>
14 
15 #include "log.h"
16 
17 // Utility classes
18 struct ProtocolInfo {
19     // Win32 notification variables
20     static const int READ_DIR_CHANGE_BUFFER_SIZE = 4096;
21 
22     void* handle_;
23     static const unsigned long buffer_length_ = READ_DIR_CHANGE_BUFFER_SIZE;
24     char buffer_[buffer_length_];
25 };
26 
27 // List of files and observers
28 template <typename Driver>
29 struct ObservedDir {
30     ObservedDir( const std::string this_path ) : path { this_path } {}
31 
32     // Returns the address of the protocol specific informations
33     ProtocolInfo* protocolInfo() { return &protocol_info_; }
34 
35     std::string path;
36     typename Driver::DirId dir_id_;
37     // Contains data specific to the protocol (inotify/Win32...)
38     ProtocolInfo protocol_info_;
39 };
40 
41 template <typename Driver>
42 struct ObservedFile {
43     ObservedFile(
44             const std::string& file_name,
45             std::shared_ptr<void> callback,
46             typename Driver::FileId file_id,
47             typename Driver::SymlinkId symlink_id )
48         : file_name_( file_name ) {
49         addCallback( callback );
50 
51         file_id_    = file_id;
52         symlink_id_ = symlink_id;
53         dir_        = nullptr;
54 
55         markAsChanged();
56     }
57 
58     void addCallback( std::shared_ptr<void> callback ) {
59         callbacks.push_back( callback );
60     }
61 
62     // Records the file has changed
63     void markAsChanged() {
64         change_token_.readFromFile( file_name_ );
65         last_check_time_ = std::chrono::steady_clock::now();
66     }
67 
68     // Returns whether a file has changed
69     // (for polling)
70     bool hasChanged() {
71         typename Driver::FileChangeToken new_token( file_name_ );
72         last_check_time_ = std::chrono::steady_clock::now();
73         return change_token_ != new_token;
74     }
75 
76     std::chrono::steady_clock::time_point timeForLastCheck() {
77         return last_check_time_;
78     }
79 
80     std::string file_name_;
81     // List of callbacks for this file
82     std::vector<std::shared_ptr<void>> callbacks;
83 
84     // watch descriptor for the file itself
85     typename Driver::FileId file_id_;
86     // watch descriptor for the symlink (if file is a symlink)
87     typename Driver::SymlinkId symlink_id_;
88 
89     // link to the dir containing the file
90     std::shared_ptr<ObservedDir<Driver>> dir_;
91 
92     // token to identify modification
93     // (the token change when the file is modified, this is used when polling)
94     typename Driver::FileChangeToken change_token_;
95 
96     // Last time a check has been done
97     std::chrono::steady_clock::time_point last_check_time_ =
98         std::chrono::steady_clock::time_point::min();
99 };
100 
101 // A list of the observed files and directories
102 // This class is not thread safe
103 template<typename Driver>
104 class ObservedFileList {
105     public:
106         ObservedFileList() :
107             heartBeat_ { std::shared_ptr<void>((void*) 0xDEADC0DE, [] (void*) {}) }
108             { }
109         ~ObservedFileList() = default;
110 
111         // The functions return a pointer to the existing file (if exists)
112         // but keep ownership of the object.
113         ObservedFile<Driver>* searchByName( const std::string& file_name );
114         ObservedFile<Driver>* searchByFileOrSymlinkWd(
115                 typename Driver::FileId file_id,
116                 typename Driver::SymlinkId symlink_id );
117         ObservedFile<Driver>* searchByDirWdAndName(
118                 typename Driver::DirId id, const char* name );
119         std::vector<ObservedFile<Driver>*> searchByDirWd(
120                 typename Driver::DirId id );
121 
122         // Add a new file, the list returns a pointer to the added file,
123         // but has ownership of the file.
124         ObservedFile<Driver>* addNewObservedFile( ObservedFile<Driver> new_observed );
125         // Remove a callback, remove and returns the file object if
126         // it was the last callback on this object, nullptr if not.
127         // The caller has ownership of the object.
128         std::shared_ptr<ObservedFile<Driver>> removeCallback(
129                 std::shared_ptr<void> callback );
130 
131         // Return the watched directory if it is watched, or nullptr
132         std::shared_ptr<ObservedDir<Driver>> watchedDirectory( const std::string& dir_name );
133         // Create a new watched directory for dir_name, the client is passed
134         // shared ownership and have to keep the shared_ptr (the list only
135         // maintain a weak link).
136         // The remove notification is called just before the reference to
137         // the directory is destroyed.
138         std::shared_ptr<ObservedDir<Driver>> addWatchedDirectory(
139             const std::string& dir_name,
140             std::function<void( ObservedDir<Driver>* )> remove_notification );
141 
142         // Similar to previous functions but extract the name of the
143         // directory from the file name.
144         std::shared_ptr<ObservedDir<Driver>> watchedDirectoryForFile( const std::string& file_name );
145         std::shared_ptr<ObservedDir<Driver>> addWatchedDirectoryForFile( const std::string& file_name,
146                 std::function<void( ObservedDir<Driver>* )> remove_notification );
147 
148         // Removal of directories is done when there is no shared reference
149         // left (RAII)
150 
151         // Number of watched directories (for tests)
152         unsigned int numberWatchedDirectories() const;
153 
154         // Iterator
155         template<typename Container>
156         class iterator : std::iterator<std::input_iterator_tag, ObservedFile<Driver>> {
157           public:
158             iterator( Container* list,
159                    const typename Container::iterator& iter )
160             { list_ = list; pos_ = iter; }
161 
162             iterator operator++()
163             { ++pos_; return *this; }
164 
165             bool operator==( const iterator& other )
166             { return ( pos_ == other.pos_ ); }
167 
168             bool operator!=( const iterator& other )
169             { return ! operator==( other ); }
170 
171             typename Container::iterator operator*()
172             { return pos_; }
173 
174           private:
175             Container* list_;
176             typename Container::iterator pos_;
177         };
178 
179         iterator<std::list<ObservedFile<Driver>>> begin()
180         { return iterator<std::list<ObservedFile<Driver>>>( &observed_files_, observed_files_.begin() ); }
181         iterator<std::list<ObservedFile<Driver>>> end()
182         { return iterator<std::list<ObservedFile<Driver>>>( &observed_files_, observed_files_.end() ); }
183 
184     private:
185         // List of observed files
186         std::list<ObservedFile<Driver>> observed_files_;
187 
188         // List of observed dirs, key-ed by name
189         std::map<std::string, std::weak_ptr<ObservedDir<Driver>>> observed_dirs_;
190 
191         // Map the inotify file (including symlinks) wds to the observed file
192         std::map<int, ObservedFile<Driver>*> by_file_wd_;
193         // Map the inotify directory wds to the observed files
194         std::map<int, ObservedFile<Driver>*> by_dir_wd_;
195 
196         // Heartbeat
197         std::shared_ptr<void> heartBeat_;
198 
199         // Clean all reference to any expired directory
200         void cleanRefsToExpiredDirs();
201 };
202 
203 namespace {
204     std::string directory_path( const std::string& path );
205 };
206 
207 // ObservedFileList class
208 template <typename Driver>
209 ObservedFile<Driver>* ObservedFileList<Driver>::searchByName(
210         const std::string& file_name )
211 {
212     // Look for an existing observer on this file
213     auto existing_observer = observed_files_.begin();
214     for ( ; existing_observer != observed_files_.end(); ++existing_observer )
215     {
216         if ( existing_observer->file_name_ == file_name )
217         {
218             LOG(logDEBUG) << "Found " << file_name;
219             break;
220         }
221     }
222 
223     if ( existing_observer != observed_files_.end() )
224         return &( *existing_observer );
225     else
226         return nullptr;
227 }
228 
229 template <typename Driver>
230 ObservedFile<Driver>* ObservedFileList<Driver>::searchByFileOrSymlinkWd(
231         typename Driver::FileId file_id,
232         typename Driver::SymlinkId symlink_id )
233 {
234     auto result = find_if( observed_files_.begin(), observed_files_.end(),
235             [file_id, symlink_id] (ObservedFile<Driver> file) -> bool {
236                 return ( file_id == file.file_id_ ) ||
237                     ( symlink_id == file.symlink_id_ );
238                 } );
239 
240     if ( result != observed_files_.end() )
241         return &( *result );
242     else
243         return nullptr;
244 }
245 
246 template <typename Driver>
247 ObservedFile<Driver>* ObservedFileList<Driver>::searchByDirWdAndName(
248         typename Driver::DirId id, const char* name )
249 {
250     auto dir = find_if( observed_dirs_.begin(), observed_dirs_.end(),
251             [id] (std::pair<std::string,std::weak_ptr<ObservedDir<Driver>>> d) -> bool {
252             if ( auto dir = d.second.lock() ) {
253                 return ( id == dir->dir_id_ );
254             }
255             else {
256                 return false; } } );
257 
258     if ( dir != observed_dirs_.end() ) {
259         std::string path = dir->first + "/" + name;
260 
261         // LOG(logDEBUG) << "Testing path: " << path;
262 
263         // Looking for the path in the files we are watching
264         return searchByName( path );
265     }
266     else {
267         return nullptr;
268     }
269 }
270 
271 template <typename Driver>
272 std::vector<ObservedFile<Driver>*> ObservedFileList<Driver>::searchByDirWd(
273         typename Driver::DirId id )
274 {
275     std::vector<ObservedFile<Driver>*> result;
276 
277     auto dir = find_if( observed_dirs_.begin(), observed_dirs_.end(),
278             [id] (std::pair<std::string,std::weak_ptr<ObservedDir<Driver>>> d) -> bool {
279             if ( auto dir = d.second.lock() ) {
280                 return ( id == dir->dir_id_ );
281             }
282             else {
283                 return false; } } );
284 
285     if ( dir != observed_dirs_.end() ) {
286         for ( auto i = observed_files_.begin(); i != observed_files_.end(); ++i ) {
287             if ( auto d = dir->second.lock() ) {
288                 if ( d.get() == i->dir_.get() ) {
289                     result.push_back( &(*i) );
290                 }
291             }
292         }
293     }
294 
295     return result;
296 }
297 
298 template <typename Driver>
299 ObservedFile<Driver>* ObservedFileList<Driver>::addNewObservedFile(
300         ObservedFile<Driver> new_observed )
301 {
302     auto new_file = observed_files_.insert( std::begin( observed_files_ ), new_observed );
303 
304     return &( *new_file );
305 }
306 
307 template <typename Driver>
308 std::shared_ptr<ObservedFile<Driver>> ObservedFileList<Driver>::removeCallback(
309         std::shared_ptr<void> callback )
310 {
311     std::shared_ptr<ObservedFile<Driver>> returned_file = nullptr;
312 
313     for ( auto observer = std::begin( observed_files_ );
314             observer != std::end( observed_files_ ); )
315     {
316         std::vector<std::shared_ptr<void>>& callbacks = observer->callbacks;
317         callbacks.erase( std::remove(
318                     std::begin( callbacks ), std::end( callbacks ), callback ),
319                 std::end( callbacks ) );
320 
321         /* See if all notifications have been deleted for this file */
322         if ( callbacks.empty() ) {
323             LOG(logDEBUG) << "Empty notification list for " << observer->file_name_
324                 << ", removing the watched file";
325             returned_file = std::make_shared<ObservedFile<Driver>>( *observer );
326             observer = observed_files_.erase( observer );
327         }
328         else {
329             ++observer;
330         }
331     }
332 
333     return returned_file;
334 }
335 
336 template <typename Driver>
337 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::watchedDirectory(
338         const std::string& dir_name )
339 {
340     std::shared_ptr<ObservedDir<Driver>> dir = nullptr;
341 
342     if ( observed_dirs_.find( dir_name ) != std::end( observed_dirs_ ) )
343         dir = observed_dirs_[ dir_name ].lock();
344 
345     return dir;
346 }
347 
348 template <typename Driver>
349 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::addWatchedDirectory(
350         const std::string& dir_name,
351         std::function<void( ObservedDir<Driver>* )> remove_notification )
352 {
353     std::weak_ptr<void> weakHeartBeat(heartBeat_);
354 
355     std::shared_ptr<ObservedDir<Driver>> dir = {
356             new ObservedDir<Driver>( dir_name ),
357             [this, remove_notification, weakHeartBeat] (ObservedDir<Driver>* d) {
358                 if ( auto heart_beat = weakHeartBeat.lock() ) {
359                     remove_notification( d );
360                     this->cleanRefsToExpiredDirs();
361                 }
362                 delete d; } };
363 
364     observed_dirs_[ dir_name ] = std::weak_ptr<ObservedDir<Driver>>( dir );
365 
366     return dir;
367 }
368 
369 template <typename Driver>
370 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::watchedDirectoryForFile(
371         const std::string& file_name )
372 {
373     return watchedDirectory( directory_path( file_name ) );
374 }
375 
376 template <typename Driver>
377 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::addWatchedDirectoryForFile(
378         const std::string& file_name,
379         std::function<void( ObservedDir<Driver>* )> remove_notification )
380 {
381     return addWatchedDirectory( directory_path( file_name ),
382             remove_notification );
383 }
384 
385 template <typename Driver>
386 unsigned int ObservedFileList<Driver>::numberWatchedDirectories() const
387 {
388     return observed_dirs_.size();
389 }
390 
391 // Private functions
392 template <typename Driver>
393 void ObservedFileList<Driver>::cleanRefsToExpiredDirs()
394 {
395     for ( auto it = std::begin( observed_dirs_ );
396             it != std::end( observed_dirs_ ); )
397     {
398         if ( it->second.expired() ) {
399             it = observed_dirs_.erase( it );
400         }
401         else {
402             ++it;
403         }
404     }
405 }
406 
407 namespace {
408     std::string directory_path( const std::string& path )
409     {
410         size_t slash_pos = path.rfind( '/' );
411 
412 #ifdef _WIN32
413         if ( slash_pos == std::string::npos ) {
414             slash_pos = path.rfind( '\\' );
415         }
416 
417         // We need to include the final slash on Windows
418         ++slash_pos;
419 #endif
420 
421         return std::string( path, 0, slash_pos );
422     }
423 };
424 #endif
425