Merge lp:~dobey/ubuntuone-control-panel/update-4-0 into lp:ubuntuone-control-panel/stable-4-0

Proposed by dobey
Status: Merged
Approved by: dobey
Approved revision: 343
Merged at revision: 343
Proposed branch: lp:~dobey/ubuntuone-control-panel/update-4-0
Merge into: lp:ubuntuone-control-panel/stable-4-0
Diff against target: 1026 lines (+715/-31)
8 files modified
ubuntuone/controlpanel/gui/qt/share_file.py (+7/-5)
ubuntuone/controlpanel/gui/qt/share_links.py (+23/-9)
ubuntuone/controlpanel/gui/qt/share_links_search.py (+14/-3)
ubuntuone/controlpanel/gui/qt/tests/test_share_file.py (+5/-2)
ubuntuone/controlpanel/gui/qt/tests/test_share_links.py (+18/-0)
ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py (+47/-12)
ubuntuone/controlpanel/utils/darwin.py (+375/-0)
ubuntuone/controlpanel/utils/tests/test_darwin.py (+226/-0)
To merge this branch: bzr merge lp:~dobey/ubuntuone-control-panel/update-4-0
Reviewer Review Type Date Requested Status
Mike McCracken (community) Approve
Review via email: mp+127559@code.launchpad.net

Commit message

[Diego Sarmentero]

    - Accept mouse events in shares search pop-up list. (LP: #1056192)

[Mike McCracken]

    - Use darwin ServiceManagement API to install/check/upgrade fsevents daemon.

[Brian Curtin]

    - Use os.path.join to construct platform-safe paths in share links tests.

To post a comment you must log in.
Revision history for this message
Mike McCracken (mikemc) :
review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (192.8 KiB)

The attempt to merge lp:~dobey/ubuntuone-control-panel/update-4-0 into lp:ubuntuone-control-panel/stable-4-0 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]
    t...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntuone/controlpanel/gui/qt/share_file.py'
2--- ubuntuone/controlpanel/gui/qt/share_file.py 2012-08-27 15:00:01 +0000
3+++ ubuntuone/controlpanel/gui/qt/share_file.py 2012-10-02 18:05:36 +0000
4@@ -20,6 +20,8 @@
5
6 from PyQt4 import QtGui, QtCore
7
8+from ubuntuone.platform import expand_user
9+
10 from ubuntuone.controlpanel import cache
11 from ubuntuone.controlpanel.gui.qt.share_links_search import (
12 get_system_icon_for_filename,
13@@ -40,11 +42,12 @@
14 super(ShareFileWidget, self).__init__(*args, **kwargs)
15 self.ui = share_file_ui.Ui_Form()
16 self.ui.setupUi(self)
17- self.file_path = file_path
18+ full_path = expand_user(file_path.encode('utf-8'))
19+ self.file_path = full_path.decode('utf-8')
20
21 self.ui.lbl_filename.setText(os.path.basename(file_path))
22 self.ui.lbl_path.setText(file_path)
23- icon = get_system_icon_for_filename(os.path.expanduser(file_path))
24+ icon = get_system_icon_for_filename(full_path)
25 pixmap = icon.pixmap(24)
26 self.ui.lbl_icon.setPixmap(pixmap)
27
28@@ -53,11 +56,10 @@
29
30 def open_file(self):
31 """Open the specified file."""
32- path = u'file://%s' % self.file_path
33+ path = 'file://%s' % self.file_path
34 QtGui.QDesktopServices.openUrl(QtCore.QUrl(path))
35
36 def disable_link(self, val):
37 """Change the access of the file to Not Public."""
38- self.backend.change_public_access(
39- os.path.expanduser(self.file_path), False)
40+ self.backend.change_public_access(self.file_path, False)
41 self.linkDisabled.emit()
42
43=== modified file 'ubuntuone/controlpanel/gui/qt/share_links.py'
44--- ubuntuone/controlpanel/gui/qt/share_links.py 2012-08-27 15:00:01 +0000
45+++ ubuntuone/controlpanel/gui/qt/share_links.py 2012-10-02 18:05:36 +0000
46@@ -21,6 +21,8 @@
47 from PyQt4 import QtGui, QtCore
48 from twisted.internet.defer import inlineCallbacks
49
50+from ubuntuone.platform import expand_user
51+
52 from ubuntuone.controlpanel.logger import setup_logging
53 from ubuntuone.controlpanel.gui import (
54 COPY_LINK,
55@@ -57,6 +59,7 @@
56 logger = logger
57 _enhanced_line = None
58 home_dir = ''
59+ _shared_files = {}
60
61 def _setup(self):
62 """Do some extra setupping for the UI."""
63@@ -91,17 +94,26 @@
64 @inlineCallbacks
65 def share_file(self, file_path):
66 """Clean the previous file share details and publish file_path."""
67+ file_path = unicode(file_path)
68 if self.ui.hbox_share_file.count() > 0:
69 widget = self.ui.hbox_share_file.takeAt(0).widget()
70 widget.close()
71- self.is_processing = True
72- file_path = unicode(file_path)
73- share_file_widget = ShareFileWidget(file_path)
74- self.ui.hbox_share_file.addWidget(share_file_widget)
75- share_file_widget.linkDisabled.connect(
76- lambda: self.ui.line_copy_link.setText(''))
77- yield self.backend.change_public_access(
78- os.path.expanduser(file_path), True)
79+ full_path = expand_user(file_path.encode('utf-8')).decode('utf-8')
80+ if full_path not in self._shared_files:
81+ self.is_processing = True
82+ share_file_widget = ShareFileWidget(file_path)
83+ self.ui.hbox_share_file.addWidget(share_file_widget)
84+ share_file_widget.linkDisabled.connect(
85+ lambda: self.ui.line_copy_link.setText(''))
86+ yield self.backend.change_public_access(
87+ full_path, True)
88+ else:
89+ share_file_widget = ShareFileWidget(file_path)
90+ self.ui.hbox_share_file.addWidget(share_file_widget)
91+ share_file_widget.linkDisabled.connect(
92+ lambda: self.ui.line_copy_link.setText(''))
93+ self.ui.line_copy_link.setText(self._shared_files[full_path])
94+ self.ui.stacked_widget.setCurrentIndex(1)
95
96 def _file_shared(self, info):
97 """Receive the notification that the file has been published."""
98@@ -135,17 +147,19 @@
99 def _load_public_files(self, publicfiles):
100 """Load the list of public files."""
101 self.ui.tree_shared_files.clear()
102+ self._shared_files = {}
103 for pfile in publicfiles:
104 item = QtGui.QTreeWidgetItem()
105 path = pfile['path']
106 public_url = pfile['public_url']
107+ self._shared_files[path] = public_url
108 name = os.path.basename(path)
109 item.setText(FILE_NAME_COL, name)
110 tooltip = path
111 if tooltip.startswith(self.home_dir):
112 tooltip = tooltip.replace(self.home_dir, '~', 1)
113 item.setToolTip(FILE_NAME_COL, tooltip)
114- icon = get_system_icon_for_filename(path)
115+ icon = get_system_icon_for_filename(path.encode('utf-8'))
116 item.setIcon(FILE_NAME_COL, icon)
117
118 self.ui.tree_shared_files.setColumnWidth(PUBLIC_LINK_COL, 300)
119
120=== modified file 'ubuntuone/controlpanel/gui/qt/share_links_search.py'
121--- ubuntuone/controlpanel/gui/qt/share_links_search.py 2012-09-14 20:23:13 +0000
122+++ ubuntuone/controlpanel/gui/qt/share_links_search.py 2012-10-02 18:05:36 +0000
123@@ -21,6 +21,8 @@
124 from PyQt4 import QtGui, QtCore
125 from twisted.internet.defer import inlineCallbacks
126
127+from ubuntuone.platform import expand_user
128+
129 from ubuntuone.controlpanel import cache
130 from ubuntuone.controlpanel.logger import setup_logging
131
132@@ -29,7 +31,7 @@
133
134 def get_system_icon_for_filename(file_path):
135 """Return the icon used for the system to represent this file."""
136- fileinfo = QtCore.QFileInfo(os.path.expanduser(file_path))
137+ fileinfo = QtCore.QFileInfo(expand_user(file_path))
138 icon_provider = QtGui.QFileIconProvider()
139 icon = icon_provider.icon(fileinfo)
140 return icon
141@@ -65,10 +67,19 @@
142 QtCore.Qt.Key_Enter: self._key_return_pressed,
143 }
144
145+ self.popup.list_widget.itemPressed.connect(self._set_selected_item)
146+ self.popup.list_widget.verticalScrollBar().valueChanged.connect(
147+ self._scroll_fetch_more)
148 self.textChanged.connect(self.filter)
149
150 self._get_volumes_info()
151
152+ def _scroll_fetch_more(self, value):
153+ """Fetch more items into the list on scroll."""
154+ if self.popup.list_widget.verticalScrollBar().maximum() == value:
155+ filenames = self._get_filtered_list(self.temp_u1_files)
156+ self.popup.fetch_more(filenames)
157+
158 @inlineCallbacks
159 def _get_volumes_info(self):
160 """Get the volumes info."""
161@@ -260,7 +271,7 @@
162 file_widget = FileItem(file_)
163 self.list_widget.addItem(item)
164 self.list_widget.setItemWidget(item, file_widget)
165- icon = get_system_icon_for_filename(file_)
166+ icon = get_system_icon_for_filename(file_.encode('utf-8'))
167 item.setIcon(icon)
168 if file_items:
169 self.list_widget.setCurrentRow(0)
170@@ -272,7 +283,7 @@
171 file_widget = FileItem(file_)
172 self.list_widget.addItem(item)
173 self.list_widget.setItemWidget(item, file_widget)
174- icon = get_system_icon_for_filename(file_)
175+ icon = get_system_icon_for_filename(file_.encode('utf-8'))
176 item.setIcon(icon)
177
178 def showEvent(self, event):
179
180=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_file.py'
181--- ubuntuone/controlpanel/gui/qt/tests/test_share_file.py 2012-08-28 00:32:41 +0000
182+++ ubuntuone/controlpanel/gui/qt/tests/test_share_file.py 2012-10-02 18:05:36 +0000
183@@ -20,6 +20,8 @@
184
185 from PyQt4 import QtGui, QtCore
186
187+from ubuntuone.platform import expand_user
188+
189 from ubuntuone.controlpanel.gui.qt import share_file as gui
190 from ubuntuone.controlpanel.gui.qt.tests import (
191 BaseTestCase,
192@@ -54,7 +56,7 @@
193 self.patch(QtGui, "QDesktopServices", fake_desktop_service)
194 self.ui.ui.btn_open.click()
195
196- expected = QtCore.QUrl(u'file://%s' % self.file_path)
197+ expected = QtCore.QUrl('file://%s' % self.file_path)
198 self.assertEqual(expected, fake_desktop_service.opened_url)
199
200 def test_disable_link(self):
201@@ -71,6 +73,7 @@
202 fake_change_access)
203 self.ui.ui.btn_disable.click()
204
205- expected = [os.path.expanduser(self.file_path), False]
206+ expected = [expand_user(
207+ self.file_path.encode('utf-8')).decode('utf-8'), False]
208 self.assertEqual(data, expected)
209 self.assertEqual(self._called, ((), {}))
210
211=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links.py'
212--- ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2012-08-28 00:32:41 +0000
213+++ ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2012-10-02 18:05:36 +0000
214@@ -78,6 +78,24 @@
215 self.assertEqual(self.ui.ui.stacked_widget.currentIndex(), 1)
216 self.assertFalse(self.ui.is_processing)
217
218+ def test_file_already_shared(self):
219+ """Check the behavior of the widgets when there is a shared file."""
220+ data = []
221+
222+ def fake_method(self, *args):
223+ """Fake callback."""
224+ data.append((args))
225+
226+ self.patch(self.ui.backend, "change_public_access", fake_method)
227+ path = '/home/user/Ubuntu One/file1.txt'
228+ shared = {
229+ '/home/user/Ubuntu One/file1.txt': 'http://ubuntuone.com/asd123'}
230+ self.ui._shared_files = shared
231+ self.ui.share_file(path)
232+ self.assertEqual(self.ui.ui.line_copy_link.text(), shared[path])
233+ self.assertEqual(self.ui.ui.stacked_widget.currentIndex(), 1)
234+ self.assertEqual(data, [])
235+
236 def test_open_in_browser(self):
237 """Test the execution of open_in_browser."""
238 fake_desktop_service = FakeDesktopService()
239
240=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py'
241--- ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2012-09-14 20:23:13 +0000
242+++ ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2012-10-02 18:05:36 +0000
243@@ -18,6 +18,7 @@
244
245 import os
246
247+from dirspec import utils
248 from twisted.internet import defer
249
250 from ubuntuone.controlpanel.gui.tests import USER_HOME
251@@ -27,6 +28,7 @@
252
253 # pylint: disable=W0212
254
255+
256 class SearchBoxTestCase(BaseTestCase):
257 """Test the qt control panel."""
258
259@@ -36,6 +38,7 @@
260 def setUp(self):
261 yield super(SearchBoxTestCase, self).setUp()
262
263+ self.patch(utils, "user_home", USER_HOME)
264 self.patch(self.ui._thread_explore, "get_folder_info",
265 self.fake_get_folder_info)
266 self.patch(self.ui._thread_explore, "start",
267@@ -52,6 +55,11 @@
268 os.path.join(USER_HOME, 'blabla', 'iop'),
269 ]
270 }
271+ self._slot_item = None
272+
273+ def fake_slot(self, item):
274+ """Fake function to be called when itemSelected is emitted."""
275+ self._slot_item = item
276
277 def fake_get_folder_info(self, folder):
278 """Fake get_folder_info."""
279@@ -132,7 +140,33 @@
280 self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)])
281 self.ui.popup.list_widget.setCurrentRow(1)
282 current = self.ui.popup.list_widget.currentRow()
283+ self.ui.itemSelected.connect(self.fake_slot)
284 self.ui._key_return_pressed(current)
285+ expected = unicode(self._slot_item).replace('~', USER_HOME)
286+ self.assertEqual(expected, self.folder_info['folder2'][2])
287+
288+ def test_mouse_click_pressed(self):
289+ """Check the proper actions are executed when click is pressed."""
290+ data1 = [{'path': 'folder1'}]
291+ data2 = [{'realpath': 'folder2'}]
292+ self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)])
293+ self.ui.popup.list_widget.setCurrentRow(1)
294+ current = self.ui.popup.list_widget.currentItem()
295+ self.ui.itemSelected.connect(self.fake_slot)
296+ self.ui.popup.list_widget.itemPressed.emit(current)
297+ expected = unicode(self._slot_item).replace('~', USER_HOME)
298+ self.assertEqual(expected, self.folder_info['folder2'][2])
299+
300+ def test_mouse_scroll(self):
301+ """Check that fetch_more is called when we reach the end of scroll."""
302+ data1 = [{'path': 'folder1'}]
303+ data2 = [{'realpath': 'folder2'}]
304+ self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)])
305+ self.patch(self.ui.popup, "fetch_more", self._set_called)
306+ self.ui._scroll_fetch_more(
307+ self.ui.popup.list_widget.verticalScrollBar().maximum())
308+ expected = (([],), {})
309+ self.assertEqual(expected, self._called)
310
311 def test_key_space_pressed(self):
312 """Check the proper actions are executed on key space pressed."""
313@@ -163,12 +197,12 @@
314 data2 = [{'realpath': 'folder2'}]
315 self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)])
316 expected = [
317- 'other_path/test/qwe',
318- '~/blabla/iop',
319- '~/one/file3',
320- '~/test/asd',
321- '~/ubuntu/file1',
322- '~/ubuntu/file2']
323+ os.path.join('other_path', 'test', 'qwe'),
324+ os.path.join('~', 'blabla', 'iop'),
325+ os.path.join('~', 'one', 'file3'),
326+ os.path.join('~', 'test', 'asd'),
327+ os.path.join('~', 'ubuntu', 'file1'),
328+ os.path.join('~', 'ubuntu', 'file2')]
329 self.assertEqual(self.ui._thread_explore.u1_files, expected)
330 self.assertEqual(self.ui.popup.list_widget.count(), 6)
331
332@@ -183,15 +217,15 @@
333 data2 = [{'realpath': 'folder2'}]
334 self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)])
335 self.ui.filter('p')
336- expected = ['~/blabla/iop']
337+ expected = [os.path.join('~', 'blabla', 'iop')]
338 self.assertEqual(expected, self.ui.temp_u1_files)
339
340 self.ui.filter('i')
341 expected = [
342- '~/blabla/iop',
343- '~/one/file3',
344- '~/ubuntu/file1',
345- '~/ubuntu/file2']
346+ os.path.join('~', 'blabla', 'iop'),
347+ os.path.join('~', 'one', 'file3'),
348+ os.path.join('~', 'ubuntu', 'file1'),
349+ os.path.join('~', 'ubuntu', 'file2')]
350 self.assertEqual(expected, self.ui.temp_u1_files)
351
352 def test_set_selected_item(self):
353@@ -204,7 +238,8 @@
354 self.ui.popup.list_widget.setCurrentRow(0)
355 item = self.ui.popup.list_widget.currentItem()
356 self.ui._set_selected_item(item)
357- self.assertEqual(self._called, ((u'other_path/test/qwe',), {}))
358+ self.assertEqual(self._called,
359+ ((os.path.join(u'other_path', u'test', u'qwe'),), {}))
360 self.assertEqual(self.ui.text(), '')
361
362
363
364=== modified file 'ubuntuone/controlpanel/utils/darwin.py'
365--- ubuntuone/controlpanel/utils/darwin.py 2012-08-06 16:47:44 +0000
366+++ ubuntuone/controlpanel/utils/darwin.py 2012-10-02 18:05:36 +0000
367@@ -20,15 +20,322 @@
368 import shutil
369 import sys
370
371+from ctypes import (
372+ byref,
373+ CDLL,
374+ POINTER,
375+ Structure,
376+ c_bool,
377+ c_char_p,
378+ c_double,
379+ c_int32,
380+ c_long,
381+ c_uint32,
382+ c_void_p)
383+
384+from ctypes.util import find_library
385+
386 from twisted.internet import defer
387
388 from dirspec.basedir import save_config_path
389 from ubuntuone.controlpanel.logger import setup_logging
390
391 logger = setup_logging('utils.darwin')
392+
393 AUTOUPDATE_BIN_NAME = 'autoupdate-darwin'
394 UNINSTALL_BIN_NAME = 'uninstall-darwin'
395
396+FSEVENTSD_JOB_LABEL = "com.ubuntu.one.fsevents"
397+
398+# pylint: disable=C0103
399+CFPath = find_library("CoreFoundation")
400+CF = CDLL(CFPath)
401+
402+CFRelease = CF.CFRelease
403+CFRelease.restype = None
404+CFRelease.argtypes = [c_void_p]
405+
406+kCFStringEncodingUTF8 = 0x08000100
407+
408+
409+class CFRange(Structure):
410+ """CFRange Struct"""
411+ _fields_ = [("location", c_long),
412+ ("length", c_long)]
413+
414+CFShow = CF.CFShow
415+CFShow.argtypes = [c_void_p]
416+CFShow.restype = None
417+
418+CFStringCreateWithCString = CF.CFStringCreateWithCString
419+CFStringCreateWithCString.restype = c_void_p
420+CFStringCreateWithCString.argtypes = [c_void_p, c_void_p, c_uint32]
421+
422+kCFAllocatorDefault = c_void_p()
423+
424+CFErrorCopyDescription = CF.CFErrorCopyDescription
425+CFErrorCopyDescription.restype = c_void_p
426+CFErrorCopyDescription.argtypes = [c_void_p]
427+
428+CFDictionaryGetValue = CF.CFDictionaryGetValue
429+CFDictionaryGetValue.restype = c_void_p
430+CFDictionaryGetValue.argtypes = [c_void_p, c_void_p]
431+
432+CFArrayGetValueAtIndex = CF.CFArrayGetValueAtIndex
433+CFArrayGetValueAtIndex.restype = c_void_p
434+CFArrayGetValueAtIndex.argtypes = [c_void_p, c_uint32]
435+
436+CFURLCreateWithFileSystemPath = CF.CFURLCreateWithFileSystemPath
437+CFURLCreateWithFileSystemPath.restype = c_void_p
438+CFURLCreateWithFileSystemPath.argtypes = [c_void_p, c_void_p, c_uint32, c_bool]
439+
440+CFBundleCopyInfoDictionaryForURL = CF.CFBundleCopyInfoDictionaryForURL
441+CFBundleCopyInfoDictionaryForURL.restype = c_void_p
442+CFBundleCopyInfoDictionaryForURL.argtypes = [c_void_p]
443+
444+CFStringGetDoubleValue = CF.CFStringGetDoubleValue
445+CFStringGetDoubleValue.restype = c_double
446+CFStringGetDoubleValue.argtypes = [c_void_p]
447+
448+SecurityPath = find_library("Security")
449+Security = CDLL(SecurityPath)
450+
451+AuthorizationCreate = Security.AuthorizationCreate
452+AuthorizationCreate.restype = c_int32
453+AuthorizationCreate.argtypes = [c_void_p, c_void_p, c_int32, c_void_p]
454+
455+AuthorizationFree = Security.AuthorizationFree
456+AuthorizationFree.restype = c_uint32
457+AuthorizationFree.argtypes = [c_void_p, c_uint32]
458+
459+kAuthorizationFlagDefaults = 0
460+kAuthorizationFlagInteractionAllowed = 1 << 0
461+kAuthorizationFlagExtendRights = 1 << 1
462+kAuthorizationFlagDestroyRights = 1 << 3
463+kAuthorizationFlagPreAuthorize = 1 << 4
464+
465+kAuthorizationEmptyEnvironment = None
466+
467+errAuthorizationSuccess = 0
468+errAuthorizationDenied = -60005
469+errAuthorizationCanceled = -60006
470+errAuthorizationInteractionNotAllowed = -60007
471+
472+ServiceManagementPath = find_library("ServiceManagement")
473+ServiceManagement = CDLL(ServiceManagementPath)
474+
475+kSMRightBlessPrivilegedHelper = "com.apple.ServiceManagement.blesshelper"
476+kSMRightModifySystemDaemons = "com.apple.ServiceManagement.daemons.modify"
477+
478+# pylint: disable=E1101
479+# c_void_p has no "in_dll" member:
480+kSMDomainSystemLaunchd = c_void_p.in_dll(ServiceManagement,
481+ "kSMDomainSystemLaunchd")
482+# pylint: enable=E1101
483+
484+SMJobBless = ServiceManagement.SMJobBless
485+SMJobBless.restype = c_bool
486+SMJobBless.argtypes = [c_void_p, c_void_p, c_void_p, POINTER(c_void_p)]
487+
488+SMJobRemove = ServiceManagement.SMJobRemove
489+SMJobRemove.restype = c_bool
490+SMJobRemove.argtypes = [c_void_p, c_void_p, c_void_p, c_bool,
491+ POINTER(c_void_p)]
492+
493+SMJobCopyDictionary = ServiceManagement.SMJobCopyDictionary
494+SMJobCopyDictionary.restype = c_void_p
495+SMJobCopyDictionary.argtypes = [c_void_p, c_void_p]
496+
497+
498+class AuthorizationItem(Structure):
499+ """AuthorizationItem Struct"""
500+ _fields_ = [("name", c_char_p),
501+ ("valueLength", c_uint32),
502+ ("value", c_void_p),
503+ ("flags", c_uint32)]
504+
505+
506+class AuthorizationRights(Structure):
507+ """AuthorizationRights Struct"""
508+ _fields_ = [("count", c_uint32),
509+ # * 1 here is specific to our use below
510+ ("items", POINTER(AuthorizationItem))]
511+
512+
513+class DaemonInstallException(Exception):
514+ """Error securely installing daemon."""
515+
516+
517+class AuthUserCanceledException(Exception):
518+ """The user canceled the authorization."""
519+
520+
521+class AuthFailedException(Exception):
522+ """The authorization faild for some reason."""
523+
524+
525+class DaemonRemoveException(Exception):
526+ """Error removing existing daemon."""
527+
528+
529+class DaemonVersionMismatchException(Exception):
530+ """Incompatible version of the daemon found."""
531+
532+
533+def create_cfstr(s):
534+ """Creates a CFString from a python string.
535+
536+ Note - because this is a "create" function, you have to CFRelease
537+ the returned string.
538+ """
539+ return CFStringCreateWithCString(kCFAllocatorDefault,
540+ s.encode('utf8'),
541+ kCFStringEncodingUTF8)
542+
543+
544+def get_bundle_version(path_cfstr):
545+ """Returns a float version from the plist of the given bundle.
546+
547+ path_cfstr must be a CFStringRef.
548+ """
549+ cfurl = CFURLCreateWithFileSystemPath(None, # use default allocator
550+ path_cfstr,
551+ 0, # POSIX style path
552+ False)
553+ plist = CFBundleCopyInfoDictionaryForURL(cfurl)
554+ ver_key_cfstr = create_cfstr("CFBundleVersion")
555+ version_cfstr = CFDictionaryGetValue(plist, ver_key_cfstr)
556+
557+ version = CFStringGetDoubleValue(version_cfstr)
558+
559+ CFRelease(cfurl)
560+ CFRelease(plist)
561+ CFRelease(ver_key_cfstr)
562+
563+ return version
564+
565+
566+def get_fsevents_daemon_installed_version():
567+ """Returns helper version or None if helper is not installed."""
568+
569+ label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL)
570+ job_data_cfdict = SMJobCopyDictionary(kSMDomainSystemLaunchd,
571+ label_cfstr)
572+ CFRelease(label_cfstr)
573+
574+ if job_data_cfdict is not None:
575+ key_cfstr = create_cfstr("ProgramArguments")
576+ args_cfarray = CFDictionaryGetValue(job_data_cfdict, key_cfstr)
577+
578+ path_cfstr = CFArrayGetValueAtIndex(args_cfarray, 0)
579+ version = get_bundle_version(path_cfstr)
580+
581+ # only release things "copied" or "created", not "got".
582+ CFRelease(job_data_cfdict)
583+ CFRelease(key_cfstr)
584+ return version
585+
586+ return None
587+
588+
589+def get_authorization():
590+ """Get authorization to remove and/or install daemons."""
591+
592+ # pylint: disable=W0201
593+ authItemBless = AuthorizationItem()
594+ authItemBless.name = kSMRightBlessPrivilegedHelper
595+ authItemBless.valueLength = 0
596+ authItemBless.value = None
597+ authItemBless.flags = 0
598+
599+ authRights = AuthorizationRights()
600+ authRights.count = 1
601+ authRights.items = (AuthorizationItem * 1)(authItemBless)
602+
603+ flags = (kAuthorizationFlagDefaults |
604+ kAuthorizationFlagInteractionAllowed |
605+ kAuthorizationFlagPreAuthorize |
606+ kAuthorizationFlagExtendRights)
607+
608+ authRef = c_void_p()
609+
610+ status = AuthorizationCreate(byref(authRights),
611+ kAuthorizationEmptyEnvironment,
612+ flags,
613+ byref(authRef))
614+
615+ if status != errAuthorizationSuccess:
616+
617+ if status == errAuthorizationInteractionNotAllowed:
618+ raise AuthFailedException("Authorization failed: "
619+ "interaction not allowed.")
620+
621+ elif status == errAuthorizationDenied:
622+ raise AuthFailedException("Authorization failed: auth denied.")
623+
624+ else:
625+ raise AuthUserCanceledException()
626+
627+ if authRef is None:
628+ raise AuthFailedException("No authRef from AuthorizationCreate: %r"
629+ % status)
630+ return authRef
631+
632+
633+def install_fsevents_daemon(authRef):
634+ """Call SMJobBless to install daemon.
635+
636+ No return, raises on error.
637+ """
638+
639+ desc_cfstr = None
640+
641+ try:
642+ error = c_void_p()
643+
644+ label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL)
645+ ok = SMJobBless(kSMDomainSystemLaunchd,
646+ label_cfstr,
647+ authRef,
648+ byref(error))
649+ CFRelease(label_cfstr)
650+
651+ if not ok:
652+ desc_cfstr = CFErrorCopyDescription(error)
653+ CFShow(desc_cfstr)
654+ raise DaemonInstallException("SMJobBless error (see above)")
655+
656+ finally:
657+ if desc_cfstr:
658+ CFRelease(desc_cfstr)
659+
660+
661+def remove_fsevents_daemon(authRef):
662+ """Call SMJobRemove to remove daemon.
663+
664+ No return, raises on error.
665+ """
666+ desc_cfstr = None
667+ try:
668+ error = c_void_p()
669+
670+ label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL)
671+ ok = SMJobRemove(kSMDomainSystemLaunchd,
672+ label_cfstr,
673+ authRef,
674+ True,
675+ byref(error))
676+
677+ CFRelease(label_cfstr)
678+
679+ if not ok:
680+ desc_cfstr = CFErrorCopyDescription(error)
681+ CFShow(desc_cfstr)
682+ raise DaemonRemoveException("SMJobRemove error (see above)")
683+ finally:
684+ if desc_cfstr:
685+ CFRelease(desc_cfstr)
686+
687
688 def add_to_autostart():
689 """Add syncdaemon to the session's autostart."""
690@@ -50,6 +357,72 @@
691 return folders
692
693
694+def check_and_install_fsevents_daemon(main_app_dir):
695+ """Checks version of running daemon, maybe installs.
696+
697+ 'main_app_dir' is the path to the running app.
698+
699+ This will securely install the daemon bundled with the running app
700+ if there is no currently installed one, or upgrade it if the
701+ installed one is old. If the installed one is newer, it raises
702+ a DaemonVersionMismatchException.
703+ """
704+
705+ daemon_path = os.path.join(main_app_dir, 'Contents',
706+ 'Library', 'LaunchServices',
707+ FSEVENTSD_JOB_LABEL)
708+ bundled_version = get_bundle_version(create_cfstr(daemon_path))
709+
710+ installed_version = get_fsevents_daemon_installed_version()
711+
712+ if installed_version == bundled_version:
713+ logger.info("Current fsevents daemon already installed: version %r" %
714+ installed_version)
715+ return
716+
717+ if installed_version > bundled_version:
718+ desc = ("Found newer fsevents daemon:"
719+ " installed %r > bundled version %r." %
720+ (installed_version, bundled_version))
721+ logger.error(desc)
722+ raise DaemonVersionMismatchException(desc)
723+
724+ authRef = get_authorization()
725+
726+ if (installed_version is not None and
727+ installed_version < bundled_version):
728+ logger.info("Found installed daemon version %r < %r, removing." %
729+ (installed_version, bundled_version))
730+
731+ try:
732+ remove_fsevents_daemon(authRef)
733+ except DaemonRemoveException, e:
734+ logger.exception("Problem removing running daemon: %r" % e)
735+
736+ AuthorizationFree(authRef, kAuthorizationFlagDestroyRights)
737+
738+ logger.info("Installing daemon version %r" % bundled_version)
739+
740+ try:
741+ authRef = get_authorization()
742+ install_fsevents_daemon(authRef)
743+
744+ installed_version = get_fsevents_daemon_installed_version()
745+
746+ if installed_version:
747+ logger.info("Installed fsevents daemon successfully: version %r" %
748+ installed_version)
749+ else:
750+ logger.error("Error installing fsevents daemon, see system.log.")
751+
752+ except DaemonInstallException, e:
753+ logger.exception("Exception in fsevents daemon installation: %r" % e)
754+ raise e
755+
756+ finally:
757+ AuthorizationFree(authRef, kAuthorizationFlagDestroyRights)
758+
759+
760 def install_config_and_daemons():
761 """Install required data files and fsevents daemon.
762
763@@ -80,6 +453,8 @@
764 if not os.path.exists(dest_path):
765 shutil.copyfile(src_path, dest_path)
766
767+ check_and_install_fsevents_daemon(main_app_dir)
768+
769
770 def perform_update():
771 """Spawn the autoupdate process and call the stop function."""
772
773=== modified file 'ubuntuone/controlpanel/utils/tests/test_darwin.py'
774--- ubuntuone/controlpanel/utils/tests/test_darwin.py 2012-08-06 16:47:44 +0000
775+++ ubuntuone/controlpanel/utils/tests/test_darwin.py 2012-10-02 18:05:36 +0000
776@@ -19,6 +19,9 @@
777 import os
778 import sys
779
780+from collections import defaultdict
781+from functools import partial
782+
783 from twisted.internet import defer
784
785 from ubuntuone.controlpanel import utils
786@@ -28,6 +31,28 @@
787 # pylint: disable=W0212
788
789
790+class CallRecordingTestCase(TestCase):
791+ """Base class with multi-call checker."""
792+
793+ @defer.inlineCallbacks
794+ def setUp(self):
795+ """Set up call checker."""
796+ yield super(CallRecordingTestCase, self).setUp()
797+ self._called = defaultdict(list)
798+
799+ def _patch_and_track(self, module, funcs):
800+ """Record calls along with function name."""
801+
802+ def record_call(fname, retval, *args, **kwargs):
803+ """Wrapper for a single function."""
804+ self._called[fname].append((args, kwargs))
805+ return retval
806+
807+ for (fname, retval) in funcs:
808+ wrapper = partial(record_call, fname, retval)
809+ self.patch(module, fname, wrapper)
810+
811+
812 class InstallConfigTestCase(TestCase):
813 """Test install_config_and_daemons."""
814
815@@ -37,6 +62,10 @@
816 yield super(InstallConfigTestCase, self).setUp()
817 self._called = []
818
819+ self.patch(utils.darwin,
820+ 'check_and_install_fsevents_daemon',
821+ lambda _: None)
822+
823 def _set_called(self, *args, **kwargs):
824 """Store 'args' and 'kwargs for test assertions."""
825 self._called.append((args, kwargs))
826@@ -76,3 +105,200 @@
827 """When frozen, we do not copy the conf files if they do exist."""
828 self._test_copying_conf_files(True)
829 self.assertEqual(self._called, [])
830+
831+
832+class InstallDaemonTestCase(CallRecordingTestCase):
833+ """Test fsevents daemon installation."""
834+
835+ @defer.inlineCallbacks
836+ def setUp(self):
837+ """Set up patched & tracked calls."""
838+ yield super(InstallDaemonTestCase, self).setUp()
839+
840+ self._patch_and_track(utils.darwin,
841+ [('get_authorization', 'Fake AuthRef'),
842+ ('remove_fsevents_daemon', None),
843+ ('install_fsevents_daemon', None),
844+ ('AuthorizationFree', None)])
845+
846+ def _patch_versions(self, installed, bundled):
847+ """Convenience to patch the version-getting functions."""
848+ self.patch(utils.darwin,
849+ "get_bundle_version",
850+ lambda _: bundled)
851+ self.patch(utils.darwin,
852+ "get_fsevents_daemon_installed_version",
853+ lambda: installed)
854+
855+ def test_check_and_install_current_version(self):
856+ """Test that we do nothing on current version"""
857+
858+ self._patch_versions(installed=47.0, bundled=47.0)
859+
860+ utils.darwin.check_and_install_fsevents_daemon('NOT A REAL DIR')
861+
862+ self.assertEqual(self._called.keys(), [])
863+
864+ def test_check_and_install_upgrade(self):
865+ """Test removing old daemon and installing new one."""
866+
867+ self._patch_versions(installed=35.0, bundled=35.1)
868+
869+ utils.darwin.check_and_install_fsevents_daemon('NOT A REAL DIR')
870+
871+ self.assertEqual(self._called['get_authorization'],
872+ [((), {}), ((), {})])
873+ self.assertEqual(self._called['remove_fsevents_daemon'],
874+ [(('Fake AuthRef',), {})])
875+ self.assertEqual(self._called['install_fsevents_daemon'],
876+ [(('Fake AuthRef',), {})])
877+ self.assertEqual(self._called['AuthorizationFree'],
878+ [(('Fake AuthRef',
879+ utils.darwin.kAuthorizationFlagDestroyRights), {}),
880+ (('Fake AuthRef',
881+ utils.darwin.kAuthorizationFlagDestroyRights), {})
882+ ])
883+
884+ def test_check_and_install_mismatch(self):
885+ """Test raising when we're older than the daemon."""
886+
887+ self._patch_versions(installed=102.5, bundled=66.0)
888+
889+ self.assertRaises(utils.darwin.DaemonVersionMismatchException,
890+ utils.darwin.check_and_install_fsevents_daemon,
891+ 'NOT A REAL DIR')
892+ self.assertEqual(self._called.keys(), [])
893+
894+
895+class CFCallsTestCase(CallRecordingTestCase):
896+ """Test functions that call CoreFoundation API."""
897+
898+ @defer.inlineCallbacks
899+ def setUp(self):
900+ """Set up call checker."""
901+ yield super(CFCallsTestCase, self).setUp()
902+ self._called = defaultdict(list)
903+ self.patch(utils.darwin, 'create_cfstr',
904+ lambda s: s)
905+ self.patch(utils.darwin, 'CFShow',
906+ lambda _: None)
907+ self.patch(utils.darwin, 'kSMDomainSystemLaunchd',
908+ 'not a c_void_p')
909+
910+ def test_remove_daemon_ok(self):
911+ """Test that we call SMJobRemove and don't raise when it returns OK."""
912+ self._patch_and_track(utils.darwin, [('SMJobRemove', True),
913+ ('c_void_p', 'notaptr'),
914+ ('byref', 'not a **'),
915+ ('CFRelease', 'ignore')])
916+
917+ utils.darwin.remove_fsevents_daemon('not an authref')
918+ self.assertEqual(self._called['SMJobRemove'],
919+ [(('not a c_void_p',
920+ utils.darwin.FSEVENTSD_JOB_LABEL,
921+ 'not an authref',
922+ True, 'not a **'), {})])
923+ self.assertEqual(self._called['CFRelease'],
924+ [((utils.darwin.FSEVENTSD_JOB_LABEL,), {})])
925+
926+ def test_remove_daemon_not_ok(self):
927+ """Test that we raise when SMJobRemove returns not OK."""
928+ self._patch_and_track(utils.darwin, [('SMJobRemove', False),
929+ ('c_void_p', 'notaptr'),
930+ ('byref', 'not a **'),
931+ ('CFRelease', 'ignore'),
932+ ('CFErrorCopyDescription',
933+ 'Houston, we have a problem')])
934+
935+ self.assertRaises(utils.darwin.DaemonRemoveException,
936+ utils.darwin.remove_fsevents_daemon,
937+ 'not an authref')
938+
939+ def test_install_daemon_ok(self):
940+ """Test that we call SMJobBless and don't raise when it returns OK."""
941+ self._patch_and_track(utils.darwin, [('SMJobBless', True),
942+ ('c_void_p', 'notaptr'),
943+ ('byref', 'not a **'),
944+ ('CFRelease', 'ignore')])
945+
946+ utils.darwin.install_fsevents_daemon('not an authref')
947+ self.assertEqual(self._called['SMJobBless'],
948+ [(('not a c_void_p',
949+ utils.darwin.FSEVENTSD_JOB_LABEL,
950+ 'not an authref',
951+ 'not a **'), {})])
952+ self.assertEqual(self._called['CFRelease'],
953+ [((utils.darwin.FSEVENTSD_JOB_LABEL,), {})])
954+
955+ def test_install_daemon_not_ok(self):
956+ """Test that we raise when SMJobBless returns not OK."""
957+ self._patch_and_track(utils.darwin, [('SMJobBless', False),
958+ ('c_void_p', 'notaptr'),
959+ ('byref', 'not a **'),
960+ ('CFRelease', 'ignore'),
961+ ('CFErrorCopyDescription',
962+ 'Houston, we have a problem')])
963+
964+ self.assertRaises(utils.darwin.DaemonInstallException,
965+ utils.darwin.install_fsevents_daemon,
966+ 'not an authref')
967+
968+ def test_get_bundle_version(self):
969+ """Simple test of #calls in get_bundle_version."""
970+ # This list includes expected counts. This test is mostly good
971+ # to check that we match the number of 'create's with the
972+ # number of 'releases', which is 3 (note create_cfstr is
973+ # patched in setUp.)
974+ to_track = [('CFURLCreateWithFileSystemPath',
975+ 'url', 1),
976+ ('CFBundleCopyInfoDictionaryForURL',
977+ 'dict', 1),
978+ ('CFDictionaryGetValue', 'val', 1),
979+ ('CFStringGetDoubleValue', 102.5, 1),
980+ ('CFRelease', 'ignore', 3)]
981+ self._patch_and_track(utils.darwin, [(n, r) for (n, r, _) in to_track])
982+
983+ utils.darwin.get_bundle_version("not a cfstr")
984+ for (name, _, num) in to_track:
985+ self.assertEqual(len(self._called[name]), num)
986+
987+ def test_get_fsevents_daemon_installed_version_ok(self):
988+ """Test that we return the version if the dictionary is there."""
989+ to_track = [('SMJobCopyDictionary', 'not none'),
990+ ('CFRelease', 'None'),
991+ ('CFDictionaryGetValue', 'val from dict'),
992+ ('CFArrayGetValueAtIndex', 'val'),
993+ ('get_bundle_version', 1.0)]
994+ self._patch_and_track(utils.darwin, to_track)
995+ utils.darwin.get_fsevents_daemon_installed_version()
996+
997+ self.assertEqual(self._called['CFDictionaryGetValue'],
998+ [(('not none', "ProgramArguments"), {})])
999+ self.assertEqual(self._called['CFArrayGetValueAtIndex'],
1000+ [(('val from dict', 0), {})])
1001+ self.assertEqual(self._called['get_bundle_version'],
1002+ [(('val',), {})])
1003+
1004+ def test_get_fsevents_daemon_installed_version_not_found(self):
1005+ """Test that we return None if the dictionary is not there."""
1006+ to_track = [('SMJobCopyDictionary', None),
1007+ ('CFRelease', 'None'),
1008+ ('CFDictionaryGetValue', 'val from dict'),
1009+ ('CFArrayGetValueAtIndex', 'val'),
1010+ ('get_bundle_version', 1.0)]
1011+ self._patch_and_track(utils.darwin, to_track)
1012+ utils.darwin.get_fsevents_daemon_installed_version()
1013+
1014+ self.assertTrue('CFDictionaryGetValue' not in self._called.keys())
1015+ self.assertTrue('CFArrayGetValueAtIndex' not in self._called.keys())
1016+ self.assertTrue('get_bundle_version' not in self._called.keys())
1017+
1018+ def test_get_authorization_ok(self):
1019+ """Test successful call of AuthorizationCreate does not raise."""
1020+ to_track = [('c_void_p', 'not void p'),
1021+ ('byref', 'not **'),
1022+ ('AuthorizationCreate',
1023+ utils.darwin.errAuthorizationSuccess)]
1024+ self._patch_and_track(utils.darwin, to_track)
1025+ auth_ref = utils.darwin.get_authorization()
1026+ self.assertEqual(auth_ref, 'not void p')

Subscribers

People subscribed via source and target branches

to all changes: