1# Copyright 2020 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Advertisement Monitor Test Application.""" 6 7import dbus 8import dbus.mainloop.glib 9import dbus.service 10import gobject 11import logging 12 13from multiprocessing import Process, Pipe 14from threading import Thread 15 16DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager' 17DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties' 18 19BLUEZ_SERVICE_NAME = 'org.bluez' 20 21ADV_MONITOR_MANAGER_IFACE = 'org.bluez.AdvertisementMonitorManager1' 22ADV_MONITOR_IFACE = 'org.bluez.AdvertisementMonitor1' 23ADV_MONITOR_APP_BASE_PATH = '/org/bluez/adv_monitor_app' 24 25 26class AdvMonitor(dbus.service.Object): 27 """A monitor object. 28 29 This class exposes a dbus monitor object along with its properties 30 and methods. 31 32 More information can be found at BlueZ documentation: 33 doc/advertisement-monitor-api.txt 34 35 """ 36 37 # Indexes of the Monitor object parameters in a monitor data list. 38 MONITOR_TYPE = 0 39 RSSI_FILTER = 1 40 PATTERNS = 2 41 42 # Indexes of the RSSI filter parameters in a monitor data list. 43 RSSI_H_THRESH = 0 44 RSSI_H_TIMEOUT = 1 45 RSSI_L_THRESH = 2 46 RSSI_L_TIMEOUT = 3 47 48 # Indexes of the Patterns filter parameters in a monitor data list. 49 PATTERN_START_POS = 0 50 PATTERN_AD_TYPE = 1 51 PATTERN_DATA = 2 52 53 def __init__(self, bus, app_path, monitor_id, monitor_data): 54 """Construction of a Monitor object. 55 56 @param bus: a dbus system bus. 57 @param app_path: application path. 58 @param monitor_id: unique monitor id. 59 60 """ 61 self.path = app_path + '/monitor' + str(monitor_id) 62 self.bus = bus 63 64 self.events = dict() 65 self.events['Activate'] = 0 66 self.events['Release'] = 0 67 self.events['DeviceFound'] = 0 68 self.events['DeviceLost'] = 0 69 70 self._set_type(monitor_data[self.MONITOR_TYPE]) 71 self._set_rssi(monitor_data[self.RSSI_FILTER]) 72 self._set_patterns(monitor_data[self.PATTERNS]) 73 74 super(AdvMonitor, self).__init__(self.bus, self.path) 75 76 77 def get_path(self): 78 """Get the dbus object path of the monitor. 79 80 @returns: the monitor object path. 81 82 """ 83 return dbus.ObjectPath(self.path) 84 85 86 def get_properties(self): 87 """Get the properties dictionary of the monitor. 88 89 @returns: the monitor properties dictionary. 90 91 """ 92 properties = dict() 93 properties['Type'] = dbus.String(self.monitor_type) 94 properties['RSSIThresholdsAndTimers'] = dbus.Struct(self.rssi, 95 signature='nqnq') 96 properties['Patterns'] = dbus.Array(self.patterns, signature='(yyay)') 97 return {ADV_MONITOR_IFACE: properties} 98 99 100 def _set_type(self, monitor_type): 101 """Set the monitor type. 102 103 @param monitor_type: the type of a monitor. 104 105 """ 106 self.monitor_type = monitor_type 107 108 109 def _set_rssi(self, rssi): 110 """Set the RSSI filter values. 111 112 @param rssi: the list of rssi threshold and timeout values. 113 114 """ 115 h_thresh = dbus.Int16(rssi[self.RSSI_H_THRESH]) 116 h_timeout = dbus.UInt16(rssi[self.RSSI_H_TIMEOUT]) 117 l_thresh = dbus.Int16(rssi[self.RSSI_L_THRESH]) 118 l_timeout = dbus.UInt16(rssi[self.RSSI_L_TIMEOUT]) 119 self.rssi = (h_thresh, h_timeout, l_thresh, l_timeout) 120 121 122 def _set_patterns(self, patterns): 123 """Set the content filter patterns. 124 125 @param patterns: the list of start position, ad type and patterns. 126 127 """ 128 self.patterns = [] 129 for pattern in patterns: 130 start_pos = dbus.Byte(pattern[self.PATTERN_START_POS]) 131 ad_type = dbus.Byte(pattern[self.PATTERN_AD_TYPE]) 132 ad_data = [] 133 for byte in pattern[self.PATTERN_DATA]: 134 ad_data.append(dbus.Byte(byte)) 135 adv_pattern = dbus.Struct((start_pos, ad_type, ad_data), 136 signature='yyay') 137 self.patterns.append(adv_pattern) 138 139 140 def remove_monitor(self): 141 """Remove the monitor object. 142 143 Invoke the dbus method to remove current monitor object from the 144 connection. 145 146 """ 147 self.remove_from_connection() 148 149 150 def _update_event_count(self, event): 151 """Update the event count. 152 153 @param event: name of the event. 154 155 """ 156 self.events[event] += 1 157 158 159 def get_event_count(self, event): 160 """Read the event count. 161 162 @param event: name of the specific event or 'All' for all events. 163 164 @returns: count of the specific event or dict of counts of all events. 165 166 """ 167 if event == 'All': 168 return self.events 169 170 return self.events.get(event) 171 172 173 def reset_event_count(self, event): 174 """Reset the event count. 175 176 @param event: name of the specific event or 'All' for all events. 177 178 @returns: True on success, False otherwise. 179 180 """ 181 if event == 'All': 182 for event_key in self.events: 183 self.events[event_key] = 0 184 return True 185 186 if event in self.events: 187 self.events[event] = 0 188 return True 189 190 return False 191 192 193 @dbus.service.method(DBUS_PROP_IFACE, 194 in_signature='s', 195 out_signature='a{sv}') 196 def GetAll(self, interface): 197 """Get the properties dictionary of the monitor. 198 199 @param interface: the bluetooth dbus interface. 200 201 @returns: the monitor properties dictionary. 202 203 """ 204 logging.info('%s: %s GetAll', self.path, interface) 205 206 if interface != ADV_MONITOR_IFACE: 207 logging.error('%s: GetAll: Invalid arg %s', self.path, interface) 208 return {} 209 210 return self.get_properties()[ADV_MONITOR_IFACE] 211 212 213 @dbus.service.method(ADV_MONITOR_IFACE, 214 in_signature='', 215 out_signature='') 216 def Activate(self): 217 """The method callback at Activate.""" 218 logging.info('%s: Monitor Activated!', self.path) 219 self._update_event_count('Activate') 220 221 222 @dbus.service.method(ADV_MONITOR_IFACE, 223 in_signature='', 224 out_signature='') 225 def Release(self): 226 """The method callback at Release.""" 227 logging.info('%s: Monitor Released!', self.path) 228 self._update_event_count('Release') 229 230 231 @dbus.service.method(ADV_MONITOR_IFACE, 232 in_signature='o', 233 out_signature='') 234 def DeviceFound(self, device): 235 """The method callback at DeviceFound. 236 237 @param device: the dbus object path of the found device. 238 239 """ 240 logging.info('%s: %s Device Found!', self.path, device) 241 self._update_event_count('DeviceFound') 242 243 244 @dbus.service.method(ADV_MONITOR_IFACE, 245 in_signature='o', 246 out_signature='') 247 def DeviceLost(self, device): 248 """The method callback at DeviceLost. 249 250 @param device: the dbus object path of the lost device. 251 252 """ 253 logging.info('%s: %s Device Lost!', self.path, device) 254 self._update_event_count('DeviceLost') 255 256 257class AdvMonitorApp(dbus.service.Object): 258 """The test application. 259 260 This class implements a test application to manage monitor objects. 261 262 """ 263 264 def __init__(self, bus, dbus_mainloop, advmon_manager, app_id): 265 """Construction of a test application object. 266 267 @param bus: a dbus system bus. 268 @param dbus_mainloop: an instance of mainloop. 269 @param advmon_manager: AdvertisementMonitorManager1 interface on 270 the adapter. 271 @param app_id: application id (to create application path). 272 273 """ 274 self.bus = bus 275 self.mainloop = dbus_mainloop 276 self.advmon_mgr = advmon_manager 277 self.app_path = ADV_MONITOR_APP_BASE_PATH + str(app_id) 278 279 self.monitors = dict() 280 281 super(AdvMonitorApp, self).__init__(self.bus, self.app_path) 282 283 284 def get_app_path(self): 285 """Get the dbus object path of the application. 286 287 @returns: the application path. 288 289 """ 290 return dbus.ObjectPath(self.app_path) 291 292 293 def add_monitor(self, monitor_data): 294 """Create a monitor object. 295 296 @param monitor_data: the list containing monitor type, RSSI filter 297 values and patterns. 298 299 @returns: monitor id, once the monitor is created. 300 301 """ 302 monitor_id = 0 303 while monitor_id in self.monitors: 304 monitor_id += 1 305 306 monitor = AdvMonitor(self.bus, self.app_path, monitor_id, monitor_data) 307 308 # Emit the InterfacesAdded signal once the Monitor object is created. 309 self.InterfacesAdded(monitor.get_path(), monitor.get_properties()) 310 311 self.monitors[monitor_id] = monitor 312 313 return monitor_id 314 315 316 def remove_monitor(self, monitor_id): 317 """Remove a monitor object based on the given monitor id. 318 319 @param monitor_id: the monitor id. 320 321 @returns: True on success, False otherwise. 322 323 """ 324 if monitor_id not in self.monitors: 325 return False 326 327 monitor = self.monitors[monitor_id] 328 329 # Emit the InterfacesRemoved signal before removing the Monitor object. 330 self.InterfacesRemoved(monitor.get_path(), 331 monitor.get_properties().keys()) 332 333 monitor.remove_monitor() 334 335 self.monitors.pop(monitor_id) 336 337 return True 338 339 340 def get_event_count(self, monitor_id, event): 341 """Read the count of a particular event on the given monitor. 342 343 @param monitor_id: the monitor id. 344 @param event: name of the specific event or 'All' for all events. 345 346 @returns: count of the specific event or dict of counts of all events. 347 348 """ 349 if monitor_id not in self.monitors: 350 return None 351 352 return self.monitors[monitor_id].get_event_count(event) 353 354 355 def reset_event_count(self, monitor_id, event): 356 """Reset the count of a particular event on the given monitor. 357 358 @param monitor_id: the monitor id. 359 @param event: name of the specific event or 'All' for all events. 360 361 @returns: True on success, False otherwise. 362 363 """ 364 if monitor_id not in self.monitors: 365 return False 366 367 return self.monitors[monitor_id].reset_event_count(event) 368 369 370 def _mainloop_thread(self): 371 """Run the dbus mainloop thread. 372 373 Callback methods on the monitor objects get invoked only when the 374 dbus mainloop is running. This thread starts when app is registered 375 and stops when app is unregistered. 376 377 """ 378 self.mainloop.run() # blocks until mainloop.quit() is called 379 380 381 def register_app(self): 382 """Register an advertisement monitor app. 383 384 @returns: True on success, False otherwise. 385 386 """ 387 if self.mainloop.is_running(): 388 self.mainloop.quit() 389 390 self.register_successful = False 391 392 def register_cb(): 393 """Handler when RegisterMonitor succeeded.""" 394 logging.info('%s: RegisterMonitor successful!', self.app_path) 395 self.register_successful = True 396 self.mainloop.quit() 397 398 def register_error_cb(error): 399 """Handler when RegisterMonitor failed.""" 400 logging.error('%s: RegisterMonitor failed: %s', self.app_path, 401 str(error)) 402 self.register_successful = False 403 self.mainloop.quit() 404 405 self.advmon_mgr.RegisterMonitor(self.get_app_path(), 406 reply_handler=register_cb, 407 error_handler=register_error_cb) 408 self.mainloop.run() # blocks until mainloop.quit() is called 409 410 # Start a background thread to run mainloop.run(). This is required for 411 # the bluetoothd to be able to invoke methods on the monitor object. 412 # Mark this thread as a daemon to make sure that the thread is killed 413 # in case the parent process dies unexpectedly. 414 t = Thread(target=self._mainloop_thread) 415 t.daemon = True 416 t.start() 417 418 return self.register_successful 419 420 421 def unregister_app(self): 422 """Unregister an advertisement monitor app. 423 424 @returns: True on success, False otherwise. 425 426 """ 427 if self.mainloop.is_running(): 428 self.mainloop.quit() 429 430 self.unregister_successful = False 431 432 def unregister_cb(): 433 """Handler when UnregisterMonitor succeeded.""" 434 logging.info('%s: UnregisterMonitor successful!', self.app_path) 435 self.unregister_successful = True 436 self.mainloop.quit() 437 438 def unregister_error_cb(error): 439 """Handler when UnregisterMonitor failed.""" 440 logging.error('%s: UnregisterMonitor failed: %s', self.app_path, 441 str(error)) 442 self.unregister_successful = False 443 self.mainloop.quit() 444 445 self.advmon_mgr.UnregisterMonitor(self.get_app_path(), 446 reply_handler=unregister_cb, 447 error_handler=unregister_error_cb) 448 self.mainloop.run() # blocks until mainloop.quit() is called 449 450 return self.unregister_successful 451 452 453 @dbus.service.method(DBUS_OM_IFACE, out_signature='a{oa{sa{sv}}}') 454 def GetManagedObjects(self): 455 """Get the list of managed monitor objects. 456 457 @returns: the list of managed objects and their properties. 458 459 """ 460 logging.info('%s: GetManagedObjects', self.app_path) 461 462 objects = dict() 463 for monitor_id in self.monitors: 464 monitor = self.monitors[monitor_id] 465 objects[monitor.get_path()] = monitor.get_properties() 466 467 return objects 468 469 470 @dbus.service.signal(DBUS_OM_IFACE, signature='oa{sa{sv}}') 471 def InterfacesAdded(self, object_path, interfaces_and_properties): 472 """Emit the InterfacesAdded signal for a given monitor object. 473 474 Invoking this method emits the InterfacesAdded signal, 475 nothing needs to be done here. 476 477 @param object_path: the dbus object path of a monitor. 478 @param interfaces_and_properties: the monitor properties dictionary. 479 480 """ 481 return 482 483 484 @dbus.service.signal(DBUS_OM_IFACE, signature='oas') 485 def InterfacesRemoved(self, object_path, interfaces): 486 """Emit the InterfacesRemoved signal for a given monitor object. 487 488 Invoking this method emits the InterfacesRemoved signal, 489 nothing needs to be done here. 490 491 @param object_path: the dbus object path of a monitor. 492 @param interfaces: the list of monitor interfaces. 493 494 """ 495 return 496 497 498class AdvMonitorAppMgr(): 499 """The app manager for Advertisement Monitor Test Apps. 500 501 This class manages instances of multiple advertisement monitor test 502 applications. 503 504 """ 505 506 # List of commands used by AdvMonitor AppMgr, AppMgr-helper process and 507 # AdvMonitor Test Application for communication between each other. 508 CMD_EXIT_HELPER = 0 509 CMD_CREATE_APP = 1 510 CMD_EXIT_APP = 2 511 CMD_KILL_APP = 3 512 CMD_REGISTER_APP = 4 513 CMD_UNREGISTER_APP = 5 514 CMD_ADD_MONITOR = 6 515 CMD_REMOVE_MONITOR = 7 516 CMD_GET_EVENT_COUNT = 8 517 CMD_RESET_EVENT_COUNT = 9 518 519 def __init__(self): 520 """Construction of applications manager object.""" 521 522 # Due to a limitation of python, it is not possible to fork a new 523 # process once any dbus connections are established. So, create a 524 # helper process before making any dbus connections. This helper 525 # process can be used to create more processes on demand. 526 parent_conn, child_conn = Pipe() 527 p = Process(target=self._appmgr_helper, args=(child_conn,)) 528 p.start() 529 530 self._helper_proc = p 531 self._helper_conn = parent_conn 532 self.apps = [] 533 534 535 def _appmgr_helper(self, appmgr_conn): 536 """AppMgr helper process. 537 538 This process is used to create new instances of the AdvMonitor Test 539 Application on demand and acts as a communication bridge between the 540 AppMgr and test applications. 541 542 @param appmgr_conn: an object of AppMgr connection pipe. 543 544 """ 545 app_conns = dict() 546 547 done = False 548 while not done: 549 cmd, app_id, data = appmgr_conn.recv() 550 ret = None 551 552 if cmd == self.CMD_EXIT_HELPER: 553 # Terminate any outstanding test application instances before 554 # exiting the helper process. 555 for app_id in app_conns: 556 p, app_conn = app_conns[app_id] 557 if p.is_alive(): 558 # Try to exit the app gracefully first, terminate if it 559 # doesn't work. 560 app_conn.send((self.CMD_EXIT_APP, None)) 561 if not app_conn.recv() or p.is_alive(): 562 p.terminate() 563 p.join() # wait for test app to terminate 564 done = True 565 ret = True 566 567 elif cmd == self.CMD_CREATE_APP: 568 if app_id not in app_conns: 569 parent_conn, child_conn = Pipe() 570 p = Process(target=self._testapp_main, 571 args=(child_conn, app_id,)) 572 p.start() 573 574 app_conns[app_id] = (p, parent_conn) 575 ret = app_id 576 577 elif cmd == self.CMD_KILL_APP: 578 if app_id in app_conns: 579 p, _ = app_conns[app_id] 580 if p.is_alive(): 581 p.terminate() 582 p.join() # wait for test app to terminate 583 584 app_conns.pop(app_id) 585 ret = not p.is_alive() 586 587 else: 588 if app_id in app_conns: 589 p, app_conn = app_conns[app_id] 590 591 app_conn.send((cmd, data)) 592 ret = app_conn.recv() 593 594 if cmd == self.CMD_EXIT_APP: 595 p.join() # wait for test app to terminate 596 597 app_conns.pop(app_id) 598 ret = not p.is_alive() 599 600 appmgr_conn.send(ret) 601 602 603 def _testapp_main(self, helper_conn, app_id): 604 """AdvMonitor Test Application Process. 605 606 This process acts as a client application for AdvMonitor tests and used 607 to host AdvMonitor dbus objects. 608 609 @param helper_conn: an object of AppMgr-helper process connection pipe. 610 @param app_id: the app id of this test app process. 611 612 """ 613 # Initialize threads in gobject/dbus-glib before creating local threads. 614 gobject.threads_init() 615 dbus.mainloop.glib.threads_init() 616 617 # Arrange for the GLib main loop to be the default. 618 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 619 620 def get_advmon_mgr(bus): 621 """Finds the AdvMonitor Manager object exported by bluetoothd.""" 622 remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'), 623 DBUS_OM_IFACE) 624 objects = remote_om.GetManagedObjects() 625 626 for o, props in objects.items(): 627 if ADV_MONITOR_MANAGER_IFACE in props: 628 return dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, o), 629 ADV_MONITOR_MANAGER_IFACE) 630 return None 631 632 bus = dbus.SystemBus() 633 mainloop = gobject.MainLoop() 634 advmon_mgr = get_advmon_mgr(bus) 635 636 app = AdvMonitorApp(bus, mainloop, advmon_mgr, app_id) 637 638 done = False 639 while not done: 640 cmd, data = helper_conn.recv() 641 ret = None 642 643 if cmd == self.CMD_EXIT_APP: 644 done = True 645 ret = True 646 647 elif cmd == self.CMD_REGISTER_APP: 648 ret = app.register_app() 649 650 elif cmd == self.CMD_UNREGISTER_APP: 651 ret = app.unregister_app() 652 653 elif cmd == self.CMD_ADD_MONITOR: 654 ret = app.add_monitor(data) 655 656 elif cmd == self.CMD_REMOVE_MONITOR: 657 ret = app.remove_monitor(data) 658 659 elif cmd == self.CMD_GET_EVENT_COUNT: 660 ret = app.get_event_count(*data) 661 662 elif cmd == self.CMD_RESET_EVENT_COUNT: 663 ret = app.reset_event_count(*data) 664 665 helper_conn.send(ret) 666 667 668 def _send_to_helper(self, cmd, app_id=None, data=None): 669 """Sends commands to the helper process. 670 671 @param cmd: command number from the above set of CMD_* commands. 672 @param app_id: the app id. 673 @param data: the command data. 674 675 @returns: outcome of the command returned by the helper process. 676 677 """ 678 if not self._helper_proc.is_alive(): 679 return None 680 681 self._helper_conn.send((cmd, app_id, data)) 682 return self._helper_conn.recv() 683 684 685 def create_app(self): 686 """Create an advertisement monitor app. 687 688 @returns: app id, once the app is created. 689 690 """ 691 app_id = 0 692 while app_id in self.apps: 693 app_id += 1 694 695 self.apps.append(app_id) 696 697 return self._send_to_helper(self.CMD_CREATE_APP, app_id) 698 699 700 def exit_app(self, app_id): 701 """Exit an advertisement monitor app. 702 703 @param app_id: the app id. 704 705 @returns: True on success, False otherwise. 706 707 """ 708 if app_id not in self.apps: 709 return False 710 711 self.apps.remove(app_id) 712 713 return self._send_to_helper(self.CMD_EXIT_APP, app_id) 714 715 716 def kill_app(self, app_id): 717 """Kill an advertisement monitor app by sending SIGKILL. 718 719 @param app_id: the app id. 720 721 @returns: True on success, False otherwise. 722 723 """ 724 if app_id not in self.apps: 725 return False 726 727 self.apps.remove(app_id) 728 729 return self._send_to_helper(self.CMD_KILL_APP, app_id) 730 731 732 def register_app(self, app_id): 733 """Register an advertisement monitor app. 734 735 @param app_id: the app id. 736 737 @returns: True on success, False otherwise. 738 739 """ 740 if app_id not in self.apps: 741 return False 742 743 return self._send_to_helper(self.CMD_REGISTER_APP, app_id) 744 745 746 def unregister_app(self, app_id): 747 """Unregister an advertisement monitor app. 748 749 @param app_id: the app id. 750 751 @returns: True on success, False otherwise. 752 753 """ 754 if app_id not in self.apps: 755 return False 756 757 return self._send_to_helper(self.CMD_UNREGISTER_APP, app_id) 758 759 760 def add_monitor(self, app_id, monitor_data): 761 """Create a monitor object. 762 763 @param app_id: the app id. 764 @param monitor_data: the list containing monitor type, RSSI filter 765 values and patterns. 766 767 @returns: monitor id, once the monitor is created, None otherwise. 768 769 """ 770 if app_id not in self.apps: 771 return None 772 773 return self._send_to_helper(self.CMD_ADD_MONITOR, app_id, monitor_data) 774 775 776 def remove_monitor(self, app_id, monitor_id): 777 """Remove a monitor object based on the given monitor id. 778 779 @param app_id: the app id. 780 @param monitor_id: the monitor id. 781 782 @returns: True on success, False otherwise. 783 784 """ 785 if app_id not in self.apps: 786 return False 787 788 return self._send_to_helper(self.CMD_REMOVE_MONITOR, app_id, monitor_id) 789 790 791 def get_event_count(self, app_id, monitor_id, event): 792 """Read the count of a particular event on the given monitor. 793 794 @param app_id: the app id. 795 @param monitor_id: the monitor id. 796 @param event: name of the specific event or 'All' for all events. 797 798 @returns: count of the specific event or dict of counts of all events. 799 800 """ 801 if app_id not in self.apps: 802 return None 803 804 return self._send_to_helper(self.CMD_GET_EVENT_COUNT, app_id, 805 (monitor_id, event)) 806 807 808 def reset_event_count(self, app_id, monitor_id, event): 809 """Reset the count of a particular event on the given monitor. 810 811 @param app_id: the app id. 812 @param monitor_id: the monitor id. 813 @param event: name of the specific event or 'All' for all events. 814 815 @returns: True on success, False otherwise. 816 817 """ 818 if app_id not in self.apps: 819 return False 820 821 return self._send_to_helper(self.CMD_RESET_EVENT_COUNT, app_id, 822 (monitor_id, event)) 823 824 825 def destroy(self): 826 """Clean up the helper process and test app processes.""" 827 828 self._send_to_helper(self.CMD_EXIT_HELPER) 829 830 if self._helper_proc.is_alive(): 831 self._helper_proc.terminate() 832 self._helper_proc.join() # wait for helper process to terminate 833 834 return not self._helper_proc.is_alive() 835