Merge lp:~mikemc/ubuntuone-control-panel/launchdaemon into lp:ubuntuone-control-panel

Proposed by Mike McCracken
Status: Merged
Approved by: Mike McCracken
Approved revision: 361
Merged at revision: 360
Proposed branch: lp:~mikemc/ubuntuone-control-panel/launchdaemon
Merge into: lp:ubuntuone-control-panel
Diff against target: 663 lines (+601/-0)
2 files modified
ubuntuone/controlpanel/utils/darwin.py (+375/-0)
ubuntuone/controlpanel/utils/tests/test_darwin.py (+226/-0)
To merge this branch: bzr merge lp:~mikemc/ubuntuone-control-panel/launchdaemon
Reviewer Review Type Date Requested Status
Manuel de la Peña (community) Approve
Roberto Alsina (community) Approve
Review via email: mp+124847@code.launchpad.net

Commit message

- Darwin: use ServiceManagement API to securely install, check versions, and upgrade root fsevents daemon.

Description of the change

- Darwin: use ServiceManagement API to securely install, check versions, and upgrade root fsevents daemon.

** NOTES ABOUT WHAT IS GOING ON HERE **

This calls a bunch of CoreFoundation APIs using ctypes so the first 140 lines or so of the diff are ctypes boilerplate that sets up the types for the CF functions.

start reading at check_and_install_fsevents_daemon().
This function compares the version of the daemon that's packaged in the app's bundle (at daemon_path) to the version that's installed, if it exists.

If the installed one is the same, we're done. If it's newer, we raise an exception.
If it's older, we remove it. Then we install the new one.

The twist in this method is that we have to get authorization (show a dialog box) once to remove the old job, and then kill that authorization (calling AuthorizationFree with the kAuthorizationFlagDestroyRights flag, to remove it from the cache as well as freeing the authref's memory) before we can get the **same** authorization again to install the new job. This is annoying and I only figured it out via trial and error, one Apple dev rep said it shouldn't be necessary, but it's the only way I could get it to work.

Nothing else is all that tricky, the other functions are just wrapping a CoreFoundation call and checking the error message.

-- about CFShow: I had half a day of unicode annoyance trying to get the error description copied into a printable python string, and gave up and used CFShow. It just prints the description to stdout. It's the CF equivalent of NSLog or 'print'. It's sufficient to get the error messages somewhere readable.

-- About CFRelease(): the rule is that if you call a function that has "create" or "copy" in its name, then you'll need to release its return value. If you call a function that has "get", you don't own it and shouldn't release it.

-- about get_authorization(): the name of the right we request here and the flags we pass are from apple's sample code. If you check their docs, you might notice that SMJobRemove should require a different right. The docs are wrong or at least incomplete. We only need kSMRightBlessPrivilegedHelper.

** notes on testing **

This includes new tests in ubuntuone/controlpanel/utils/tests/test_darwin.py.
Those tests pass for me, but trunk still has some failing tests in share_links.py, which are unrelated.
pylint doesn't work right for me on darwin, but this branch adds no new pyflakes errors.

To IRL-test, talk to mmcc on IRC, or use the bundles I've built.
Building the bundle correctly with code signing - but not using my cert - will require changing the cert CN in the code for the daemon (in a branch not in trunk there) as well as the setup-mac script.

Using my bundles, here's how to test:
(Note that you need to have the line saying 'fs_monitor.default' end with 'daemon' not 'default' in syncdaemon.conf for the code to run the monitor. It'll still try to install either way.)

Test that running the app installs a daemon using this:
% sudo launchctl list com.ubuntu.one.fsevents

remove it with
% sudo launchctl remove com.ubuntu.one.fsevents

then install an old version of the daemon by running an old build of the app. Not the alpha 1, it didn't have this branch. -- Again, talk to me, I have an old one laying around.

Then run the new one again, with U1_DEBUG=1 and look at the log, does it say it found 1.0 and replaced it with 1.1? Then it worked.

Also, does the file sync work? Only version 1.1 of the fsevents daemon correctly sends events to the client.

To post a comment you must log in.
Revision history for this message
Mike McCracken (mikemc) wrote :

If you want to review this before I wake up, here's a signed build with this branch that just uploaded itself: http://ubuntuone.com/4KKHgfhAK46ABkOuvrdZci

Revision history for this message
Roberto Alsina (ralsina) wrote :

Tested IRL and read the code. AFAICS it looks good.

review: Approve
Revision history for this message
Manuel de la Peña (mandel) wrote :

In the following code:

354 + try:
355 + remove_fsevents_daemon(authRef)
356 + except Exception, e:
357 + logger.exception("Problem removing running daemon: %r" % e)
358 +
359 + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights)

shouldn't the Free be called in a finally clause?

In the InstallDaemonTestCase you can move the following to the setup instead of calling it in each of the tests:

469 + self._patch_and_track(utils.darwin,
470 + [('get_authorization', 'Fake AuthRef'),
471 + ('remove_fsevents_daemon', None),
472 + ('install_fsevents_daemon', None),
473 + ('AuthorizationFree', None)])

review: Needs Fixing
Revision history for this message
Mike McCracken (mikemc) wrote :

We just log that exception and don't re-raise it, so no - we will always call that AuthorizationFree.

The repeated patch call is now in setup.

Revision history for this message
Manuel de la Peña (mandel) :
review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (163.0 KiB)

The attempt to merge lp:~mikemc/ubuntuone-control-panel/launchdaemon into lp:ubuntuone-control-panel failed. Below is the output from the failed tests.

*** Running DBus test suite ***
ubuntuone.controlpanel.dbustests.test_dbus_service
  BaseTestCase
    runTest ... [OK]
  DBusServiceMainTestCase
    test_dbus_service_cant_register ... Control panel backend already running.
                                   [OK]
    test_dbus_service_main ... [OK]
  DBusServiceTestCase
    test_cant_register_twice ... [SKIPPED]
    test_dbus_busname_created ... [OK]
    test_error_handler_default ... [OK]
    test_error_handler_with_exception ... [OK]
    test_error_handler_with_failure ... [OK]
    test_error_handler_with_non_string_dict ... [OK]
    test_error_handler_with_string_dict ... [OK]
    test_register_service ... [OK]
  FileSyncTestCase
    test_file_sync_status_changed ... [OK]
    test_file_sync_status_disabled ... [OK]
    test_file_sync_status_disconnected ... [OK]
    test_file_sync_status_error ... [OK]
    test_file_sync_status_idle ... [OK]
    test_file_sync_status_starting ... [OK]
    test_file_sync_status_stopped ... [OK]
    test_file_sync_status_syncing ... [OK]
    test_file_sync_status_unknown ... [OK]
    test_status_changed_handler ... [OK]
    test_status_changed_handler_after_status_requested ... [OK]
    test_status_changed_handler_after_status_requested_twice ... [OK]
  OperationsAuthErrorTestCase
    test_account_info_returned ... [OK]
    test_change_device_settings ... [OK]
    test_change_replication_settings ... [OK]
    test_change_volume_settings ... [OK]
    test_connect_files ... [OK]
    test_devices_info_returned ... [OK]
    test_disable_files ... [OK]
    test_disconnect_files ... [OK]
    test_enable_files ... [OK]
    test_remove_device ... [OK]
    test_replications_info ... [OK]
    test_restart_files ... [OK]
    test_star...

361. By Mike McCracken

fix lint errors

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntuone/controlpanel/utils/darwin.py'
2--- ubuntuone/controlpanel/utils/darwin.py 2012-08-06 16:47:44 +0000
3+++ ubuntuone/controlpanel/utils/darwin.py 2012-09-28 18:45:25 +0000
4@@ -20,15 +20,322 @@
5 import shutil
6 import sys
7
8+from ctypes import (
9+ byref,
10+ CDLL,
11+ POINTER,
12+ Structure,
13+ c_bool,
14+ c_char_p,
15+ c_double,
16+ c_int32,
17+ c_long,
18+ c_uint32,
19+ c_void_p)
20+
21+from ctypes.util import find_library
22+
23 from twisted.internet import defer
24
25 from dirspec.basedir import save_config_path
26 from ubuntuone.controlpanel.logger import setup_logging
27
28 logger = setup_logging('utils.darwin')
29+
30 AUTOUPDATE_BIN_NAME = 'autoupdate-darwin'
31 UNINSTALL_BIN_NAME = 'uninstall-darwin'
32
33+FSEVENTSD_JOB_LABEL = "com.ubuntu.one.fsevents"
34+
35+# pylint: disable=C0103
36+CFPath = find_library("CoreFoundation")
37+CF = CDLL(CFPath)
38+
39+CFRelease = CF.CFRelease
40+CFRelease.restype = None
41+CFRelease.argtypes = [c_void_p]
42+
43+kCFStringEncodingUTF8 = 0x08000100
44+
45+
46+class CFRange(Structure):
47+ """CFRange Struct"""
48+ _fields_ = [("location", c_long),
49+ ("length", c_long)]
50+
51+CFShow = CF.CFShow
52+CFShow.argtypes = [c_void_p]
53+CFShow.restype = None
54+
55+CFStringCreateWithCString = CF.CFStringCreateWithCString
56+CFStringCreateWithCString.restype = c_void_p
57+CFStringCreateWithCString.argtypes = [c_void_p, c_void_p, c_uint32]
58+
59+kCFAllocatorDefault = c_void_p()
60+
61+CFErrorCopyDescription = CF.CFErrorCopyDescription
62+CFErrorCopyDescription.restype = c_void_p
63+CFErrorCopyDescription.argtypes = [c_void_p]
64+
65+CFDictionaryGetValue = CF.CFDictionaryGetValue
66+CFDictionaryGetValue.restype = c_void_p
67+CFDictionaryGetValue.argtypes = [c_void_p, c_void_p]
68+
69+CFArrayGetValueAtIndex = CF.CFArrayGetValueAtIndex
70+CFArrayGetValueAtIndex.restype = c_void_p
71+CFArrayGetValueAtIndex.argtypes = [c_void_p, c_uint32]
72+
73+CFURLCreateWithFileSystemPath = CF.CFURLCreateWithFileSystemPath
74+CFURLCreateWithFileSystemPath.restype = c_void_p
75+CFURLCreateWithFileSystemPath.argtypes = [c_void_p, c_void_p, c_uint32, c_bool]
76+
77+CFBundleCopyInfoDictionaryForURL = CF.CFBundleCopyInfoDictionaryForURL
78+CFBundleCopyInfoDictionaryForURL.restype = c_void_p
79+CFBundleCopyInfoDictionaryForURL.argtypes = [c_void_p]
80+
81+CFStringGetDoubleValue = CF.CFStringGetDoubleValue
82+CFStringGetDoubleValue.restype = c_double
83+CFStringGetDoubleValue.argtypes = [c_void_p]
84+
85+SecurityPath = find_library("Security")
86+Security = CDLL(SecurityPath)
87+
88+AuthorizationCreate = Security.AuthorizationCreate
89+AuthorizationCreate.restype = c_int32
90+AuthorizationCreate.argtypes = [c_void_p, c_void_p, c_int32, c_void_p]
91+
92+AuthorizationFree = Security.AuthorizationFree
93+AuthorizationFree.restype = c_uint32
94+AuthorizationFree.argtypes = [c_void_p, c_uint32]
95+
96+kAuthorizationFlagDefaults = 0
97+kAuthorizationFlagInteractionAllowed = 1 << 0
98+kAuthorizationFlagExtendRights = 1 << 1
99+kAuthorizationFlagDestroyRights = 1 << 3
100+kAuthorizationFlagPreAuthorize = 1 << 4
101+
102+kAuthorizationEmptyEnvironment = None
103+
104+errAuthorizationSuccess = 0
105+errAuthorizationDenied = -60005
106+errAuthorizationCanceled = -60006
107+errAuthorizationInteractionNotAllowed = -60007
108+
109+ServiceManagementPath = find_library("ServiceManagement")
110+ServiceManagement = CDLL(ServiceManagementPath)
111+
112+kSMRightBlessPrivilegedHelper = "com.apple.ServiceManagement.blesshelper"
113+kSMRightModifySystemDaemons = "com.apple.ServiceManagement.daemons.modify"
114+
115+# pylint: disable=E1101
116+# c_void_p has no "in_dll" member:
117+kSMDomainSystemLaunchd = c_void_p.in_dll(ServiceManagement,
118+ "kSMDomainSystemLaunchd")
119+# pylint: enable=E1101
120+
121+SMJobBless = ServiceManagement.SMJobBless
122+SMJobBless.restype = c_bool
123+SMJobBless.argtypes = [c_void_p, c_void_p, c_void_p, POINTER(c_void_p)]
124+
125+SMJobRemove = ServiceManagement.SMJobRemove
126+SMJobRemove.restype = c_bool
127+SMJobRemove.argtypes = [c_void_p, c_void_p, c_void_p, c_bool,
128+ POINTER(c_void_p)]
129+
130+SMJobCopyDictionary = ServiceManagement.SMJobCopyDictionary
131+SMJobCopyDictionary.restype = c_void_p
132+SMJobCopyDictionary.argtypes = [c_void_p, c_void_p]
133+
134+
135+class AuthorizationItem(Structure):
136+ """AuthorizationItem Struct"""
137+ _fields_ = [("name", c_char_p),
138+ ("valueLength", c_uint32),
139+ ("value", c_void_p),
140+ ("flags", c_uint32)]
141+
142+
143+class AuthorizationRights(Structure):
144+ """AuthorizationRights Struct"""
145+ _fields_ = [("count", c_uint32),
146+ # * 1 here is specific to our use below
147+ ("items", POINTER(AuthorizationItem))]
148+
149+
150+class DaemonInstallException(Exception):
151+ """Error securely installing daemon."""
152+
153+
154+class AuthUserCanceledException(Exception):
155+ """The user canceled the authorization."""
156+
157+
158+class AuthFailedException(Exception):
159+ """The authorization faild for some reason."""
160+
161+
162+class DaemonRemoveException(Exception):
163+ """Error removing existing daemon."""
164+
165+
166+class DaemonVersionMismatchException(Exception):
167+ """Incompatible version of the daemon found."""
168+
169+
170+def create_cfstr(s):
171+ """Creates a CFString from a python string.
172+
173+ Note - because this is a "create" function, you have to CFRelease
174+ the returned string.
175+ """
176+ return CFStringCreateWithCString(kCFAllocatorDefault,
177+ s.encode('utf8'),
178+ kCFStringEncodingUTF8)
179+
180+
181+def get_bundle_version(path_cfstr):
182+ """Returns a float version from the plist of the given bundle.
183+
184+ path_cfstr must be a CFStringRef.
185+ """
186+ cfurl = CFURLCreateWithFileSystemPath(None, # use default allocator
187+ path_cfstr,
188+ 0, # POSIX style path
189+ False)
190+ plist = CFBundleCopyInfoDictionaryForURL(cfurl)
191+ ver_key_cfstr = create_cfstr("CFBundleVersion")
192+ version_cfstr = CFDictionaryGetValue(plist, ver_key_cfstr)
193+
194+ version = CFStringGetDoubleValue(version_cfstr)
195+
196+ CFRelease(cfurl)
197+ CFRelease(plist)
198+ CFRelease(ver_key_cfstr)
199+
200+ return version
201+
202+
203+def get_fsevents_daemon_installed_version():
204+ """Returns helper version or None if helper is not installed."""
205+
206+ label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL)
207+ job_data_cfdict = SMJobCopyDictionary(kSMDomainSystemLaunchd,
208+ label_cfstr)
209+ CFRelease(label_cfstr)
210+
211+ if job_data_cfdict is not None:
212+ key_cfstr = create_cfstr("ProgramArguments")
213+ args_cfarray = CFDictionaryGetValue(job_data_cfdict, key_cfstr)
214+
215+ path_cfstr = CFArrayGetValueAtIndex(args_cfarray, 0)
216+ version = get_bundle_version(path_cfstr)
217+
218+ # only release things "copied" or "created", not "got".
219+ CFRelease(job_data_cfdict)
220+ CFRelease(key_cfstr)
221+ return version
222+
223+ return None
224+
225+
226+def get_authorization():
227+ """Get authorization to remove and/or install daemons."""
228+
229+ # pylint: disable=W0201
230+ authItemBless = AuthorizationItem()
231+ authItemBless.name = kSMRightBlessPrivilegedHelper
232+ authItemBless.valueLength = 0
233+ authItemBless.value = None
234+ authItemBless.flags = 0
235+
236+ authRights = AuthorizationRights()
237+ authRights.count = 1
238+ authRights.items = (AuthorizationItem * 1)(authItemBless)
239+
240+ flags = (kAuthorizationFlagDefaults |
241+ kAuthorizationFlagInteractionAllowed |
242+ kAuthorizationFlagPreAuthorize |
243+ kAuthorizationFlagExtendRights)
244+
245+ authRef = c_void_p()
246+
247+ status = AuthorizationCreate(byref(authRights),
248+ kAuthorizationEmptyEnvironment,
249+ flags,
250+ byref(authRef))
251+
252+ if status != errAuthorizationSuccess:
253+
254+ if status == errAuthorizationInteractionNotAllowed:
255+ raise AuthFailedException("Authorization failed: "
256+ "interaction not allowed.")
257+
258+ elif status == errAuthorizationDenied:
259+ raise AuthFailedException("Authorization failed: auth denied.")
260+
261+ else:
262+ raise AuthUserCanceledException()
263+
264+ if authRef is None:
265+ raise AuthFailedException("No authRef from AuthorizationCreate: %r"
266+ % status)
267+ return authRef
268+
269+
270+def install_fsevents_daemon(authRef):
271+ """Call SMJobBless to install daemon.
272+
273+ No return, raises on error.
274+ """
275+
276+ desc_cfstr = None
277+
278+ try:
279+ error = c_void_p()
280+
281+ label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL)
282+ ok = SMJobBless(kSMDomainSystemLaunchd,
283+ label_cfstr,
284+ authRef,
285+ byref(error))
286+ CFRelease(label_cfstr)
287+
288+ if not ok:
289+ desc_cfstr = CFErrorCopyDescription(error)
290+ CFShow(desc_cfstr)
291+ raise DaemonInstallException("SMJobBless error (see above)")
292+
293+ finally:
294+ if desc_cfstr:
295+ CFRelease(desc_cfstr)
296+
297+
298+def remove_fsevents_daemon(authRef):
299+ """Call SMJobRemove to remove daemon.
300+
301+ No return, raises on error.
302+ """
303+ desc_cfstr = None
304+ try:
305+ error = c_void_p()
306+
307+ label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL)
308+ ok = SMJobRemove(kSMDomainSystemLaunchd,
309+ label_cfstr,
310+ authRef,
311+ True,
312+ byref(error))
313+
314+ CFRelease(label_cfstr)
315+
316+ if not ok:
317+ desc_cfstr = CFErrorCopyDescription(error)
318+ CFShow(desc_cfstr)
319+ raise DaemonRemoveException("SMJobRemove error (see above)")
320+ finally:
321+ if desc_cfstr:
322+ CFRelease(desc_cfstr)
323+
324
325 def add_to_autostart():
326 """Add syncdaemon to the session's autostart."""
327@@ -50,6 +357,72 @@
328 return folders
329
330
331+def check_and_install_fsevents_daemon(main_app_dir):
332+ """Checks version of running daemon, maybe installs.
333+
334+ 'main_app_dir' is the path to the running app.
335+
336+ This will securely install the daemon bundled with the running app
337+ if there is no currently installed one, or upgrade it if the
338+ installed one is old. If the installed one is newer, it raises
339+ a DaemonVersionMismatchException.
340+ """
341+
342+ daemon_path = os.path.join(main_app_dir, 'Contents',
343+ 'Library', 'LaunchServices',
344+ FSEVENTSD_JOB_LABEL)
345+ bundled_version = get_bundle_version(create_cfstr(daemon_path))
346+
347+ installed_version = get_fsevents_daemon_installed_version()
348+
349+ if installed_version == bundled_version:
350+ logger.info("Current fsevents daemon already installed: version %r" %
351+ installed_version)
352+ return
353+
354+ if installed_version > bundled_version:
355+ desc = ("Found newer fsevents daemon:"
356+ " installed %r > bundled version %r." %
357+ (installed_version, bundled_version))
358+ logger.error(desc)
359+ raise DaemonVersionMismatchException(desc)
360+
361+ authRef = get_authorization()
362+
363+ if (installed_version is not None and
364+ installed_version < bundled_version):
365+ logger.info("Found installed daemon version %r < %r, removing." %
366+ (installed_version, bundled_version))
367+
368+ try:
369+ remove_fsevents_daemon(authRef)
370+ except DaemonRemoveException, e:
371+ logger.exception("Problem removing running daemon: %r" % e)
372+
373+ AuthorizationFree(authRef, kAuthorizationFlagDestroyRights)
374+
375+ logger.info("Installing daemon version %r" % bundled_version)
376+
377+ try:
378+ authRef = get_authorization()
379+ install_fsevents_daemon(authRef)
380+
381+ installed_version = get_fsevents_daemon_installed_version()
382+
383+ if installed_version:
384+ logger.info("Installed fsevents daemon successfully: version %r" %
385+ installed_version)
386+ else:
387+ logger.error("Error installing fsevents daemon, see system.log.")
388+
389+ except DaemonInstallException, e:
390+ logger.exception("Exception in fsevents daemon installation: %r" % e)
391+ raise e
392+
393+ finally:
394+ AuthorizationFree(authRef, kAuthorizationFlagDestroyRights)
395+
396+
397 def install_config_and_daemons():
398 """Install required data files and fsevents daemon.
399
400@@ -80,6 +453,8 @@
401 if not os.path.exists(dest_path):
402 shutil.copyfile(src_path, dest_path)
403
404+ check_and_install_fsevents_daemon(main_app_dir)
405+
406
407 def perform_update():
408 """Spawn the autoupdate process and call the stop function."""
409
410=== modified file 'ubuntuone/controlpanel/utils/tests/test_darwin.py'
411--- ubuntuone/controlpanel/utils/tests/test_darwin.py 2012-08-06 16:47:44 +0000
412+++ ubuntuone/controlpanel/utils/tests/test_darwin.py 2012-09-28 18:45:25 +0000
413@@ -19,6 +19,9 @@
414 import os
415 import sys
416
417+from collections import defaultdict
418+from functools import partial
419+
420 from twisted.internet import defer
421
422 from ubuntuone.controlpanel import utils
423@@ -28,6 +31,28 @@
424 # pylint: disable=W0212
425
426
427+class CallRecordingTestCase(TestCase):
428+ """Base class with multi-call checker."""
429+
430+ @defer.inlineCallbacks
431+ def setUp(self):
432+ """Set up call checker."""
433+ yield super(CallRecordingTestCase, self).setUp()
434+ self._called = defaultdict(list)
435+
436+ def _patch_and_track(self, module, funcs):
437+ """Record calls along with function name."""
438+
439+ def record_call(fname, retval, *args, **kwargs):
440+ """Wrapper for a single function."""
441+ self._called[fname].append((args, kwargs))
442+ return retval
443+
444+ for (fname, retval) in funcs:
445+ wrapper = partial(record_call, fname, retval)
446+ self.patch(module, fname, wrapper)
447+
448+
449 class InstallConfigTestCase(TestCase):
450 """Test install_config_and_daemons."""
451
452@@ -37,6 +62,10 @@
453 yield super(InstallConfigTestCase, self).setUp()
454 self._called = []
455
456+ self.patch(utils.darwin,
457+ 'check_and_install_fsevents_daemon',
458+ lambda _: None)
459+
460 def _set_called(self, *args, **kwargs):
461 """Store 'args' and 'kwargs for test assertions."""
462 self._called.append((args, kwargs))
463@@ -76,3 +105,200 @@
464 """When frozen, we do not copy the conf files if they do exist."""
465 self._test_copying_conf_files(True)
466 self.assertEqual(self._called, [])
467+
468+
469+class InstallDaemonTestCase(CallRecordingTestCase):
470+ """Test fsevents daemon installation."""
471+
472+ @defer.inlineCallbacks
473+ def setUp(self):
474+ """Set up patched & tracked calls."""
475+ yield super(InstallDaemonTestCase, self).setUp()
476+
477+ self._patch_and_track(utils.darwin,
478+ [('get_authorization', 'Fake AuthRef'),
479+ ('remove_fsevents_daemon', None),
480+ ('install_fsevents_daemon', None),
481+ ('AuthorizationFree', None)])
482+
483+ def _patch_versions(self, installed, bundled):
484+ """Convenience to patch the version-getting functions."""
485+ self.patch(utils.darwin,
486+ "get_bundle_version",
487+ lambda _: bundled)
488+ self.patch(utils.darwin,
489+ "get_fsevents_daemon_installed_version",
490+ lambda: installed)
491+
492+ def test_check_and_install_current_version(self):
493+ """Test that we do nothing on current version"""
494+
495+ self._patch_versions(installed=47.0, bundled=47.0)
496+
497+ utils.darwin.check_and_install_fsevents_daemon('NOT A REAL DIR')
498+
499+ self.assertEqual(self._called.keys(), [])
500+
501+ def test_check_and_install_upgrade(self):
502+ """Test removing old daemon and installing new one."""
503+
504+ self._patch_versions(installed=35.0, bundled=35.1)
505+
506+ utils.darwin.check_and_install_fsevents_daemon('NOT A REAL DIR')
507+
508+ self.assertEqual(self._called['get_authorization'],
509+ [((), {}), ((), {})])
510+ self.assertEqual(self._called['remove_fsevents_daemon'],
511+ [(('Fake AuthRef',), {})])
512+ self.assertEqual(self._called['install_fsevents_daemon'],
513+ [(('Fake AuthRef',), {})])
514+ self.assertEqual(self._called['AuthorizationFree'],
515+ [(('Fake AuthRef',
516+ utils.darwin.kAuthorizationFlagDestroyRights), {}),
517+ (('Fake AuthRef',
518+ utils.darwin.kAuthorizationFlagDestroyRights), {})
519+ ])
520+
521+ def test_check_and_install_mismatch(self):
522+ """Test raising when we're older than the daemon."""
523+
524+ self._patch_versions(installed=102.5, bundled=66.0)
525+
526+ self.assertRaises(utils.darwin.DaemonVersionMismatchException,
527+ utils.darwin.check_and_install_fsevents_daemon,
528+ 'NOT A REAL DIR')
529+ self.assertEqual(self._called.keys(), [])
530+
531+
532+class CFCallsTestCase(CallRecordingTestCase):
533+ """Test functions that call CoreFoundation API."""
534+
535+ @defer.inlineCallbacks
536+ def setUp(self):
537+ """Set up call checker."""
538+ yield super(CFCallsTestCase, self).setUp()
539+ self._called = defaultdict(list)
540+ self.patch(utils.darwin, 'create_cfstr',
541+ lambda s: s)
542+ self.patch(utils.darwin, 'CFShow',
543+ lambda _: None)
544+ self.patch(utils.darwin, 'kSMDomainSystemLaunchd',
545+ 'not a c_void_p')
546+
547+ def test_remove_daemon_ok(self):
548+ """Test that we call SMJobRemove and don't raise when it returns OK."""
549+ self._patch_and_track(utils.darwin, [('SMJobRemove', True),
550+ ('c_void_p', 'notaptr'),
551+ ('byref', 'not a **'),
552+ ('CFRelease', 'ignore')])
553+
554+ utils.darwin.remove_fsevents_daemon('not an authref')
555+ self.assertEqual(self._called['SMJobRemove'],
556+ [(('not a c_void_p',
557+ utils.darwin.FSEVENTSD_JOB_LABEL,
558+ 'not an authref',
559+ True, 'not a **'), {})])
560+ self.assertEqual(self._called['CFRelease'],
561+ [((utils.darwin.FSEVENTSD_JOB_LABEL,), {})])
562+
563+ def test_remove_daemon_not_ok(self):
564+ """Test that we raise when SMJobRemove returns not OK."""
565+ self._patch_and_track(utils.darwin, [('SMJobRemove', False),
566+ ('c_void_p', 'notaptr'),
567+ ('byref', 'not a **'),
568+ ('CFRelease', 'ignore'),
569+ ('CFErrorCopyDescription',
570+ 'Houston, we have a problem')])
571+
572+ self.assertRaises(utils.darwin.DaemonRemoveException,
573+ utils.darwin.remove_fsevents_daemon,
574+ 'not an authref')
575+
576+ def test_install_daemon_ok(self):
577+ """Test that we call SMJobBless and don't raise when it returns OK."""
578+ self._patch_and_track(utils.darwin, [('SMJobBless', True),
579+ ('c_void_p', 'notaptr'),
580+ ('byref', 'not a **'),
581+ ('CFRelease', 'ignore')])
582+
583+ utils.darwin.install_fsevents_daemon('not an authref')
584+ self.assertEqual(self._called['SMJobBless'],
585+ [(('not a c_void_p',
586+ utils.darwin.FSEVENTSD_JOB_LABEL,
587+ 'not an authref',
588+ 'not a **'), {})])
589+ self.assertEqual(self._called['CFRelease'],
590+ [((utils.darwin.FSEVENTSD_JOB_LABEL,), {})])
591+
592+ def test_install_daemon_not_ok(self):
593+ """Test that we raise when SMJobBless returns not OK."""
594+ self._patch_and_track(utils.darwin, [('SMJobBless', False),
595+ ('c_void_p', 'notaptr'),
596+ ('byref', 'not a **'),
597+ ('CFRelease', 'ignore'),
598+ ('CFErrorCopyDescription',
599+ 'Houston, we have a problem')])
600+
601+ self.assertRaises(utils.darwin.DaemonInstallException,
602+ utils.darwin.install_fsevents_daemon,
603+ 'not an authref')
604+
605+ def test_get_bundle_version(self):
606+ """Simple test of #calls in get_bundle_version."""
607+ # This list includes expected counts. This test is mostly good
608+ # to check that we match the number of 'create's with the
609+ # number of 'releases', which is 3 (note create_cfstr is
610+ # patched in setUp.)
611+ to_track = [('CFURLCreateWithFileSystemPath',
612+ 'url', 1),
613+ ('CFBundleCopyInfoDictionaryForURL',
614+ 'dict', 1),
615+ ('CFDictionaryGetValue', 'val', 1),
616+ ('CFStringGetDoubleValue', 102.5, 1),
617+ ('CFRelease', 'ignore', 3)]
618+ self._patch_and_track(utils.darwin, [(n, r) for (n, r, _) in to_track])
619+
620+ utils.darwin.get_bundle_version("not a cfstr")
621+ for (name, _, num) in to_track:
622+ self.assertEqual(len(self._called[name]), num)
623+
624+ def test_get_fsevents_daemon_installed_version_ok(self):
625+ """Test that we return the version if the dictionary is there."""
626+ to_track = [('SMJobCopyDictionary', 'not none'),
627+ ('CFRelease', 'None'),
628+ ('CFDictionaryGetValue', 'val from dict'),
629+ ('CFArrayGetValueAtIndex', 'val'),
630+ ('get_bundle_version', 1.0)]
631+ self._patch_and_track(utils.darwin, to_track)
632+ utils.darwin.get_fsevents_daemon_installed_version()
633+
634+ self.assertEqual(self._called['CFDictionaryGetValue'],
635+ [(('not none', "ProgramArguments"), {})])
636+ self.assertEqual(self._called['CFArrayGetValueAtIndex'],
637+ [(('val from dict', 0), {})])
638+ self.assertEqual(self._called['get_bundle_version'],
639+ [(('val',), {})])
640+
641+ def test_get_fsevents_daemon_installed_version_not_found(self):
642+ """Test that we return None if the dictionary is not there."""
643+ to_track = [('SMJobCopyDictionary', None),
644+ ('CFRelease', 'None'),
645+ ('CFDictionaryGetValue', 'val from dict'),
646+ ('CFArrayGetValueAtIndex', 'val'),
647+ ('get_bundle_version', 1.0)]
648+ self._patch_and_track(utils.darwin, to_track)
649+ utils.darwin.get_fsevents_daemon_installed_version()
650+
651+ self.assertTrue('CFDictionaryGetValue' not in self._called.keys())
652+ self.assertTrue('CFArrayGetValueAtIndex' not in self._called.keys())
653+ self.assertTrue('get_bundle_version' not in self._called.keys())
654+
655+ def test_get_authorization_ok(self):
656+ """Test successful call of AuthorizationCreate does not raise."""
657+ to_track = [('c_void_p', 'not void p'),
658+ ('byref', 'not **'),
659+ ('AuthorizationCreate',
660+ utils.darwin.errAuthorizationSuccess)]
661+ self._patch_and_track(utils.darwin, to_track)
662+ auth_ref = utils.darwin.get_authorization()
663+ self.assertEqual(auth_ref, 'not void p')

Subscribers

People subscribed via source and target branches