Merge lp:~barry/ubuntu-system-image/lp1277589-udm into lp:~registry/ubuntu-system-image/client

Proposed by Barry Warsaw
Status: Merged
Merged at revision: 234
Proposed branch: lp:~barry/ubuntu-system-image/lp1277589-udm
Merge into: lp:~registry/ubuntu-system-image/client
Diff against target: 540 lines (+203/-16)
11 files modified
NEWS.rst (+3/-0)
systemimage/api.py (+4/-0)
systemimage/dbus.py (+35/-3)
systemimage/download.py (+35/-1)
systemimage/service.py (+2/-1)
systemimage/testing/controller.py (+6/-2)
systemimage/testing/helpers.py (+8/-5)
systemimage/testing/nose.py (+10/-1)
systemimage/tests/data/config_03.ini (+1/-1)
systemimage/tests/data/index_24.json (+36/-0)
systemimage/tests/test_dbus.py (+63/-2)
To merge this branch: bzr merge lp:~barry/ubuntu-system-image/lp1277589-udm
Reviewer Review Type Date Requested Status
Registry Administrators Pending
Review via email: mp+207026@code.launchpad.net

Description of the change

More fixes for concurrency protection.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS.rst'
--- NEWS.rst 2014-02-14 22:47:33 +0000
+++ NEWS.rst 2014-02-18 20:26:47 +0000
@@ -17,6 +17,9 @@
17 * Return the empty string from `ApplyUpdate()` D-Bus method. This restores17 * Return the empty string from `ApplyUpdate()` D-Bus method. This restores
18 the original API (patch merged from Ubuntu package, given by Didier18 the original API (patch merged from Ubuntu package, given by Didier
19 Roche). (LP: #1260768)19 Roche). (LP: #1260768)
20 * Request ubuntu-download-manager to download all files to temporary
21 destinations, then atomically rename them into place. This avoids
22 clobbering by multiple processes and mimic changes coming in u-d-m.
2023
212.0.5 (2014-01-30)242.0.5 (2014-01-30)
22==================25==================
2326
=== modified file 'systemimage/api.py'
--- systemimage/api.py 2014-02-13 18:16:17 +0000
+++ systemimage/api.py 2014-02-18 20:26:47 +0000
@@ -73,6 +73,10 @@
73 self._update = None73 self._update = None
74 self._callback = callback74 self._callback = callback
7575
76 def __repr__(self):
77 return '<Mediator at 0x{:x} | State at 0x{:x}>'.format(
78 id(self), id(self._state))
79
76 def cancel(self):80 def cancel(self):
77 self._state.downloader.cancel()81 self._state.downloader.cancel()
7882
7983
=== modified file 'systemimage/dbus.py'
--- systemimage/dbus.py 2014-02-14 22:47:33 +0000
+++ systemimage/dbus.py 2014-02-18 20:26:47 +0000
@@ -23,6 +23,7 @@
2323
24import os24import os
25import sys25import sys
26import logging
26import traceback27import traceback
2728
28from dbus.service import Object, method, signal29from dbus.service import Object, method, signal
@@ -35,6 +36,7 @@
3536
3637
37EMPTYSTRING = ''38EMPTYSTRING = ''
39log = logging.getLogger('systemimage')
3840
3941
40class Loop:42class Loop:
@@ -69,6 +71,7 @@
69 super().__init__(bus, object_path)71 super().__init__(bus, object_path)
70 self._loop = loop72 self._loop = loop
71 self._api = Mediator(self._progress_callback)73 self._api = Mediator(self._progress_callback)
74 log.info('Mediator created {}', self._api)
72 self._checking = Lock()75 self._checking = Lock()
73 self._update = None76 self._update = None
74 self._downloading = False77 self._downloading = False
@@ -79,17 +82,22 @@
7982
80 def _check_for_update(self):83 def _check_for_update(self):
81 # Asynchronous method call.84 # Asynchronous method call.
85 log.info('Checking for update')
82 self._update = self._api.check_for_update()86 self._update = self._api.check_for_update()
83 # Do we have an update and can we auto-download it?87 # Do we have an update and can we auto-download it?
84 downloading = False88 downloading = False
85 if self._update.is_available:89 if self._update.is_available:
86 settings = Settings()90 settings = Settings()
87 auto = settings.get('auto_download')91 auto = settings.get('auto_download')
92 log.info('Update available; auto-download: {}', auto)
88 if auto in ('1', '2'):93 if auto in ('1', '2'):
89 # XXX When we have access to the download service, we can94 # XXX When we have access to the download service, we can
90 # check if we're on the wifi (auto == '1').95 # check if we're on the wifi (auto == '1').
91 GLib.timeout_add(50, self._download)96 GLib.timeout_add(50, self._download, self._checking.release)
92 downloading = True97 downloading = True
98 else:
99 log.info('release checking lock from _check_for_update()')
100 self._checking.release()
93 self.UpdateAvailableStatus(101 self.UpdateAvailableStatus(
94 self._update.is_available,102 self._update.is_available,
95 downloading,103 downloading,
@@ -100,7 +108,6 @@
100 # array of dictionaries data type. LP: #1215586108 # array of dictionaries data type. LP: #1215586
101 #self._update.descriptions,109 #self._update.descriptions,
102 "")110 "")
103 self._checking.release()
104 # Stop GLib from calling this method again.111 # Stop GLib from calling this method again.
105 return False112 return False
106113
@@ -122,12 +129,16 @@
122 """129 """
123 self._loop.keepalive()130 self._loop.keepalive()
124 # Check-and-acquire the lock.131 # Check-and-acquire the lock.
132 log.info('test and acquire checking lock')
125 if not self._checking.acquire(blocking=False):133 if not self._checking.acquire(blocking=False):
126 # Check is already in progress, so there's nothing more to do.134 # Check is already in progress, so there's nothing more to do.
135 log.info('checking lock not acquired')
127 return136 return
137 log.info('checking lock acquired')
128 # We've now acquired the lock. Reset any failure or in-progress138 # We've now acquired the lock. Reset any failure or in-progress
129 # state. Get a new mediator to reset any of its state.139 # state. Get a new mediator to reset any of its state.
130 self._api = Mediator(self._progress_callback)140 self._api = Mediator(self._progress_callback)
141 log.info('Mediator recreated {}', self._api)
131 self._failure_count = 0142 self._failure_count = 0
132 self._last_error = ''143 self._last_error = ''
133 # Arrange for the actual check to happen in a little while, so that144 # Arrange for the actual check to happen in a little while, so that
@@ -142,21 +153,25 @@
142 eta = 0153 eta = 0
143 self.UpdateProgress(percentage, eta)154 self.UpdateProgress(percentage, eta)
144155
145 def _download(self):156 def _download(self, release_checking=None):
146 if self._downloading and self._paused:157 if self._downloading and self._paused:
147 self._api.resume()158 self._api.resume()
148 self._paused = False159 self._paused = False
160 log.info('Download previously paused')
149 return161 return
150 if (self._downloading # Already in progress.162 if (self._downloading # Already in progress.
151 or self._update is None # Not yet checked.163 or self._update is None # Not yet checked.
152 or not self._update.is_available # No update available.164 or not self._update.is_available # No update available.
153 ):165 ):
166 log.info('Download already in progress or not available')
154 return167 return
155 if self._failure_count > 0:168 if self._failure_count > 0:
156 self._failure_count += 1169 self._failure_count += 1
157 self.UpdateFailed(self._failure_count, self._last_error)170 self.UpdateFailed(self._failure_count, self._last_error)
171 log.info('Update failure count: {}', self._failure_count)
158 return172 return
159 self._downloading = True173 self._downloading = True
174 log.info('Update is downloading')
160 try:175 try:
161 # Always start by sending a UpdateProgress(0, 0). This is176 # Always start by sending a UpdateProgress(0, 0). This is
162 # enough to get the u/i's attention.177 # enough to get the u/i's attention.
@@ -168,13 +183,20 @@
168 # value, but not the traceback.183 # value, but not the traceback.
169 self._last_error = EMPTYSTRING.join(184 self._last_error = EMPTYSTRING.join(
170 traceback.format_exception_only(*sys.exc_info()[:2]))185 traceback.format_exception_only(*sys.exc_info()[:2]))
186 log.info('Update failed: {}', self._last_error)
171 self.UpdateFailed(self._failure_count, self._last_error)187 self.UpdateFailed(self._failure_count, self._last_error)
172 else:188 else:
189 log.info('Update downloaded')
173 self.UpdateDownloaded()190 self.UpdateDownloaded()
174 self._failure_count = 0191 self._failure_count = 0
175 self._last_error = ''192 self._last_error = ''
176 self._rebootable = True193 self._rebootable = True
177 self._downloading = False194 self._downloading = False
195 log.info('release checking lock from _download()')
196 if release_checking is not None:
197 # We were auto-downloading, so we now have to release the checking
198 # lock. If we were manually downloading, there would be no lock.
199 release_checking()
178 # Stop GLib from calling this method again.200 # Stop GLib from calling this method again.
179 return False201 return False
180202
@@ -297,31 +319,40 @@
297 #descriptions,319 #descriptions,
298 error_reason):320 error_reason):
299 """Signal sent in response to a CheckForUpdate()."""321 """Signal sent in response to a CheckForUpdate()."""
322 log.info('EMIT UpdateAvailableStatus({}, {}, {}, {}, {}, {})',
323 is_available, downloading, available_version, update_size,
324 last_update_date, repr(error_reason))
300 self._loop.keepalive()325 self._loop.keepalive()
301326
302 @signal('com.canonical.SystemImage', signature='id')327 @signal('com.canonical.SystemImage', signature='id')
303 def UpdateProgress(self, percentage, eta):328 def UpdateProgress(self, percentage, eta):
304 """Download progress."""329 """Download progress."""
330 log.info('EMIT UpdateProgress({}, {})', percentage, eta)
305 self._loop.keepalive()331 self._loop.keepalive()
306332
307 @signal('com.canonical.SystemImage')333 @signal('com.canonical.SystemImage')
308 def UpdateDownloaded(self):334 def UpdateDownloaded(self):
309 """The update has been successfully downloaded."""335 """The update has been successfully downloaded."""
336 log.info('EMIT UpdateDownloaded()')
310 self._loop.keepalive()337 self._loop.keepalive()
311338
312 @signal('com.canonical.SystemImage', signature='is')339 @signal('com.canonical.SystemImage', signature='is')
313 def UpdateFailed(self, consecutive_failure_count, last_reason):340 def UpdateFailed(self, consecutive_failure_count, last_reason):
314 """The update failed for some reason."""341 """The update failed for some reason."""
342 log.info('EMIT UpdateFailed({}, {})',
343 consecutive_failure_count, repr(last_reason))
315 self._loop.keepalive()344 self._loop.keepalive()
316345
317 @signal('com.canonical.SystemImage', signature='i')346 @signal('com.canonical.SystemImage', signature='i')
318 def UpdatePaused(self, percentage):347 def UpdatePaused(self, percentage):
319 """The download got paused."""348 """The download got paused."""
349 log.info('EMIT UpdatePaused({})', percentage)
320 self._loop.keepalive()350 self._loop.keepalive()
321351
322 @signal('com.canonical.SystemImage', signature='ss')352 @signal('com.canonical.SystemImage', signature='ss')
323 def SettingChanged(self, key, new_value):353 def SettingChanged(self, key, new_value):
324 """A setting value has change."""354 """A setting value has change."""
355 log.info('EMIT SettingChanged({}, {})', repr(key), repr(new_value))
325 self._loop.keepalive()356 self._loop.keepalive()
326357
327 @signal('com.canonical.SystemImage', signature='b')358 @signal('com.canonical.SystemImage', signature='b')
@@ -329,3 +360,4 @@
329 """The system is rebooting."""360 """The system is rebooting."""
330 # We don't need to keep the loop alive since we're probably just going361 # We don't need to keep the loop alive since we're probably just going
331 # to shutdown anyway.362 # to shutdown anyway.
363 log.info('EMIT Rebooting({})', status)
332364
=== modified file 'systemimage/download.py'
--- systemimage/download.py 2014-02-13 18:16:17 +0000
+++ systemimage/download.py 2014-02-18 20:26:47 +0000
@@ -22,12 +22,16 @@
22 ]22 ]
2323
2424
25import os
25import dbus26import dbus
26import logging27import logging
28import tempfile
2729
30from contextlib import ExitStack
28from io import StringIO31from io import StringIO
29from pprint import pformat32from pprint import pformat
30from systemimage.config import config33from systemimage.config import config
34from systemimage.helpers import safe_remove
31from systemimage.reactor import Reactor35from systemimage.reactor import Reactor
3236
3337
@@ -191,8 +195,25 @@
191 for url, dst in downloads:195 for url, dst in downloads:
192 print('\t{} -> {}'.format(url, dst), file=fp)196 print('\t{} -> {}'.format(url, dst), file=fp)
193 log.info('{}'.format(fp.getvalue()))197 log.info('{}'.format(fp.getvalue()))
198 # As a workaround for LP: #1277589, ask u-d-m to download the files to
199 # .tmp files, and if they succeed, then atomically move them into
200 # their real location.
201 renames = []
202 requests = []
203 for url, dst in downloads:
204 head, tail = os.path.split(dst)
205 fd, path = tempfile.mkstemp(suffix='.tmp', prefix='', dir=head)
206 os.close(fd)
207 renames.append((path, dst))
208 requests.append((url, path, ''))
209 # mkstemp() creates the file system path, but if the files exist when
210 # the group download is requested, ubuntu-download-manager will
211 # complain and return an error. So, delete all temporary files now so
212 # udm has a clear path to download to.
213 for path, dst in renames:
214 os.remove(path)
194 object_path = iface.createDownloadGroup(215 object_path = iface.createDownloadGroup(
195 [(url, dst, '') for url, dst in downloads],216 requests, # The temporary requests.
196 '', # No hashes yet.217 '', # No hashes yet.
197 False, # Don't allow GSM yet.218 False, # Don't allow GSM yet.
198 # https://bugs.freedesktop.org/show_bug.cgi?id=55594219 # https://bugs.freedesktop.org/show_bug.cgi?id=55594
@@ -220,6 +241,19 @@
220 raise Canceled241 raise Canceled
221 if reactor.timed_out:242 if reactor.timed_out:
222 raise TimeoutError243 raise TimeoutError
244 # Now that everything succeeded, rename the temporary files. Just to
245 # be extra cautious, set up a context manager to safely remove all
246 # temporary files in case of an error. If there are no errors, then
247 # there will be nothing to remove.
248 with ExitStack() as resources:
249 for tmp, dst in renames:
250 resources.callback(safe_remove, tmp)
251 for tmp, dst in renames:
252 os.rename(tmp, dst)
253 # We only get here if all the renames succeeded, so there will be
254 # no temporary files to remove, so we can throw away the new
255 # ExitStack, which holds all the removals.
256 resources.pop_all()
223257
224 def cancel(self):258 def cancel(self):
225 """Cancel any current downloads."""259 """Cancel any current downloads."""
226260
=== modified file 'systemimage/service.py'
--- systemimage/service.py 2014-02-14 20:10:42 +0000
+++ systemimage/service.py 2014-02-18 20:26:47 +0000
@@ -30,7 +30,7 @@
30from dbus.mainloop.glib import DBusGMainLoop30from dbus.mainloop.glib import DBusGMainLoop
31from pkg_resources import resource_string as resource_bytes31from pkg_resources import resource_string as resource_bytes
32from systemimage.config import config32from systemimage.config import config
33from systemimage.dbus import Loop, Service33from systemimage.dbus import Loop
34from systemimage.helpers import makedirs34from systemimage.helpers import makedirs
35from systemimage.logging import initialize35from systemimage.logging import initialize
36from systemimage.main import DEFAULT_CONFIG_FILE36from systemimage.main import DEFAULT_CONFIG_FILE
@@ -114,6 +114,7 @@
114 config.dbus_service = get_service(114 config.dbus_service = get_service(
115 testing_mode, system_bus, '/Service', loop)115 testing_mode, system_bus, '/Service', loop)
116 else:116 else:
117 from systemimage.dbus import Service
117 config.dbus_service = Service(system_bus, '/Service', loop)118 config.dbus_service = Service(system_bus, '/Service', loop)
118 try:119 try:
119 loop.run()120 loop.run()
120121
=== modified file 'systemimage/testing/controller.py'
--- systemimage/testing/controller.py 2014-02-13 18:16:17 +0000
+++ systemimage/testing/controller.py 2014-02-18 20:26:47 +0000
@@ -125,7 +125,7 @@
125class Controller:125class Controller:
126 """Start and stop D-Bus service under test."""126 """Start and stop D-Bus service under test."""
127127
128 def __init__(self):128 def __init__(self, logfile=None):
129 # Non-public.129 # Non-public.
130 self._stack = ExitStack()130 self._stack = ExitStack()
131 self._stoppers = []131 self._stoppers = []
@@ -148,11 +148,15 @@
148 # We need a client.ini file for the subprocess.148 # We need a client.ini file for the subprocess.
149 ini_tmpdir = self._stack.enter_context(temporary_directory())149 ini_tmpdir = self._stack.enter_context(temporary_directory())
150 ini_vardir = self._stack.enter_context(temporary_directory())150 ini_vardir = self._stack.enter_context(temporary_directory())
151 ini_logfile = (os.path.join(ini_tmpdir, 'client.log')
152 if logfile is None
153 else logfile)
151 self.ini_path = os.path.join(self.tmpdir, 'client.ini')154 self.ini_path = os.path.join(self.tmpdir, 'client.ini')
152 template = resource_bytes(155 template = resource_bytes(
153 'systemimage.tests.data', 'config_03.ini').decode('utf-8')156 'systemimage.tests.data', 'config_03.ini').decode('utf-8')
154 with open(self.ini_path, 'w', encoding='utf-8') as fp:157 with open(self.ini_path, 'w', encoding='utf-8') as fp:
155 print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir),158 print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir,
159 logfile=ini_logfile),
156 file=fp)160 file=fp)
157161
158 def _configure_services(self):162 def _configure_services(self):
159163
=== modified file 'systemimage/testing/helpers.py'
--- systemimage/testing/helpers.py 2014-02-13 21:44:16 +0000
+++ systemimage/testing/helpers.py 2014-02-18 20:26:47 +0000
@@ -347,7 +347,7 @@
347 setup_keyring_txz(keyring + '.gpg', signing_kr, json_data, dst)347 setup_keyring_txz(keyring + '.gpg', signing_kr, json_data, dst)
348348
349349
350def setup_index(index, todir, keyring):350def setup_index(index, todir, keyring, write_callback=None):
351 for image in get_index(index).images:351 for image in get_index(index).images:
352 for filerec in image.files:352 for filerec in image.files:
353 path = (filerec.path[1:]353 path = (filerec.path[1:]
@@ -355,10 +355,13 @@
355 else filerec.path)355 else filerec.path)
356 dst = os.path.join(todir, path)356 dst = os.path.join(todir, path)
357 makedirs(os.path.dirname(dst))357 makedirs(os.path.dirname(dst))
358 contents = EMPTYSTRING.join(358 if write_callback is None:
359 os.path.splitext(filerec.path)[0].split('/'))359 contents = EMPTYSTRING.join(
360 with open(dst, 'w', encoding='utf-8') as fp:360 os.path.splitext(filerec.path)[0].split('/'))
361 fp.write(contents)361 with open(dst, 'w', encoding='utf-8') as fp:
362 fp.write(contents)
363 else:
364 write_callback(dst)
362 # Sign with the specified signing key.365 # Sign with the specified signing key.
363 sign(dst, keyring)366 sign(dst, keyring)
364367
365368
=== modified file 'systemimage/testing/nose.py'
--- systemimage/testing/nose.py 2014-02-14 20:12:08 +0000
+++ systemimage/testing/nose.py 2014-02-18 20:26:47 +0000
@@ -75,15 +75,24 @@
75 super().__init__()75 super().__init__()
76 self.patterns = []76 self.patterns = []
77 self.verbosity = 077 self.verbosity = 0
78 self.log_file = None
78 self.addArgument(self.patterns, 'P', 'pattern',79 self.addArgument(self.patterns, 'P', 'pattern',
79 'Add a test matching pattern')80 'Add a test matching pattern')
80 def bump(ignore):81 def bump(ignore):
81 self.verbosity += 182 self.verbosity += 1
82 self.addFlag(bump, 'V', 'Verbosity',83 self.addFlag(bump, 'V', 'Verbosity',
83 'Increase system-image verbosity')84 'Increase system-image verbosity')
85 def set_log_file(path):
86 self.log_file = path[0]
87 self.addOption(set_log_file, 'L', 'logfile',
88 'Set the log file for the test run',
89 nargs=1)
8490
85 @configuration91 @configuration
86 def startTestRun(self, event):92 def startTestRun(self, event):
93 from systemimage.config import config
94 if self.log_file is not None:
95 config.system.logfile = self.log_file
87 DBusGMainLoop(set_as_default=True)96 DBusGMainLoop(set_as_default=True)
88 initialize(verbosity=self.verbosity)97 initialize(verbosity=self.verbosity)
89 # We need to set up the dbus service controller, since all the tests98 # We need to set up the dbus service controller, since all the tests
@@ -92,7 +101,7 @@
92 # individual services, and we can write new dbus configuration files101 # individual services, and we can write new dbus configuration files
93 # and HUP the dbus-launch to re-read them, but we cannot change bus102 # and HUP the dbus-launch to re-read them, but we cannot change bus
94 # addresses after the initial one is set.103 # addresses after the initial one is set.
95 SystemImagePlugin.controller = Controller()104 SystemImagePlugin.controller = Controller(self.log_file)
96 SystemImagePlugin.controller.start()105 SystemImagePlugin.controller.start()
97 atexit.register(SystemImagePlugin.controller.stop)106 atexit.register(SystemImagePlugin.controller.stop)
98107
99108
=== modified file 'systemimage/tests/data/config_03.ini'
--- systemimage/tests/data/config_03.ini 2013-11-12 19:57:39 +0000
+++ systemimage/tests/data/config_03.ini 2014-02-18 20:26:47 +0000
@@ -14,7 +14,7 @@
14timeout: 1s14timeout: 1s
15build_file: {tmpdir}/ubuntu-build15build_file: {tmpdir}/ubuntu-build
16tempdir: {tmpdir}/tmp16tempdir: {tmpdir}/tmp
17logfile: {tmpdir}/client.log17logfile: {logfile}
18loglevel: info18loglevel: info
19settings_db: {vardir}/settings.db19settings_db: {vardir}/settings.db
2020
2121
=== added file 'systemimage/tests/data/index_24.json'
--- systemimage/tests/data/index_24.json 1970-01-01 00:00:00 +0000
+++ systemimage/tests/data/index_24.json 2014-02-18 20:26:47 +0000
@@ -0,0 +1,36 @@
1{
2 "global": {
3 "generated_at": "Thu Aug 01 08:01:00 UTC 2013"
4 },
5 "images": [
6 {
7 "description": "Full",
8 "files": [
9 {
10 "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc",
11 "order": 3,
12 "path": "/3/4/5.txt",
13 "signature": "/3/4/5.txt.asc",
14 "size": 104857600
15 },
16 {
17 "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc",
18 "order": 1,
19 "path": "/4/5/6.txt",
20 "signature": "/4/5/6.txt.asc",
21 "size": 104857600
22 },
23 {
24 "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc",
25 "order": 2,
26 "path": "/5/6/7.txt",
27 "signature": "/5/6/7.txt.asc",
28 "size": 104857600
29 }
30 ],
31 "type": "full",
32 "version": 1600,
33 "bootme": true
34 }
35 ]
36}
037
=== modified file 'systemimage/tests/test_dbus.py'
--- systemimage/tests/test_dbus.py 2014-02-14 17:10:38 +0000
+++ systemimage/tests/test_dbus.py 2014-02-18 20:26:47 +0000
@@ -23,6 +23,7 @@
23 'TestDBusGetSet',23 'TestDBusGetSet',
24 'TestDBusInfo',24 'TestDBusInfo',
25 'TestDBusInfoNoDetails',25 'TestDBusInfoNoDetails',
26 'TestDBusLP1277589',
26 'TestDBusMockFailApply',27 'TestDBusMockFailApply',
27 'TestDBusMockFailPause',28 'TestDBusMockFailPause',
28 'TestDBusMockFailResume',29 'TestDBusMockFailResume',
@@ -122,6 +123,23 @@
122 self.quit()123 self.quit()
123124
124125
126class DoubleCheckingReactor(Reactor):
127 def __init__(self, iface):
128 super().__init__(dbus.SystemBus())
129 self.iface = iface
130 self.uas_signals = []
131 self.react_to('UpdateAvailableStatus')
132 self.react_to('UpdateDownloaded')
133 self.schedule(self.iface.CheckForUpdate)
134
135 def _do_UpdateAvailableStatus(self, signal, path, *args, **kws):
136 self.uas_signals.append(args)
137 self.schedule(self.iface.CheckForUpdate)
138
139 def _do_UpdateDownloaded(self, *args, **kws):
140 self.quit()
141
142
125class _TestBase(unittest.TestCase):143class _TestBase(unittest.TestCase):
126 """Base class for all DBus testing."""144 """Base class for all DBus testing."""
127145
@@ -262,13 +280,14 @@
262 safe_remove(self.reboot_log)280 safe_remove(self.reboot_log)
263 super().tearDown()281 super().tearDown()
264282
265 def _prepare_index(self, index_file):283 def _prepare_index(self, index_file, write_callback=None):
266 serverdir = SystemImagePlugin.controller.serverdir284 serverdir = SystemImagePlugin.controller.serverdir
267 index_path = os.path.join(serverdir, 'stable', 'nexus7', 'index.json')285 index_path = os.path.join(serverdir, 'stable', 'nexus7', 'index.json')
268 head, tail = os.path.split(index_path)286 head, tail = os.path.split(index_path)
269 copy(index_file, head, tail)287 copy(index_file, head, tail)
270 sign(index_path, 'device-signing.gpg')288 sign(index_path, 'device-signing.gpg')
271 setup_index(index_file, serverdir, 'device-signing.gpg')289 setup_index(index_file, serverdir, 'device-signing.gpg',
290 write_callback)
272291
273 def _touch_build(self, version):292 def _touch_build(self, version):
274 # Unlike the touch_build() helper, this one uses our own config object293 # Unlike the touch_build() helper, this one uses our own config object
@@ -1542,3 +1561,45 @@
1542update 5.txt 5.txt.asc1561update 5.txt 5.txt.asc
1543unmount system1562unmount system
1544""")1563""")
1564
1565
1566class TestDBusLP1277589(_LiveTesting):
1567 def test_multiple_check_for_updates(self):
1568 # Log analysis of LP: #1277589 appears to show the following scenario,
1569 # reproduced in this test case:
1570 #
1571 # * Automatic updates are enabled.
1572 # * No image signing or image master keys are present.
1573 # * A full update is checked.
1574 # - A new image master key and image signing key is downloaded.
1575 # - Update is available
1576 #
1577 # Start by creating some big files which will take a while to
1578 # download.
1579 def write_callback(dst):
1580 # Write a 100 MiB sized file.
1581 with open(dst, 'wb') as fp:
1582 for i in range(25600):
1583 fp.write(b'x' * 4096)
1584 self._prepare_index('index_24.json', write_callback)
1585 # Create a reactor that will exit when the UpdateDownloaded signal is
1586 # received. We're going to issue a CheckForUpdate with automatic
1587 # updates enabled. As soon as we receive the UpdateAvailableStatus
1588 # signal, we'll immediately issue *another* CheckForUpdate, which
1589 # should run while the auto-download is working.
1590 #
1591 # At the end, we should not get another UpdateAvailableStatus signal,
1592 # but we should get the UpdateDownloaded signal.
1593 reactor = DoubleCheckingReactor(self.iface)
1594 reactor.run()
1595 self.assertEqual(len(reactor.uas_signals), 1)
1596 (is_available, downloading, available_version, update_size,
1597 last_update_date,
1598 #descriptions,
1599 error_reason) = reactor.uas_signals[0]
1600 self.assertTrue(is_available)
1601 self.assertTrue(downloading)
1602 self.assertEqual(available_version, '1600')
1603 self.assertEqual(update_size, 314572800)
1604 self.assertEqual(last_update_date, 'Unknown')
1605 self.assertEqual(error_reason, '')

Subscribers

People subscribed via source and target branches