Merge lp:~barry/ubuntu-system-image/lp1277589-udm into lp:~registry/ubuntu-system-image/client
- lp1277589-udm
- Merge into 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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Registry Administrators | Pending | ||
Review via email: mp+207026@code.launchpad.net |
Commit message
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
1 | === modified file 'NEWS.rst' | |||
2 | --- NEWS.rst 2014-02-14 22:47:33 +0000 | |||
3 | +++ NEWS.rst 2014-02-18 20:26:47 +0000 | |||
4 | @@ -17,6 +17,9 @@ | |||
5 | 17 | * Return the empty string from `ApplyUpdate()` D-Bus method. This restores | 17 | * Return the empty string from `ApplyUpdate()` D-Bus method. This restores |
6 | 18 | the original API (patch merged from Ubuntu package, given by Didier | 18 | the original API (patch merged from Ubuntu package, given by Didier |
7 | 19 | Roche). (LP: #1260768) | 19 | Roche). (LP: #1260768) |
8 | 20 | * Request ubuntu-download-manager to download all files to temporary | ||
9 | 21 | destinations, then atomically rename them into place. This avoids | ||
10 | 22 | clobbering by multiple processes and mimic changes coming in u-d-m. | ||
11 | 20 | 23 | ||
12 | 21 | 2.0.5 (2014-01-30) | 24 | 2.0.5 (2014-01-30) |
13 | 22 | ================== | 25 | ================== |
14 | 23 | 26 | ||
15 | === modified file 'systemimage/api.py' | |||
16 | --- systemimage/api.py 2014-02-13 18:16:17 +0000 | |||
17 | +++ systemimage/api.py 2014-02-18 20:26:47 +0000 | |||
18 | @@ -73,6 +73,10 @@ | |||
19 | 73 | self._update = None | 73 | self._update = None |
20 | 74 | self._callback = callback | 74 | self._callback = callback |
21 | 75 | 75 | ||
22 | 76 | def __repr__(self): | ||
23 | 77 | return '<Mediator at 0x{:x} | State at 0x{:x}>'.format( | ||
24 | 78 | id(self), id(self._state)) | ||
25 | 79 | |||
26 | 76 | def cancel(self): | 80 | def cancel(self): |
27 | 77 | self._state.downloader.cancel() | 81 | self._state.downloader.cancel() |
28 | 78 | 82 | ||
29 | 79 | 83 | ||
30 | === modified file 'systemimage/dbus.py' | |||
31 | --- systemimage/dbus.py 2014-02-14 22:47:33 +0000 | |||
32 | +++ systemimage/dbus.py 2014-02-18 20:26:47 +0000 | |||
33 | @@ -23,6 +23,7 @@ | |||
34 | 23 | 23 | ||
35 | 24 | import os | 24 | import os |
36 | 25 | import sys | 25 | import sys |
37 | 26 | import logging | ||
38 | 26 | import traceback | 27 | import traceback |
39 | 27 | 28 | ||
40 | 28 | from dbus.service import Object, method, signal | 29 | from dbus.service import Object, method, signal |
41 | @@ -35,6 +36,7 @@ | |||
42 | 35 | 36 | ||
43 | 36 | 37 | ||
44 | 37 | EMPTYSTRING = '' | 38 | EMPTYSTRING = '' |
45 | 39 | log = logging.getLogger('systemimage') | ||
46 | 38 | 40 | ||
47 | 39 | 41 | ||
48 | 40 | class Loop: | 42 | class Loop: |
49 | @@ -69,6 +71,7 @@ | |||
50 | 69 | super().__init__(bus, object_path) | 71 | super().__init__(bus, object_path) |
51 | 70 | self._loop = loop | 72 | self._loop = loop |
52 | 71 | self._api = Mediator(self._progress_callback) | 73 | self._api = Mediator(self._progress_callback) |
53 | 74 | log.info('Mediator created {}', self._api) | ||
54 | 72 | self._checking = Lock() | 75 | self._checking = Lock() |
55 | 73 | self._update = None | 76 | self._update = None |
56 | 74 | self._downloading = False | 77 | self._downloading = False |
57 | @@ -79,17 +82,22 @@ | |||
58 | 79 | 82 | ||
59 | 80 | def _check_for_update(self): | 83 | def _check_for_update(self): |
60 | 81 | # Asynchronous method call. | 84 | # Asynchronous method call. |
61 | 85 | log.info('Checking for update') | ||
62 | 82 | self._update = self._api.check_for_update() | 86 | self._update = self._api.check_for_update() |
63 | 83 | # Do we have an update and can we auto-download it? | 87 | # Do we have an update and can we auto-download it? |
64 | 84 | downloading = False | 88 | downloading = False |
65 | 85 | if self._update.is_available: | 89 | if self._update.is_available: |
66 | 86 | settings = Settings() | 90 | settings = Settings() |
67 | 87 | auto = settings.get('auto_download') | 91 | auto = settings.get('auto_download') |
68 | 92 | log.info('Update available; auto-download: {}', auto) | ||
69 | 88 | if auto in ('1', '2'): | 93 | if auto in ('1', '2'): |
70 | 89 | # XXX When we have access to the download service, we can | 94 | # XXX When we have access to the download service, we can |
71 | 90 | # check if we're on the wifi (auto == '1'). | 95 | # check if we're on the wifi (auto == '1'). |
73 | 91 | GLib.timeout_add(50, self._download) | 96 | GLib.timeout_add(50, self._download, self._checking.release) |
74 | 92 | downloading = True | 97 | downloading = True |
75 | 98 | else: | ||
76 | 99 | log.info('release checking lock from _check_for_update()') | ||
77 | 100 | self._checking.release() | ||
78 | 93 | self.UpdateAvailableStatus( | 101 | self.UpdateAvailableStatus( |
79 | 94 | self._update.is_available, | 102 | self._update.is_available, |
80 | 95 | downloading, | 103 | downloading, |
81 | @@ -100,7 +108,6 @@ | |||
82 | 100 | # array of dictionaries data type. LP: #1215586 | 108 | # array of dictionaries data type. LP: #1215586 |
83 | 101 | #self._update.descriptions, | 109 | #self._update.descriptions, |
84 | 102 | "") | 110 | "") |
85 | 103 | self._checking.release() | ||
86 | 104 | # Stop GLib from calling this method again. | 111 | # Stop GLib from calling this method again. |
87 | 105 | return False | 112 | return False |
88 | 106 | 113 | ||
89 | @@ -122,12 +129,16 @@ | |||
90 | 122 | """ | 129 | """ |
91 | 123 | self._loop.keepalive() | 130 | self._loop.keepalive() |
92 | 124 | # Check-and-acquire the lock. | 131 | # Check-and-acquire the lock. |
93 | 132 | log.info('test and acquire checking lock') | ||
94 | 125 | if not self._checking.acquire(blocking=False): | 133 | if not self._checking.acquire(blocking=False): |
95 | 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. |
96 | 135 | log.info('checking lock not acquired') | ||
97 | 127 | return | 136 | return |
98 | 137 | log.info('checking lock acquired') | ||
99 | 128 | # We've now acquired the lock. Reset any failure or in-progress | 138 | # We've now acquired the lock. Reset any failure or in-progress |
100 | 129 | # state. Get a new mediator to reset any of its state. | 139 | # state. Get a new mediator to reset any of its state. |
101 | 130 | self._api = Mediator(self._progress_callback) | 140 | self._api = Mediator(self._progress_callback) |
102 | 141 | log.info('Mediator recreated {}', self._api) | ||
103 | 131 | self._failure_count = 0 | 142 | self._failure_count = 0 |
104 | 132 | self._last_error = '' | 143 | self._last_error = '' |
105 | 133 | # Arrange for the actual check to happen in a little while, so that | 144 | # Arrange for the actual check to happen in a little while, so that |
106 | @@ -142,21 +153,25 @@ | |||
107 | 142 | eta = 0 | 153 | eta = 0 |
108 | 143 | self.UpdateProgress(percentage, eta) | 154 | self.UpdateProgress(percentage, eta) |
109 | 144 | 155 | ||
111 | 145 | def _download(self): | 156 | def _download(self, release_checking=None): |
112 | 146 | if self._downloading and self._paused: | 157 | if self._downloading and self._paused: |
113 | 147 | self._api.resume() | 158 | self._api.resume() |
114 | 148 | self._paused = False | 159 | self._paused = False |
115 | 160 | log.info('Download previously paused') | ||
116 | 149 | return | 161 | return |
117 | 150 | if (self._downloading # Already in progress. | 162 | if (self._downloading # Already in progress. |
118 | 151 | or self._update is None # Not yet checked. | 163 | or self._update is None # Not yet checked. |
119 | 152 | or not self._update.is_available # No update available. | 164 | or not self._update.is_available # No update available. |
120 | 153 | ): | 165 | ): |
121 | 166 | log.info('Download already in progress or not available') | ||
122 | 154 | return | 167 | return |
123 | 155 | if self._failure_count > 0: | 168 | if self._failure_count > 0: |
124 | 156 | self._failure_count += 1 | 169 | self._failure_count += 1 |
125 | 157 | self.UpdateFailed(self._failure_count, self._last_error) | 170 | self.UpdateFailed(self._failure_count, self._last_error) |
126 | 171 | log.info('Update failure count: {}', self._failure_count) | ||
127 | 158 | return | 172 | return |
128 | 159 | self._downloading = True | 173 | self._downloading = True |
129 | 174 | log.info('Update is downloading') | ||
130 | 160 | try: | 175 | try: |
131 | 161 | # Always start by sending a UpdateProgress(0, 0). This is | 176 | # Always start by sending a UpdateProgress(0, 0). This is |
132 | 162 | # enough to get the u/i's attention. | 177 | # enough to get the u/i's attention. |
133 | @@ -168,13 +183,20 @@ | |||
134 | 168 | # value, but not the traceback. | 183 | # value, but not the traceback. |
135 | 169 | self._last_error = EMPTYSTRING.join( | 184 | self._last_error = EMPTYSTRING.join( |
136 | 170 | traceback.format_exception_only(*sys.exc_info()[:2])) | 185 | traceback.format_exception_only(*sys.exc_info()[:2])) |
137 | 186 | log.info('Update failed: {}', self._last_error) | ||
138 | 171 | self.UpdateFailed(self._failure_count, self._last_error) | 187 | self.UpdateFailed(self._failure_count, self._last_error) |
139 | 172 | else: | 188 | else: |
140 | 189 | log.info('Update downloaded') | ||
141 | 173 | self.UpdateDownloaded() | 190 | self.UpdateDownloaded() |
142 | 174 | self._failure_count = 0 | 191 | self._failure_count = 0 |
143 | 175 | self._last_error = '' | 192 | self._last_error = '' |
144 | 176 | self._rebootable = True | 193 | self._rebootable = True |
145 | 177 | self._downloading = False | 194 | self._downloading = False |
146 | 195 | log.info('release checking lock from _download()') | ||
147 | 196 | if release_checking is not None: | ||
148 | 197 | # We were auto-downloading, so we now have to release the checking | ||
149 | 198 | # lock. If we were manually downloading, there would be no lock. | ||
150 | 199 | release_checking() | ||
151 | 178 | # Stop GLib from calling this method again. | 200 | # Stop GLib from calling this method again. |
152 | 179 | return False | 201 | return False |
153 | 180 | 202 | ||
154 | @@ -297,31 +319,40 @@ | |||
155 | 297 | #descriptions, | 319 | #descriptions, |
156 | 298 | error_reason): | 320 | error_reason): |
157 | 299 | """Signal sent in response to a CheckForUpdate().""" | 321 | """Signal sent in response to a CheckForUpdate().""" |
158 | 322 | log.info('EMIT UpdateAvailableStatus({}, {}, {}, {}, {}, {})', | ||
159 | 323 | is_available, downloading, available_version, update_size, | ||
160 | 324 | last_update_date, repr(error_reason)) | ||
161 | 300 | self._loop.keepalive() | 325 | self._loop.keepalive() |
162 | 301 | 326 | ||
163 | 302 | @signal('com.canonical.SystemImage', signature='id') | 327 | @signal('com.canonical.SystemImage', signature='id') |
164 | 303 | def UpdateProgress(self, percentage, eta): | 328 | def UpdateProgress(self, percentage, eta): |
165 | 304 | """Download progress.""" | 329 | """Download progress.""" |
166 | 330 | log.info('EMIT UpdateProgress({}, {})', percentage, eta) | ||
167 | 305 | self._loop.keepalive() | 331 | self._loop.keepalive() |
168 | 306 | 332 | ||
169 | 307 | @signal('com.canonical.SystemImage') | 333 | @signal('com.canonical.SystemImage') |
170 | 308 | def UpdateDownloaded(self): | 334 | def UpdateDownloaded(self): |
171 | 309 | """The update has been successfully downloaded.""" | 335 | """The update has been successfully downloaded.""" |
172 | 336 | log.info('EMIT UpdateDownloaded()') | ||
173 | 310 | self._loop.keepalive() | 337 | self._loop.keepalive() |
174 | 311 | 338 | ||
175 | 312 | @signal('com.canonical.SystemImage', signature='is') | 339 | @signal('com.canonical.SystemImage', signature='is') |
176 | 313 | def UpdateFailed(self, consecutive_failure_count, last_reason): | 340 | def UpdateFailed(self, consecutive_failure_count, last_reason): |
177 | 314 | """The update failed for some reason.""" | 341 | """The update failed for some reason.""" |
178 | 342 | log.info('EMIT UpdateFailed({}, {})', | ||
179 | 343 | consecutive_failure_count, repr(last_reason)) | ||
180 | 315 | self._loop.keepalive() | 344 | self._loop.keepalive() |
181 | 316 | 345 | ||
182 | 317 | @signal('com.canonical.SystemImage', signature='i') | 346 | @signal('com.canonical.SystemImage', signature='i') |
183 | 318 | def UpdatePaused(self, percentage): | 347 | def UpdatePaused(self, percentage): |
184 | 319 | """The download got paused.""" | 348 | """The download got paused.""" |
185 | 349 | log.info('EMIT UpdatePaused({})', percentage) | ||
186 | 320 | self._loop.keepalive() | 350 | self._loop.keepalive() |
187 | 321 | 351 | ||
188 | 322 | @signal('com.canonical.SystemImage', signature='ss') | 352 | @signal('com.canonical.SystemImage', signature='ss') |
189 | 323 | def SettingChanged(self, key, new_value): | 353 | def SettingChanged(self, key, new_value): |
190 | 324 | """A setting value has change.""" | 354 | """A setting value has change.""" |
191 | 355 | log.info('EMIT SettingChanged({}, {})', repr(key), repr(new_value)) | ||
192 | 325 | self._loop.keepalive() | 356 | self._loop.keepalive() |
193 | 326 | 357 | ||
194 | 327 | @signal('com.canonical.SystemImage', signature='b') | 358 | @signal('com.canonical.SystemImage', signature='b') |
195 | @@ -329,3 +360,4 @@ | |||
196 | 329 | """The system is rebooting.""" | 360 | """The system is rebooting.""" |
197 | 330 | # We don't need to keep the loop alive since we're probably just going | 361 | # We don't need to keep the loop alive since we're probably just going |
198 | 331 | # to shutdown anyway. | 362 | # to shutdown anyway. |
199 | 363 | log.info('EMIT Rebooting({})', status) | ||
200 | 332 | 364 | ||
201 | === modified file 'systemimage/download.py' | |||
202 | --- systemimage/download.py 2014-02-13 18:16:17 +0000 | |||
203 | +++ systemimage/download.py 2014-02-18 20:26:47 +0000 | |||
204 | @@ -22,12 +22,16 @@ | |||
205 | 22 | ] | 22 | ] |
206 | 23 | 23 | ||
207 | 24 | 24 | ||
208 | 25 | import os | ||
209 | 25 | import dbus | 26 | import dbus |
210 | 26 | import logging | 27 | import logging |
211 | 28 | import tempfile | ||
212 | 27 | 29 | ||
213 | 30 | from contextlib import ExitStack | ||
214 | 28 | from io import StringIO | 31 | from io import StringIO |
215 | 29 | from pprint import pformat | 32 | from pprint import pformat |
216 | 30 | from systemimage.config import config | 33 | from systemimage.config import config |
217 | 34 | from systemimage.helpers import safe_remove | ||
218 | 31 | from systemimage.reactor import Reactor | 35 | from systemimage.reactor import Reactor |
219 | 32 | 36 | ||
220 | 33 | 37 | ||
221 | @@ -191,8 +195,25 @@ | |||
222 | 191 | for url, dst in downloads: | 195 | for url, dst in downloads: |
223 | 192 | print('\t{} -> {}'.format(url, dst), file=fp) | 196 | print('\t{} -> {}'.format(url, dst), file=fp) |
224 | 193 | log.info('{}'.format(fp.getvalue())) | 197 | log.info('{}'.format(fp.getvalue())) |
225 | 198 | # As a workaround for LP: #1277589, ask u-d-m to download the files to | ||
226 | 199 | # .tmp files, and if they succeed, then atomically move them into | ||
227 | 200 | # their real location. | ||
228 | 201 | renames = [] | ||
229 | 202 | requests = [] | ||
230 | 203 | for url, dst in downloads: | ||
231 | 204 | head, tail = os.path.split(dst) | ||
232 | 205 | fd, path = tempfile.mkstemp(suffix='.tmp', prefix='', dir=head) | ||
233 | 206 | os.close(fd) | ||
234 | 207 | renames.append((path, dst)) | ||
235 | 208 | requests.append((url, path, '')) | ||
236 | 209 | # mkstemp() creates the file system path, but if the files exist when | ||
237 | 210 | # the group download is requested, ubuntu-download-manager will | ||
238 | 211 | # complain and return an error. So, delete all temporary files now so | ||
239 | 212 | # udm has a clear path to download to. | ||
240 | 213 | for path, dst in renames: | ||
241 | 214 | os.remove(path) | ||
242 | 194 | object_path = iface.createDownloadGroup( | 215 | object_path = iface.createDownloadGroup( |
244 | 195 | [(url, dst, '') for url, dst in downloads], | 216 | requests, # The temporary requests. |
245 | 196 | '', # No hashes yet. | 217 | '', # No hashes yet. |
246 | 197 | False, # Don't allow GSM yet. | 218 | False, # Don't allow GSM yet. |
247 | 198 | # https://bugs.freedesktop.org/show_bug.cgi?id=55594 | 219 | # https://bugs.freedesktop.org/show_bug.cgi?id=55594 |
248 | @@ -220,6 +241,19 @@ | |||
249 | 220 | raise Canceled | 241 | raise Canceled |
250 | 221 | if reactor.timed_out: | 242 | if reactor.timed_out: |
251 | 222 | raise TimeoutError | 243 | raise TimeoutError |
252 | 244 | # Now that everything succeeded, rename the temporary files. Just to | ||
253 | 245 | # be extra cautious, set up a context manager to safely remove all | ||
254 | 246 | # temporary files in case of an error. If there are no errors, then | ||
255 | 247 | # there will be nothing to remove. | ||
256 | 248 | with ExitStack() as resources: | ||
257 | 249 | for tmp, dst in renames: | ||
258 | 250 | resources.callback(safe_remove, tmp) | ||
259 | 251 | for tmp, dst in renames: | ||
260 | 252 | os.rename(tmp, dst) | ||
261 | 253 | # We only get here if all the renames succeeded, so there will be | ||
262 | 254 | # no temporary files to remove, so we can throw away the new | ||
263 | 255 | # ExitStack, which holds all the removals. | ||
264 | 256 | resources.pop_all() | ||
265 | 223 | 257 | ||
266 | 224 | def cancel(self): | 258 | def cancel(self): |
267 | 225 | """Cancel any current downloads.""" | 259 | """Cancel any current downloads.""" |
268 | 226 | 260 | ||
269 | === modified file 'systemimage/service.py' | |||
270 | --- systemimage/service.py 2014-02-14 20:10:42 +0000 | |||
271 | +++ systemimage/service.py 2014-02-18 20:26:47 +0000 | |||
272 | @@ -30,7 +30,7 @@ | |||
273 | 30 | from dbus.mainloop.glib import DBusGMainLoop | 30 | from dbus.mainloop.glib import DBusGMainLoop |
274 | 31 | from pkg_resources import resource_string as resource_bytes | 31 | from pkg_resources import resource_string as resource_bytes |
275 | 32 | from systemimage.config import config | 32 | from systemimage.config import config |
277 | 33 | from systemimage.dbus import Loop, Service | 33 | from systemimage.dbus import Loop |
278 | 34 | from systemimage.helpers import makedirs | 34 | from systemimage.helpers import makedirs |
279 | 35 | from systemimage.logging import initialize | 35 | from systemimage.logging import initialize |
280 | 36 | from systemimage.main import DEFAULT_CONFIG_FILE | 36 | from systemimage.main import DEFAULT_CONFIG_FILE |
281 | @@ -114,6 +114,7 @@ | |||
282 | 114 | config.dbus_service = get_service( | 114 | config.dbus_service = get_service( |
283 | 115 | testing_mode, system_bus, '/Service', loop) | 115 | testing_mode, system_bus, '/Service', loop) |
284 | 116 | else: | 116 | else: |
285 | 117 | from systemimage.dbus import Service | ||
286 | 117 | config.dbus_service = Service(system_bus, '/Service', loop) | 118 | config.dbus_service = Service(system_bus, '/Service', loop) |
287 | 118 | try: | 119 | try: |
288 | 119 | loop.run() | 120 | loop.run() |
289 | 120 | 121 | ||
290 | === modified file 'systemimage/testing/controller.py' | |||
291 | --- systemimage/testing/controller.py 2014-02-13 18:16:17 +0000 | |||
292 | +++ systemimage/testing/controller.py 2014-02-18 20:26:47 +0000 | |||
293 | @@ -125,7 +125,7 @@ | |||
294 | 125 | class Controller: | 125 | class Controller: |
295 | 126 | """Start and stop D-Bus service under test.""" | 126 | """Start and stop D-Bus service under test.""" |
296 | 127 | 127 | ||
298 | 128 | def __init__(self): | 128 | def __init__(self, logfile=None): |
299 | 129 | # Non-public. | 129 | # Non-public. |
300 | 130 | self._stack = ExitStack() | 130 | self._stack = ExitStack() |
301 | 131 | self._stoppers = [] | 131 | self._stoppers = [] |
302 | @@ -148,11 +148,15 @@ | |||
303 | 148 | # We need a client.ini file for the subprocess. | 148 | # We need a client.ini file for the subprocess. |
304 | 149 | ini_tmpdir = self._stack.enter_context(temporary_directory()) | 149 | ini_tmpdir = self._stack.enter_context(temporary_directory()) |
305 | 150 | ini_vardir = self._stack.enter_context(temporary_directory()) | 150 | ini_vardir = self._stack.enter_context(temporary_directory()) |
306 | 151 | ini_logfile = (os.path.join(ini_tmpdir, 'client.log') | ||
307 | 152 | if logfile is None | ||
308 | 153 | else logfile) | ||
309 | 151 | self.ini_path = os.path.join(self.tmpdir, 'client.ini') | 154 | self.ini_path = os.path.join(self.tmpdir, 'client.ini') |
310 | 152 | template = resource_bytes( | 155 | template = resource_bytes( |
311 | 153 | 'systemimage.tests.data', 'config_03.ini').decode('utf-8') | 156 | 'systemimage.tests.data', 'config_03.ini').decode('utf-8') |
312 | 154 | with open(self.ini_path, 'w', encoding='utf-8') as fp: | 157 | with open(self.ini_path, 'w', encoding='utf-8') as fp: |
314 | 155 | print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir), | 158 | print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir, |
315 | 159 | logfile=ini_logfile), | ||
316 | 156 | file=fp) | 160 | file=fp) |
317 | 157 | 161 | ||
318 | 158 | def _configure_services(self): | 162 | def _configure_services(self): |
319 | 159 | 163 | ||
320 | === modified file 'systemimage/testing/helpers.py' | |||
321 | --- systemimage/testing/helpers.py 2014-02-13 21:44:16 +0000 | |||
322 | +++ systemimage/testing/helpers.py 2014-02-18 20:26:47 +0000 | |||
323 | @@ -347,7 +347,7 @@ | |||
324 | 347 | setup_keyring_txz(keyring + '.gpg', signing_kr, json_data, dst) | 347 | setup_keyring_txz(keyring + '.gpg', signing_kr, json_data, dst) |
325 | 348 | 348 | ||
326 | 349 | 349 | ||
328 | 350 | def setup_index(index, todir, keyring): | 350 | def setup_index(index, todir, keyring, write_callback=None): |
329 | 351 | for image in get_index(index).images: | 351 | for image in get_index(index).images: |
330 | 352 | for filerec in image.files: | 352 | for filerec in image.files: |
331 | 353 | path = (filerec.path[1:] | 353 | path = (filerec.path[1:] |
332 | @@ -355,10 +355,13 @@ | |||
333 | 355 | else filerec.path) | 355 | else filerec.path) |
334 | 356 | dst = os.path.join(todir, path) | 356 | dst = os.path.join(todir, path) |
335 | 357 | makedirs(os.path.dirname(dst)) | 357 | makedirs(os.path.dirname(dst)) |
340 | 358 | contents = EMPTYSTRING.join( | 358 | if write_callback is None: |
341 | 359 | os.path.splitext(filerec.path)[0].split('/')) | 359 | contents = EMPTYSTRING.join( |
342 | 360 | with open(dst, 'w', encoding='utf-8') as fp: | 360 | os.path.splitext(filerec.path)[0].split('/')) |
343 | 361 | fp.write(contents) | 361 | with open(dst, 'w', encoding='utf-8') as fp: |
344 | 362 | fp.write(contents) | ||
345 | 363 | else: | ||
346 | 364 | write_callback(dst) | ||
347 | 362 | # Sign with the specified signing key. | 365 | # Sign with the specified signing key. |
348 | 363 | sign(dst, keyring) | 366 | sign(dst, keyring) |
349 | 364 | 367 | ||
350 | 365 | 368 | ||
351 | === modified file 'systemimage/testing/nose.py' | |||
352 | --- systemimage/testing/nose.py 2014-02-14 20:12:08 +0000 | |||
353 | +++ systemimage/testing/nose.py 2014-02-18 20:26:47 +0000 | |||
354 | @@ -75,15 +75,24 @@ | |||
355 | 75 | super().__init__() | 75 | super().__init__() |
356 | 76 | self.patterns = [] | 76 | self.patterns = [] |
357 | 77 | self.verbosity = 0 | 77 | self.verbosity = 0 |
358 | 78 | self.log_file = None | ||
359 | 78 | self.addArgument(self.patterns, 'P', 'pattern', | 79 | self.addArgument(self.patterns, 'P', 'pattern', |
360 | 79 | 'Add a test matching pattern') | 80 | 'Add a test matching pattern') |
361 | 80 | def bump(ignore): | 81 | def bump(ignore): |
362 | 81 | self.verbosity += 1 | 82 | self.verbosity += 1 |
363 | 82 | self.addFlag(bump, 'V', 'Verbosity', | 83 | self.addFlag(bump, 'V', 'Verbosity', |
364 | 83 | 'Increase system-image verbosity') | 84 | 'Increase system-image verbosity') |
365 | 85 | def set_log_file(path): | ||
366 | 86 | self.log_file = path[0] | ||
367 | 87 | self.addOption(set_log_file, 'L', 'logfile', | ||
368 | 88 | 'Set the log file for the test run', | ||
369 | 89 | nargs=1) | ||
370 | 84 | 90 | ||
371 | 85 | @configuration | 91 | @configuration |
372 | 86 | def startTestRun(self, event): | 92 | def startTestRun(self, event): |
373 | 93 | from systemimage.config import config | ||
374 | 94 | if self.log_file is not None: | ||
375 | 95 | config.system.logfile = self.log_file | ||
376 | 87 | DBusGMainLoop(set_as_default=True) | 96 | DBusGMainLoop(set_as_default=True) |
377 | 88 | initialize(verbosity=self.verbosity) | 97 | initialize(verbosity=self.verbosity) |
378 | 89 | # We need to set up the dbus service controller, since all the tests | 98 | # We need to set up the dbus service controller, since all the tests |
379 | @@ -92,7 +101,7 @@ | |||
380 | 92 | # individual services, and we can write new dbus configuration files | 101 | # individual services, and we can write new dbus configuration files |
381 | 93 | # and HUP the dbus-launch to re-read them, but we cannot change bus | 102 | # and HUP the dbus-launch to re-read them, but we cannot change bus |
382 | 94 | # addresses after the initial one is set. | 103 | # addresses after the initial one is set. |
384 | 95 | SystemImagePlugin.controller = Controller() | 104 | SystemImagePlugin.controller = Controller(self.log_file) |
385 | 96 | SystemImagePlugin.controller.start() | 105 | SystemImagePlugin.controller.start() |
386 | 97 | atexit.register(SystemImagePlugin.controller.stop) | 106 | atexit.register(SystemImagePlugin.controller.stop) |
387 | 98 | 107 | ||
388 | 99 | 108 | ||
389 | === modified file 'systemimage/tests/data/config_03.ini' | |||
390 | --- systemimage/tests/data/config_03.ini 2013-11-12 19:57:39 +0000 | |||
391 | +++ systemimage/tests/data/config_03.ini 2014-02-18 20:26:47 +0000 | |||
392 | @@ -14,7 +14,7 @@ | |||
393 | 14 | timeout: 1s | 14 | timeout: 1s |
394 | 15 | build_file: {tmpdir}/ubuntu-build | 15 | build_file: {tmpdir}/ubuntu-build |
395 | 16 | tempdir: {tmpdir}/tmp | 16 | tempdir: {tmpdir}/tmp |
397 | 17 | logfile: {tmpdir}/client.log | 17 | logfile: {logfile} |
398 | 18 | loglevel: info | 18 | loglevel: info |
399 | 19 | settings_db: {vardir}/settings.db | 19 | settings_db: {vardir}/settings.db |
400 | 20 | 20 | ||
401 | 21 | 21 | ||
402 | === added file 'systemimage/tests/data/index_24.json' | |||
403 | --- systemimage/tests/data/index_24.json 1970-01-01 00:00:00 +0000 | |||
404 | +++ systemimage/tests/data/index_24.json 2014-02-18 20:26:47 +0000 | |||
405 | @@ -0,0 +1,36 @@ | |||
406 | 1 | { | ||
407 | 2 | "global": { | ||
408 | 3 | "generated_at": "Thu Aug 01 08:01:00 UTC 2013" | ||
409 | 4 | }, | ||
410 | 5 | "images": [ | ||
411 | 6 | { | ||
412 | 7 | "description": "Full", | ||
413 | 8 | "files": [ | ||
414 | 9 | { | ||
415 | 10 | "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", | ||
416 | 11 | "order": 3, | ||
417 | 12 | "path": "/3/4/5.txt", | ||
418 | 13 | "signature": "/3/4/5.txt.asc", | ||
419 | 14 | "size": 104857600 | ||
420 | 15 | }, | ||
421 | 16 | { | ||
422 | 17 | "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", | ||
423 | 18 | "order": 1, | ||
424 | 19 | "path": "/4/5/6.txt", | ||
425 | 20 | "signature": "/4/5/6.txt.asc", | ||
426 | 21 | "size": 104857600 | ||
427 | 22 | }, | ||
428 | 23 | { | ||
429 | 24 | "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", | ||
430 | 25 | "order": 2, | ||
431 | 26 | "path": "/5/6/7.txt", | ||
432 | 27 | "signature": "/5/6/7.txt.asc", | ||
433 | 28 | "size": 104857600 | ||
434 | 29 | } | ||
435 | 30 | ], | ||
436 | 31 | "type": "full", | ||
437 | 32 | "version": 1600, | ||
438 | 33 | "bootme": true | ||
439 | 34 | } | ||
440 | 35 | ] | ||
441 | 36 | } | ||
442 | 0 | 37 | ||
443 | === modified file 'systemimage/tests/test_dbus.py' | |||
444 | --- systemimage/tests/test_dbus.py 2014-02-14 17:10:38 +0000 | |||
445 | +++ systemimage/tests/test_dbus.py 2014-02-18 20:26:47 +0000 | |||
446 | @@ -23,6 +23,7 @@ | |||
447 | 23 | 'TestDBusGetSet', | 23 | 'TestDBusGetSet', |
448 | 24 | 'TestDBusInfo', | 24 | 'TestDBusInfo', |
449 | 25 | 'TestDBusInfoNoDetails', | 25 | 'TestDBusInfoNoDetails', |
450 | 26 | 'TestDBusLP1277589', | ||
451 | 26 | 'TestDBusMockFailApply', | 27 | 'TestDBusMockFailApply', |
452 | 27 | 'TestDBusMockFailPause', | 28 | 'TestDBusMockFailPause', |
453 | 28 | 'TestDBusMockFailResume', | 29 | 'TestDBusMockFailResume', |
454 | @@ -122,6 +123,23 @@ | |||
455 | 122 | self.quit() | 123 | self.quit() |
456 | 123 | 124 | ||
457 | 124 | 125 | ||
458 | 126 | class DoubleCheckingReactor(Reactor): | ||
459 | 127 | def __init__(self, iface): | ||
460 | 128 | super().__init__(dbus.SystemBus()) | ||
461 | 129 | self.iface = iface | ||
462 | 130 | self.uas_signals = [] | ||
463 | 131 | self.react_to('UpdateAvailableStatus') | ||
464 | 132 | self.react_to('UpdateDownloaded') | ||
465 | 133 | self.schedule(self.iface.CheckForUpdate) | ||
466 | 134 | |||
467 | 135 | def _do_UpdateAvailableStatus(self, signal, path, *args, **kws): | ||
468 | 136 | self.uas_signals.append(args) | ||
469 | 137 | self.schedule(self.iface.CheckForUpdate) | ||
470 | 138 | |||
471 | 139 | def _do_UpdateDownloaded(self, *args, **kws): | ||
472 | 140 | self.quit() | ||
473 | 141 | |||
474 | 142 | |||
475 | 125 | class _TestBase(unittest.TestCase): | 143 | class _TestBase(unittest.TestCase): |
476 | 126 | """Base class for all DBus testing.""" | 144 | """Base class for all DBus testing.""" |
477 | 127 | 145 | ||
478 | @@ -262,13 +280,14 @@ | |||
479 | 262 | safe_remove(self.reboot_log) | 280 | safe_remove(self.reboot_log) |
480 | 263 | super().tearDown() | 281 | super().tearDown() |
481 | 264 | 282 | ||
483 | 265 | def _prepare_index(self, index_file): | 283 | def _prepare_index(self, index_file, write_callback=None): |
484 | 266 | serverdir = SystemImagePlugin.controller.serverdir | 284 | serverdir = SystemImagePlugin.controller.serverdir |
485 | 267 | index_path = os.path.join(serverdir, 'stable', 'nexus7', 'index.json') | 285 | index_path = os.path.join(serverdir, 'stable', 'nexus7', 'index.json') |
486 | 268 | head, tail = os.path.split(index_path) | 286 | head, tail = os.path.split(index_path) |
487 | 269 | copy(index_file, head, tail) | 287 | copy(index_file, head, tail) |
488 | 270 | sign(index_path, 'device-signing.gpg') | 288 | sign(index_path, 'device-signing.gpg') |
490 | 271 | setup_index(index_file, serverdir, 'device-signing.gpg') | 289 | setup_index(index_file, serverdir, 'device-signing.gpg', |
491 | 290 | write_callback) | ||
492 | 272 | 291 | ||
493 | 273 | def _touch_build(self, version): | 292 | def _touch_build(self, version): |
494 | 274 | # Unlike the touch_build() helper, this one uses our own config object | 293 | # Unlike the touch_build() helper, this one uses our own config object |
495 | @@ -1542,3 +1561,45 @@ | |||
496 | 1542 | update 5.txt 5.txt.asc | 1561 | update 5.txt 5.txt.asc |
497 | 1543 | unmount system | 1562 | unmount system |
498 | 1544 | """) | 1563 | """) |
499 | 1564 | |||
500 | 1565 | |||
501 | 1566 | class TestDBusLP1277589(_LiveTesting): | ||
502 | 1567 | def test_multiple_check_for_updates(self): | ||
503 | 1568 | # Log analysis of LP: #1277589 appears to show the following scenario, | ||
504 | 1569 | # reproduced in this test case: | ||
505 | 1570 | # | ||
506 | 1571 | # * Automatic updates are enabled. | ||
507 | 1572 | # * No image signing or image master keys are present. | ||
508 | 1573 | # * A full update is checked. | ||
509 | 1574 | # - A new image master key and image signing key is downloaded. | ||
510 | 1575 | # - Update is available | ||
511 | 1576 | # | ||
512 | 1577 | # Start by creating some big files which will take a while to | ||
513 | 1578 | # download. | ||
514 | 1579 | def write_callback(dst): | ||
515 | 1580 | # Write a 100 MiB sized file. | ||
516 | 1581 | with open(dst, 'wb') as fp: | ||
517 | 1582 | for i in range(25600): | ||
518 | 1583 | fp.write(b'x' * 4096) | ||
519 | 1584 | self._prepare_index('index_24.json', write_callback) | ||
520 | 1585 | # Create a reactor that will exit when the UpdateDownloaded signal is | ||
521 | 1586 | # received. We're going to issue a CheckForUpdate with automatic | ||
522 | 1587 | # updates enabled. As soon as we receive the UpdateAvailableStatus | ||
523 | 1588 | # signal, we'll immediately issue *another* CheckForUpdate, which | ||
524 | 1589 | # should run while the auto-download is working. | ||
525 | 1590 | # | ||
526 | 1591 | # At the end, we should not get another UpdateAvailableStatus signal, | ||
527 | 1592 | # but we should get the UpdateDownloaded signal. | ||
528 | 1593 | reactor = DoubleCheckingReactor(self.iface) | ||
529 | 1594 | reactor.run() | ||
530 | 1595 | self.assertEqual(len(reactor.uas_signals), 1) | ||
531 | 1596 | (is_available, downloading, available_version, update_size, | ||
532 | 1597 | last_update_date, | ||
533 | 1598 | #descriptions, | ||
534 | 1599 | error_reason) = reactor.uas_signals[0] | ||
535 | 1600 | self.assertTrue(is_available) | ||
536 | 1601 | self.assertTrue(downloading) | ||
537 | 1602 | self.assertEqual(available_version, '1600') | ||
538 | 1603 | self.assertEqual(update_size, 314572800) | ||
539 | 1604 | self.assertEqual(last_update_date, 'Unknown') | ||
540 | 1605 | self.assertEqual(error_reason, '') |