QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCFileWatcher.cc
Go to the documentation of this file.
1#include "QGCFileWatcher.h"
3
4#include <QtCore/QDir>
5#include <QtCore/QFileInfo>
6
7QGC_LOGGING_CATEGORY(QGCFileWatcherLog, "Utilities.QGCFileWatcher")
8
9// ============================================================================
10// Construction / Destruction
11// ============================================================================
12
14 : QObject(parent)
15 , _watcher(new QFileSystemWatcher(this))
16 , _debounceTimer(new QTimer(this))
17{
18 connect(_watcher, &QFileSystemWatcher::fileChanged,
19 this, &QGCFileWatcher::_onFileChanged);
20 connect(_watcher, &QFileSystemWatcher::directoryChanged,
21 this, &QGCFileWatcher::_onDirectoryChanged);
22
23 _debounceTimer->setSingleShot(true);
24 connect(_debounceTimer, &QTimer::timeout,
25 this, &QGCFileWatcher::_processPendingChanges);
26}
27
32
33// ============================================================================
34// Configuration
35// ============================================================================
36
38{
39 _debounceDelay = qMax(0, milliseconds);
40}
41
42// ============================================================================
43// File Watching
44// ============================================================================
45
46bool QGCFileWatcher::watchFile(const QString &filePath, ChangeCallback callback)
47{
48 if (filePath.isEmpty()) {
49 qCWarning(QGCFileWatcherLog) << "watchFile: empty path";
50 return false;
51 }
52
53 const QString canonicalPath = QFileInfo(filePath).absoluteFilePath();
54
55 if (!QFileInfo::exists(canonicalPath)) {
56 qCWarning(QGCFileWatcherLog) << "watchFile: file does not exist:" << filePath;
57 return false;
58 }
59
60 if (_watcher->addPath(canonicalPath)) {
61 if (callback) {
62 _fileCallbacks[canonicalPath] = callback;
63 }
64 qCDebug(QGCFileWatcherLog) << "Watching file:" << canonicalPath;
65 return true;
66 }
67
68 // Already watching
69 if (_watcher->files().contains(canonicalPath)) {
70 if (callback) {
71 _fileCallbacks[canonicalPath] = callback;
72 }
73 return true;
74 }
75
76 qCWarning(QGCFileWatcherLog) << "watchFile: failed to add:" << filePath;
77 return false;
78}
79
80bool QGCFileWatcher::unwatchFile(const QString &filePath)
81{
82 const QString canonicalPath = QFileInfo(filePath).absoluteFilePath();
83
84 _fileCallbacks.remove(canonicalPath);
85 _persistentFiles.remove(canonicalPath);
86 _pendingFileChanges.remove(canonicalPath);
87
88 if (_watcher->removePath(canonicalPath)) {
89 qCDebug(QGCFileWatcherLog) << "Stopped watching file:" << canonicalPath;
90 return true;
91 }
92
93 return false;
94}
95
96bool QGCFileWatcher::isWatchingFile(const QString &filePath) const
97{
98 const QString canonicalPath = QFileInfo(filePath).absoluteFilePath();
99 return _watcher->files().contains(canonicalPath);
100}
101
103{
104 return _watcher->files();
105}
106
107// ============================================================================
108// Directory Watching
109// ============================================================================
110
111bool QGCFileWatcher::watchDirectory(const QString &directoryPath, ChangeCallback callback)
112{
113 if (directoryPath.isEmpty()) {
114 qCWarning(QGCFileWatcherLog) << "watchDirectory: empty path";
115 return false;
116 }
117
118 const QString canonicalPath = QFileInfo(directoryPath).absoluteFilePath();
119
120 if (!QFileInfo(canonicalPath).isDir()) {
121 qCWarning(QGCFileWatcherLog) << "watchDirectory: not a directory:" << directoryPath;
122 return false;
123 }
124
125 if (_watcher->addPath(canonicalPath)) {
126 if (callback) {
127 _directoryCallbacks[canonicalPath] = callback;
128 }
129 qCDebug(QGCFileWatcherLog) << "Watching directory:" << canonicalPath;
130 return true;
131 }
132
133 // Already watching
134 if (_watcher->directories().contains(canonicalPath)) {
135 if (callback) {
136 _directoryCallbacks[canonicalPath] = callback;
137 }
138 return true;
139 }
140
141 qCWarning(QGCFileWatcherLog) << "watchDirectory: failed to add:" << directoryPath;
142 return false;
143}
144
145bool QGCFileWatcher::unwatchDirectory(const QString &directoryPath)
146{
147 const QString canonicalPath = QFileInfo(directoryPath).absoluteFilePath();
148
149 _directoryCallbacks.remove(canonicalPath);
150 _pendingDirectoryChanges.remove(canonicalPath);
151
152 if (_watcher->removePath(canonicalPath)) {
153 qCDebug(QGCFileWatcherLog) << "Stopped watching directory:" << canonicalPath;
154 return true;
155 }
156
157 return false;
158}
159
160bool QGCFileWatcher::isWatchingDirectory(const QString &directoryPath) const
161{
162 const QString canonicalPath = QFileInfo(directoryPath).absoluteFilePath();
163 return _watcher->directories().contains(canonicalPath);
164}
165
167{
168 return _watcher->directories();
169}
170
171// ============================================================================
172// Bulk Operations
173// ============================================================================
174
175int QGCFileWatcher::watchFiles(const QStringList &filePaths, ChangeCallback callback)
176{
177 int count = 0;
178 for (const QString &path : filePaths) {
179 if (watchFile(path, callback)) {
180 count++;
181 }
182 }
183 return count;
184}
185
186int QGCFileWatcher::watchDirectories(const QStringList &directoryPaths, ChangeCallback callback)
187{
188 int count = 0;
189 for (const QString &path : directoryPaths) {
190 if (watchDirectory(path, callback)) {
191 count++;
192 }
193 }
194 return count;
195}
196
198{
199 const QStringList files = _watcher->files();
200 if (!files.isEmpty()) {
201 _watcher->removePaths(files);
202 }
203
204 const QStringList dirs = _watcher->directories();
205 if (!dirs.isEmpty()) {
206 _watcher->removePaths(dirs);
207 }
208
209 _fileCallbacks.clear();
210 _directoryCallbacks.clear();
211 _persistentFiles.clear();
212 _pendingFileChanges.clear();
213 _pendingDirectoryChanges.clear();
214 _debounceTimer->stop();
215
216 qCDebug(QGCFileWatcherLog) << "Cleared all watches";
217}
218
219// ============================================================================
220// Persistent File Watching
221// ============================================================================
222
223bool QGCFileWatcher::watchFilePersistent(const QString &filePath, ChangeCallback callback)
224{
225 const QString canonicalPath = QFileInfo(filePath).absoluteFilePath();
226
227 // Watch the parent directory to detect file recreation
228 const QString parentDir = QFileInfo(canonicalPath).absolutePath();
229 if (!_watcher->directories().contains(parentDir)) {
230 _watcher->addPath(parentDir);
231 }
232
233 _persistentFiles.insert(canonicalPath);
234
235 if (QFileInfo::exists(canonicalPath)) {
236 return watchFile(canonicalPath, callback);
237 }
238
239 // File doesn't exist yet - store callback for when it's created
240 if (callback) {
241 _fileCallbacks[canonicalPath] = callback;
242 }
243
244 qCDebug(QGCFileWatcherLog) << "Watching file (persistent):" << canonicalPath;
245 return true;
246}
247
248// ============================================================================
249// Internal Slots
250// ============================================================================
251
252void QGCFileWatcher::_onFileChanged(const QString &path)
253{
254 qCDebug(QGCFileWatcherLog) << "File changed:" << path;
255
256 // Handle persistent files that were deleted
257 if (_persistentFiles.contains(path) && !QFileInfo::exists(path)) {
258 // File was deleted - will be re-added when directory changes detect recreation
259 qCDebug(QGCFileWatcherLog) << "Persistent file deleted, waiting for recreation:" << path;
260 }
261
262 _scheduleCallback(path, false);
263}
264
265void QGCFileWatcher::_onDirectoryChanged(const QString &path)
266{
267 qCDebug(QGCFileWatcherLog) << "Directory changed:" << path;
268
269 // Check for recreated persistent files
270 for (const QString &persistentPath : std::as_const(_persistentFiles)) {
271 if (persistentPath.startsWith(path) && QFileInfo::exists(persistentPath)) {
272 if (!_watcher->files().contains(persistentPath)) {
273 _watcher->addPath(persistentPath);
274 qCDebug(QGCFileWatcherLog) << "Re-added persistent file:" << persistentPath;
275
276 // Trigger callback for recreated file
277 _scheduleCallback(persistentPath, false);
278 }
279 }
280 }
281
282 _scheduleCallback(path, true);
283}
284
285void QGCFileWatcher::_scheduleCallback(const QString &path, bool isDirectory)
286{
287 if (isDirectory) {
288 _pendingDirectoryChanges.insert(path);
289 } else {
290 _pendingFileChanges.insert(path);
291 }
292
293 if (_debounceDelay > 0) {
294 if (!_debounceTimer->isActive()) {
295 _debounceTimer->start(_debounceDelay);
296 }
297 } else if (!_processingPendingChanges) {
298 // No debounce - process immediately
299 _processPendingChanges();
300 }
301}
302
303void QGCFileWatcher::_processPendingChanges()
304{
305 if (_processingPendingChanges) {
306 return;
307 }
308
309 _processingPendingChanges = true;
310
311 do {
312 const QSet<QString> fileChanges = _pendingFileChanges;
313 const QSet<QString> directoryChanges = _pendingDirectoryChanges;
314 _pendingFileChanges.clear();
315 _pendingDirectoryChanges.clear();
316
317 for (const QString &path : fileChanges) {
318 emit fileChanged(path);
319
320 auto it = _fileCallbacks.find(path);
321 if (it != _fileCallbacks.end() && it.value()) {
322 it.value()(path);
323 }
324 }
325
326 for (const QString &path : directoryChanges) {
327 emit directoryChanged(path);
328
329 auto it = _directoryCallbacks.find(path);
330 if (it != _directoryCallbacks.end() && it.value()) {
331 it.value()(path);
332 }
333 }
334 } while ((_debounceDelay == 0) && (!_pendingFileChanges.isEmpty() || !_pendingDirectoryChanges.isEmpty()));
335
336 _processingPendingChanges = false;
337}
Wrapper around QFileSystemWatcher with callback-based API.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
bool unwatchDirectory(const QString &directoryPath)
bool isWatchingDirectory(const QString &directoryPath) const
void clear()
Stop watching all files and directories.
int watchFiles(const QStringList &filePaths, ChangeCallback callback)
int watchDirectories(const QStringList &directoryPaths, ChangeCallback callback)
~QGCFileWatcher() override
QStringList watchedFiles() const
void setDebounceDelay(int milliseconds)
bool isWatchingFile(const QString &filePath) const
bool watchFilePersistent(const QString &filePath, ChangeCallback callback)
void directoryChanged(const QString &path)
void fileChanged(const QString &path)
std::function< void(const QString &path)> ChangeCallback
QStringList watchedDirectories() const
bool unwatchFile(const QString &filePath)
bool watchFile(const QString &filePath, ChangeCallback callback)
bool watchDirectory(const QString &directoryPath, ChangeCallback callback)