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