Unravel Engine C++ Reference
Loading...
Searching...
No Matches
watcher_wtr.cpp
Go to the documentation of this file.
1#include "watcher_wtr.h"
2#include <array>
3#include <chrono>
4#include <filesystem>
5#include <sstream>
6#include <utility>
7#include <set>
9#include <iostream>
10#include <wtr/watcher.hpp>
11namespace fs
12{
13using namespace std::literals;
14
15namespace
16{
17
18void log_path(const fs::path& /*unused*/)
19{
20}
21
22struct observed_changes
23{
24 std::vector<watcher::entry> entries;
25
26 std::vector<size_t> created;
27 std::vector<size_t> modified;
28};
29
30} // namespace
31
33{
34public:
35 directory_listener(const fs::path& path, bool recursive)
36 : root_(path)
37 , recursive_(recursive)
38 , processing_timer_(std::chrono::steady_clock::now())
39 , stop_processing_{false}
41 {
42 // Create initial wtr::watch instance in ring buffer
43 // Capture watcher index to filter out events from inactive watchers
44 file_watchers_[0] = std::make_unique<::wtr::watch>(path, [this, watcher_idx = 0](const ::wtr::event& event) -> void
45 {
46 handle_raw_event(event, watcher_idx);
47 });
48
49 // Start the processing thread
50 processing_thread_ = std::thread([this]() -> void
51 {
52 processing_thread_func();
53 });
54 }
55
57 {
58 // Stop processing thread
59 {
60 std::lock_guard<std::mutex> lock(mutex_);
61 stop_processing_ = true;
62 }
63 processing_cv_.notify_all();
64
65 if(processing_thread_.joinable())
66 {
67 processing_thread_.join();
68 }
69
70 // Process any remaining events before destruction
72
73 // Close both watchers in ring buffer
74 for(auto& watcher : file_watchers_)
75 {
76 if(watcher)
77 {
78 watcher->close();
79 }
80 }
81 }
82
83 auto get_path() const -> const fs::path&
84 {
85 return root_;
86 }
87
88 auto get_recursive() const -> bool
89 {
90 return recursive_;
91 }
92
93private:
94 void advance_watcher()
95 {
96
97 int current_index = active_watcher_index_;
98
99 auto& current_watcher = file_watchers_[current_index];
100 if(current_watcher)
101 {
102 current_watcher->close();
103 current_watcher.reset();
104 }
105
106 // Get next index in ring buffer (0 -> 1, 1 -> 0)
107 int next_index = (active_watcher_index_ + 1) % 2;
108
109 // Create new watcher at next index with the new path
110 // This creates a fresh wtr::watch with correct inotify path mappings
111 // Capture the watcher index so events from this watcher can be identified
112 // Note: We don't lock here because this is called from handle_raw_event which already holds the lock
113 file_watchers_[next_index] = std::make_unique<::wtr::watch>(root_, [this, watcher_idx = next_index](const ::wtr::event& event) -> void
114 {
115 handle_raw_event(event, watcher_idx);
116 });
117
118 // Switch to new watcher atomically
119 active_watcher_index_ = next_index;
120
121 }
122
123 void handle_raw_event(const ::wtr::event& event, int watcher_idx)
124 {
125 // Skip events from inactive watcher (has stale path mappings)
126 if(watcher_idx != active_watcher_index_)
127 {
128 return;
129 }
130
131 // Skip watcher events (status messages)
132 if(event.path_type == ::wtr::event::path_type::watcher)
133 {
134 return;
135 }
136
137 // Skip other event types (owner, other)
138 if(event.effect_type != ::wtr::event::effect_type::create &&
139 event.effect_type != ::wtr::event::effect_type::modify &&
140 event.effect_type != ::wtr::event::effect_type::destroy &&
141 event.effect_type != ::wtr::event::effect_type::rename)
142 {
143 return;
144 }
145
146 std::lock_guard<std::mutex> lock(mutex_);
147
148
149 pending_events_.push_back(event);
150
151 processing_timer_ = std::chrono::steady_clock::now();
152 processing_cv_.notify_one();
153 }
154
155 void processing_thread_func()
156 {
157 platform::set_thread_name("fs::watcher");
158
159 std::unique_lock<std::mutex> lock(mutex_);
160
161 while(!stop_processing_.load())
162 {
163 // Wait for events or timeout (250ms debounce delay)
164 auto timeout = std::chrono::milliseconds(250);
165
166 processing_cv_.wait(lock, [this]() -> bool { return stop_processing_.load() || !pending_events_.empty(); });
167
168 // Check if enough time has passed since last event
169 auto now = std::chrono::steady_clock::now();
170 auto time_since_last_event = now - processing_timer_;
171 auto diff = timeout - time_since_last_event;
172
173 while(diff > std::chrono::milliseconds(0))
174 {
175 lock.unlock();
176 std::this_thread::sleep_for(diff);
177 lock.lock();
178
179 now = std::chrono::steady_clock::now();
180
181 if(processing_timer_ > now)
182 {
183 diff = std::chrono::milliseconds(0);
184 break;
185 }
186 time_since_last_event = now - processing_timer_;
187
188 if(time_since_last_event > timeout)
189 {
190 diff = std::chrono::milliseconds(0);
191 break;
192 }
193 diff = timeout - time_since_last_event;
194 }
195
196 if(!pending_events_.empty())
197 {
198 // Process events
199 process_events_unlocked();
200 }
201 }
202
203 // Process any remaining events before thread exits
204 if(!pending_events_.empty())
205 {
206 process_events_unlocked();
207 }
208 }
209
210 void process_events_unlocked()
211 {
212 if(pending_events_.empty())
213 {
214 return;
215 }
216
217 bool should_advance_watcher = false;
218 // Check if this is a rename any directory
219 for(const auto& event : pending_events_)
220 {
221 if(event.effect_type == ::wtr::event::effect_type::rename &&
222 event.associated &&
223 event.path_type == ::wtr::event::path_type::dir)
224 {
225 // Advance the watcher
226 should_advance_watcher = true;
227 break;
228 }
229 }
230
231 std::vector<::wtr::event> flattened_events = flatten_raw_events(pending_events_);
232
233 // Clear processed events
234 pending_events_.clear();
235
236 // Unlock mutex before emitting to avoid deadlock
237 mutex_.unlock();
238
239 if(should_advance_watcher)
240 {
241 advance_watcher();
242 }
243
244 if(!flattened_events.empty())
245 {
246 std::cout << "--------------------------------" << std::endl;
247
248 std::cout << "Emitting " << flattened_events.size() << " raw events" << std::endl;
249 for(const auto& event : flattened_events)
250 {
251 std::cout << "Event: " << event << std::endl;
252 }
253 std::cout << "--------------------------------" << std::endl;
254
255 on_raw_events.emit(flattened_events);
256 }
257 mutex_.lock();
258 }
259
260 // Flatten raw events: create + modify -> create (with modify timestamp)
261 // Multiple modify -> single modify (with latest timestamp)
262 // rename + destroy (of old path) -> rename only
263 static auto flatten_raw_events(const std::vector<::wtr::event>& raw_events) -> std::vector<::wtr::event>
264 {
265 // First pass: collect rename events and track old paths that were renamed
266 std::set<fs::path> renamed_old_paths;
267 std::vector<::wtr::event> rename_events;
268
269 for(const auto& event : raw_events)
270 {
271 if(event.effect_type == ::wtr::event::effect_type::rename)
272 {
273 renamed_old_paths.insert(event.path_name);
274 rename_events.push_back(event);
275 }
276 }
277
278 // Group non-rename events by path
279 std::map<fs::path, std::vector<::wtr::event>> path_to_events;
280
281 for(const auto& event : raw_events)
282 {
283 // Skip rename events in this pass (handled separately)
284 if(event.effect_type == ::wtr::event::effect_type::rename)
285 {
286 continue;
287 }
288
289 // Skip destroy events for paths that were renamed (they're part of the rename)
290 if(event.effect_type == ::wtr::event::effect_type::destroy && renamed_old_paths.contains(event.path_name))
291 {
292 continue;
293 }
294
295 path_to_events[event.path_name].push_back(event);
296 }
297
298 std::vector<::wtr::event> flattened;
299
300 // Process each path's events
301 for(auto& [path, events] : path_to_events)
302 {
303 // Sort by effect_time
304 std::sort(events.begin(), events.end(),
305 [](const ::wtr::event& a, const ::wtr::event& b) -> bool
306 {
307 return a.effect_time < b.effect_time;
308 });
309
310 // Check what types of events we have
311 bool has_create = false;
312 bool has_modify = false;
313 bool has_destroy = false;
314 ::wtr::event last_modify;
315 ::wtr::event last_destroy;
316
317 for(const auto& event : events)
318 {
319 if(event.effect_type == ::wtr::event::effect_type::create)
320 {
321 has_create = true;
322 }
323 else if(event.effect_type == ::wtr::event::effect_type::modify)
324 {
325 has_modify = true;
326 last_modify = event;
327 }
328 else if(event.effect_type == ::wtr::event::effect_type::destroy)
329 {
330 has_destroy = true;
331 last_destroy = event;
332 }
333 }
334
335 // If create + modify, convert to single create with modify timestamp
336 if(has_create && has_modify)
337 {
338 // Find the create event
339 for(auto& event : events)
340 {
341 if(event.effect_type == ::wtr::event::effect_type::create)
342 {
343 // Use the effect_time from the last modify event
344 event.effect_time = last_modify.effect_time;
345 flattened.push_back(event);
346 break;
347 }
348 }
349 }
350 // If multiple modify (without create), keep only the last one
351 else if(has_modify && !has_create)
352 {
353 flattened.push_back(last_modify);
354 }
355 // If destroy, keep it (only one destroy per path expected, but keep last if multiple)
356 else if(has_destroy)
357 {
358 flattened.push_back(last_destroy);
359 }
360 // If only create (no modify), keep it
361 else if(has_create)
362 {
363 for(const auto& event : events)
364 {
365 if(event.effect_type == ::wtr::event::effect_type::create)
366 {
367 flattened.push_back(event);
368 break;
369 }
370 }
371 }
372 }
373
374 // Add rename events (they're already flattened - one per old path)
375 flattened.insert(flattened.end(), rename_events.begin(), rename_events.end());
376
377 return flattened;
378 }
379
380public:
382 hpp::event<void(const std::vector<::wtr::event>&)> on_raw_events;
383
385 {
386 std::lock_guard<std::mutex> lock(mutex_);
387 process_events_unlocked();
388 }
389
391 fs::path root_;
395 std::array<std::unique_ptr<::wtr::watch>, 2> file_watchers_;
399 std::vector<::wtr::event> pending_events_;
401 std::chrono::steady_clock::time_point processing_timer_;
403 std::mutex mutex_;
405 std::condition_variable processing_cv_;
409 std::atomic<bool> stop_processing_;
410};
411
413{
414public:
415 impl(const fs::path& path,
416 const pattern_filter& filter,
417 bool recursive,
418 bool initial_list,
419 watcher::clock_t::duration poll_interval,
421 std::shared_ptr<directory_listener> listener,
422 const std::string& watcher_name)
423 : path_(path)
424 , filter_(filter)
425 , recursive_(recursive)
426 , callback_(std::move(callback))
427 , listener_(std::move(listener))
428 , init_time_(watcher::clock_t::now())
429 , init_time_timestamp_(std::chrono::system_clock::now())
430 , watcher_name_(watcher_name)
431 {
432 // Initialize entries cache and optionally emit initial list
433 initialize_entries(initial_list);
434
435 // Connect to the listener's raw events
436 slot_key_ = listener_->on_raw_events.connect([this](const std::vector<::wtr::event>& raw_events) -> void
437 {
438 handle_raw_events(raw_events);
439 });
440 }
441
443 {
444 if(listener_)
445 {
446 listener_->on_raw_events.disconnect(slot_key_);
447 }
448 }
449
450 void pause()
451 {
452 paused_ = true;
453 }
454
455 void resume()
456 {
457 paused_ = false;
458 // Process buffered changes
459 if(!buffered_changes_.empty())
460 {
461 std::vector<watcher::entry> changes_to_process;
462 std::swap(changes_to_process, buffered_changes_);
463
464 if(!changes_to_process.empty() && callback_)
465 {
466 callback_(changes_to_process, false);
467 }
468 }
469 }
470
471 void watch()
472 {
473
474 }
475
476 auto get_path() const -> const fs::path&
477 {
478 return path_;
479 }
480
481 auto get_listener() const -> std::shared_ptr<directory_listener>
482 {
483 return listener_;
484 }
485
486private:
487 void initialize_entries(bool emit_initial_list)
488 {
489 // Iterate through the directory and populate entries_ cache
490 fs::error_code err;
491 std::vector<watcher::entry> initial_entries;
492
493 if(recursive_)
494 {
495 for(auto& entry : fs::recursive_directory_iterator(path_, err))
496 {
497 bool filter_passed = filter_.should_include(entry.path());
498
499 fs::error_code err2;
500 fs::file_status file_status = fs::status(entry.path(), err2);
501 auto file_type = file_status.type();
502 if(filter_passed || (file_type == fs::file_type::directory))
503 {
505 e.path = entry.path();
506 e.last_path = entry.path();
508
509 fs::error_code err3;
510 e.last_mod_time = fs::last_write_time(entry.path(), err3);
511 e.size = fs::file_size(entry.path(), err3);
512 e.type = file_type;
513
514 // Add to cache
515 std::string key = e.path.string();
516 entries_[key] = e;
517
518 if(emit_initial_list && filter_passed)
519 {
520 initial_entries.push_back(e);
521 }
522 }
523 }
524 }
525 else
526 {
527 for(auto& entry : fs::directory_iterator(path_, err))
528 {
529 bool filter_passed = filter_.should_include(entry.path());
530 fs::error_code err2;
531 fs::file_status file_status = fs::status(entry.path(), err2);
532 auto file_type = file_status.type();
533 if(filter_passed || (file_type == fs::file_type::directory))
534 {
535 watcher::entry e;
536 e.path = entry.path();
537 e.last_path = entry.path();
539
540 fs::error_code err3;
541 e.last_mod_time = fs::last_write_time(entry.path(), err3);
542 e.size = fs::file_size(entry.path(), err3);
543 e.type = file_type;
544
545 // Add to cache
546 std::string key = e.path.string();
547 entries_[key] = e;
548
549 if(emit_initial_list && filter_passed)
550 {
551 initial_entries.push_back(e);
552 }
553 }
554 }
555 }
556
557 // Emit initial list if requested
558 if(emit_initial_list && !initial_entries.empty() && callback_)
559 {
560 callback_(initial_entries, true);
561 }
562 }
563
564 void handle_raw_events(const std::vector<::wtr::event>& raw_events)
565 {
566 // Process and filter events according to this impl's filter
567 observed_changes changes = process_and_filter_events(raw_events);
568
569 if(changes.entries.empty())
570 {
571 return;
572 }
573
574 // Check if paused
575 if(paused_)
576 {
577 // Buffer changes when paused
578 buffered_changes_.insert(buffered_changes_.end(), changes.entries.begin(), changes.entries.end());
579 return;
580 }
581
582 // Call callback
583 if(callback_)
584 {
585 callback_(changes.entries, false);
586 }
587 }
588
589 auto is_path_under_watch(const fs::path& event_path) const -> bool
590 {
591 // Path-level filtering: When reusing a parent listener, we receive events for
592 // the entire parent directory tree. We must filter to only events under our specific path.
593 // Example: If parent listener watches /home/default and we watch /home/default/test,
594 // we should skip events from /home/default/other
595 fs::error_code ec;
596 auto canonical_event_path = fs::weakly_canonical(event_path, ec);
597 auto canonical_watch_path = fs::weakly_canonical(path_, ec);
598
599 // Check if event path is under our watched path
600 auto rel = canonical_event_path.lexically_relative(canonical_watch_path);
601 return !(rel.empty() || rel.string().substr(0, 2) == "..");
602 }
603
604
605 auto get_system_timestamp(const ::wtr::event& event) -> std::chrono::system_clock::time_point
606 {
607
608 // For these we need the last write time of the file, not the event processed time
609 if(event.effect_type == ::wtr::event::effect_type::modify || event.effect_type == ::wtr::event::effect_type::create)
610 {
611 std::chrono::system_clock::time_point system_timestamp;
612 fs::error_code err;
613 auto file_timestamp = fs::last_write_time(event.path_name, err);
614 if(!err)
615 {
616 system_timestamp = fs::filetime_to_system_clock(file_timestamp);
617 }
618 else
619 {
620 system_timestamp = std::chrono::system_clock::now();
621 }
622
623 return system_timestamp;
624 }
625 auto effect_time = std::chrono::nanoseconds(event.effect_time);
626 auto system_timestamp = std::chrono::system_clock::time_point(std::chrono::duration_cast<std::chrono::system_clock::duration>(effect_time));
627 return system_timestamp;
628 }
629
630 auto get_file_type(const ::wtr::event& event) -> fs::file_type
631 {
632 switch(event.path_type)
633 {
634 case ::wtr::event::path_type::dir:
635 {
636 return fs::file_type::directory;
637 }
638 case ::wtr::event::path_type::file:
639 {
640 return fs::file_type::regular;
641 }
642 case ::wtr::event::path_type::hard_link:
643 {
644 return fs::file_type::symlink;
645 }
646 case ::wtr::event::path_type::sym_link:
647 {
648 return fs::file_type::symlink;
649 }
650 default:
651 {
652 return fs::file_type::not_found;
653 }
654 }
655 return fs::file_type::not_found;
656 }
657
658 auto process_and_filter_events(const std::vector<::wtr::event>& raw_events) -> observed_changes
659 {
660 observed_changes changes;
661
662 if(!watcher_name_.empty())
663 {
664 std::cout << "--------------------------------" << std::endl;
665 }
666
667 if(entries_.size() > 1)
668 {
669 int a = 0;
670 a++;
671
672 }
673
674 // Process events: renames are taken as-is, creates/destroys/modifications are collected for post-processing
675 for(const auto& event : raw_events)
676 {
677 watcher::entry e;
678
679 // Handle rename events separately - they are correct as-is from wtr
680 if(event.effect_type == ::wtr::event::effect_type::rename && event.associated)
681 {
682 e.path = event.associated->path_name;
683 e.last_path = event.path_name;
684
685 e.type = get_file_type(event);
686
687 if(e.type == fs::file_type::not_found)
688 {
689 continue;
690 }
691
692 auto system_timestamp = get_system_timestamp(event);
693
694 if(system_timestamp < init_time_timestamp_)
695 {
696 continue;
697 }
698
699 // Get file info first to determine if it's a directory
700 fs::error_code err;
701 if(fs::exists(e.path, err))
702 {
703 e.last_mod_time = fs::last_write_time(e.path, err);
704 e.size = fs::file_size(e.path, err);
705 }
706 else if(fs::exists(e.last_path, err))
707 {
708 e.last_mod_time = fs::last_write_time(e.path, err);
709 e.size = fs::file_size(e.path, err);
710 }
711
712 // Check if we should apply the filter
713 // Returns false for directory renames (parent or subdirectory)
714 if(e.type == fs::file_type::regular)
715 {
716 // Check if event is under our watched path (for parent listener reuse)
717 if(!is_path_under_watch(e.path) && !is_path_under_watch(e.last_path))
718 {
719 continue;
720 }
721
722 if(!filter_.should_include(e.path))
723 {
724 continue;
725 }
726 }
727 else if(e.type == fs::file_type::directory)
728 {
729 // Directory rename - check if we need to update our watch path
730 // Only update path_ if it's a parent directory rename or exact match
731 fs::error_code ec;
732 auto canonical_old_path = fs::weakly_canonical(e.last_path, ec);
733 auto canonical_watch_path = fs::weakly_canonical(path_, ec);
734
735 if(canonical_old_path == canonical_watch_path)
736 {
737 // Exact match - update watch path directly
738 // Example: watching /test/f1 and /test/f1 renamed to /test/f2
739 path_ = e.path;
740 }
741 else if(fs::is_any_parent_path(canonical_old_path, canonical_watch_path))
742 {
743 // Parent was renamed - update watch path accordingly
744 // Example: watching /test/f1/sub/*.png and /test/f1 renamed to /test/f2
745 // Update path_ from /test/f1/sub to /test/f2/sub
746 fs::path relative_path = canonical_watch_path.lexically_relative(canonical_old_path);
747 path_ = e.path / relative_path;
748 }
749 // Note: For subdirectory renames (e.g., watching /test/*.png and /test/subdir renamed),
750 // we don't update path_ - only process_modifications will update cached entries
751 }
752
753 // Check if old path was in cache
754 if(entries_.contains(e.last_path.string()))
755 {
757
758 // Remove old path from cache and add new path
759 std::string old_key = e.last_path.string();
760 std::string new_key = e.path.string();
761 entries_.erase(old_key);
762 entries_[new_key] = e;
763
764 changes.entries.push_back(e);
765 }
766 else if(e.type == fs::file_type::directory)
767 {
768 // Directory rename not in cache - pass to process_modifications
769 // to update all cached child entries
771 changes.entries.push_back(e);
772 }
773 else
774 {
775 // Old path not in cache, treat as create
777 e.last_path = e.path;
778
779 std::string new_key = e.path.string();
780 entries_[new_key] = e;
781
782 changes.entries.push_back(e);
783 changes.created.push_back(changes.entries.size() - 1);
784 }
785
786 continue;
787 }
788
789 // For non-rename events, use path_name directly
790 e.path = event.path_name;
791 e.last_path = event.path_name;
792 e.type = get_file_type(event);
793
794 if(e.type == fs::file_type::not_found)
795 {
796 continue;
797 }
798
799 // Check if event is under our watched path (for parent listener reuse)
800 if(!is_path_under_watch(e.path))
801 {
802 continue;
803 }
804
805 // Check filter
806 if(!filter_.should_include(e.path))
807 {
808 continue;
809 }
810
811 // Check timestamp
812 auto system_timestamp = get_system_timestamp(event);
813
814 if(system_timestamp < init_time_timestamp_)
815 {
816 continue;
817 }
818
819 // Process create/destroy/modify events
820 switch(event.effect_type)
821 {
822 case ::wtr::event::effect_type::create:
823 {
824 std::string key = e.path.string();
825
826 // Get file info from filesystem
827 fs::error_code err;
828 if(fs::exists(e.path, err))
829 {
830 e.last_mod_time = fs::last_write_time(e.path, err);
831 e.size = fs::file_size(e.path, err);
833
834 changes.entries.push_back(e);
835 changes.created.push_back(changes.entries.size() - 1);
836
837 entries_[key] = e;
838
839 }
840 break;
841 }
842 case ::wtr::event::effect_type::destroy:
843 {
844 // Get file info from cache and add to cache temporarily for rename detection
845 std::string key = e.path.string();
846 auto it = entries_.find(key);
847 if(it != entries_.end())
848 {
849 e.last_mod_time = it->second.last_mod_time;
850 e.size = it->second.size;
852
853 // Add to cache temporarily (will be removed if matched with rename)
854 entries_[key] = e;
855
856 changes.entries.push_back(e);
857
858 }
859 break;
860 }
861 case ::wtr::event::effect_type::modify:
862 {
863 // Get file info from filesystem
864 std::string key = e.path.string();
865 fs::error_code err;
866 if(fs::exists(e.path, err))
867 {
868 e.last_mod_time = fs::last_write_time(e.path, err);
869 e.size = fs::file_size(e.path, err);
871
872 changes.entries.push_back(e);
873 changes.modified.push_back(changes.entries.size() - 1);
874
875 entries_[key] = e;
876 }
877 break;
878 }
879 default:
880 {
881 break;
882 }
883 }
884 }
885
886 // Post-process: detect renames from create+destroy pairs (cross-folder moves)
887 this->process_modifications(entries_, changes);
888 if(changes.entries.size() > 0)
889 {
890 std::cout << "--------------------------------" << std::endl;
891 std::cout << "Watcher : " << watcher_name_ << std::endl;
892 std::cout << "Path: " << path_.string() << std::endl;
893 std::cout << "Recursive: " << recursive_ << std::endl;
894 std::cout << "Filter Exclude Patterns: ";
895 for(const auto& pattern : filter_.get_exclude_patterns())
896 {
897 std::cout << pattern.get_pattern() << " ";
898 }
899 std::cout << std::endl;
900 std::cout << "Filter Include Patterns: ";
901 for(const auto& pattern : filter_.get_include_patterns())
902 {
903 std::cout << pattern.get_pattern() << " ";
904 }
905 std::cout << std::endl;
906 std::cout << "Changes: " << changes.entries.size() << std::endl;
907 for(const auto& entry : changes.entries)
908 {
909 std::cout << "Status: " << to_string(entry) << std::endl;
910 }
911 std::cout << "--------------------------------" << std::endl;
912
913 }
914 return changes;
915 }
916
917
918 static auto get_original_path(const fs::path& old_path, const fs::path& renamed_path, const fs::path& new_path) -> fs::path
919 {
920 fs::path relative_path = fs::relative(new_path, renamed_path);
921 fs::path original_path = old_path / relative_path;
922 fs::error_code err;
923 return fs::weakly_canonical(original_path, err);
924 }
925
926 static auto check_if_same_extension(const fs::path& p1, const fs::path& p2) -> bool
927 {
928 bool same_extensions = true;
929
930 auto ep = p1;
931 auto fp = p2;
932
933 while(ep.has_extension() || fp.has_extension())
934 {
935 same_extensions &= ep.extension() == fp.extension();
936 ep = ep.stem();
937 fp = fp.stem();
938 }
939
940 return same_extensions;
941 }
942
943 static auto check_if_parent_dir_was_renamed(const std::vector<size_t>& renamed_dirs, const std::vector<watcher::entry>& entries, watcher::entry& e) -> bool
944 {
945 //check if parent_dir was renamed
946 for(const auto& renamed_idx : renamed_dirs)
947 {
948 const auto& renamed_e = entries[renamed_idx];
949
950 if(fs::is_any_parent_path(renamed_e.path, e.path))
951 {
953 e.last_path = get_original_path(renamed_e.last_path, renamed_e.path, e.path);
954
955 return true;
956 }
957 }
958 return false;
959 }
960
961 template<typename Container>
962 static auto check_if_renamed(watcher::entry& e, Container& container) -> bool
963 {
964 auto it = std::begin(container);
965 while(it != std::end(container))
966 {
967 auto& fi = it->second;
968 fs::error_code err;
969 if(!fs::exists(fi.path, err))
970 {
971 if(e.size == fi.size)
972 {
973 auto diff = (e.last_mod_time - fi.last_mod_time);
974 auto d = std::chrono::duration_cast<std::chrono::milliseconds>(diff);
975
976 if(d <= std::chrono::milliseconds(10))
977 {
978 bool same_extensions = check_if_same_extension(e.path, fi.path);
979 if(same_extensions)
980 {
981 if(d <= std::chrono::milliseconds(0))
982 {
984 e.last_path = fi.path;
985
986 // remove the cached old path entry
987 container.erase(it);
988 return true;
989 }
990
991 std::cout << "Same file modification time difference: " << std::endl;
992 std::cout << to_string(e) << std::endl;
993 std::cout << to_string(fi) << std::endl;
994 std::cout << "Difference: " << d.count() << " milliseconds" << std::endl;
995 std::cout << "--------------------------------" << std::endl;
996 }
997 }
998 }
999 }
1000
1001 it++;
1002 }
1003
1004 return false;
1005 }
1006
1007 template<typename Container>
1008 static void prune_removed_entries(Container& container)
1009 {
1010
1011 auto it = std::begin(container);
1012 while(it != std::end(container))
1013 {
1014 auto& fi = it->second;
1015 fs::error_code err;
1016 // if(!fs::exists(fi.path, err))
1017 if(fi.status == watcher::entry_status::removed)
1018 {
1019 it = container.erase(it);
1020 }
1021 else
1022 {
1023 it++;
1024 }
1025 }
1026 }
1027
1028 template<typename Container>
1029 void process_modifications(Container& old_entries, observed_changes& changes)
1030 {
1031 if(changes.entries.empty())
1032 {
1033 return;
1034 }
1035
1036 std::vector<size_t> renamed_dirs;
1037
1038 // First pass: detect renames from create/destroy pairs
1039 for(auto idx : changes.created)
1040 {
1041 auto& e = changes.entries[idx];
1042
1043 //check if parent_dir was renamed
1044 if(check_if_parent_dir_was_renamed(renamed_dirs, changes.entries, e))
1045 {
1046 // remove the cached old path entry
1047 old_entries.erase(e.last_path.string());
1048 continue;
1049 }
1050
1051 // check for rename heuristic
1052 if(check_if_renamed(e, old_entries))
1053 {
1054 if(e.type == fs::file_type::directory)
1055 {
1056 renamed_dirs.emplace_back(idx);
1057 }
1058 continue;
1059 }
1060 }
1061
1062 // Second pass: for each renamed directory in changes, update all child paths in the cache
1063 for(const auto& entry : changes.entries)
1064 {
1065 // Skip if not a renamed directory
1066 if(entry.status != watcher::entry_status::renamed || entry.type != fs::file_type::directory)
1067 {
1068 continue;
1069 }
1070
1071 // Find all entries in cache that are children of the old path
1072 std::vector<std::pair<std::string, watcher::entry>> children_to_update;
1073
1074 for(auto& [key, cached_entry] : old_entries)
1075 {
1076 // Check if this cached entry is a child of the OLD directory path
1077 // (cached entries still have their old paths)
1078 if(fs::is_any_parent_path(entry.last_path, cached_entry.path))
1079 {
1080 // Calculate the new path for this child
1081 // get_original_path(A, B, C) returns: A / relative(C, B)
1082 // We want: new_dir / relative(old_child, old_dir)
1083 watcher::entry updated_entry = cached_entry;
1084 updated_entry.last_path = cached_entry.path; // old path
1085 updated_entry.path = get_original_path(entry.path, entry.last_path, cached_entry.path); // new path
1086 updated_entry.status = watcher::entry_status::renamed;
1087
1088 // Store for updating after iteration
1089 children_to_update.push_back({key, updated_entry});
1090 }
1091 }
1092
1093 // Update cache: remove old paths and add new paths
1094 for(const auto& [old_key, updated_entry] : children_to_update)
1095 {
1096 old_entries.erase(old_key);
1097 std::string new_key = updated_entry.path.string();
1098 old_entries[new_key] = updated_entry;
1099
1100 // Add to changes
1101 changes.entries.push_back(updated_entry);
1102 }
1103 }
1104
1105 prune_removed_entries(old_entries);
1106 }
1107
1108 fs::path path_;
1109 pattern_filter filter_;
1110 bool recursive_;
1111 watcher::notify_callback callback_;
1112 std::shared_ptr<directory_listener> listener_;
1113
1114 std::chrono::steady_clock::time_point init_time_;
1115 std::chrono::system_clock::time_point init_time_timestamp_;
1116 uint64_t slot_key_ = 0;
1117 std::atomic<bool> paused_ = false;
1118 std::vector<watcher::entry> buffered_changes_;
1120 std::map<std::string, watcher::entry> entries_;
1121 std::string watcher_name_;
1122};
1123
1125{
1126 close();
1127}
1128
1130{
1131 std::lock_guard<std::mutex> lock(mutex_);
1132 for(auto& kvp : watchers_)
1133 {
1134 kvp.second->pause();
1135 }
1136}
1137
1139{
1140 std::lock_guard<std::mutex> lock(mutex_);
1141 for(auto& kvp : watchers_)
1142 {
1143 kvp.second->resume();
1144 }
1145}
1146
1148{
1149 // Remove all watchers
1151
1152 // Clear directory listeners
1153 {
1154 std::lock_guard<std::mutex> lock(mutex_);
1155 directory_listeners_.clear();
1156 }
1157}
1158
1160{
1161 watching_ = true;
1162
1163}
1164
1165auto watcher_wtr::watch_impl(const fs::path& path,
1166 const pattern_filter& filter,
1167 bool recursive,
1168 bool initial_list,
1169 watcher::clock_t::duration poll_interval,
1170 watcher::notify_callback callback,
1171 const std::string& watcher_name) -> std::uint64_t
1172{
1173 if(!callback)
1174 {
1175 return 0;
1176 }
1177 std::shared_ptr<directory_listener> listener;
1178 {
1179 std::lock_guard<std::mutex> lock(mutex_);
1180 fs::error_code err;
1181 fs::path abs_path = fs::absolute(path, err);
1182 auto it = directory_listeners_.find(abs_path);
1183 if(it != directory_listeners_.end())
1184 {
1185 listener = it->second;
1186 }
1187 else
1188 {
1189 for(auto& [watched_path, existing_listener] : directory_listeners_)
1190 {
1191 if(existing_listener->get_recursive() && fs::is_any_parent_path(watched_path, abs_path))
1192 {
1193 listener = existing_listener;
1194 break;
1195 }
1196 }
1197 if(!listener)
1198 {
1199 listener = std::make_shared<directory_listener>(abs_path, recursive);
1200 directory_listeners_[abs_path] = listener;
1201 }
1202 }
1203 }
1204 static std::atomic<std::uint64_t> free_id = {1};
1205 auto key = free_id++;
1206 auto impl = std::make_shared<watcher_wtr::impl>(path, filter, recursive, initial_list, poll_interval, std::move(callback), listener, watcher_name);
1207 {
1208 std::lock_guard<std::mutex> lock(mutex_);
1209 watchers_[key] = impl;
1210 }
1211 return key;
1212}
1213
1214void watcher_wtr::unwatch_impl(std::uint64_t key)
1215{
1216 {
1217 std::lock_guard<std::mutex> lock(mutex_);
1218 watchers_.erase(key);
1219 }
1220 std::set<std::string> stale_listeners;
1221 {
1222 std::lock_guard<std::mutex> lock(mutex_);
1223 for(auto& [path, listener] : directory_listeners_)
1224 {
1225 if(listener.use_count() == 1)
1226 {
1227 stale_listeners.insert(path.string());
1228 }
1229 }
1230 }
1231 for(auto& path : stale_listeners)
1232 {
1233 std::lock_guard<std::mutex> lock(mutex_);
1234 directory_listeners_.erase(path);
1235 }
1236}
1237
1239{
1240 {
1241 std::lock_guard<std::mutex> lock(mutex_);
1242 watchers_.clear();
1243 }
1244 {
1245 std::lock_guard<std::mutex> lock(mutex_);
1246 directory_listeners_.clear();
1247 }
1248}
1249
1250
1251} // namespace fs
entt::handle b
event_type event
entt::handle a
A filter that combines include and exclude patterns for file/directory filtering.
auto get_include_patterns() const -> const std::vector< wildcard_pattern > &
Gets all include patterns.
auto get_exclude_patterns() const -> const std::vector< wildcard_pattern > &
Gets all exclude patterns.
auto should_include(const fs::path &path) const -> bool
Tests if a path should be included based on the filter rules Logic: (matches any include pattern OR n...
bool recursive_
Whether to watch recursively.
std::condition_variable processing_cv_
Condition variable for processing thread.
int active_watcher_index_
Active watcher index in ring buffer (0 or 1)
std::array< std::unique_ptr<::wtr::watch >, 2 > file_watchers_
Ring buffer of wtr::watch instances (2 slots for seamless recreation)
std::mutex mutex_
Mutex for thread safety.
directory_listener(const fs::path &path, bool recursive)
std::chrono::steady_clock::time_point processing_timer_
Timer for debouncing event processing.
fs::path root_
Path being watched.
hpp::event< void(const std::vector<::wtr::event > &)> on_raw_events
Event that emits raw events to all connected impls.
auto get_path() const -> const fs::path &
std::vector<::wtr::event > pending_events_
Raw events waiting to be processed.
std::atomic< bool > stop_processing_
Flag to stop processing thread.
std::thread processing_thread_
Processing thread.
auto get_path() const -> const fs::path &
impl(const fs::path &path, const pattern_filter &filter, bool recursive, bool initial_list, watcher::clock_t::duration poll_interval, watcher::notify_callback callback, std::shared_ptr< directory_listener > listener, const std::string &watcher_name)
auto get_listener() const -> std::shared_ptr< directory_listener >
std::map< std::uint64_t, std::shared_ptr< impl > > watchers_
Definition watcher_wtr.h:88
auto watch_impl(const fs::path &path, const pattern_filter &filter, bool recursive, bool initial_list, watcher::clock_t::duration poll_interval, watcher::notify_callback callback, const std::string &watcher_name) -> std::uint64_t
std::atomic< bool > watching_
Atomic bool sync.
Definition watcher_wtr.h:82
std::mutex mutex_
Mutex for the file watchers.
Definition watcher_wtr.h:80
void unwatch_impl(std::uint64_t key)
std::map< fs::path, std::shared_ptr< directory_listener > > directory_listeners_
Definition watcher_wtr.h:92
std::function< void(const std::vector< entry > &, bool)> notify_callback
Definition watcher.h:41
std::vector< render_pass_entry > entries
Definition cache.hpp:11
auto filetime_to_system_clock(fs::file_time_type ft) -> std::chrono::system_clock::time_point
Definition watcher.h:106
bool is_any_parent_path(const path &parent, const path &child)
auto to_string(const watcher::entry &e) -> std::string
Definition watcher.cpp:99
void set_thread_name(const char *threadName)
Definition thread.hpp:160
Hash specialization for batch_key to enable use in std::unordered_map.