Coverage Report

Created: 2025-06-13 06:45

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