• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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