Chris@538
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@538
|
2
|
Chris@538
|
3 /*
|
Chris@538
|
4 EasyMercurial
|
Chris@538
|
5
|
Chris@538
|
6 Based on hgExplorer by Jari Korhonen
|
Chris@538
|
7 Copyright (c) 2010 Jari Korhonen
|
Chris@538
|
8 Copyright (c) 2011 Chris Cannam
|
Chris@538
|
9 Copyright (c) 2011 Queen Mary, University of London
|
Chris@538
|
10
|
Chris@538
|
11 This program is free software; you can redistribute it and/or
|
Chris@538
|
12 modify it under the terms of the GNU General Public License as
|
Chris@538
|
13 published by the Free Software Foundation; either version 2 of the
|
Chris@538
|
14 License, or (at your option) any later version. See the file
|
Chris@538
|
15 COPYING included with this distribution for more information.
|
Chris@538
|
16 */
|
Chris@538
|
17
|
Chris@538
|
18 #include "fswatcher.h"
|
Chris@540
|
19 #include "debug.h"
|
Chris@538
|
20
|
Chris@538
|
21 #include <QMutexLocker>
|
Chris@538
|
22 #include <QDir>
|
Chris@538
|
23
|
Chris@538
|
24 #include <deque>
|
Chris@538
|
25
|
Chris@540
|
26 #define DEBUG_FSWATCHER 1
|
Chris@540
|
27
|
Chris@539
|
28 /*
|
Chris@539
|
29 * Watching the filesystem is trickier than it seems at first glance.
|
Chris@539
|
30 *
|
Chris@539
|
31 * We ideally should watch every directory, and every file that is
|
Chris@539
|
32 * tracked by Hg. If a new file is created in a directory, then we
|
Chris@539
|
33 * need to respond in order to show it as a potential candidate to be
|
Chris@539
|
34 * added.
|
Chris@539
|
35 *
|
Chris@539
|
36 * Complicating matters though is that Hg itself might modify the
|
Chris@539
|
37 * filesystem. This can happen even in "read-only" operations: for
|
Chris@539
|
38 * example, hg stat creates files called hg-checklink and hg-checkexec
|
Chris@539
|
39 * to test properties of the filesystem. So we need to know to ignore
|
Chris@539
|
40 * those files; unfortunately, when watching a directory (which is how
|
Chris@539
|
41 * we find out about the creation of new files) we are notified only
|
Chris@540
|
42 * that the directory has changed -- we aren't told what changed.
|
Chris@540
|
43 *
|
Chris@540
|
44 * This means that, when a directory changes, we need to rescan the
|
Chris@540
|
45 * directory to learn whether the set of files in it _excluding_ files
|
Chris@540
|
46 * matching our ignore patterns differs from the previous scan, and
|
Chris@540
|
47 * ignore the change if it doesn't.
|
Chris@539
|
48 */
|
Chris@539
|
49
|
Chris@538
|
50 FsWatcher::FsWatcher() :
|
Chris@538
|
51 m_lastToken(0),
|
Chris@538
|
52 m_lastCounter(0)
|
Chris@538
|
53 {
|
Chris@538
|
54 connect(&m_watcher, SIGNAL(directoryChanged(QString)),
|
Chris@538
|
55 this, SLOT(fsDirectoryChanged(QString)));
|
Chris@538
|
56 connect(&m_watcher, SIGNAL(fileChanged(QString)),
|
Chris@538
|
57 this, SLOT(fsFileChanged(QString)));
|
Chris@538
|
58 }
|
Chris@538
|
59
|
Chris@538
|
60 FsWatcher::~FsWatcher()
|
Chris@538
|
61 {
|
Chris@538
|
62 }
|
Chris@538
|
63
|
Chris@538
|
64 void
|
Chris@538
|
65 FsWatcher::setWorkDirPath(QString path)
|
Chris@538
|
66 {
|
Chris@538
|
67 QMutexLocker locker(&m_mutex);
|
Chris@538
|
68 m_watcher.removePaths(m_watcher.directories());
|
Chris@538
|
69 m_watcher.removePaths(m_watcher.files());
|
Chris@538
|
70 m_workDirPath = path;
|
Chris@538
|
71 addWorkDirectory(path);
|
Chris@540
|
72 debugPrint();
|
Chris@538
|
73 }
|
Chris@538
|
74
|
Chris@538
|
75 void
|
Chris@539
|
76 FsWatcher::setTrackedFilePaths(QStringList paths)
|
Chris@539
|
77 {
|
Chris@539
|
78 QMutexLocker locker(&m_mutex);
|
Chris@539
|
79 m_watcher.removePaths(m_watcher.files());
|
Chris@539
|
80 foreach (QString path, paths) {
|
Chris@539
|
81 m_watcher.addPath(path);
|
Chris@539
|
82 }
|
Chris@540
|
83 debugPrint();
|
Chris@539
|
84 }
|
Chris@539
|
85
|
Chris@539
|
86 void
|
Chris@538
|
87 FsWatcher::addWorkDirectory(QString path)
|
Chris@538
|
88 {
|
Chris@538
|
89 // QFileSystemWatcher will refuse to add a file or directory to
|
Chris@538
|
90 // its watch list that it is already watching -- fine -- but it
|
Chris@538
|
91 // prints a warning when this happens, which we wouldn't want. So
|
Chris@538
|
92 // we'll check for duplicates ourselves.
|
Chris@538
|
93 QSet<QString> alreadyWatched =
|
Chris@538
|
94 QSet<QString>::fromList(m_watcher.directories());
|
Chris@538
|
95
|
Chris@538
|
96 std::deque<QString> pending;
|
Chris@538
|
97 pending.push_back(path);
|
Chris@538
|
98
|
Chris@538
|
99 while (!pending.empty()) {
|
Chris@538
|
100
|
Chris@538
|
101 QString path = pending.front();
|
Chris@538
|
102 pending.pop_front();
|
Chris@538
|
103 if (!alreadyWatched.contains(path)) {
|
Chris@538
|
104 m_watcher.addPath(path);
|
Chris@540
|
105 m_dirContents[path] = scanDirectory(path);
|
Chris@538
|
106 }
|
Chris@538
|
107
|
Chris@538
|
108 QDir d(path);
|
Chris@538
|
109 if (d.exists()) {
|
Chris@538
|
110 d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
|
Chris@538
|
111 QDir::Readable | QDir::NoSymLinks);
|
Chris@538
|
112 foreach (QString entry, d.entryList()) {
|
Chris@538
|
113 if (entry.startsWith('.')) continue;
|
Chris@538
|
114 QString entryPath = d.absoluteFilePath(entry);
|
Chris@538
|
115 pending.push_back(entryPath);
|
Chris@538
|
116 }
|
Chris@538
|
117 }
|
Chris@538
|
118 }
|
Chris@538
|
119 }
|
Chris@538
|
120
|
Chris@538
|
121 void
|
Chris@538
|
122 FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
|
Chris@538
|
123 {
|
Chris@538
|
124 QMutexLocker locker(&m_mutex);
|
Chris@538
|
125 m_ignoredPrefixes = prefixes;
|
Chris@538
|
126 }
|
Chris@538
|
127
|
Chris@538
|
128 void
|
Chris@538
|
129 FsWatcher::setIgnoredFileSuffixes(QStringList suffixes)
|
Chris@538
|
130 {
|
Chris@538
|
131 QMutexLocker locker(&m_mutex);
|
Chris@538
|
132 m_ignoredSuffixes = suffixes;
|
Chris@538
|
133 }
|
Chris@538
|
134
|
Chris@538
|
135 int
|
Chris@538
|
136 FsWatcher::getNewToken()
|
Chris@538
|
137 {
|
Chris@538
|
138 QMutexLocker locker(&m_mutex);
|
Chris@538
|
139 int token = ++m_lastToken;
|
Chris@538
|
140 m_tokenMap[token] = m_lastCounter;
|
Chris@538
|
141 return token;
|
Chris@538
|
142 }
|
Chris@538
|
143
|
Chris@538
|
144 QSet<QString>
|
Chris@538
|
145 FsWatcher::getChangedPaths(int token)
|
Chris@538
|
146 {
|
Chris@538
|
147 QMutexLocker locker(&m_mutex);
|
Chris@538
|
148 size_t lastUpdatedAt = m_tokenMap[token];
|
Chris@538
|
149 QSet<QString> changed;
|
Chris@538
|
150 for (QHash<QString, size_t>::const_iterator i = m_changes.begin();
|
Chris@538
|
151 i != m_changes.end(); ++i) {
|
Chris@538
|
152 if (i.value() > lastUpdatedAt) {
|
Chris@538
|
153 changed.insert(i.key());
|
Chris@538
|
154 }
|
Chris@538
|
155 }
|
Chris@538
|
156 m_tokenMap[token] = m_lastCounter;
|
Chris@538
|
157 return changed;
|
Chris@538
|
158 }
|
Chris@538
|
159
|
Chris@538
|
160 void
|
Chris@538
|
161 FsWatcher::fsDirectoryChanged(QString path)
|
Chris@538
|
162 {
|
Chris@538
|
163 {
|
Chris@538
|
164 QMutexLocker locker(&m_mutex);
|
Chris@540
|
165
|
Chris@538
|
166 if (shouldIgnore(path)) return;
|
Chris@540
|
167
|
Chris@540
|
168 QSet<QString> files = scanDirectory(path);
|
Chris@540
|
169 if (files == m_dirContents[path]) {
|
Chris@540
|
170 #ifdef DEBUG_FSWATCHER
|
Chris@540
|
171 std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring" << std::endl;
|
Chris@540
|
172 #endif
|
Chris@540
|
173 return;
|
Chris@540
|
174 }
|
Chris@540
|
175 else m_dirContents[path] = files;
|
Chris@540
|
176
|
Chris@540
|
177 size_t counter = ++m_lastCounter;
|
Chris@540
|
178 m_changes[path] = counter;
|
Chris@538
|
179 }
|
Chris@540
|
180
|
Chris@538
|
181 emit changed();
|
Chris@538
|
182 }
|
Chris@538
|
183
|
Chris@538
|
184 void
|
Chris@538
|
185 FsWatcher::fsFileChanged(QString path)
|
Chris@538
|
186 {
|
Chris@540
|
187 {
|
Chris@540
|
188 QMutexLocker locker(&m_mutex);
|
Chris@540
|
189
|
Chris@540
|
190 // We don't check whether the file matches an ignore pattern,
|
Chris@540
|
191 // because we are only notified for file changes if we are
|
Chris@540
|
192 // watching the file explicitly, i.e. the file is in the
|
Chris@540
|
193 // tracked file paths list. So we never want to ignore them
|
Chris@540
|
194
|
Chris@540
|
195 size_t counter = ++m_lastCounter;
|
Chris@540
|
196 m_changes[path] = counter;
|
Chris@540
|
197 }
|
Chris@540
|
198
|
Chris@540
|
199 emit changed();
|
Chris@538
|
200 }
|
Chris@538
|
201
|
Chris@538
|
202 bool
|
Chris@538
|
203 FsWatcher::shouldIgnore(QString path)
|
Chris@538
|
204 {
|
Chris@540
|
205 QFileInfo fi(path);
|
Chris@540
|
206 QString fn(fi.fileName());
|
Chris@540
|
207 foreach (QString pfx, m_ignoredPrefixes) {
|
Chris@540
|
208 if (fn.startsWith(pfx)) return true;
|
Chris@540
|
209 }
|
Chris@540
|
210 foreach (QString sfx, m_ignoredSuffixes) {
|
Chris@540
|
211 if (fn.endsWith(sfx)) return true;
|
Chris@540
|
212 }
|
Chris@540
|
213 return false;
|
Chris@538
|
214 }
|
Chris@538
|
215
|
Chris@540
|
216 QSet<QString>
|
Chris@540
|
217 FsWatcher::scanDirectory(QString path)
|
Chris@540
|
218 {
|
Chris@540
|
219 QSet<QString> files;
|
Chris@540
|
220 QDir d(path);
|
Chris@540
|
221 if (d.exists()) {
|
Chris@540
|
222 d.setFilter(QDir::Files | QDir::NoDotAndDotDot |
|
Chris@540
|
223 QDir::Readable | QDir::NoSymLinks);
|
Chris@540
|
224 foreach (QString entry, d.entryList()) {
|
Chris@540
|
225 if (entry.startsWith('.')) continue;
|
Chris@540
|
226 if (shouldIgnore(entry)) continue;
|
Chris@540
|
227 files.insert(entry);
|
Chris@540
|
228 }
|
Chris@540
|
229 }
|
Chris@540
|
230 return files;
|
Chris@540
|
231 }
|
Chris@540
|
232
|
Chris@540
|
233 void
|
Chris@540
|
234 FsWatcher::debugPrint()
|
Chris@540
|
235 {
|
Chris@540
|
236 #ifdef DEBUG_FSWATCHER
|
Chris@540
|
237 std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
|
Chris@540
|
238 << " directories and " << m_watcher.files().size()
|
Chris@540
|
239 << " files" << std::endl;
|
Chris@540
|
240 #endif
|
Chris@540
|
241 }
|