PageRenderTime 41ms CodeModel.GetById 16ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

/guitone-1.0rc5/src/model/InventoryWatcher.cpp

#
C++ | 386 lines | 285 code | 50 blank | 51 comment | 76 complexity | 49dd9a1f7436dd05fb6c3220004b41a2 MD5 | raw file
Possible License(s): GPL-3.0
  1/***************************************************************************
  2 *   Copyright (C) 2008 by Thomas Keller                                   *
  3 *   me@thomaskeller.biz                                                   *
  4 *                                                                         *
  5 *   This program is free software; you can redistribute it and/or modify  *
  6 *   it under the terms of the GNU General Public License as published by  *
  7 *   the Free Software Foundation, either version 3 of the License, or     *
  8 *   (at your option) any later version.                                   *
  9 *                                                                         *
 10 *   This program is distributed in the hope that it will be useful,       *
 11 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 12 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 13 *   GNU General Public License for more details.                          *
 14 *                                                                         *
 15 *   You should have received a copy of the GNU General Public License     *
 16 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 17 ***************************************************************************/
 18
 19#include "InventoryWatcher.h"
 20#include "InventoryItem.h"
 21#include "BasicIOParser.h"
 22#include "GuitoneCore.h"
 23
 24InventoryWatcher::InventoryWatcher(QObject * parent)
 25  : QFileSystemWatcher(parent),
 26    // do not watch more than 200 paths at once
 27    lruCache(200)
 28{
 29    connect(
 30        this, SIGNAL(directoryChanged(const QString &)),
 31        this, SLOT(pathChanged(const QString &))
 32    );
 33
 34    connect(
 35        this, SIGNAL(fileChanged(const QString &)),
 36        this, SLOT(pathChanged(const QString &))
 37    );
 38
 39    notifyTimer = new QTimer(this);
 40
 41    connect(
 42        notifyTimer, SIGNAL(timeout()),
 43        this, SLOT(notifyChangedPaths())
 44    );
 45}
 46
 47InventoryWatcher::~InventoryWatcher()
 48{
 49    delete notifyTimer;
 50}
 51
 52//! TODO: monitor _MTN/options for changes as well
 53void InventoryWatcher::setMonotoneHandle(const MonotoneHandlePtr & handle)
 54{
 55    monotoneHandle = handle;
 56
 57    QString workspace = handle->getData();
 58    QString revPath(workspace + "/_MTN/revision");
 59    QFile file(revPath);
 60    if (!file.open(QIODevice::ReadOnly))
 61    {
 62        C(QString("cannot open %1/_MTN/revision for reading").arg(workspace));
 63    }
 64    QString contents(file.readAll());
 65    file.close();
 66
 67    BasicIOParser parser(contents);
 68    if (!parser.parse())
 69    {
 70        C("could not parse contents of _MTN/revision");
 71    }
 72    oldRevisionEntries = parser.getStanzas();
 73    addPath(revPath);
 74}
 75
 76void InventoryWatcher::watchPaths(const QStringList & paths)
 77{
 78    QStringList watchedPaths = files() + directories();
 79    QString workspace = monotoneHandle->getData();
 80    foreach (const QString & path, paths)
 81    {
 82        QString fullPath(workspace);
 83        if (path != "/") fullPath += "/" + path;
 84        if (watchedPaths.indexOf(fullPath) != -1) continue;
 85        D(QString("adding %1").arg(fullPath));
 86        addPath(fullPath);
 87
 88        // directories end with a slash and should keep a little longer
 89        // around, thats why we give them one additional heat point
 90        // more initially
 91        QString removedPath = lruCache.add(
 92            path, path.right(1) == "/" ? 2 : 1, 1
 93        );
 94
 95        if (!removedPath.isEmpty())
 96        {
 97            QString fullRemovedPath(workspace);
 98            if (removedPath != "/") fullRemovedPath += "/" + removedPath;
 99            D(QString("removing %1 because of LRU").arg(fullRemovedPath));
100            removePath(fullRemovedPath);
101        }
102    }
103}
104
105void InventoryWatcher::unwatchPaths(const QStringList & paths)
106{
107    QStringList watchedPaths = files() + directories();
108
109    foreach (const QString & path, paths)
110    {
111        QString fullPath(monotoneHandle->getData());
112        if (path != "/") fullPath += "/" + path;
113        if (watchedPaths.indexOf(fullPath) == -1) continue;
114        removePath(fullPath);
115        lruCache.remove(path);
116    }
117}
118
119void InventoryWatcher::pathChanged(const QString & path)
120{
121    QString workspace = monotoneHandle->getData();
122    I(path.indexOf(workspace) != -1);
123
124    if (path == QString(workspace + "/_MTN/revision"))
125    {
126        checkForBookkeepChanges();
127        // we need to re-add this path since
128        // mtn apparently re-creates the file
129        addPath(workspace + "/_MTN/revision");
130        return;
131    }
132
133    QString nodePath =
134        path.mid(path.indexOf(workspace) + workspace.length() + 1);
135
136    if (nodePath == ".mtn-ignore")
137    {
138        // automate stdio up until 0.46 needs to be restarted in order to
139        // pick up changes in .mtn-ignore
140        L(QString(".mtn-ignore changed, stopping all threads for %1")
141          .arg(workspace));
142        APP->manager()->stopThreads(monotoneHandle);
143
144        // if something inside .mtn-ignore has changed, the complete workspace
145        // could be affected, so do a full reload
146        markPathForNotification("");
147        return;
148    }
149
150    markPathForNotification(nodePath);
151}
152
153void InventoryWatcher::markPathForNotification(const QString & newPath)
154{
155    //
156    // The following algorithm only picks those paths for an update
157    // which are not a sub path of an already existing path
158    //
159    // current:     "a/b" "b/c"
160    // add "g":     "a/b" "b/c" "g"
161    // add "b":     "a/b" "b"
162    // add "a/b/c": "a/b" "b/c"
163    // add "":      ""
164    //
165    QStringList actualNotifiedPaths;
166    if (newPath.isEmpty())
167    {
168        actualNotifiedPaths.append("");
169    }
170    else
171    {
172        enum { skip, replace, add } action = add;
173
174        foreach (QString oldPath, changedPaths)
175        {
176            if (newPath.startsWith(oldPath) ||
177                newPath == oldPath || oldPath == "")
178            {
179                action = skip;
180                break;
181            }
182
183            if (oldPath.startsWith(newPath))
184            {
185                action = replace;
186            }
187        }
188
189        if (action == add)
190        {
191            actualNotifiedPaths = changedPaths;
192            actualNotifiedPaths.append(newPath);
193        }
194        else
195        if (action == replace)
196        {
197            foreach (QString oldPath, changedPaths)
198            {
199                if (!oldPath.startsWith(newPath))
200                {
201                    actualNotifiedPaths.append(oldPath);
202                }
203            }
204            actualNotifiedPaths.append(newPath);
205        }
206        else
207        if (action == skip)
208        {
209            actualNotifiedPaths = changedPaths;
210        }
211        else
212            I(false);
213    }
214
215    changedPaths = actualNotifiedPaths;
216
217    if (notifyTimer->isActive())
218    {
219        notifyTimer->stop();
220    }
221
222    // FIXME: make this configurable eventually
223    notifyTimer->start(500);
224}
225
226void InventoryWatcher::notifyChangedPaths()
227{
228    notifyTimer->stop();
229    D(QString("notifying changed paths %1").arg(changedPaths.join(",")));
230    foreach (QString path, changedPaths)
231    {
232        emit changedPath(path);
233    }
234    changedPaths.clear();
235}
236
237void InventoryWatcher::checkForBookkeepChanges()
238{
239    QString workspace = monotoneHandle->getData();
240    QFile file(workspace + "/_MTN/revision");
241    if (!file.open(QIODevice::ReadOnly))
242    {
243        C(QString("cannot open %1/_MTN/revision for reading").arg(workspace));
244        return;
245    }
246    QString contents(file.readAll());
247    file.close();
248
249    BasicIOParser parser(contents);
250    if (!parser.parse())
251    {
252        C("could not parse contents of _MTN/revision");
253        return;
254    }
255
256    StanzaList newRevisionEntries = parser.getStanzas();
257
258    foreach (const Stanza & newStanza, newRevisionEntries)
259    {
260        QString newEntryType = newStanza.at(0).sym;
261        // we don't care about patches, since we're notified of them
262        // separately; and the other two are uninteresting for us as well
263        if (newEntryType == "format_version" ||
264            newEntryType == "new_manifest" ||
265            newEntryType == "patch")
266        {
267            continue;
268        }
269
270        bool foundOrAlreadyHandled = false;
271        enum { unknown, file, dir } type = unknown;
272
273        // Unfortunately monotone's revision format doesn't record the node type
274        // so we can only derive type info from add_dir or add_file, but
275        // unfortunately not for deleted nodes or attribute changes.
276        // In this case this code only works as long as the file is still
277        // still present in the file system so we can manually query its type.
278
279        type = newEntryType == "add_dir" ? dir :
280                    newEntryType == "add_file" ? file : unknown;
281
282        for (int i=0; i<oldRevisionEntries.size(); )
283        {
284            Stanza oldStanza = oldRevisionEntries.at(i);
285            QString oldEntryType = oldStanza.at(0).sym;
286            if (oldEntryType == "format_version" ||
287                oldEntryType == "new_manifest" ||
288                oldEntryType == "patch")
289            {
290                oldRevisionEntries.removeAt(i);
291                foundOrAlreadyHandled = true;
292                continue;
293            }
294
295            if (newEntryType != oldEntryType)
296            {
297                i++;
298                continue;
299            }
300
301            if (newEntryType == "old_revision")
302            {
303                if (oldStanza.at(0).vals.at(0) != newStanza.at(0).vals.at(0))
304                {
305                    emit changedBaseRevision(newStanza.at(0).vals.at(0));
306                }
307                oldRevisionEntries.removeAt(i);
308                foundOrAlreadyHandled = true;
309                break;
310            }
311
312            // one stanza entries to compare
313            if (newEntryType == "delete" ||
314                newEntryType == "add_file" ||
315                newEntryType == "add_dir")
316            {
317                if (oldStanza.at(0).vals.at(0) == newStanza.at(0).vals.at(0))
318                {
319                    oldRevisionEntries.removeAt(i);
320                    foundOrAlreadyHandled = true;
321                    break;
322                }
323            }
324
325            // two stanza entries to compare
326            if (newEntryType == "rename" || newEntryType == "clear")
327            {
328                if (oldStanza.at(0).vals.at(0) == newStanza.at(0).vals.at(0) &&
329                    oldStanza.at(1).vals.at(0) == newStanza.at(1).vals.at(0))
330                {
331                    oldRevisionEntries.removeAt(i);
332                    foundOrAlreadyHandled = true;
333                    break;
334                }
335            }
336
337            // three stanza entries to compare
338            if (newEntryType == "set")
339            {
340                if (oldStanza.at(0).vals.at(0) == newStanza.at(0).vals.at(0) &&
341                    oldStanza.at(1).vals.at(0) == newStanza.at(1).vals.at(0) &&
342                    oldStanza.at(2).vals.at(0) == newStanza.at(2).vals.at(0))
343                {
344                    oldRevisionEntries.removeAt(i);
345                    foundOrAlreadyHandled = true;
346                    break;
347                }
348            }
349
350            i++;
351        }
352
353        if (foundOrAlreadyHandled) continue;
354
355        QString path = newStanza.at(0).vals.at(0);
356        if (type == unknown)
357        {
358            QFileInfo info(path);
359            type = info.isDir() ? dir : unknown;
360        }
361
362        if (type == dir)
363            path += "/";
364
365        markPathForNotification(path);
366    }
367
368    // now loop through all the left old items;
369    // we should only have a subset of items in this list now
370    foreach (const Stanza & oldStanza, oldRevisionEntries)
371    {
372        markPathForNotification(oldStanza.at(0).vals.at(0));
373    }
374
375    oldRevisionEntries = newRevisionEntries;
376}
377
378void InventoryWatcher::clearAllWatches()
379{
380    notifyTimer->stop();
381    changedPaths.clear();
382    removePaths(directories());
383    removePaths(files());
384    lruCache.clear();
385}
386