This file is indexed.

/usr/lib/plainbox-providers-1/checkbox/bin/removable_storage_watcher is in plainbox-provider-checkbox 0.3-2.

This file is owned by root:root, with mode 0o755.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
#!/usr/bin/env python3

import argparse
import collections
import copy
import dbus
import logging
import sys

from gi.repository import GObject, GUdev

from checkbox_support.dbus import connect_to_system_bus
from checkbox_support.dbus.udisks2 import UDisks2Model, UDisks2Observer
from checkbox_support.dbus.udisks2 import is_udisks2_supported
from checkbox_support.dbus.udisks2 import lookup_udev_device
from checkbox_support.dbus.udisks2 import map_udisks1_connection_bus
from checkbox_support.heuristics.udisks2 import is_memory_card
from checkbox_support.parsers.udevadm import CARD_READER_RE, GENERIC_RE, FLASH_RE
from checkbox_support.udev import get_interconnect_speed, get_udev_block_devices

# Record representing properties of a UDisks1 Drive object needed by the
# UDisks1 version of the watcher implementation
UDisks1DriveProperties = collections.namedtuple(
    'UDisks1DriveProperties', 'file bus speed model vendor media')

# Delta record that encapsulates difference:
# delta_dir -- directon of the difference, either DELTA_DIR_PLUS or
#              DELTA_DIR_MINUS
# value -- the actual value being removed or added, either InterfaceDelta or
# PropertyDelta instance, see below
DeltaRecord = collections.namedtuple("DeltaRecord", "delta_dir value")

# Delta value for representing interface changes
InterfaceDelta = collections.namedtuple(
    "InterfaceDelta",
    "delta_type object_path iface_name")

# Delta value for representing property changes
PropertyDelta = collections.namedtuple(
    "PropertyDelta",
    "delta_type object_path iface_name prop_name prop_value")

# Tokens that encode additions and removals
DELTA_DIR_PLUS = '+'
DELTA_DIR_MINUS = '-'

# Tokens that encode interface and property deltas
DELTA_TYPE_IFACE = 'i'
DELTA_TYPE_PROP = 'p'


def format_bytes(size):
    """
    Format size to be easily read by humans

    The result is disk-size compatible (using multiples of 10
    rather than 2) string like "4.5GB"
    """
    for index, prefix in enumerate(" KMGTPEZY", 0):
        factor = 10 ** (index * 3)
        if size // factor <= 1000:
            break
    return "{}{}B".format(size // factor, prefix.strip())


class UDisks1StorageDeviceListener:

    def __init__(self, system_bus, loop, action, devices, minimum_speed,
                 memorycard):
        self._action = action
        self._devices = devices
        self._minimum_speed = minimum_speed
        self._memorycard = memorycard
        self._bus = system_bus
        self._loop = loop
        self._error = False
        self._change_cache = []

    def check(self, timeout):
        udisks = 'org.freedesktop.UDisks'
        if self._action == 'insert':
            signal = 'DeviceAdded'
            logging.debug("Adding signal listener for %s.%s", udisks, signal)
            self._bus.add_signal_receiver(self.add_detected,
                                          signal_name=signal,
                                          dbus_interface=udisks)
        elif self._action == 'remove':
            signal = 'DeviceRemoved'
            logging.debug("Adding signal listener for %s.%s", udisks, signal)
            self._bus.add_signal_receiver(self.remove_detected,
                                          signal_name=signal,
                                          dbus_interface=udisks)

        self._starting_devices = self.get_existing_devices()
        logging.debug("Starting with the following devices: %r",
                      self._starting_devices)

        def timeout_callback():
            print("%s seconds have expired "
                  "waiting for the device to be inserted." % timeout)
            self._error = True
            self._loop.quit()

        logging.debug("Adding timeout listener, timeout=%r", timeout)
        GObject.timeout_add_seconds(timeout, timeout_callback)
        logging.debug("Starting event loop...")
        self._loop.run()

        return self._error

    def verify_device_change(self, changed_devices, message=""):
        logging.debug("Verifying device change: %s", changed_devices)
        # Filter the applicable bus types, as provided on the command line
        # (values of self._devices can be 'usb', 'firewire', etc)
        desired_bus_devices = [
            device
            for device in changed_devices
            if device.bus in self._devices]
        logging.debug("Desired bus devices: %s", desired_bus_devices)
        for dev in desired_bus_devices:
            if self._memorycard:
                if (dev.bus != 'sdio'
                    and not FLASH_RE.search(dev.media)
                    and not CARD_READER_RE.search(dev.model)
                    and not GENERIC_RE.search(dev.vendor)):
                    logging.debug("The device does not seem to be a memory"
                                  " card (bus: %r, model: %r), skipping",
                                 dev.bus, dev.model)
                    return
                print(message % {'bus': 'memory card', 'file': dev.file})
            else:
                if (FLASH_RE.search(dev.media)
                    or CARD_READER_RE.search(dev.model)
                    or GENERIC_RE.search(dev.vendor)):
                    logging.debug("The device seems to be a memory"
                                  " card (bus: %r (model: %r), skipping",
                                  dev.bus, dev.model)
                    return
                print(message % {'bus': dev.bus, 'file': dev.file})
            if self._minimum_speed:
                if dev.speed >= self._minimum_speed:
                    print("with speed of %(speed)s bits/s "
                          "higher than %(min_speed)s bits/s" %
                          {'speed': dev.speed,
                           'min_speed': self._minimum_speed})
                else:
                    print("ERROR: speed of %(speed)s bits/s lower "
                          "than %(min_speed)s bits/s" %
                          {'speed': dev.speed,
                           'min_speed': self._minimum_speed})
                    self._error = True
            logging.debug("Device matches requirements, exiting event loop")
            self._loop.quit()

    def job_change_detected(self, devices, job_in_progress, job_id,
                            job_num_tasks, job_cur_task_id,
                            job_cur_task_percentage):
        logging.debug("UDisks1 reports a job change has been detected:"
                      " devices: %s, job_in_progress: %s, job_id: %s,"
                      " job_num_tasks: %s, job_cur_task_id: %s,"
                      " job_cur_task_percentage: %s",
                      devices, job_in_progress, job_id, job_num_tasks,
                      job_cur_task_id, job_cur_task_percentage)
        if job_id == "FilesystemMount":
            if devices in self._change_cache:
                logging.debug("Ignoring filesystem mount,"
                              " the device is present in change cache")
                return
            logging.debug("Adding devices to change cache: %r", devices)
            self._change_cache.append(devices)
            logging.debug("Starting devices were: %s", self._starting_devices)
            current_devices = self.get_existing_devices()
            logging.debug("Current devices are: %s", current_devices)
            inserted_devices = list(set(current_devices) -
                                    set(self._starting_devices))
            logging.debug("Computed inserted devices: %s", inserted_devices)
            if self._memorycard:
                message = "Expected memory card device %(file)s inserted"
            else:
                message = "Expected %(bus)s device %(file)s inserted"
            self.verify_device_change(inserted_devices,
                      message=message)

    def add_detected(self, added_path):
        logging.debug("UDisks1 reports device has been added: %s", added_path)
        logging.debug("Resetting change_cache to []")
        self._change_cache = []
        signal_name = 'DeviceJobChanged'
        dbus_interface = 'org.freedesktop.UDisks'
        logging.debug("Adding signal listener for %s.%s",
                      dbus_interface, signal_name)
        self._bus.add_signal_receiver(self.job_change_detected,
                                      signal_name=signal_name,
                                      dbus_interface=dbus_interface)

    def remove_detected(self, removed_path):
        logging.debug("UDisks1 reports device has been removed: %s",
                      removed_path)

        logging.debug("Starting devices were: %s", self._starting_devices)
        current_devices = self.get_existing_devices()
        logging.debug("Current devices are: %s", current_devices)
        removed_devices = list(set(self._starting_devices) -
                               set(current_devices))
        logging.debug("Computed removed devices: %s", removed_devices)
        self.verify_device_change(removed_devices,
                  message="Removable %(bus)s device %(file)s has been removed")

    def get_existing_devices(self):
        logging.debug("Getting existing devices from UDisks1")
        ud_manager_obj = self._bus.get_object("org.freedesktop.UDisks",
                                              "/org/freedesktop/UDisks")
        ud_manager = dbus.Interface(ud_manager_obj, 'org.freedesktop.UDisks')
        existing_devices = []
        for dev in ud_manager.EnumerateDevices():
            try:
                device_obj = self._bus.get_object("org.freedesktop.UDisks",
                                                  dev)
                device_props = dbus.Interface(device_obj,
                                              dbus.PROPERTIES_IFACE)
                udisks = 'org.freedesktop.UDisks.Device'
                _device_file = device_props.Get(udisks,
                                           "DeviceFile")
                _bus = device_props.Get(udisks,
                                           "DriveConnectionInterface")
                _speed = device_props.Get(udisks,
                                                "DriveConnectionSpeed")
                _parent_model = ''
                _parent_media = ''
                _parent_vendor = ''

                if device_props.Get(udisks, "DeviceIsPartition"):
                    parent_obj = self._bus.get_object(
                        "org.freedesktop.UDisks",
                        device_props.Get(udisks, "PartitionSlave"))
                    parent_props = dbus.Interface(
                        parent_obj, dbus.PROPERTIES_IFACE)
                    _parent_model = parent_props.Get(udisks, "DriveModel")
                    _parent_vendor = parent_props.Get(udisks, "DriveVendor")
                    _parent_media = parent_props.Get(udisks, "DriveMedia")

                if not device_props.Get(udisks, "DeviceIsDrive"):
                    device = UDisks1DriveProperties(
                        file=str(_device_file),
                        bus=str(_bus),
                        speed=int(_speed),
                        model=str(_parent_model),
                        vendor=str(_parent_vendor),
                        media=str(_parent_media))
                    existing_devices.append(device)

            except dbus.DBusException:
                pass

        return existing_devices


def udisks2_objects_delta(old, new):
    """
    Compute the delta between two snapshots of udisks2 objects

    The objects are encoded as {s:{s:{s:v}}} where the first dictionary maps
    from DBus object path to a dictionary that maps from interface name to a
    dictionary that finally maps from property name to property value.

    The result is a generator of DeltaRecord objects that encodes the changes:
        * the 'delta_dir' is either DELTA_DIR_PLUS or DELTA_DIR_MINUS
        * the 'value' is a tuple that differs for interfaces and properties.
          Interfaces use the format (DELTA_TYPE_IFACE, object_path, iface_name)
          while properties use the format (DELTA_TYPE_PROP, object_path,
          iface_name, prop_name, prop_value)

    Interfaces are never "changed", they are only added or removed. Properties
    can be changed and this is encoded as removal followed by an addition where
    both differ only by the 'delta_dir' and the last element of the 'value'
    tuple.
    """
    # Traverse all objects, old or new
    all_object_paths = set()
    all_object_paths |= old.keys()
    all_object_paths |= new.keys()
    for object_path in sorted(all_object_paths):
        old_object = old.get(object_path, {})
        new_object = new.get(object_path, {})
        # Traverse all interfaces of each object, old or new
        all_iface_names = set()
        all_iface_names |= old_object.keys()
        all_iface_names |= new_object.keys()
        for iface_name in sorted(all_iface_names):
            if iface_name not in old_object and iface_name in new_object:
                # Report each ADDED interface
                assert iface_name in new_object
                delta_value = InterfaceDelta(
                    DELTA_TYPE_IFACE, object_path, iface_name)
                yield DeltaRecord(DELTA_DIR_PLUS, delta_value)
                # Report all properties ADDED on that interface
                for prop_name, prop_value in new_object[iface_name].items():
                    delta_value = PropertyDelta(DELTA_TYPE_PROP, object_path,
                                                iface_name, prop_name,
                                                prop_value)
                    yield DeltaRecord(DELTA_DIR_PLUS, delta_value)
            elif iface_name not in new_object and iface_name in old_object:
                # Report each REMOVED interface
                assert iface_name in old_object
                delta_value = InterfaceDelta(
                    DELTA_TYPE_IFACE, object_path, iface_name)
                yield DeltaRecord(DELTA_DIR_MINUS, delta_value)
                # Report all properties REMOVED on that interface
                for prop_name, prop_value in old_object[iface_name].items():
                    delta_value = PropertyDelta(DELTA_TYPE_PROP, object_path,
                                                iface_name, prop_name,
                                                prop_value)
                    yield DeltaRecord(DELTA_DIR_MINUS, delta_value)
            else:
                # Analyze properties of each interface that existed both in old
                # and new object trees.
                assert iface_name in new_object
                assert iface_name in old_object
                old_props = old_object[iface_name]
                new_props = new_object[iface_name]
                all_prop_names = set()
                all_prop_names |= old_props.keys()
                all_prop_names |= new_props.keys()
                # Traverse all properties, old or new
                for prop_name in sorted(all_prop_names):
                    if prop_name not in old_props and prop_name in new_props:
                        # Report each ADDED property
                        delta_value = PropertyDelta(
                            DELTA_TYPE_PROP, object_path, iface_name,
                            prop_name, new_props[prop_name])
                        yield DeltaRecord(DELTA_DIR_PLUS, delta_value)
                    elif prop_name not in new_props and prop_name in old_props:
                        # Report each REMOVED property
                        delta_value = PropertyDelta(
                            DELTA_TYPE_PROP, object_path, iface_name,
                            prop_name, old_props[prop_name])
                        yield DeltaRecord(DELTA_DIR_MINUS, delta_value)
                    else:
                        old_value = old_props[prop_name]
                        new_value = new_props[prop_name]
                        if old_value != new_value:
                            # Report each changed property
                            yield DeltaRecord(DELTA_DIR_MINUS, PropertyDelta(
                                DELTA_TYPE_PROP, object_path, iface_name,
                                prop_name, old_value))
                            yield DeltaRecord(DELTA_DIR_PLUS, PropertyDelta(
                                DELTA_TYPE_PROP, object_path, iface_name,
                                prop_name, new_value))


class UDisks2StorageDeviceListener:
    """
    Implementation of the storage device listener concept for UDisks2 backend.
    Loosely modeled on the UDisks-based implementation above.

    Implementation details
    ^^^^^^^^^^^^^^^^^^^^^^

    The class, once configured reacts to asynchronous events from the event
    loop. Those are either DBus signals or GLib timeout.

    The timeout, if reached, terminates the test and fails with an appropriate
    end-user message. The user is expected to manipulate storage devices while
    the test is running.

    DBus signals (that correspond to UDisks2 DBus signals) cause callbacks into
    this code. Each time a signal is reported "delta" is computed and verified
    to determine if there was a successful match. The delta contains a list or
    DeltaRecord objects that encode difference (either addition or removal) and
    the value of the difference (interface name or interface property value).
    This delta is computed by udisks2_objects_delta(). The delta is then passed
    to _validate_delta() which has a chance to end the test but also prints
    diagnostic messages in verbose mode. This is very useful for understanding
    what the test actually sees occurring.

    Insertion/removal detection strategy
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    Compared to initial state, the following changes objects need to be
    detected

    * At least one UDisks2 object with the following _all_ interfaces:
        * UDisks2.Partition
            (because we want a partitioned device)
        * UDisks2.Block
            (because we want that device to have a block device that users can
            format)
            - having IdUsage == 'filesystem'
            (because it should not be a piece of raid or lvm)
            - having Size > 0
            (because it should not be and empty removable storage reader)
        * UDisks2.Filesystem
            (because we want to ensure that a filesystem gets mounted)
            - having MountPoints != []
            - as a special exception this rule is REMOVED from eSATA and SATA
              devices as they are not automatically mounted anymore.

    This object must be traceable to an UDisks.Drive object:
        (because we need the medium to be inserted somewhere)
            - having ConnectionBus in (desired_connection_buses)
            - as a special exception this rule is weakened for eSATA because
              for such devices the ConnectionBus property is empty.
    """

    # Name of the DBus interface exposed UDisks2 for various drives
    UDISKS2_DRIVE_INTERFACE = "org.freedesktop.UDisks2.Drive"

    # Name of the DBus property provided by the "Drive" interface above
    UDISKS2_DRIVE_PROPERTY_CONNECTION_BUS = "ConnectionBus"

    def __init__(self, system_bus, loop, action, devices, minimum_speed,
                 memorycard):
        # Store the desired minimum speed of the device in Mbit/s. The argument
        # is passed as the number of bits per second so let's fix that.
        self._desired_minimum_speed = minimum_speed / 10 ** 6
        # Compute the allowed UDisks2.Drive.ConnectionBus value based on the
        # legacy arguments passed from the command line.
        self._desired_connection_buses = set([
            map_udisks1_connection_bus(device) for device in devices])
        # Check if we are explicitly looking for memory cards
        self._desired_memory_card = memorycard
        # Store the desired "delta" direction depending on
        # whether we test for insertion or removal
        if action == "insert":
            self._desired_delta_dir = DELTA_DIR_PLUS
        elif action == "remove":
            self._desired_delta_dir = DELTA_DIR_MINUS
        else:
            raise ValueError("Unsupported action: {}".format(action))
        # Store DBus bus object as we need to pass it to UDisks2 observer
        self._bus = system_bus
        # Store event loop object
        self._loop = loop
        # Setup UDisks2Observer class to track changes published by UDisks2
        self._udisks2_observer = UDisks2Observer()
        # Set the initial value of reference_objects.
        # The actual value is only set once in check()
        self._reference_objects = None
        # As above, just initializing in init for sake of consistency
        self._is_reference = None
        # Setup UDisks2Model to know what the current state is. This is needed
        # when remove events are reported as they don't carry enough state for
        # the program to work correctly. Since UDisks2Model only applies the
        # changes _after_ processing the signals from UDisks2Observer we can
        # reliably check all of the properties of the removed object / device.
        self._udisks2_model = UDisks2Model(self._udisks2_observer)
        # Whenever anything changes call our local change handler
        # This handler always computes the full delta (versus the
        # reference state) and decides if we have a match or not
        self._udisks2_model.on_change.connect(self._on_change)
        # We may need an udev context for checking the speed of USB devices
        self._udev_client = GUdev.Client()
        # A snapshot of udev devices, set in check()
        self._reference_udev_devices = None
        # Assume the test passes, this is changed when timeout expires or when
        # an incorrect device gets inserted.
        self._error = False

    def _dump_reference_udisks_objects(self):
        logging.debug("Reference UDisks2 objects:")
        for udisks2_object in self._reference_objects:
            logging.debug(" - %s", udisks2_object)

    def _dump_reference_udev_devices(self):
        logging.debug("Reference udev devices:")
        for udev_device in self._reference_udev_devices:
            interconnect_speed = get_interconnect_speed(udev_device)
            if interconnect_speed:
                logging.debug(" - %s (USB %dMBit/s)",
                              udev_device.get_device_file(),
                              interconnect_speed)
            else:
                logging.debug(" - %s", udev_device.get_device_file())

    def check(self, timeout):
        """
        Run the configured test and return the result

        The result is False if the test has failed.  The timeout, when
        non-zero, will make the test fail after the specified seconds have
        elapsed without conclusive result.
        """
        # Setup a timeout if requested
        if timeout > 0:
            GObject.timeout_add_seconds(timeout, self._on_timeout_expired)
        # Connect the observer to the bus. This will start giving us events
        # (actually when the loop starts later below)
        self._udisks2_observer.connect_to_bus(self._bus)
        # Get the reference snapshot of available devices
        self._reference_objects = copy.deepcopy(self._current_objects)
        self._dump_reference_udisks_objects()
        # Mark the current _reference_objects as ... reference, this is sadly
        # needed by _summarize_changes() as it sees the snapshot _after_ a
        # change has occurred and cannot determine if the slope of the 'edge'
        # of the change. It is purely needed for UI in verbose mode
        self._is_reference = True
        # A collection of objects that we gladly ignore because we already
        # reported on them being somehow inappropriate
        self._ignored_objects = set()
        # Get the reference snapshot of available udev devices
        self._reference_udev_devices = get_udev_block_devices(
            self._udev_client)
        self._dump_reference_udev_devices()
        # Start the loop and wait. The loop will exit either when:
        # 1) A proper device has been detected (either insertion or removal)
        # 2) A timeout (optional) has expired
        self._loop.run()
        # Return the outcome of the test
        return self._error

    def _on_timeout_expired(self):
        """
        Internal function called when the timer expires.

        Basically it's just here to tell the user the test failed or that the
        user was unable to alter the device during the allowed time.
        """
        print("You have failed to perform the required manipulation in time")
        # Fail the test when the timeout was reached
        self._error = True
        # Stop the loop now
        self._loop.quit()

    def _on_change(self):
        """
        Internal method called by UDisks2Model whenever a change had occurred
        """
        # Compute the changes that had occurred since the reference point
        delta_records = list(self._get_delta_records())
        # Display a summary of changes when we are done
        self._summarize_changes(delta_records)
        # If the changes are what we wanted stop the loop
        matching_devices = self._get_matching_devices(delta_records)
        if matching_devices:
            print("Expected device manipulation complete: {}".format(
                ', '.join(matching_devices)))
            # And call it a day
            self._loop.quit()

    def _get_matching_devices(self, delta_records):
        """
        Internal method called that checks if the delta records match the type
        of device manipulation we were expecting. Only called from _on_change()

        Returns a set of paths of block devices that matched
        """
        # Results
        results = set()
        # Group changes by DBus object path
        grouped_records = collections.defaultdict(list)
        for record in delta_records:
            grouped_records[record.value.object_path].append(record)
        # Create another snapshot od udev devices so that we don't do it over
        # and over in the loop below (besides, if we did that then results
        # could differ each time).
        current_udev_devices = get_udev_block_devices(self._udev_client)
        # Iterate over all UDisks2 objects and their delta records
        for object_path, records_for_object in grouped_records.items():
            # Skip objects we already ignored and complained about before
            if object_path in self._ignored_objects:
                continue
            needs = set(('block-fs', 'partition', 'non-empty', 'mounted'))
            # As a special exception when the ConnectionBus is allowed to be
            # empty, as is the case with eSATA devices, do not require the
            # filesystem to be mounted as gvfs may choose not to mount it
            # automatically.
            found = set()
            drive_object_path = None
            object_block_device = None
            for record in records_for_object:
                # Skip changes opposite to the ones we need
                if record.delta_dir != self._desired_delta_dir:
                    continue
                # For devices with empty "ConnectionBus" property, don't
                # require the device to be mounted
                if (record.value.iface_name ==
                    "org.freedesktop.UDisks2.Drive"
                    and record.value.delta_type == DELTA_TYPE_PROP
                    and record.value.prop_name == "ConnectionBus"
                    and record.value.prop_value == ""):
                    needs.remove('mounted')
                # Detect block devices designated for filesystems
                if (record.value.iface_name ==
                    "org.freedesktop.UDisks2.Block"
                    and record.value.delta_type == DELTA_TYPE_PROP
                    and record.value.prop_name == "IdUsage"
                    and record.value.prop_value == "filesystem"):
                    found.add('block-fs')
                # Memorize the block device path
                elif (record.value.iface_name ==
                    "org.freedesktop.UDisks2.Block"
                    and record.value.delta_type == DELTA_TYPE_PROP
                    and record.value.prop_name == "PreferredDevice"):
                    object_block_device = record.value.prop_value
                # Ensure the device is a partition
                elif (record.value.iface_name ==
                      "org.freedesktop.UDisks2.Partition"
                      and record.value.delta_type == DELTA_TYPE_IFACE):
                    found.add('partition')
                # Ensure the device is not empty
                elif (record.value.iface_name ==
                      "org.freedesktop.UDisks2.Block"
                      and record.value.delta_type == DELTA_TYPE_PROP
                      and record.value.prop_name == "Size"
                      and record.value.prop_value > 0):
                    found.add('non-empty')
                # Ensure the filesystem is mounted
                elif (record.value.iface_name ==
                      "org.freedesktop.UDisks2.Filesystem"
                      and record.value.delta_type == DELTA_TYPE_PROP
                      and record.value.prop_name == "MountPoints"
                      and record.value.prop_value != []):
                    found.add('mounted')
                # Finally memorize the drive the block device belongs to
                elif (record.value.iface_name ==
                      "org.freedesktop.UDisks2.Block"
                      and record.value.delta_type == DELTA_TYPE_PROP
                      and record.value.prop_name == "Drive"):
                    drive_object_path = record.value.prop_value
            logging.debug("Finished analyzing %s, found: %s, needs: %s"
                          " drive_object_path: %s", object_path, found, needs,
                          drive_object_path)
            if needs != found or drive_object_path is None:
                continue
            # We've found our candidate, let's look at the drive it belongs
            # to. We need to do this as some properties are associated with
            # the drive, not the filesystem/block device and the drive may
            # not have been inserted at all.
            try:
                drive_object = self._current_objects[drive_object_path]
            except KeyError:
                # The drive may be removed along with the device, let's check
                # if we originally saw it
                try:
                    drive_object = self._reference_objects[drive_object_path]
                except KeyError:
                    logging.error(
                        "A block device belongs to a drive we could not find")
                    logging.error("missing drive: %r", drive_object_path)
                    continue
            try:
                drive_props = drive_object["org.freedesktop.UDisks2.Drive"]
            except KeyError:
                logging.error(
                    "A block device belongs to an object that is not a Drive")
                logging.error("strange object: %r", drive_object_path)
                continue
            # Ensure the drive is on the appropriate bus
            connection_bus = drive_props["ConnectionBus"]
            if connection_bus not in self._desired_connection_buses:
                logging.warning("The object %r belongs to drive %r that"
                                " is attached to the bus %r but but we are"
                                " looking for one of %r so it cannot match",
                                object_block_device, drive_object_path,
                                connection_bus,
                                ", ".join(self._desired_connection_buses))
                # Ignore this object so that we don't spam the user twice
                self._ignored_objects.add(object_path)
                continue
            # Ensure it is a media card reader if this was explicitly requested
            drive_is_reader = is_memory_card(
                drive_props['Vendor'], drive_props['Model'],
                drive_props['Media'])
            if self._desired_memory_card and not drive_is_reader:
                logging.warning(
                    "The object %s belongs to drive %s that does not seem to"
                    " be a media reader", object_block_device,
                    drive_object_path)
                # Ignore this object so that we don't spam the user twice
                self._ignored_objects.add(object_path)
                continue
            # Ensure the desired minimum speed is enforced
            if self._desired_minimum_speed:
                # We need to discover the speed of the UDisks2 object that is
                # about to be matched. Sadly UDisks2 no longer supports this
                # property so we need to poke deeper and resort to udev.
                #
                # The UDisks2 object that we are interested in implements a
                # number of interfaces, most notably
                # org.freedesktop.UDisks2.Block, that has the Device property
                # holding the unix filesystem path (like /dev/sdb1). We already
                # hold a reference to that as 'object_block_device'
                #
                # We take this as a start and attempt to locate the udev Device
                # (don't confuse with UDisks2.Device, they are _not_ the same)
                # that is associated with that path.
                if self._desired_delta_dir == DELTA_DIR_PLUS:
                    # If we are looking for additions then look at _current_
                    # collection of udev devices
                    udev_devices = current_udev_devices
                    udisks2_object = self._current_objects[object_path]
                else:
                    # If we are looking for removals then look at referece
                    # collection of udev devices
                    udev_devices = self._reference_udev_devices
                    udisks2_object = self._reference_objects[object_path]
                try:
                    # Try to locate the corresponding udev device among the
                    # collection we've selected. Use the drive object as the
                    # key -- this looks for the drive, not partition objects!
                    udev_device = lookup_udev_device(udisks2_object,
                                                     udev_devices)
                except LookupError:
                    logging.error("Unable to map UDisks2 object %s to udev",
                                  object_block_device)
                    # Ignore this object so that we don't spam the user twice
                    self._ignored_objects.add(object_path)
                    continue
                interconnect_speed = get_interconnect_speed(udev_device)
                # Now that we know the speed of the interconnect we can try to
                # validate it against our desired speed.
                if interconnect_speed is None:
                    logging.warning("Unable to determine interconnect speed of"
                                    " device %s", object_block_device)
                    # Ignore this object so that we don't spam the user twice
                    self._ignored_objects.add(object_path)
                    continue
                elif interconnect_speed < self._desired_minimum_speed:
                    logging.warning(
                        "Device %s is connected via an interconnect that has"
                        " the speed of %dMbit/s but the required speed was"
                        " %dMbit/s", object_block_device, interconnect_speed,
                        self._desired_minimum_speed)
                    # Ignore this object so that we don't spam the user twice
                    self._ignored_objects.add(object_path)
                    continue
                else:
                    logging.info("Device %s is connected via an USB"
                                 " interconnect with the speed of %dMbit/s",
                                 object_block_device, interconnect_speed)
            # Yay, success
            results.add(object_block_device)
        return results

    @property
    def _current_objects(self):
        return self._udisks2_model.managed_objects

    def _get_delta_records(self):
        """
        Internal method used to compute the delta between reference devices and
        current devices. The result is a generator of DeltaRecord objects.
        """
        assert self._reference_objects is not None, "Only usable after check()"
        old = self._reference_objects
        new = self._current_objects
        return udisks2_objects_delta(old, new)

    def _summarize_changes(self, delta_records):
        """
        Internal method used to summarize changes (compared to reference state)
        called whenever _on_change() gets called. Only visible in verbose mode
        """
        # Filter out anything but interface changes
        flat_records = [record
                        for record in delta_records
                        if record.value.delta_type == DELTA_TYPE_IFACE]
        # Group changes by DBus object path
        grouped_records = collections.defaultdict(list)
        for record in flat_records:
            grouped_records[record.value.object_path].append(record)
        # Bail out quickly when nothing got changed
        if not flat_records:
            if not self._is_reference:
                logging.info("You have returned to the reference state")
                self._is_reference = True
            return
        else:
            self._is_reference = False
        # Iterate over grouped delta records for all objects
        logging.info("Compared to the reference state you have:")
        for object_path in sorted(grouped_records.keys()):
            records_for_object = sorted(
                grouped_records[object_path],
                key=lambda record: record.value.iface_name)
            # Skip any job objects as they just add noise
            if any((record.value.iface_name == "org.freedesktop.UDisks2.Job"
                    for record in records_for_object)):
                continue
            logging.info("For object %s", object_path)
            for record in records_for_object:
                # Ignore property changes for now
                if record.value.delta_type != DELTA_TYPE_IFACE:
                    continue
                # Get the name of the interface that was affected
                iface_name = record.value.iface_name
                # Get the properties for that interface (for removals get the
                # reference values, for additions get the current values)
                if record.delta_dir == DELTA_DIR_PLUS:
                    props = self._current_objects[object_path][iface_name]
                    action = "inserted"
                else:
                    props = self._reference_objects[object_path][iface_name]
                    action = "removed"
                # Display some human-readable information associated with each
                # interface change
                if iface_name == "org.freedesktop.UDisks2.Drive":
                    logging.info("\t * %s a drive", action)
                    logging.info("\t   vendor and name: %r %r",
                                 props['Vendor'], props['Model'])
                    logging.info("\t   bus: %s", props['ConnectionBus'])
                    logging.info("\t   size: %s", format_bytes(props['Size']))
                    logging.info("\t   is media card: %s", is_memory_card(
                        props['Vendor'], props['Model'], props['Media']))
                    logging.info("\t   current media: %s",
                                 props['Media'] or "???" if
                                 props['MediaAvailable'] else "N/A")
                elif iface_name == "org.freedesktop.UDisks2.Block":
                    logging.info("\t * %s block device", action)
                    logging.info("\t   from drive: %s", props['Drive'])
                    logging.info("\t   having device: %s", props['Device'])
                    logging.info("\t   having usage, type and version:"
                                 " %s %s %s", props['IdUsage'],
                                 props['IdType'], props['IdVersion'])
                    logging.info("\t   having label: %s", props['IdLabel'])
                elif iface_name == "org.freedesktop.UDisks2.PartitionTable":
                    logging.info("\t * %s partition table", action)
                    logging.info("\t   having type: %r", props['Type'])
                elif iface_name == "org.freedesktop.UDisks2.Partition":
                    logging.info("\t * %s partition", action)
                    logging.info("\t   from partition table: %s",
                                 props['Table'])
                    logging.info("\t   having size: %s",
                                 format_bytes(props['Size']))
                    logging.info("\t   having name: %r", props['Name'])
                elif iface_name == "org.freedesktop.UDisks2.Filesystem":
                    logging.info("\t * %s file system", action)
                    logging.info("\t   having mount points: %r",
                                 props['MountPoints'])


def main():
    description = "Wait for the specified device to be inserted or removed."
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument('action', choices=['insert', 'remove'])
    parser.add_argument('device', choices=['usb', 'sdio', 'firewire', 'scsi',
                                 'ata_serial_esata'], nargs="+")
    memorycard_help = ("Memory cards devices on bus other than sdio require "
                       "this parameter to identify them as such")
    parser.add_argument('--memorycard', action="store_true",
                        help=memorycard_help)
    parser.add_argument('--timeout', type=int, default=20)
    min_speed_help = ("Will only accept a device if its connection speed "
                      "attribute is higher than this value "
                      "(in bits/s)")
    parser.add_argument('--minimum_speed', '-m', help=min_speed_help,
                        type=int, default=0)
    parser.add_argument('--verbose', action='store_const', const=logging.INFO,
                        dest='logging_level', help="Enable verbose output")
    parser.add_argument('--debug', action='store_const', const=logging.DEBUG,
                        dest='logging_level', help="Enable debugging")
    parser.set_defaults(logging_level=logging.WARNING)
    args = parser.parse_args()

    # Configure logging as requested
    # XXX: This may be incorrect as logging.basicConfig() fails after any other
    # call to logging.log(). The proper solution is to setup a verbose logging
    # configuration and I didn't want to do it now.
    logging.basicConfig(
        level=args.logging_level,
        format='[%(asctime)s] %(levelname)s:%(name)s:%(message)s')

    # Connect to the system bus, we also get the event
    # loop as we need it to start listening for signals.
    system_bus, loop = connect_to_system_bus()

    # Check if system bus has the UDisks2 object
    if is_udisks2_supported(system_bus):
        # Construct the listener with all of the arguments provided on the
        # command line and the explicit system_bus, loop objects.
        logging.debug("Using UDisks2 interface")
        listener = UDisks2StorageDeviceListener(
            system_bus, loop,
            args.action, args.device, args.minimum_speed, args.memorycard)
    else:
        # Construct the listener with all of the arguments provided on the
        # command line and the explicit system_bus, loop objects.
        logging.debug("Using UDisks1 interface")
        listener = UDisks1StorageDeviceListener(
            system_bus, loop,
            args.action, args.device, args.minimum_speed, args.memorycard)
    # Run the actual listener and wait till it either times out of discovers
    # the appropriate media changes
    try:
        return listener.check(args.timeout)
    except KeyboardInterrupt:
        return 1

if __name__ == "__main__":
    sys.exit(main())