Merge lp:~jamesh/mediascanner2/bug-1489656 into lp:mediascanner2

Proposed by James Henstridge
Status: Merged
Approved by: Michi Henning
Approved revision: 339
Merged at revision: 331
Proposed branch: lp:~jamesh/mediascanner2/bug-1489656
Merge into: lp:mediascanner2
Diff against target: 835 lines (+552/-112)
11 files modified
CMakeLists.txt (+3/-2)
debian/changelog (+8/-0)
src/daemon/CMakeLists.txt (+1/-0)
src/daemon/InvalidationSender.cc (+4/-0)
src/daemon/InvalidationSender.hh (+4/-0)
src/daemon/SubtreeWatcher.hh (+2/-3)
src/daemon/VolumeManager.cc (+232/-0)
src/daemon/VolumeManager.hh (+49/-0)
src/daemon/scannerdaemon.cc (+8/-107)
test/CMakeLists.txt (+7/-0)
test/test_volumemanager.cc (+234/-0)
To merge this branch: bzr merge lp:~jamesh/mediascanner2/bug-1489656
Reviewer Review Type Date Requested Status
Michi Henning (community) Approve
unity-api-1-bot continuous-integration Needs Fixing
Review via email: mp+304350@code.launchpad.net

Commit message

When multiple volumes are mounted in quick succession, scan them serially to avoid reentrancy problems in the initial scan.

Description of the change

Queue up volume mount/unmount events so they are processed serially rather than all at once.

To post a comment you must log in.
Revision history for this message
unity-api-1-bot (unity-api-1-bot) wrote :

FAILED: Continuous integration, rev:339
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~jamesh/mediascanner2/bug-1489656/+merge/304350/+edit-commit-message

https://jenkins.canonical.com/unity-api-1/job/lp-mediascanner2-ci/5/
Executed test runs:
    FAILURE: https://jenkins.canonical.com/unity-api-1/job/build/527/console
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-0-fetch/533
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-1-sourcepkg/release=vivid+overlay/435
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-1-sourcepkg/release=xenial+overlay/435
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-1-sourcepkg/release=yakkety/435
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=vivid+overlay/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=vivid+overlay/365/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/365/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=yakkety/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=yakkety/365/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=vivid+overlay/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=vivid+overlay/365/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/365/artifact/output/*zip*/output.zip
    FAILURE: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=yakkety/365/console
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=vivid+overlay/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=vivid+overlay/365/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/365/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=yakkety/365
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=yakkety/365/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/unity-api-1/job/lp-mediascanner2-ci/5/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :

From reading the code, this looks nice to me. I'm not familiar with the code though. Approving seeing that there is no other suitable candidate.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2016-02-25 01:53:30 +0000
3+++ CMakeLists.txt 2016-08-31 03:52:20 +0000
4@@ -1,7 +1,7 @@
5+cmake_minimum_required(VERSION 3.0.2)
6 project(mediascanner2 CXX C)
7-cmake_minimum_required(VERSION 2.8.9)
8
9-set(MEDIASCANNER_VERSION "0.111")
10+set(MEDIASCANNER_VERSION "0.112")
11
12 execute_process(
13 COMMAND /bin/sh ${CMAKE_CURRENT_SOURCE_DIR}/get-soversion.sh
14@@ -93,6 +93,7 @@
15 test_sqliteutils
16 test_mfbuilder
17 test_subtreewatcher
18+ test_volumemanager
19 test_dbus
20 test_qml
21 test_util
22
23=== modified file 'debian/changelog'
24--- debian/changelog 2016-05-26 06:02:56 +0000
25+++ debian/changelog 2016-08-31 03:52:20 +0000
26@@ -1,3 +1,11 @@
27+mediascanner2 (0.112-0ubuntu1) UNRELEASED; urgency=medium
28+
29+ * When multiple volumes are mounted in quick succession, scan them
30+ serially to avoid reentrancy problems in the initial scan. (LP:
31+ #1489656)
32+
33+ -- James Henstridge <james.henstridge@canonical.com> Wed, 31 Aug 2016 11:26:18 +0800
34+
35 mediascanner2 (0.111+16.10.20160526-0ubuntu1) yakkety; urgency=medium
36
37 [ Alex Tu ]
38
39=== modified file 'src/daemon/CMakeLists.txt'
40--- src/daemon/CMakeLists.txt 2015-11-02 09:38:48 +0000
41+++ src/daemon/CMakeLists.txt 2016-08-31 03:52:20 +0000
42@@ -4,6 +4,7 @@
43 add_library(scannerstuff STATIC
44 InvalidationSender.cc
45 MountWatcher.cc
46+ VolumeManager.cc
47 SubtreeWatcher.cc
48 Scanner.cc
49 ../mediascanner/utils.cc
50
51=== modified file 'src/daemon/InvalidationSender.cc'
52--- src/daemon/InvalidationSender.cc 2016-03-16 07:13:27 +0000
53+++ src/daemon/InvalidationSender.cc 2016-08-31 03:52:20 +0000
54@@ -31,6 +31,8 @@
55 static const char SCOPES_DBUS_PATH[] = "/com/canonical/unity/scopes";
56 static const char SCOPES_INVALIDATE_RESULTS[] = "InvalidateResults";
57
58+namespace mediascanner {
59+
60 InvalidationSender::InvalidationSender() :
61 bus(nullptr, g_object_unref) {
62 }
63@@ -87,3 +89,5 @@
64 invalidator->timeout_id = 0;
65 return G_SOURCE_REMOVE;
66 }
67+
68+}
69
70=== modified file 'src/daemon/InvalidationSender.hh'
71--- src/daemon/InvalidationSender.hh 2016-03-16 07:13:27 +0000
72+++ src/daemon/InvalidationSender.hh 2016-08-31 03:52:20 +0000
73@@ -24,6 +24,8 @@
74
75 typedef struct _GDBusConnection GDBusConnection;
76
77+namespace mediascanner {
78+
79 /**
80 * A class that sends a broadcast signal that the state of media
81 * files has changed.
82@@ -48,4 +50,6 @@
83 int delay = 0;
84 };
85
86+}
87+
88 #endif
89
90=== modified file 'src/daemon/SubtreeWatcher.hh'
91--- src/daemon/SubtreeWatcher.hh 2016-02-09 03:36:14 +0000
92+++ src/daemon/SubtreeWatcher.hh 2016-08-31 03:52:20 +0000
93@@ -22,10 +22,9 @@
94
95 #include<string>
96
97+namespace mediascanner {
98+
99 class InvalidationSender;
100-
101-namespace mediascanner {
102-
103 class MediaStore;
104 class MetadataExtractor;
105
106
107=== added file 'src/daemon/VolumeManager.cc'
108--- src/daemon/VolumeManager.cc 1970-01-01 00:00:00 +0000
109+++ src/daemon/VolumeManager.cc 2016-08-31 03:52:20 +0000
110@@ -0,0 +1,232 @@
111+/*
112+ * Copyright (C) 2016 Canonical, Ltd.
113+ *
114+ * Authors:
115+ * James Henstridge <james.henstridge@canonical.com>
116+ *
117+ * This library is free software; you can redistribute it and/or modify it under
118+ * the terms of version 3 of the GNU General Public License as published
119+ * by the Free Software Foundation.
120+ *
121+ * This library is distributed in the hope that it will be useful, but WITHOUT
122+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
123+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
124+ * details.
125+ *
126+ * You should have received a copy of the GNU General Public License
127+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
128+ */
129+
130+#include "VolumeManager.hh"
131+
132+#include <mediascanner/MediaFile.hh>
133+#include <mediascanner/MediaStore.hh>
134+#include <extractor/DetectedFile.hh>
135+#include <extractor/MetadataExtractor.hh>
136+#include "InvalidationSender.hh"
137+#include "Scanner.hh"
138+#include "SubtreeWatcher.hh"
139+#include "../mediascanner/internal/utils.hh"
140+
141+#include <glib.h>
142+
143+#include <cassert>
144+#include <cstdio>
145+#include <map>
146+#include <deque>
147+
148+using namespace std;
149+
150+namespace {
151+
152+enum class VolumeEventType {
153+ added,
154+ removed,
155+};
156+
157+struct VolumeEvent {
158+ VolumeEventType type;
159+ string path;
160+
161+ VolumeEvent(VolumeEventType type, const string& path)
162+ : type(type), path(path) {}
163+};
164+
165+}
166+
167+namespace mediascanner {
168+
169+struct VolumeManagerPrivate {
170+ MediaStore& store;
171+ MetadataExtractor& extractor;
172+ InvalidationSender& invalidator;
173+
174+ map<string, unique_ptr<SubtreeWatcher>> volumes;
175+ deque<VolumeEvent> pending;
176+ unsigned int idle_id = 0;
177+
178+ VolumeManagerPrivate(MediaStore& store, MetadataExtractor& extractor,
179+ InvalidationSender& invalidator);
180+ ~VolumeManagerPrivate();
181+
182+ void queueUpdate(VolumeEventType type, const string& path);
183+ static gboolean processEvent(void *user_data) noexcept;
184+
185+ void addVolume(const string& path);
186+ void removeVolume(const string& path);
187+ void readFiles(const string& subdir, const MediaType type);
188+};
189+
190+VolumeManager::VolumeManager(MediaStore& store, MetadataExtractor& extractor,
191+ InvalidationSender& invalidator)
192+ : p(new VolumeManagerPrivate(store, extractor, invalidator)) {
193+}
194+
195+VolumeManager::~VolumeManager() = default;
196+
197+void VolumeManager::queueAddVolume(const string& path) {
198+ p->queueUpdate(VolumeEventType::added, path);
199+}
200+
201+void VolumeManager::queueRemoveVolume(const string& path) {
202+ p->queueUpdate(VolumeEventType::removed, path);
203+}
204+
205+bool VolumeManager::idle() const {
206+ // idle_id will only be reset once the scanning job has completed.
207+ return p->idle_id == 0;
208+}
209+
210+VolumeManagerPrivate::VolumeManagerPrivate(MediaStore& store,
211+ MetadataExtractor& extractor,
212+ InvalidationSender& invalidator)
213+ : store(store), extractor(extractor), invalidator(invalidator) {
214+}
215+
216+VolumeManagerPrivate::~VolumeManagerPrivate()
217+{
218+ if (idle_id != 0) {
219+ g_source_remove(idle_id);
220+ }
221+}
222+
223+void VolumeManagerPrivate::queueUpdate(VolumeEventType type,
224+ const string& path) {
225+ for (auto it = pending.begin(); it != pending.end();) {
226+ if (it->path == path) {
227+ it = pending.erase(it);
228+ } else {
229+ ++it;
230+ }
231+ }
232+ pending.emplace_back(type, path);
233+ if (idle_id == 0) {
234+ idle_id = g_idle_add(&VolumeManagerPrivate::processEvent, this);
235+ }
236+}
237+
238+gboolean VolumeManagerPrivate::processEvent(void *user_data) noexcept {
239+ auto *p = reinterpret_cast<VolumeManagerPrivate*>(user_data);
240+
241+ while (!p->pending.empty()) {
242+ auto event = move(p->pending.front());
243+ p->pending.pop_front();
244+
245+ switch (event.type) {
246+ case VolumeEventType::added:
247+ p->addVolume(event.path);
248+ break;
249+ case VolumeEventType::removed:
250+ p->removeVolume(event.path);
251+ break;
252+ }
253+ }
254+ p->invalidator.invalidate();
255+ p->idle_id = 0;
256+ return G_SOURCE_REMOVE;
257+}
258+
259+void VolumeManagerPrivate::addVolume(const string& path) {
260+ assert(path[0] == '/');
261+ if(volumes.find(path) != volumes.end()) {
262+ return;
263+ }
264+ if(is_rootlike(path)) {
265+ fprintf(stderr, "Directory %s looks like a top level root directory, skipping it (%s).\n",
266+ path.c_str(), __PRETTY_FUNCTION__);
267+ return;
268+ }
269+ if(is_optical_disc(path)) {
270+ fprintf(stderr, "Directory %s looks like an optical disc, skipping it.\n", path.c_str());
271+ return;
272+ }
273+ if(has_scanblock(path)) {
274+ fprintf(stderr, "Directory %s has a scan block file, skipping it.\n", path.c_str());
275+ return;
276+ }
277+ unique_ptr<SubtreeWatcher> sw(new SubtreeWatcher(store, extractor, invalidator));
278+ store.restoreItems(path);
279+ store.pruneDeleted();
280+ readFiles(path, AllMedia);
281+ sw->addDir(path);
282+ volumes[path] = move(sw);
283+}
284+
285+void VolumeManagerPrivate::removeVolume(const string& path) {
286+ assert(path[0] == '/');
287+ if(volumes.find(path) == volumes.end())
288+ return;
289+ store.archiveItems(path);
290+ volumes.erase(path);
291+}
292+
293+void VolumeManagerPrivate::readFiles(const string &subdir, const MediaType type) {
294+ Scanner s(&extractor, subdir, type);
295+ MediaStoreTransaction txn = store.beginTransaction();
296+ const int update_interval = 10; // How often to send invalidations.
297+ struct timespec previous_update, current_time;
298+ clock_gettime(CLOCK_MONOTONIC, &previous_update);
299+ previous_update.tv_sec -= update_interval/2; // Send the first update sooner for better visual appeal.
300+ while(true) {
301+ try {
302+ auto d = s.next();
303+ clock_gettime(CLOCK_MONOTONIC, &current_time);
304+ while(g_main_context_pending(g_main_context_default())) {
305+ g_main_context_iteration(g_main_context_default(), FALSE);
306+ }
307+ if(current_time.tv_sec - previous_update.tv_sec >= update_interval) {
308+ txn.commit();
309+ invalidator.invalidate();
310+ previous_update = current_time;
311+ }
312+ // If the file is broken or unchanged, use fallback.
313+ if (store.is_broken_file(d.filename, d.etag)) {
314+ fprintf(stderr, "Using fallback data for unscannable file %s.\n", d.filename.c_str());
315+ store.insert(extractor.fallback_extract(d));
316+ continue;
317+ }
318+ if(d.etag == store.getETag(d.filename))
319+ continue;
320+
321+ try {
322+ store.insert_broken_file(d.filename, d.etag);
323+ MediaFile media;
324+ try {
325+ media = extractor.extract(d);
326+ } catch (const runtime_error &e) {
327+ fprintf(stderr, "Error extracting from '%s': %s\n",
328+ d.filename.c_str(), e.what());
329+ media = extractor.fallback_extract(d);
330+ }
331+ store.insert(std::move(media));
332+ } catch(const exception &e) {
333+ fprintf(stderr, "Error when indexing: %s\n", e.what());
334+ }
335+ } catch(const StopIteration &stop) {
336+ break;
337+ }
338+ }
339+ txn.commit();
340+}
341+
342+}
343
344=== added file 'src/daemon/VolumeManager.hh'
345--- src/daemon/VolumeManager.hh 1970-01-01 00:00:00 +0000
346+++ src/daemon/VolumeManager.hh 2016-08-31 03:52:20 +0000
347@@ -0,0 +1,49 @@
348+/*
349+ * Copyright (C) 2016 Canonical, Ltd.
350+ *
351+ * Authors:
352+ * James Henstridge <james.henstridge@canonical.com>
353+ *
354+ * This library is free software; you can redistribute it and/or modify it under
355+ * the terms of version 3 of the GNU General Public License as published
356+ * by the Free Software Foundation.
357+ *
358+ * This library is distributed in the hope that it will be useful, but WITHOUT
359+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
360+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
361+ * details.
362+ *
363+ * You should have received a copy of the GNU General Public License
364+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
365+ */
366+
367+#pragma once
368+
369+#include <memory>
370+#include <string>
371+
372+namespace mediascanner {
373+
374+class MediaStore;
375+class MetadataExtractor;
376+class InvalidationSender;
377+
378+struct VolumeManagerPrivate;
379+
380+class VolumeManager final {
381+public:
382+ VolumeManager(MediaStore& store, MetadataExtractor& extractor, InvalidationSender& invalidator);
383+ ~VolumeManager();
384+ VolumeManager(const VolumeManager&) = delete;
385+ VolumeManager& operator=(const VolumeManager&) = delete;
386+
387+ void queueAddVolume(const std::string& path);
388+ void queueRemoveVolume(const std::string& path);
389+
390+ bool idle() const;
391+
392+private:
393+ std::unique_ptr<VolumeManagerPrivate> p;
394+};
395+
396+}
397
398=== modified file 'src/daemon/scannerdaemon.cc'
399--- src/daemon/scannerdaemon.cc 2016-03-17 02:11:17 +0000
400+++ src/daemon/scannerdaemon.cc 2016-08-31 03:52:20 +0000
401@@ -32,13 +32,10 @@
402
403 #include "../mediascanner/MediaFile.hh"
404 #include "../mediascanner/MediaStore.hh"
405-#include "../extractor/DetectedFile.hh"
406 #include "../extractor/MetadataExtractor.hh"
407 #include "MountWatcher.hh"
408-#include "SubtreeWatcher.hh"
409-#include "Scanner.hh"
410 #include "InvalidationSender.hh"
411-#include "../mediascanner/internal/utils.hh"
412+#include "VolumeManager.hh"
413
414 using namespace std;
415
416@@ -74,9 +71,6 @@
417 void setupBus();
418 void setupSignals();
419 void setupMountWatcher();
420- void readFiles(MediaStore &store, const string &subdir, const MediaType type);
421- void addDir(const string &dir);
422- void removeDir(const string &dir);
423 static gboolean signalCallback(gpointer data);
424 static void busNameLostCallback(GDBusConnection *connection, const char *name, gpointer data);
425 void mountEvent(const MountWatcher::Info &info);
426@@ -86,8 +80,8 @@
427 string cachedir;
428 unique_ptr<MediaStore> store;
429 unique_ptr<MetadataExtractor> extractor;
430- map<string, unique_ptr<SubtreeWatcher>> subtrees;
431 InvalidationSender invalidator;
432+ unique_ptr<VolumeManager> volumes;
433 unique_ptr<GMainLoop,void(*)(GMainLoop*)> main_loop;
434 unique_ptr<GDBusConnection,void(*)(void*)> session_bus;
435 unsigned int bus_name_id = 0;
436@@ -99,6 +93,7 @@
437 setupBus();
438 store.reset(new MediaStore(MS_READ_WRITE, "/media/"));
439 extractor.reset(new MetadataExtractor(session_bus.get()));
440+ volumes.reset(new VolumeManager(*store, *extractor, invalidator));
441
442 setupMountWatcher();
443
444@@ -111,13 +106,13 @@
445 // it falls back to home directory. This would mean scanning the entire home
446 // directory. This is probably not what people want so skip if this is the case.
447 if (musicdir && !is_same_directory(musicdir, homedir))
448- addDir(musicdir);
449+ volumes->queueAddVolume(musicdir);
450
451 if (videodir && !is_same_directory(videodir, homedir))
452- addDir(videodir);
453+ volumes->queueAddVolume(videodir);
454
455 if (picturesdir && !is_same_directory(picturesdir, homedir))
456- addDir(picturesdir);
457+ volumes->queueAddVolume(picturesdir);
458
459 // In case someone opened the db file before we could populate it.
460 invalidator.invalidate();
461@@ -178,89 +173,6 @@
462 sigterm_id = g_unix_signal_add(SIGTERM, &ScannerDaemon::signalCallback, this);
463 }
464
465-void ScannerDaemon::addDir(const string &dir) {
466- assert(dir[0] == '/');
467- if(subtrees.find(dir) != subtrees.end()) {
468- return;
469- }
470- if(is_rootlike(dir)) {
471- fprintf(stderr, "Directory %s looks like a top level root directory, skipping it (%s).\n",
472- dir.c_str(), __PRETTY_FUNCTION__);
473- return;
474- }
475- if(is_optical_disc(dir)) {
476- fprintf(stderr, "Directory %s looks like an optical disc, skipping it.\n", dir.c_str());
477- return;
478- }
479- if(has_scanblock(dir)) {
480- fprintf(stderr, "Directory %s has a scan block file, skipping it.\n", dir.c_str());
481- return;
482- }
483- unique_ptr<SubtreeWatcher> sw(new SubtreeWatcher(*store.get(), *extractor.get(), invalidator));
484- store->restoreItems(dir);
485- store->pruneDeleted();
486- readFiles(*store.get(), dir, AllMedia);
487- sw->addDir(dir);
488- subtrees[dir] = move(sw);
489-}
490-
491-void ScannerDaemon::removeDir(const string &dir) {
492- assert(dir[0] == '/');
493- if(subtrees.find(dir) == subtrees.end())
494- return;
495- store->archiveItems(dir);
496- subtrees.erase(dir);
497-}
498-
499-void ScannerDaemon::readFiles(MediaStore &store, const string &subdir, const MediaType type) {
500- Scanner s(extractor.get(), subdir, type);
501- MediaStoreTransaction txn = store.beginTransaction();
502- const int update_interval = 10; // How often to send invalidations.
503- struct timespec previous_update, current_time;
504- clock_gettime(CLOCK_MONOTONIC, &previous_update);
505- previous_update.tv_sec -= update_interval/2; // Send the first update sooner for better visual appeal.
506- while(true) {
507- try {
508- auto d = s.next();
509- clock_gettime(CLOCK_MONOTONIC, &current_time);
510- while(g_main_context_pending(g_main_context_default())) {
511- g_main_context_iteration(g_main_context_default(), FALSE);
512- }
513- if(current_time.tv_sec - previous_update.tv_sec >= update_interval) {
514- txn.commit();
515- invalidator.invalidate();
516- previous_update = current_time;
517- }
518- // If the file is broken or unchanged, use fallback.
519- if (store.is_broken_file(d.filename, d.etag)) {
520- fprintf(stderr, "Using fallback data for unscannable file %s.\n", d.filename.c_str());
521- store.insert(extractor->fallback_extract(d));
522- continue;
523- }
524- if(d.etag == store.getETag(d.filename))
525- continue;
526-
527- try {
528- store.insert_broken_file(d.filename, d.etag);
529- MediaFile media;
530- try {
531- media = extractor->extract(d);
532- } catch (const runtime_error &e) {
533- fprintf(stderr, "Error extracting from '%s': %s\n",
534- d.filename.c_str(), e.what());
535- media = extractor->fallback_extract(d);
536- }
537- store.insert(std::move(media));
538- } catch(const exception &e) {
539- fprintf(stderr, "Error when indexing: %s\n", e.what());
540- }
541- } catch(const StopIteration &stop) {
542- break;
543- }
544- }
545- txn.commit();
546-}
547-
548 int ScannerDaemon::run() {
549 g_main_loop_run(main_loop.get());
550 return 99;
551@@ -279,25 +191,14 @@
552 }
553
554 void ScannerDaemon::mountEvent(const MountWatcher::Info& info) {
555- bool changed = false;
556 if (info.is_mounted) {
557 printf("Volume %s was mounted.\n", info.mount_point.c_str());
558 if (info.mount_point.substr(0, 6) == "/media") {
559- addDir(info.mount_point);
560- changed = true;
561+ volumes->queueAddVolume(info.mount_point);
562 }
563 } else {
564 printf("Volume %s was unmounted.\n", info.mount_point.c_str());
565- if (subtrees.find(info.mount_point) != subtrees.end()) {
566- removeDir(info.mount_point);
567- changed = true;
568- } else {
569- // This volume was not tracked because it looked rootlike.
570- // Thus we don't need to do anything.
571- }
572- }
573- if (changed) {
574- invalidator.invalidate();
575+ volumes->queueRemoveVolume(info.mount_point);
576 }
577 }
578
579
580=== modified file 'test/CMakeLists.txt'
581--- test/CMakeLists.txt 2016-02-25 01:53:30 +0000
582+++ test/CMakeLists.txt 2016-08-31 03:52:20 +0000
583@@ -60,6 +60,13 @@
584 set_tests_properties(test_subtreewatcher PROPERTIES
585 ENVIRONMENT "GIO_MODULE_DIR=${CMAKE_CURRENT_BINARY_DIR}/modules")
586
587+add_executable(test_volumemanager test_volumemanager.cc)
588+target_link_libraries(test_volumemanager mediascanner scannerstuff gtest)
589+add_test(test_volumemanager test_volumemanager)
590+# The gvfs modules interfere with the private D-Bus test fixtures
591+set_tests_properties(test_volumemanager PROPERTIES
592+ ENVIRONMENT "GIO_MODULE_DIR=${CMAKE_CURRENT_BINARY_DIR}/modules")
593+
594 add_executable(test_sqliteutils test_sqliteutils.cc)
595 target_link_libraries(test_sqliteutils gtest ${MEDIASCANNER_DEPS_LDFLAGS})
596 add_test(test_sqliteutils test_sqliteutils)
597
598=== added file 'test/test_volumemanager.cc'
599--- test/test_volumemanager.cc 1970-01-01 00:00:00 +0000
600+++ test/test_volumemanager.cc 2016-08-31 03:52:20 +0000
601@@ -0,0 +1,234 @@
602+/*
603+ * Copyright (C) 2016 Canonical, Ltd.
604+ *
605+ * Authors:
606+ * James Henstridge <james.henstridge@canonical.com>
607+ *
608+ * This library is free software; you can redistribute it and/or modify it under
609+ * the terms of version 3 of the GNU General Public License as published
610+ * by the Free Software Foundation.
611+ *
612+ * This library is distributed in the hope that it will be useful, but WITHOUT
613+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
614+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
615+ * details.
616+ *
617+ * You should have received a copy of the GNU General Public License
618+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
619+ */
620+
621+#include <mediascanner/MediaFile.hh>
622+#include <mediascanner/MediaStore.hh>
623+#include <extractor/MetadataExtractor.hh>
624+#include <daemon/InvalidationSender.hh>
625+#include <daemon/VolumeManager.hh>
626+
627+#include "test_config.h"
628+
629+#include <fcntl.h>
630+#include <algorithm>
631+#include <cstdio>
632+#include <memory>
633+#include <stdexcept>
634+#include <string>
635+
636+#include <gio/gio.h>
637+#include <gtest/gtest.h>
638+
639+using namespace std;
640+using namespace mediascanner;
641+
642+namespace {
643+
644+typedef std::unique_ptr<GDBusConnection, decltype(&g_object_unref)> GDBusConnectionPtr;
645+
646+void copy_file(const string &src, const string &dst) {
647+ FILE* f = fopen(src.c_str(), "r");
648+ ASSERT_TRUE(f);
649+ fseek(f, 0, SEEK_END);
650+ size_t size = ftell(f);
651+
652+ char* buf = new char[size];
653+
654+ fseek(f, 0, SEEK_SET);
655+ ASSERT_EQ(fread(buf, 1, size, f), size);
656+ fclose(f);
657+
658+ f = fopen(dst.c_str(), "w");
659+ ASSERT_TRUE(f);
660+ ASSERT_EQ(fwrite(buf, 1, size, f), size);
661+ fclose(f);
662+ delete[] buf;
663+}
664+
665+}
666+
667+class VolumeManagerTest : public ::testing::Test {
668+protected:
669+ virtual void SetUp() override {
670+ main_loop_.reset(g_main_loop_new(nullptr, false));
671+
672+ tmpdir_ = TEST_DIR "/volumemanager-test.XXXXXX";
673+ ASSERT_NE(nullptr, mkdtemp(&tmpdir_[0]));
674+
675+ test_dbus_.reset(g_test_dbus_new(G_TEST_DBUS_NONE));
676+ g_test_dbus_add_service_dir(test_dbus_.get(), TEST_DIR "/services");
677+ g_test_dbus_up(test_dbus_.get());
678+ session_bus_ = make_connection();
679+
680+ store_.reset(new MediaStore(":memory:", MS_READ_WRITE));
681+ extractor_.reset(new MetadataExtractor(session_bus_.get()));
682+ invalidator_.reset(new InvalidationSender);
683+ volumes_.reset(new VolumeManager(*store_, *extractor_, *invalidator_));
684+ }
685+
686+ virtual void TearDown() override {
687+ volumes_.reset();
688+ invalidator_.reset();
689+ extractor_.reset();
690+ store_.reset();
691+
692+ session_bus_.reset();
693+ test_dbus_.reset();
694+
695+ if (!tmpdir_.empty()) {
696+ string cmd = "rm -rf " + tmpdir_;
697+ ASSERT_EQ(0, system(cmd.c_str()));
698+ }
699+ }
700+
701+ GDBusConnectionPtr make_connection() {
702+ GError *error = nullptr;
703+ char *address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error);
704+ if (!address) {
705+ string errortxt(error->message);
706+ g_error_free(error);
707+ throw std::runtime_error(
708+ string("Failed to determine session bus address: ") + errortxt);
709+ }
710+ GDBusConnectionPtr bus(
711+ g_dbus_connection_new_for_address_sync(
712+ address, static_cast<GDBusConnectionFlags>(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr, &error),
713+ g_object_unref);
714+ g_free(address);
715+ if (!bus) {
716+ string errortxt(error->message);
717+ g_error_free(error);
718+ throw std::runtime_error(
719+ string("Failed to connect to session bus: ") + errortxt);
720+ }
721+ return std::move(bus);
722+ }
723+
724+ void wait_until_idle() {
725+ while (g_main_context_iteration(nullptr, FALSE)) {
726+ if (volumes_->idle()) {
727+ break;
728+ }
729+ }
730+ }
731+
732+ string tmpdir_;
733+ unique_ptr<GTestDBus,decltype(&g_object_unref)> test_dbus_ {nullptr, g_object_unref};
734+ unique_ptr<MediaStore> store_;
735+ unique_ptr<VolumeManager> volumes_;
736+
737+private:
738+ GDBusConnectionPtr session_bus_ {nullptr, g_object_unref};
739+ unique_ptr<MetadataExtractor> extractor_;
740+ unique_ptr<InvalidationSender> invalidator_;
741+
742+ unique_ptr<GMainLoop, decltype(&g_main_loop_unref)> main_loop_ {nullptr, g_main_loop_unref};
743+};
744+
745+TEST_F(VolumeManagerTest, add_remove_volumes)
746+{
747+ const string volume1 = tmpdir_ + "/volume1";
748+ const string volume2 = tmpdir_ + "/volume2";
749+ ASSERT_EQ(0, mkdir(volume1.c_str(), 0755));
750+ ASSERT_EQ(0, mkdir(volume2.c_str(), 0755));
751+
752+ const string file1 = volume1 + "/file1.ogg";
753+ const string file2 = volume1 + "/file2.ogg";
754+ const string file3 = volume2 + "/file3.ogg";
755+
756+ copy_file(SOURCE_DIR "/media/testfile.ogg", file1);
757+ copy_file(SOURCE_DIR "/media/testfile.ogg", file2);
758+ copy_file(SOURCE_DIR "/media/testfile.ogg", file3);
759+
760+ volumes_->queueAddVolume(volume1);
761+ wait_until_idle();
762+ EXPECT_EQ(store_->size(), 2);
763+ store_->lookup(file1);
764+ store_->lookup(file2);
765+ try {
766+ store_->lookup(file3);
767+ FAIL();
768+ } catch (const runtime_error&) {
769+ }
770+
771+ // Queue up two operations
772+ volumes_->queueAddVolume(volume2);
773+ volumes_->queueRemoveVolume(volume1);
774+ wait_until_idle();
775+ EXPECT_EQ(store_->size(), 1);
776+ store_->lookup(file3);
777+ try {
778+ store_->lookup(file1);
779+ store_->lookup(file2);
780+ } catch (const runtime_error&) {
781+ }
782+
783+ volumes_->queueRemoveVolume(volume2);
784+ wait_until_idle();
785+ EXPECT_EQ(store_->size(), 0);
786+
787+ // This should result in only volume2 being added
788+ volumes_->queueAddVolume(volume1);
789+ volumes_->queueAddVolume(volume2);
790+ volumes_->queueRemoveVolume(volume1);
791+ wait_until_idle();
792+ EXPECT_EQ(store_->size(), 1);
793+ store_->lookup(file3);
794+}
795+
796+TEST_F(VolumeManagerTest, add_volume_during_initial_scan)
797+{
798+ const string volume1 = tmpdir_ + "/volume1";
799+ const string volume2 = tmpdir_ + "/volume2";
800+ ASSERT_EQ(0, mkdir(volume1.c_str(), 0755));
801+ ASSERT_EQ(0, mkdir(volume2.c_str(), 0755));
802+
803+ const string file1 = volume1 + "/file1.ogg";
804+ const string file2 = volume1 + "/file2.ogg";
805+ const string file3 = volume2 + "/file3.ogg";
806+
807+ copy_file(SOURCE_DIR "/media/testfile.ogg", file1);
808+ copy_file(SOURCE_DIR "/media/testfile.ogg", file2);
809+ copy_file(SOURCE_DIR "/media/testfile.ogg", file3);
810+
811+ // Set an idle function that should be called during the initial
812+ // scan that will queue up adding a second volume.
813+ function<void()> callback = [&] {
814+ // We expect that this is called during the scan
815+ EXPECT_FALSE(volumes_->idle());
816+ volumes_->queueAddVolume(volume2);
817+ };
818+ g_idle_add([](void *user_data) -> gboolean {
819+ auto callback = *reinterpret_cast<function<void()>*>(user_data);
820+ callback();
821+ return G_SOURCE_REMOVE;
822+ }, &callback);
823+
824+ volumes_->queueAddVolume(volume1);
825+ wait_until_idle();
826+ EXPECT_EQ(store_->size(), 3);
827+ store_->lookup(file1);
828+ store_->lookup(file2);
829+ store_->lookup(file3);
830+}
831+
832+int main(int argc, char **argv) {
833+ ::testing::InitGoogleTest(&argc, argv);
834+ return RUN_ALL_TESTS();
835+}

Subscribers

People subscribed via source and target branches