Coverage Report

Created: 2022-08-24 06:18

/src/Fast-DDS/thirdparty/filewatch/FileWatch.hpp
Line
Count
Source (jump to first uncovered line)
1
//  MIT License
2
//
3
//  Copyright(c) 2017 Thomas Monkman
4
//
5
//  Permission is hereby granted, free of charge, to any person obtaining a copy
6
//  of this software and associated documentation files(the "Software"), to deal
7
//  in the Software without restriction, including without limitation the rights
8
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
//  copies of the Software, and to permit persons to whom the Software is
10
//  furnished to do so, subject to the following conditions :
11
//
12
//  The above copyright notice and this permission notice shall be included in all
13
//  copies or substantial portions of the Software.
14
//
15
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
18
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
//  SOFTWARE.
22
23
#ifndef FILEWATCHER_H
24
#define FILEWATCHER_H
25
26
#ifdef _WIN32
27
#define WIN32_LEAN_AND_MEAN
28
#define stat _stat
29
#ifndef NOMINMAX
30
#define NOMINMAX
31
#endif
32
#include <windows.h>
33
#include <stdlib.h>
34
#include <stdio.h>
35
#include <tchar.h>
36
#include <Pathcch.h>
37
#include <shlwapi.h>
38
#endif // WIN32
39
40
#if __unix__
41
#include <stdio.h>
42
#include <stdlib.h>
43
#include <errno.h>
44
#include <sys/types.h>
45
#include <sys/inotify.h>
46
#include <sys/stat.h>
47
#include <unistd.h>
48
#endif // __unix__
49
50
#include <algorithm>
51
#include <array>
52
#include <atomic>
53
#include <chrono>
54
#include <condition_variable>
55
#include <functional>
56
#include <future>
57
#include <iostream>
58
#include <map>
59
#include <mutex>
60
#include <regex>
61
#include <string>
62
#include <system_error>
63
#include <thread>
64
#include <type_traits>
65
#include <utility>
66
#include <vector>
67
68
namespace filewatch {
69
    enum class Event {
70
        added,
71
        removed,
72
        modified,
73
        renamed_old,
74
        renamed_new
75
    };
76
77
    /**
78
    * \class FileWatch
79
    *
80
    * \brief Watches a folder or file, and will notify of changes via function callback.
81
    *
82
    * \author Thomas Monkman
83
    *
84
    */
85
    template<class T>
86
    class FileWatch
87
    {
88
        typedef typename T::value_type C;
89
        typedef std::basic_string<C, std::char_traits<C>> UnderpinningString;
90
        typedef std::basic_regex<C, std::regex_traits<C>> UnderpinningRegex;
91
92
    public:
93
94
        FileWatch(T path, UnderpinningRegex pattern, std::function<void(const T& file, const Event event_type)> callback) :
95
            _path(path),
96
            _pattern(pattern),
97
            _callback(callback),
98
            _directory(get_directory(path))
99
0
        {
100
0
            init();
101
0
        }
102
103
        FileWatch(T path, std::function<void(const T& file, const Event event_type)> callback) :
104
0
            FileWatch<T>(path, UnderpinningRegex(_regex_all), callback) {}
105
106
0
        ~FileWatch() {
107
0
            destroy();
108
0
        }
109
110
        FileWatch(const FileWatch<T>& other) : FileWatch<T>(other._path, other._callback) {}
111
112
        FileWatch<T>& operator=(const FileWatch<T>& other)
113
        {
114
            if (this == &other) { return *this; }
115
116
            destroy();
117
            _path = other._path;
118
            _callback = other._callback;
119
            _directory = get_directory(other._path);
120
            init();
121
            return *this;
122
        }
123
124
        // Const memeber varibles don't let me implent moves nicely, if moves are really wanted std::unique_ptr should be used and move that.
125
        FileWatch<T>(FileWatch<T>&&) = delete;
126
        FileWatch<T>& operator=(FileWatch<T>&&) & = delete;
127
128
    private:
129
        static constexpr C _regex_all[] = { '.', '*', '\0' };
130
        static constexpr C _this_directory[] = { '.', '/', '\0' };
131
132
        struct PathParts
133
        {
134
0
            PathParts(T directory, T filename) : directory(directory), filename(filename) {}
135
            T directory;
136
            T filename;
137
        };
138
        const T _path;
139
140
        UnderpinningRegex _pattern;
141
142
        static constexpr std::size_t _buffer_size = { 1024 * 256 };
143
144
        // only used if watch a single file
145
        bool _watching_single_file = { false };
146
        T _filename;
147
148
        std::atomic<bool> _destory = { false };
149
        std::function<void(const T& file, const Event event_type)> _callback;
150
151
        std::thread _watch_thread;
152
153
        std::condition_variable _cv;
154
        std::mutex _callback_mutex;
155
        std::vector<std::pair<T, Event>> _callback_information;
156
        std::thread _callback_thread;
157
158
        std::promise<void> _running;
159
160
        std::chrono::time_point<std::chrono::system_clock> last_write_time_;
161
        unsigned long last_size_;
162
163
#ifdef _WIN32
164
        HANDLE _directory = { nullptr };
165
        HANDLE _close_event = { nullptr };
166
167
        const DWORD _listen_filters =
168
            FILE_NOTIFY_CHANGE_SECURITY |
169
            FILE_NOTIFY_CHANGE_CREATION |
170
            FILE_NOTIFY_CHANGE_LAST_ACCESS |
171
            FILE_NOTIFY_CHANGE_LAST_WRITE |
172
            FILE_NOTIFY_CHANGE_SIZE |
173
            FILE_NOTIFY_CHANGE_ATTRIBUTES |
174
            FILE_NOTIFY_CHANGE_DIR_NAME |
175
            FILE_NOTIFY_CHANGE_FILE_NAME;
176
177
        const std::map<DWORD, Event> _event_type_mapping = {
178
            { FILE_ACTION_ADDED, Event::added },
179
            { FILE_ACTION_REMOVED, Event::removed },
180
            { FILE_ACTION_MODIFIED, Event::modified },
181
            { FILE_ACTION_RENAMED_OLD_NAME, Event::renamed_old },
182
            { FILE_ACTION_RENAMED_NEW_NAME, Event::renamed_new }
183
        };
184
185
        // time epoch translation
186
        std::pair<ULARGE_INTEGER, std::chrono::time_point<std::chrono::system_clock>> base_;
187
#endif // WIN32
188
189
#if __unix__
190
        struct FolderInfo {
191
            int folder;
192
            int watch;
193
        };
194
195
        FolderInfo  _directory;
196
197
        const std::uint32_t _listen_filters = IN_MODIFY | IN_CREATE | IN_DELETE;
198
199
        const static std::size_t event_size = (sizeof(struct inotify_event));
200
#endif // __unix__
201
202
        void init()
203
0
        {
204
#ifdef _WIN32
205
            _close_event = CreateEvent(NULL, TRUE, FALSE, NULL);
206
            if (!_close_event) {
207
                throw std::system_error(GetLastError(), std::system_category());
208
            }
209
#endif // WIN32
210
0
            _callback_thread = std::move(std::thread([this]() {
211
0
                try {
212
0
                    callback_thread();
213
0
                } catch (...) {
214
0
                    try {
215
0
                        _running.set_exception(std::current_exception());
216
0
                    }
217
0
                    catch (...) {} // set_exception() may throw too
218
0
                }
219
0
            }));
220
0
            _watch_thread = std::move(std::thread([this]() {
221
0
                try {
222
0
                    monitor_directory();
223
0
                } catch (...) {
224
0
                    try {
225
0
                        _running.set_exception(std::current_exception());
226
0
                    }
227
0
                    catch (...) {} // set_exception() may throw too
228
0
                }
229
0
            }));
230
231
0
            std::future<void> future = _running.get_future();
232
0
            future.get(); //block until the monitor_directory is up and running
233
0
        }
234
235
        void destroy()
236
0
        {
237
0
            _destory = true;
238
0
            _running = std::promise<void>();
239
#ifdef _WIN32
240
            SetEvent(_close_event);
241
#elif __unix__
242
0
            inotify_rm_watch(_directory.folder, _directory.watch);
243
0
#endif // __unix__
244
0
            _cv.notify_all();
245
0
            _watch_thread.join();
246
0
            _callback_thread.join();
247
#ifdef _WIN32
248
            CloseHandle(_directory);
249
#elif __unix__
250
0
            close(_directory.folder);
251
0
#endif // __unix__
252
0
        }
253
254
        const PathParts split_directory_and_file(const T& path) const
255
0
        {
256
0
            const auto predict = [](C character) {
257
#ifdef _WIN32
258
                return character == C('\\') || character == C('/');
259
#elif __unix__
260
0
                return character == C('/');
261
0
#endif // __unix__
262
0
            };
263
264
0
            UnderpinningString path_string = path;
265
0
            const auto pivot = std::find_if(path_string.rbegin(), path_string.rend(), predict).base();
266
            //if the path is something like "test.txt" there will be no directory part, however we still need one, so insert './'
267
0
            const T directory = [&]() {
268
0
                const auto extracted_directory = UnderpinningString(path_string.begin(), pivot);
269
0
                return (extracted_directory.size() > 0) ? extracted_directory : UnderpinningString(_this_directory);
270
0
            }();
271
0
            const T filename = UnderpinningString(pivot, path_string.end());
272
0
            return PathParts(directory, filename);
273
0
        }
274
275
        bool pass_filter(const UnderpinningString& file_path)
276
0
        {
277
0
            if (_watching_single_file) {
278
0
                const UnderpinningString extracted_filename = { split_directory_and_file(file_path).filename };
279
                //if we are watching a single file, only that file should trigger action
280
0
                return extracted_filename == _filename;
281
0
            }
282
0
            return std::regex_match(file_path, _pattern);
283
0
        }
284
285
#ifdef _WIN32
286
        template<typename... Args> DWORD GetFileAttributesX(const char* lpFileName, Args... args) {
287
            return GetFileAttributesA(lpFileName, args...);
288
        }
289
        template<typename... Args> DWORD GetFileAttributesX(const wchar_t* lpFileName, Args... args) {
290
            return GetFileAttributesW(lpFileName, args...);
291
        }
292
293
        template<typename... Args> HANDLE CreateFileX(const char* lpFileName, Args... args) {
294
            return CreateFileA(lpFileName, args...);
295
        }
296
        template<typename... Args> HANDLE CreateFileX(const wchar_t* lpFileName, Args... args) {
297
            return CreateFileW(lpFileName, args...);
298
        }
299
300
        HANDLE get_directory(const T& path)
301
        {
302
            auto file_info = GetFileAttributesX(path.c_str());
303
304
            if (file_info == INVALID_FILE_ATTRIBUTES)
305
            {
306
                throw std::system_error(GetLastError(), std::system_category());
307
            }
308
            _watching_single_file = (file_info & FILE_ATTRIBUTE_DIRECTORY) == false;
309
310
            const T watch_path = [this, &path]() {
311
                if (_watching_single_file)
312
                {
313
                    const auto parsed_path = split_directory_and_file(path);
314
                    _filename = parsed_path.filename;
315
                    return parsed_path.directory;
316
                }
317
                else
318
                {
319
                    return path;
320
                }
321
            }();
322
323
            HANDLE directory = CreateFileX(
324
                watch_path.c_str(),           // pointer to the file name
325
                FILE_LIST_DIRECTORY,    // access (read/write) mode
326
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // share mode
327
                nullptr, // security descriptor
328
                OPEN_EXISTING,         // how to create
329
                FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, // file attributes
330
                HANDLE(0));                 // file with attributes to copy
331
332
            if (directory == INVALID_HANDLE_VALUE)
333
            {
334
                throw std::system_error(GetLastError(), std::system_category());
335
            }
336
337
            init_last_write_time();
338
339
            return directory;
340
        }
341
342
        void convert_wstring(const std::wstring& wstr, std::string& out)
343
        {
344
            int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL);
345
            out.resize(size_needed, '\0');
346
            WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &out[0], size_needed, NULL, NULL);
347
        }
348
349
        void convert_wstring(const std::wstring& wstr, std::wstring& out)
350
        {
351
            out = wstr;
352
        }
353
354
        void monitor_directory()
355
        {
356
            std::vector<BYTE> buffer(_buffer_size);
357
            DWORD bytes_returned = 0;
358
            OVERLAPPED overlapped_buffer{ 0 };
359
360
            overlapped_buffer.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
361
            if (!overlapped_buffer.hEvent) {
362
                std::cerr << "Error creating monitor event" << std::endl;
363
            }
364
365
            std::array<HANDLE, 2> handles{ overlapped_buffer.hEvent, _close_event };
366
367
            auto async_pending = false;
368
            _running.set_value();
369
            do {
370
                std::vector<std::pair<T, Event>> parsed_information;
371
                ReadDirectoryChangesW(
372
                    _directory,
373
                    buffer.data(), static_cast<DWORD>(buffer.size()),
374
                    TRUE,
375
                    _listen_filters,
376
                    &bytes_returned,
377
                    &overlapped_buffer, NULL);
378
379
                async_pending = true;
380
381
                switch (WaitForMultipleObjects(2, handles.data(), FALSE, INFINITE))
382
                {
383
                case WAIT_OBJECT_0:
384
                {
385
                    if (!GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE)) {
386
                        throw std::system_error(GetLastError(), std::system_category());
387
                    }
388
                    async_pending = false;
389
390
                    // Get current time
391
                    _WIN32_FILE_ATTRIBUTE_DATA att;
392
                    GetFileAttributesExA(_path.c_str(), GetFileExInfoStandard, &att);
393
394
                    unsigned long current_size = att.nFileSizeLow;
395
                    auto current_time = base_.second
396
                        + std::chrono::duration<
397
                                typename std::chrono::time_point<std::chrono::system_clock>::rep,
398
                                std::ratio_multiply<std::hecto, typename std::chrono::nanoseconds::period>>(
399
                                    reinterpret_cast<ULARGE_INTEGER*>(&att.ftLastWriteTime)->QuadPart - base_.first.QuadPart);
400
401
                    if (bytes_returned == 0 || (current_time == last_write_time_) && current_size == last_size_ ) {
402
                        break;
403
                    }
404
405
                    FILE_NOTIFY_INFORMATION *file_information = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(&buffer[0]);
406
                    do
407
                    {
408
                        std::wstring changed_file_w{ file_information->FileName, file_information->FileNameLength / sizeof(file_information->FileName[0]) };
409
                        UnderpinningString changed_file;
410
                        convert_wstring(changed_file_w, changed_file);
411
                        if (pass_filter(changed_file))
412
                        {
413
                            parsed_information.emplace_back(T{ changed_file }, _event_type_mapping.at(file_information->Action));
414
                        }
415
416
                        last_write_time_ = current_time;
417
                        last_size_ = current_size;
418
419
                        if (file_information->NextEntryOffset == 0) {
420
                            break;
421
                        }
422
423
                        file_information = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(reinterpret_cast<BYTE*>(file_information) + file_information->NextEntryOffset);
424
                    } while (true);
425
                    break;
426
                }
427
                case WAIT_OBJECT_0 + 1:
428
                    // quit
429
                    break;
430
                case WAIT_FAILED:
431
                    break;
432
                }
433
                //dispatch callbacks
434
                {
435
                    std::lock_guard<std::mutex> lock(_callback_mutex);
436
                    _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end());
437
                }
438
                _cv.notify_all();
439
            } while (_destory == false);
440
441
            if (async_pending)
442
            {
443
                //clean up running async io
444
                CancelIo(_directory);
445
                GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE);
446
            }
447
        }
448
#endif // WIN32
449
450
#if __unix__
451
452
        bool is_file(const T& path) const
453
0
        {
454
0
            struct stat statbuf = {};
455
0
            if (stat(path.c_str(), &statbuf) != 0)
456
0
            {
457
0
                throw std::system_error(errno, std::system_category());
458
0
            }
459
0
            return S_ISREG(statbuf.st_mode);
460
0
        }
461
462
        FolderInfo get_directory(const T& path)
463
0
        {
464
0
            const auto folder = inotify_init();
465
0
            if (folder < 0)
466
0
            {
467
0
                throw std::system_error(errno, std::system_category());
468
0
            }
469
            //const auto listen_filters = _listen_filters;
470
471
0
            _watching_single_file = is_file(path);
472
473
0
            const T watch_path = [this, &path]() {
474
0
                if (_watching_single_file)
475
0
                {
476
0
                    const auto parsed_path = split_directory_and_file(path);
477
0
                    _filename = parsed_path.filename;
478
0
                    return parsed_path.directory;
479
0
                }
480
0
                else
481
0
                {
482
0
                    return path;
483
0
                }
484
0
            }();
485
486
0
            const auto watch = inotify_add_watch(folder, watch_path.c_str(), IN_MODIFY | IN_CREATE | IN_DELETE );
487
0
            if (watch < 0)
488
0
            {
489
0
                throw std::system_error(errno, std::system_category());
490
0
            }
491
492
0
            init_last_write_time();
493
494
0
            return { folder, watch };
495
0
        }
496
497
        void monitor_directory()
498
0
        {
499
0
            std::vector<char> buffer(_buffer_size);
500
501
0
            _running.set_value();
502
0
            while (_destory == false)
503
0
            {
504
0
                const auto length = read(_directory.folder, static_cast<void*>(buffer.data()), buffer.size());
505
506
0
                struct stat result;
507
0
                stat(_path.c_str(), &result);
508
509
0
                using clock = std::chrono::system_clock;
510
0
                using duration = clock::duration;
511
0
                std::chrono::time_point<clock> current_time;
512
0
                current_time += std::chrono::duration_cast<duration>(std::chrono::seconds(result.st_mtim.tv_sec));
513
0
                current_time += std::chrono::duration_cast<duration>(std::chrono::nanoseconds(result.st_mtim.tv_nsec));
514
515
0
                unsigned long current_size = result.st_size;
516
517
0
                if (length > 0 && (current_time != last_write_time_ || current_size != last_size_))
518
0
                {
519
0
                    int i = 0;
520
0
                    last_write_time_ = current_time;
521
0
                    last_size_ = current_size;
522
0
                    std::vector<std::pair<T, Event>> parsed_information;
523
0
                    bool already_modified = false;
524
0
                    while (i < length)
525
0
                    {
526
0
                        struct inotify_event *event = reinterpret_cast<struct inotify_event *>(&buffer[i]); // NOLINT
527
0
                        if (event->len)
528
0
                        {
529
0
                            const UnderpinningString changed_file{ event->name };
530
0
                            if (pass_filter(changed_file))
531
0
                            {
532
0
                                if (event->mask & IN_CREATE)
533
0
                                {
534
0
                                    parsed_information.emplace_back(T{ changed_file }, Event::added);
535
0
                                }
536
0
                                else if (event->mask & IN_DELETE)
537
0
                                {
538
0
                                    parsed_information.emplace_back(T{ changed_file }, Event::removed);
539
0
                                }
540
0
                                else if (event->mask & IN_MODIFY && !already_modified)
541
0
                                {
542
0
                                    already_modified = true;
543
0
                                    parsed_information.emplace_back(T{ changed_file }, Event::modified);
544
0
                                }
545
0
                            }
546
0
                        }
547
0
                        i += event_size + event->len;
548
0
                    }
549
                    //dispatch callbacks
550
0
                    {
551
0
                        std::lock_guard<std::mutex> lock(_callback_mutex);
552
0
                        _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end());
553
0
                    }
554
0
                    _cv.notify_all();
555
0
                }
556
0
            }
557
0
        }
558
#endif // __unix__
559
560
        void callback_thread()
561
0
        {
562
0
            while (_destory == false) {
563
0
                std::unique_lock<std::mutex> lock(_callback_mutex);
564
0
                if (_callback_information.empty() && _destory == false) {
565
0
                    _cv.wait(lock, [this] { return _callback_information.size() > 0 || _destory; });
566
0
                }
567
0
                decltype(_callback_information) callback_information = {};
568
0
                std::swap(callback_information, _callback_information);
569
0
                lock.unlock();
570
571
0
                for (const auto& file : callback_information) {
572
0
                    if (_callback) {
573
0
                        try
574
0
                        {
575
0
                            _callback(file.first, file.second);
576
0
                        }
577
0
                        catch (const std::exception&)
578
0
                        {
579
0
                        }
580
0
                    }
581
0
                }
582
0
            }
583
0
        }
584
585
        void init_last_write_time()
586
0
        {
587
#ifdef _WIN32
588
            // Define epoch reference
589
            GetSystemTimeAsFileTime((LPFILETIME)&base_.first);
590
            base_.second = std::chrono::system_clock::now();
591
592
            // Initialize last_write_time_
593
            _WIN32_FILE_ATTRIBUTE_DATA att;
594
            GetFileAttributesExA(_path.c_str(), GetFileExInfoStandard, &att);
595
596
            last_write_time_ = base_.second
597
                + std::chrono::duration<
598
                        typename std::chrono::time_point<std::chrono::system_clock>::rep,
599
                        std::ratio_multiply<std::hecto, typename std::chrono::nanoseconds::period>>(
600
                            reinterpret_cast<ULARGE_INTEGER*>(&att.ftLastWriteTime)->QuadPart - base_.first.QuadPart);
601
602
            // Initialize filesize
603
            last_size_ = att.nFileSizeLow;
604
#else
605
            // Initialize last_write_time_
606
0
            struct stat result;
607
0
            stat(_path.c_str(), &result);
608
609
0
            using duration = std::chrono::system_clock::duration;
610
0
            last_write_time_ += std::chrono::duration_cast<duration>(std::chrono::seconds(result.st_mtim.tv_sec));
611
0
            last_write_time_ += std::chrono::duration_cast<duration>(std::chrono::nanoseconds(result.st_mtim.tv_nsec));
612
613
            // Initialize filesize
614
0
            last_size_ = result.st_size;
615
0
#endif
616
            
617
0
        }
618
619
    };
620
621
    template<class T> constexpr typename FileWatch<T>::C FileWatch<T>::_regex_all[];
622
    template<class T> constexpr typename FileWatch<T>::C FileWatch<T>::_this_directory[];
623
}
624
#endif