xref: /glogg/src/watchtowerlist.h (revision 5751f354d83b823c02254f923d840a4198b4d373)
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             // Use an empty deleter since we don't really own this.
108             heartBeat_ { this, [] (void*) {} }
109             { }
110         ObservedFileList( const ObservedFileList& ) = delete;
111         ~ObservedFileList() = default;
112 
113         // The functions return a pointer to the existing file (if exists)
114         // but keep ownership of the object.
115         ObservedFile<Driver>* searchByName( const std::string& file_name );
116         ObservedFile<Driver>* searchByFileOrSymlinkWd(
117                 typename Driver::FileId file_id,
118                 typename Driver::SymlinkId symlink_id );
119         ObservedFile<Driver>* searchByDirWdAndName(
120                 typename Driver::DirId id, const char* name );
121         std::vector<ObservedFile<Driver>*> searchByDirWd(
122                 typename Driver::DirId id );
123 
124         // Add a new file, the list returns a pointer to the added file,
125         // but has ownership of the file.
126         ObservedFile<Driver>* addNewObservedFile( ObservedFile<Driver> new_observed );
127         // Remove a callback, remove and returns the file object if
128         // it was the last callback on this object, nullptr if not.
129         // The caller has ownership of the object.
130         std::shared_ptr<ObservedFile<Driver>> removeCallback(
131                 std::shared_ptr<void> callback );
132 
133         // Return the watched directory if it is watched, or nullptr
134         std::shared_ptr<ObservedDir<Driver>> watchedDirectory( const std::string& dir_name );
135         // Create a new watched directory for dir_name, the client is passed
136         // shared ownership and have to keep the shared_ptr (the list only
137         // maintain a weak link).
138         // The remove notification is called just before the reference to
139         // the directory is destroyed.
140         std::shared_ptr<ObservedDir<Driver>> addWatchedDirectory(
141             const std::string& dir_name,
142             std::function<void( ObservedDir<Driver>* )> remove_notification );
143 
144         // Similar to previous functions but extract the name of the
145         // directory from the file name.
146         std::shared_ptr<ObservedDir<Driver>> watchedDirectoryForFile( const std::string& file_name );
147         std::shared_ptr<ObservedDir<Driver>> addWatchedDirectoryForFile( const std::string& file_name,
148                 std::function<void( ObservedDir<Driver>* )> remove_notification );
149 
150         // Removal of directories is done when there is no shared reference
151         // left (RAII)
152 
153         // Number of watched directories (for tests)
154         unsigned int numberWatchedDirectories() const;
155 
156         // Iterator
157         template<typename Container>
158         class iterator : std::iterator<std::input_iterator_tag, ObservedFile<Driver>> {
159           public:
160             iterator( Container* list,
161                    const typename Container::iterator& iter )
162             { list_ = list; pos_ = iter; }
163 
164             iterator operator++()
165             { ++pos_; return *this; }
166 
167             bool operator==( const iterator& other )
168             { return ( pos_ == other.pos_ ); }
169 
170             bool operator!=( const iterator& other )
171             { return ! operator==( other ); }
172 
173             typename Container::iterator operator*()
174             { return pos_; }
175 
176           private:
177             Container* list_;
178             typename Container::iterator pos_;
179         };
180 
181         iterator<std::list<ObservedFile<Driver>>> begin()
182         { return iterator<std::list<ObservedFile<Driver>>>( &observed_files_, observed_files_.begin() ); }
183         iterator<std::list<ObservedFile<Driver>>> end()
184         { return iterator<std::list<ObservedFile<Driver>>>( &observed_files_, observed_files_.end() ); }
185 
186     private:
187         // List of observed files
188         std::list<ObservedFile<Driver>> observed_files_;
189 
190         // List of observed dirs, key-ed by name
191         std::map<std::string, std::weak_ptr<ObservedDir<Driver>>> observed_dirs_;
192 
193         // Map the inotify file (including symlinks) wds to the observed file
194         std::map<int, ObservedFile<Driver>*> by_file_wd_;
195         // Map the inotify directory wds to the observed files
196         std::map<int, ObservedFile<Driver>*> by_dir_wd_;
197 
198         // A shared_ptr to self.
199         // weak_ptr's from this can check if the list is still alive.
200         // This probably shouldn't be copied.
201         std::shared_ptr<ObservedFileList> heartBeat_;
202 
203         // Clean all reference to any expired directory
204         void cleanRefsToExpiredDirs();
205 };
206 
207 namespace {
208     std::string directory_path( const std::string& path );
209 };
210 
211 // ObservedFileList class
212 template <typename Driver>
213 ObservedFile<Driver>* ObservedFileList<Driver>::searchByName(
214         const std::string& file_name )
215 {
216     // Look for an existing observer on this file
217     auto existing_observer = observed_files_.begin();
218     for ( ; existing_observer != observed_files_.end(); ++existing_observer )
219     {
220         if ( existing_observer->file_name_ == file_name )
221         {
222             LOG(logDEBUG) << "Found " << file_name;
223             break;
224         }
225     }
226 
227     if ( existing_observer != observed_files_.end() )
228         return &( *existing_observer );
229     else
230         return nullptr;
231 }
232 
233 template <typename Driver>
234 ObservedFile<Driver>* ObservedFileList<Driver>::searchByFileOrSymlinkWd(
235         typename Driver::FileId file_id,
236         typename Driver::SymlinkId symlink_id )
237 {
238     auto result = find_if( observed_files_.begin(), observed_files_.end(),
239             [file_id, symlink_id] (ObservedFile<Driver> file) -> bool {
240                 return ( file_id == file.file_id_ ) ||
241                     ( symlink_id == file.symlink_id_ );
242                 } );
243 
244     if ( result != observed_files_.end() )
245         return &( *result );
246     else
247         return nullptr;
248 }
249 
250 template <typename Driver>
251 ObservedFile<Driver>* ObservedFileList<Driver>::searchByDirWdAndName(
252         typename Driver::DirId id, const char* name )
253 {
254     auto dir = find_if( observed_dirs_.begin(), observed_dirs_.end(),
255             [id] (std::pair<std::string,std::weak_ptr<ObservedDir<Driver>>> d) -> bool {
256             if ( auto dir = d.second.lock() ) {
257                 return ( id == dir->dir_id_ );
258             }
259             else {
260                 return false; } } );
261 
262     if ( dir != observed_dirs_.end() ) {
263         std::string path = dir->first + "/" + name;
264 
265         // LOG(logDEBUG) << "Testing path: " << path;
266 
267         // Looking for the path in the files we are watching
268         return searchByName( path );
269     }
270     else {
271         return nullptr;
272     }
273 }
274 
275 template <typename Driver>
276 std::vector<ObservedFile<Driver>*> ObservedFileList<Driver>::searchByDirWd(
277         typename Driver::DirId id )
278 {
279     std::vector<ObservedFile<Driver>*> result;
280 
281     auto dir = find_if( observed_dirs_.begin(), observed_dirs_.end(),
282             [id] (std::pair<std::string,std::weak_ptr<ObservedDir<Driver>>> d) -> bool {
283             if ( auto dir = d.second.lock() ) {
284                 return ( id == dir->dir_id_ );
285             }
286             else {
287                 return false; } } );
288 
289     if ( dir != observed_dirs_.end() ) {
290         for ( auto i = observed_files_.begin(); i != observed_files_.end(); ++i ) {
291             if ( auto d = dir->second.lock() ) {
292                 if ( d.get() == i->dir_.get() ) {
293                     result.push_back( &(*i) );
294                 }
295             }
296         }
297     }
298 
299     return result;
300 }
301 
302 template <typename Driver>
303 ObservedFile<Driver>* ObservedFileList<Driver>::addNewObservedFile(
304         ObservedFile<Driver> new_observed )
305 {
306     auto new_file = observed_files_.insert( std::begin( observed_files_ ), new_observed );
307 
308     return &( *new_file );
309 }
310 
311 template <typename Driver>
312 std::shared_ptr<ObservedFile<Driver>> ObservedFileList<Driver>::removeCallback(
313         std::shared_ptr<void> callback )
314 {
315     std::shared_ptr<ObservedFile<Driver>> returned_file = nullptr;
316 
317     for ( auto observer = std::begin( observed_files_ );
318             observer != std::end( observed_files_ ); )
319     {
320         std::vector<std::shared_ptr<void>>& callbacks = observer->callbacks;
321         callbacks.erase( std::remove(
322                     std::begin( callbacks ), std::end( callbacks ), callback ),
323                 std::end( callbacks ) );
324 
325         /* See if all notifications have been deleted for this file */
326         if ( callbacks.empty() ) {
327             LOG(logDEBUG) << "Empty notification list for " << observer->file_name_
328                 << ", removing the watched file";
329             returned_file = std::make_shared<ObservedFile<Driver>>( *observer );
330             observer = observed_files_.erase( observer );
331         }
332         else {
333             ++observer;
334         }
335     }
336 
337     return returned_file;
338 }
339 
340 template <typename Driver>
341 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::watchedDirectory(
342         const std::string& dir_name )
343 {
344     std::shared_ptr<ObservedDir<Driver>> dir = nullptr;
345 
346     if ( observed_dirs_.find( dir_name ) != std::end( observed_dirs_ ) )
347         dir = observed_dirs_[ dir_name ].lock();
348 
349     return dir;
350 }
351 
352 template <typename Driver>
353 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::addWatchedDirectory(
354         const std::string& dir_name,
355         std::function<void( ObservedDir<Driver>* )> remove_notification )
356 {
357     std::weak_ptr<ObservedFileList> weakHeartBeat(heartBeat_);
358 
359     std::shared_ptr<ObservedDir<Driver>> dir = {
360             new ObservedDir<Driver>( dir_name ),
361             [remove_notification, weakHeartBeat] (ObservedDir<Driver>* d) {
362                 if ( auto list = weakHeartBeat.lock() ) {
363                     remove_notification( d );
364                     list->cleanRefsToExpiredDirs();
365                 }
366                 delete d; } };
367 
368     observed_dirs_[ dir_name ] = std::weak_ptr<ObservedDir<Driver>>( dir );
369 
370     return dir;
371 }
372 
373 template <typename Driver>
374 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::watchedDirectoryForFile(
375         const std::string& file_name )
376 {
377     return watchedDirectory( directory_path( file_name ) );
378 }
379 
380 template <typename Driver>
381 std::shared_ptr<ObservedDir<Driver>> ObservedFileList<Driver>::addWatchedDirectoryForFile(
382         const std::string& file_name,
383         std::function<void( ObservedDir<Driver>* )> remove_notification )
384 {
385     return addWatchedDirectory( directory_path( file_name ),
386             remove_notification );
387 }
388 
389 template <typename Driver>
390 unsigned int ObservedFileList<Driver>::numberWatchedDirectories() const
391 {
392     return observed_dirs_.size();
393 }
394 
395 // Private functions
396 template <typename Driver>
397 void ObservedFileList<Driver>::cleanRefsToExpiredDirs()
398 {
399     for ( auto it = std::begin( observed_dirs_ );
400             it != std::end( observed_dirs_ ); )
401     {
402         if ( it->second.expired() ) {
403             it = observed_dirs_.erase( it );
404         }
405         else {
406             ++it;
407         }
408     }
409 }
410 
411 namespace {
412     std::string directory_path( const std::string& path )
413     {
414         size_t slash_pos = path.rfind( '/' );
415 
416 #ifdef _WIN32
417         if ( slash_pos == std::string::npos ) {
418             slash_pos = path.rfind( '\\' );
419         }
420 
421         // We need to include the final slash on Windows
422         ++slash_pos;
423 #endif
424 
425         return std::string( path, 0, slash_pos );
426     }
427 };
428 #endif
429