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.
18
is_machine_connected(): (deferred) returns connected state as bool
19
NetworkManagerState: class with listening thread, calls back with state changes
18
22
from twisted.internet import defer
24
from threading import Thread
20
26
from ubuntu_sso.networkstate import NetworkFailException
21
from ubuntu_sso.networkstate.networkstates import (
22
ONLINE, OFFLINE, UNKNOWN,
24
NM_STATE_CONNECTING_LIST,
25
NM_STATE_CONNECTED_LIST,
26
NM_STATE_DISCONNECTED_LIST,
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
HOSTNAME_TO_CHECK = 'one.ubuntu.com'
44
from ctypes.util import find_library
46
# pylint: disable=C0103
48
# Functions and constants below are from
49
# /System/Library/CoreFoundation.framework/
50
CoreFoundationPath = find_library("CoreFoundation")
51
CoreFoundation = CDLL(CoreFoundationPath)
53
# CFRunLoopRef CFRunLoopGetCurrent()
54
CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent
55
CFRunLoopGetCurrent.restype = c_void_p
56
CFRunLoopGetCurrent.argtypes = []
58
# void CFRelease(CFTypeRef)
59
CFRelease = CoreFoundation.CFRelease
60
CFRelease.restype = None
61
CFRelease.argtypes = [c_void_p]
64
CFRunLoopRun = CoreFoundation.CFRunLoopRun
66
# const CFStringRef kCFRunLoopDefaultMode
67
# pylint: disable=E1101
68
kCFRunLoopDefaultMode = c_void_p.in_dll(CoreFoundation,
69
"kCFRunLoopDefaultMode")
72
# Functions and constants below are from
73
# /System/Library/SystemConfiguration.framework/
74
SystemConfigurationPath = find_library("SystemConfiguration")
76
# SystemConfiguration abbreviated as "SC" below:
77
SC = CDLL(SystemConfigurationPath)
79
# "SCNetworkReachability" functions abbreviated to "SCNR*" here.
81
# SCNetworkReachabilityRef
82
# SCNetworkReachabilityCreateWithName(CFAllocatorRef, const char *)
83
SCNRCreateWithName = SC.SCNetworkReachabilityCreateWithName
84
SCNRCreateWithName.restype = c_void_p
86
# Boolean SCNetworkReachabilityGetFlags(SCNetworkReachabilityRef,
87
# SCNetworkReachabilityFlags)
88
SCNRGetFlags = SC.SCNetworkReachabilityGetFlags
89
SCNRGetFlags.restype = c_bool
90
SCNRGetFlags.argtypes = [c_void_p,
93
SCNRScheduleWithRunLoop = SC.SCNetworkReachabilityScheduleWithRunLoop
94
SCNRScheduleWithRunLoop.restype = c_bool
95
SCNRScheduleWithRunLoop.argtypes = [c_void_p,
99
# ctypes callback type to match SCNetworkReachabilityCallback
100
# void (*SCNetworkReachabilityCallback) (SCNetworkReachabilityRef,
101
# SCNetworkReachabilityFlags,
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.
106
# Boolean SCNetworkReachabilitySetCallback(SCNetworkReachabilityRef,
107
# SCNetworkReachabilityCallback,
108
# SCNetworkReachabilityContext)
109
SCNRSetCallback = SC.SCNetworkReachabilitySetCallback
110
SCNRSetCallback.restype = c_bool
111
SCNRSetCallback.argtypes = [c_void_p,
114
# pylint: enable=E1101
117
def check_connected_state():
118
"""Calls Synchronous SCNR API, returns bool."""
119
target = SCNRCreateWithName(None, HOSTNAME_TO_CHECK)
121
logger.error("Error creating network reachability reference.")
122
raise NetworkFailException()
125
ok = SCNRGetFlags(target, pointer(flags))
129
logger.error("Error getting reachability status of '%s'" % \
131
raise NetworkFailException()
133
return flags_say_reachable(flags.value)
136
def flags_say_reachable(flags):
137
"""Check flags returned from SCNetworkReachability API. Returns bool.
140
reachable_flag isn't enough on its own.
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.)
147
# values from SCNetworkReachability.h
148
reachable_flag = 1 << 1
149
connection_required_flag = 1 << 2
151
if flags & connection_required_flag:
153
elif flags & reachable_flag:
159
class SCNRContext(Structure):
161
"""A struct to send as SCNetworkReachabilityContext to SCNRSetCallback.
163
We don't use the fields currently.
166
_fields_ = [("version", c_long),
168
("retain", c_void_p), # func ptr
169
("release", c_void_p), # func ptr
170
("copyDescription", c_void_p)] # func ptr
33
173
class NetworkManagerState(object):
34
"""All likely to change very soon."""
175
"""Probe Network State and receive callbacks on changes.
177
This class uses both synchronous and async API from the
178
SystemConfiguration framework.
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.
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.
188
NOTE: the callback will be called from the separate listening
189
thread, except for the first call.
36
192
def __init__(self, result_cb):
37
"""Initialize this instance with a result and error callbacks."""
193
"""Initialize and save result callback function.
195
result_cb should take one argument, a networkstate object.
197
The callback will be called with one of ONLINE, OFFLINE, or
198
UNKNOWN, as defined in networkstates.py.
38
200
self.result_cb = result_cb
39
self.state_signal = None
41
def call_result_cb(self, state):
42
"""Return the state thru the result callback."""
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")
52
self.call_result_cb(OFFLINE)
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" % \
58
self.call_result_cb(UNKNOWN)
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)
67
logger.debug("Not yet connected: continuing to wait")
201
self.listener_thread = None
203
def _state_changed(self, flags):
204
"""Testable callback called by reachability_state_changed_cb.
206
Used because reachability_state_changed_cb has to have a
207
particular method signature.
209
Clients should not call this method.
211
if flags_say_reachable(flags):
212
self.result_cb(ONLINE)
214
self.result_cb(OFFLINE)
216
def _listen_on_separate_thread(self):
217
"""In separate thread, setup callback and listen for changes.
219
On error, calls result_cb(UNKNOWN) and returns.
222
def reachability_state_changed_cb(targetref, flags, info):
223
"""Callback for SCNetworkReachability API
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
229
self._state_changed(flags)
231
c_callback = SCNRCallbackType(reachability_state_changed_cb)
232
context = SCNRContext(0, None, None, None, None)
234
target = SCNRCreateWithName(None, HOSTNAME_TO_CHECK)
236
logger.error("Error creating SCNetworkReachability target")
237
self.result_cb(UNKNOWN)
240
ok = SCNRSetCallback(target, c_callback, pointer(context))
242
logger.error("error setting SCNetworkReachability callback")
244
self.result_cb(UNKNOWN)
247
ok = SCNRScheduleWithRunLoop(target,
248
CFRunLoopGetCurrent(),
249
kCFRunLoopDefaultMode)
251
logger.error("error scheduling on runloop: SCNetworkReachability")
253
self.result_cb(UNKNOWN)
258
CFRelease(target) # won't happen
260
def _start_listening_thread(self):
261
"""Start the separate listener thread.
263
Currently will not start one more than once.
264
Should be OK because we don't expect errors the listen method.
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
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()
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."""
72
self.state_changed(NM_STATE_CONNECTED_GLOBAL)
73
except Exception, e: # pylint: disable=W0703
281
if check_connected_state():
282
self.result_cb(ONLINE)
284
self.result_cb(OFFLINE)
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
291
self._start_listening_thread()
77
294
def is_machine_connected():
78
"""Return a deferred that when fired, returns if the machine is online."""
82
"""The state was retrieved from the Network Manager."""
83
result = int(state) in NM_STATE_CONNECTED_LIST
86
# pylint: disable=W0702, W0703
295
"""Return a deferred that when fired, returns online state as a bool.
297
Raises NetworkFailException for errors.
88
network = NetworkManagerState(got_state)
89
network.find_online_state()
91
logger.exception('is_machine_connected failed with:')
92
d.errback(NetworkFailException(e))
93
# pylint: enable=W0702, W0703
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))