~brian.curtin/ubuntu-sso-client/py3-iter-removal

« back to all changes in this revision

Viewing changes to ubuntu_sso/networkstate/darwin.py

- Implement internet connectivity detection in Mac OS X ("darwin") systems ( LP #984667 )

Show diffs side-by-side

added added

removed removed

Lines of Context:
13
13
#
14
14
# You should have received a copy of the GNU General Public License along
15
15
# with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 
"""Implementation of network state detection."""
 
16
"""Network state detection on OS X.
 
17
 
 
18
is_machine_connected(): (deferred) returns connected state as bool
 
19
NetworkManagerState: class with listening thread, calls back with state changes
 
20
"""
17
21
 
18
22
from twisted.internet import defer
19
23
 
 
24
from threading import Thread
 
25
 
20
26
from ubuntu_sso.networkstate import NetworkFailException
21
 
from ubuntu_sso.networkstate.networkstates import (
22
 
    ONLINE, OFFLINE, UNKNOWN,
23
 
 
24
 
    NM_STATE_CONNECTING_LIST,
25
 
    NM_STATE_CONNECTED_LIST,
26
 
    NM_STATE_DISCONNECTED_LIST,
27
 
    )
28
 
from ubuntu_sso.networkstate.networkstates import NM_STATE_CONNECTED_GLOBAL
 
27
from ubuntu_sso.networkstate.networkstates import (ONLINE, OFFLINE, UNKNOWN)
29
28
from ubuntu_sso.logger import setup_logging
30
29
logger = setup_logging("ubuntu_sso.networkstate")
31
30
 
 
31
HOSTNAME_TO_CHECK = 'one.ubuntu.com'
 
32
 
 
33
from ctypes import (
 
34
    CDLL,
 
35
    POINTER,
 
36
    CFUNCTYPE,
 
37
    Structure,
 
38
    pointer,
 
39
    c_bool,
 
40
    c_long,
 
41
    c_void_p,
 
42
    c_uint32)
 
43
 
 
44
from ctypes.util import find_library
 
45
 
 
46
# pylint: disable=C0103
 
47
 
 
48
# Functions and constants below are from
 
49
# /System/Library/CoreFoundation.framework/
 
50
CoreFoundationPath = find_library("CoreFoundation")
 
51
CoreFoundation = CDLL(CoreFoundationPath)
 
52
 
 
53
# CFRunLoopRef CFRunLoopGetCurrent()
 
54
CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent
 
55
CFRunLoopGetCurrent.restype = c_void_p
 
56
CFRunLoopGetCurrent.argtypes = []
 
57
 
 
58
# void CFRelease(CFTypeRef)
 
59
CFRelease = CoreFoundation.CFRelease
 
60
CFRelease.restype = None
 
61
CFRelease.argtypes = [c_void_p]
 
62
 
 
63
# void CFRunLoopRun()
 
64
CFRunLoopRun = CoreFoundation.CFRunLoopRun
 
65
 
 
66
# const CFStringRef kCFRunLoopDefaultMode
 
67
# pylint: disable=E1101
 
68
kCFRunLoopDefaultMode = c_void_p.in_dll(CoreFoundation,
 
69
                                        "kCFRunLoopDefaultMode")
 
70
 
 
71
 
 
72
# Functions and constants below are from
 
73
# /System/Library/SystemConfiguration.framework/
 
74
SystemConfigurationPath = find_library("SystemConfiguration")
 
75
 
 
76
# SystemConfiguration abbreviated as "SC" below:
 
77
SC = CDLL(SystemConfigurationPath)
 
78
 
 
79
# "SCNetworkReachability" functions abbreviated to "SCNR*" here.
 
80
 
 
81
# SCNetworkReachabilityRef
 
82
# SCNetworkReachabilityCreateWithName(CFAllocatorRef, const char *)
 
83
SCNRCreateWithName = SC.SCNetworkReachabilityCreateWithName
 
84
SCNRCreateWithName.restype = c_void_p
 
85
 
 
86
# Boolean SCNetworkReachabilityGetFlags(SCNetworkReachabilityRef,
 
87
#                                       SCNetworkReachabilityFlags)
 
88
SCNRGetFlags = SC.SCNetworkReachabilityGetFlags
 
89
SCNRGetFlags.restype = c_bool
 
90
SCNRGetFlags.argtypes = [c_void_p,
 
91
                         POINTER(c_uint32)]
 
92
 
 
93
SCNRScheduleWithRunLoop = SC.SCNetworkReachabilityScheduleWithRunLoop
 
94
SCNRScheduleWithRunLoop.restype = c_bool
 
95
SCNRScheduleWithRunLoop.argtypes = [c_void_p,
 
96
                                    c_void_p,
 
97
                                    c_void_p]
 
98
 
 
99
# ctypes callback type to match SCNetworkReachabilityCallback
 
100
# void (*SCNetworkReachabilityCallback) (SCNetworkReachabilityRef,
 
101
#                                        SCNetworkReachabilityFlags,
 
102
#                                        void *)
 
103
SCNRCallbackType = CFUNCTYPE(None, c_void_p, c_uint32, c_void_p)
 
104
# NOTE: need to keep this reference alive as long as a callback might occur.
 
105
 
 
106
# Boolean SCNetworkReachabilitySetCallback(SCNetworkReachabilityRef,
 
107
#                                          SCNetworkReachabilityCallback,
 
108
#                                          SCNetworkReachabilityContext)
 
109
SCNRSetCallback = SC.SCNetworkReachabilitySetCallback
 
110
SCNRSetCallback.restype = c_bool
 
111
SCNRSetCallback.argtypes = [c_void_p,
 
112
                            SCNRCallbackType,
 
113
                            c_void_p]
 
114
# pylint: enable=E1101
 
115
 
 
116
 
 
117
def check_connected_state():
 
118
    """Calls Synchronous SCNR API, returns bool."""
 
119
    target = SCNRCreateWithName(None, HOSTNAME_TO_CHECK)
 
120
    if target is None:
 
121
        logger.error("Error creating network reachability reference.")
 
122
        raise NetworkFailException()
 
123
 
 
124
    flags = c_uint32(0)
 
125
    ok = SCNRGetFlags(target, pointer(flags))
 
126
    CFRelease(target)
 
127
 
 
128
    if not ok:
 
129
        logger.error("Error getting reachability status of '%s'" % \
 
130
                     HOSTNAME_TO_CHECK)
 
131
        raise NetworkFailException()
 
132
 
 
133
    return flags_say_reachable(flags.value)
 
134
 
 
135
 
 
136
def flags_say_reachable(flags):
 
137
    """Check flags returned from SCNetworkReachability API. Returns bool.
 
138
 
 
139
    Requires some logic:
 
140
    reachable_flag isn't enough on its own.
 
141
 
 
142
    A down wifi will return flags = 7, or reachable_flag and
 
143
    connection_required_flag, meaning that the host *would be*
 
144
    reachable, but you need a connection first.  (And then you'd
 
145
    presumably be best off checking again.)
 
146
    """
 
147
    # values from SCNetworkReachability.h
 
148
    reachable_flag = 1 << 1
 
149
    connection_required_flag = 1 << 2
 
150
 
 
151
    if flags & connection_required_flag:
 
152
        return False
 
153
    elif flags & reachable_flag:
 
154
        return True
 
155
    else:
 
156
        return False
 
157
 
 
158
 
 
159
class SCNRContext(Structure):
 
160
 
 
161
    """A struct to send as SCNetworkReachabilityContext to SCNRSetCallback.
 
162
 
 
163
    We don't use the fields currently.
 
164
    """
 
165
 
 
166
    _fields_ = [("version", c_long),
 
167
                ("info", c_void_p),
 
168
                ("retain", c_void_p),           # func ptr
 
169
                ("release", c_void_p),          # func ptr
 
170
                ("copyDescription", c_void_p)]  # func ptr
 
171
 
32
172
 
33
173
class NetworkManagerState(object):
34
 
    """All likely to change very soon."""
 
174
 
 
175
    """Probe Network State and receive callbacks on changes.
 
176
 
 
177
    This class uses both synchronous and async API from the
 
178
    SystemConfiguration framework.
 
179
 
 
180
    To use: Initialize with a callback function, then call
 
181
    find_online_state. The callback will be called once immediately
 
182
    with the current state and then only on state changes.
 
183
 
 
184
    Any exceptions in checking state will result in the callback being
 
185
    called with UNKNOWN. At this point the listening thread is no
 
186
    longer runing, and a new NetworkManagerState should be created.
 
187
 
 
188
    NOTE: the callback will be called from the separate listening
 
189
    thread, except for the first call.
 
190
    """
35
191
 
36
192
    def __init__(self, result_cb):
37
 
        """Initialize this instance with a result and error callbacks."""
 
193
        """Initialize and save result callback function.
 
194
 
 
195
        result_cb should take one argument, a networkstate object.
 
196
 
 
197
        The callback will be called with one of ONLINE, OFFLINE, or
 
198
        UNKNOWN, as defined in networkstates.py.
 
199
        """
38
200
        self.result_cb = result_cb
39
 
        self.state_signal = None
40
 
 
41
 
    def call_result_cb(self, state):
42
 
        """Return the state thru the result callback."""
43
 
        self.result_cb(state)
44
 
 
45
 
    def got_state(self, state):
46
 
        """Not actually called by DBus when the state is retrieved from NM."""
47
 
        if state in NM_STATE_CONNECTED_LIST:
48
 
            self.call_result_cb(ONLINE)
49
 
        elif state in NM_STATE_CONNECTING_LIST:
50
 
            logger.debug("Currently connecting, waiting for signal")
51
 
        else:
52
 
            self.call_result_cb(OFFLINE)
53
 
 
54
 
    def got_error(self, error):
55
 
        """Not actually called by DBus when the state is retrieved from NM."""
56
 
        logger.error("Error contacting NetworkManager: %s" % \
57
 
                     str(error))
58
 
        self.call_result_cb(UNKNOWN)
59
 
 
60
 
    def state_changed(self, state):
61
 
        """Called when a signal is emmited by Network Manager."""
62
 
        if int(state) in NM_STATE_CONNECTED_LIST:
63
 
            self.call_result_cb(ONLINE)
64
 
        elif int(state) in NM_STATE_DISCONNECTED_LIST:
65
 
            self.call_result_cb(OFFLINE)
66
 
        else:
67
 
            logger.debug("Not yet connected: continuing to wait")
 
201
        self.listener_thread = None
 
202
 
 
203
    def _state_changed(self, flags):
 
204
        """Testable callback called by reachability_state_changed_cb.
 
205
 
 
206
        Used because reachability_state_changed_cb has to have a
 
207
        particular method signature.
 
208
 
 
209
        Clients should not call this method.
 
210
        """
 
211
        if flags_say_reachable(flags):
 
212
            self.result_cb(ONLINE)
 
213
        else:
 
214
            self.result_cb(OFFLINE)
 
215
 
 
216
    def _listen_on_separate_thread(self):
 
217
        """In separate thread, setup callback and listen for changes.
 
218
 
 
219
        On error, calls result_cb(UNKNOWN) and returns.
 
220
        """
 
221
 
 
222
        def reachability_state_changed_cb(targetref, flags, info):
 
223
            """Callback for SCNetworkReachability API
 
224
 
 
225
            This callback is passed to the SCNetworkReachability API,
 
226
            so its method signature has to be exactly this. Therefore,
 
227
            we declare it here and just call _state_changed with
 
228
            flags."""
 
229
            self._state_changed(flags)
 
230
 
 
231
        c_callback = SCNRCallbackType(reachability_state_changed_cb)
 
232
        context = SCNRContext(0, None, None, None, None)
 
233
 
 
234
        target = SCNRCreateWithName(None, HOSTNAME_TO_CHECK)
 
235
        if target is None:
 
236
            logger.error("Error creating SCNetworkReachability target")
 
237
            self.result_cb(UNKNOWN)
 
238
            return
 
239
 
 
240
        ok = SCNRSetCallback(target, c_callback, pointer(context))
 
241
        if not ok:
 
242
            logger.error("error setting SCNetworkReachability callback")
 
243
            CFRelease(target)
 
244
            self.result_cb(UNKNOWN)
 
245
            return
 
246
 
 
247
        ok = SCNRScheduleWithRunLoop(target,
 
248
                                     CFRunLoopGetCurrent(),
 
249
                                     kCFRunLoopDefaultMode)
 
250
        if not ok:
 
251
            logger.error("error scheduling on runloop: SCNetworkReachability")
 
252
            CFRelease(target)
 
253
            self.result_cb(UNKNOWN)
 
254
            return
 
255
 
 
256
        CFRunLoopRun()
 
257
 
 
258
        CFRelease(target)               # won't happen
 
259
 
 
260
    def _start_listening_thread(self):
 
261
        """Start the separate listener thread.
 
262
 
 
263
        Currently will not start one more than once.
 
264
        Should be OK because we don't expect errors the listen method.
 
265
 
 
266
        To add more error handling support, you could either add a
 
267
        call to join and re-start, or client could just create a new
 
268
        NetworkManagerState.
 
269
        """
 
270
 
 
271
        if self.listener_thread is None:
 
272
            self.listener_thread = Thread(
 
273
                target=self._listen_on_separate_thread,
 
274
                name="Ubuntu SSO Network Connection Monitor")
 
275
            self.listener_thread.daemon = True
 
276
            self.listener_thread.start()
68
277
 
69
278
    def find_online_state(self):
70
 
        """Get the network state and return it thru the set callback."""
 
279
        """Calls callback with current state. Starts listening thread."""
71
280
        try:
72
 
            self.state_changed(NM_STATE_CONNECTED_GLOBAL)
73
 
        except Exception, e:  # pylint: disable=W0703
74
 
            self.got_error(e)
 
281
            if check_connected_state():
 
282
                self.result_cb(ONLINE)
 
283
            else:
 
284
                self.result_cb(OFFLINE)
 
285
 
 
286
        except Exception:               # pylint: disable=W0703
 
287
            logger.exception("Getting state from SCNetworkReachability")
 
288
            self.result_cb(UNKNOWN)
 
289
            return                      # don't start thread on error
 
290
 
 
291
        self._start_listening_thread()
75
292
 
76
293
 
77
294
def is_machine_connected():
78
 
    """Return a deferred that when fired, returns if the machine is online."""
79
 
    d = defer.Deferred()
80
 
 
81
 
    def got_state(state):
82
 
        """The state was retrieved from the Network Manager."""
83
 
        result = int(state) in NM_STATE_CONNECTED_LIST
84
 
        d.callback(result)
85
 
 
86
 
    # pylint: disable=W0702, W0703
 
295
    """Return a deferred that when fired, returns online state as a bool.
 
296
 
 
297
    Raises NetworkFailException for errors.
 
298
    """
87
299
    try:
88
 
        network = NetworkManagerState(got_state)
89
 
        network.find_online_state()
90
 
    except Exception, e:
91
 
        logger.exception('is_machine_connected failed with:')
92
 
        d.errback(NetworkFailException(e))
93
 
    # pylint: enable=W0702, W0703
94
 
 
95
 
    return d
 
300
        return defer.succeed(check_connected_state())
 
301
    except Exception, e:                # pylint: disable=W0703
 
302
        logger.exception("Exception calling check_connected_state:")
 
303
        return defer.fail(NetworkFailException(e))