/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())
|