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