Status: | Needs review |
---|---|
Proposed branch: | lp:~mvo/click/acquire |
Merge into: | lp:click/devel |
Diff against target: |
740 lines (+631/-3) 8 files modified
acquire/http-udm (+28/-0) acquire/pycurl (+30/-0) click/acquire.py (+369/-0) click/commands/install.py (+17/-1) click/paths.py.in (+3/-0) click/tests/test_acquire.py (+174/-0) debian/control (+2/-2) setup.py.in (+8/-0) |
To merge this branch: | bzr merge lp:~mvo/click/acquire |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot (community) | continuous-integration | Approve | |
Colin Watson | Needs Fixing | ||
Review via email: mp+235753@code.launchpad.net |
Commit message
Provide support to download click packages.
Description of the change
This branch implements "acquire" support in click to support "click install http://
The model is similar with what apt is using and click can reuse the apt methods to download packages. A pycurl based implementation is also provided. Feedback welcome especially suggestsions if the low-level read_messages() code can be done in a more elegant way.
PS Jenkins bot (ps-jenkins) wrote : | # |
- 542. By Michael Vogt
-
click/acquire.py: make dbus optional
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:542
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 543. By Michael Vogt
-
add missing python3-pycurl depdendency
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:543
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 544. By Michael Vogt
-
add python3-pycurl to b-d (needed during the tests
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:544
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:544
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Colin Watson (cjwatson) wrote : | # |
Overall this looks like nice work, but I think you could productively simplify some things.
- 545. By Michael Vogt
-
first round of addressing review points from Colin
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:545
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 546. By Michael Vogt
-
click/acquire.py: add note about ssl options for curl
- 547. By Michael Vogt
-
rename read_messages() -> _read_messages()
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:546
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 548. By Michael Vogt
-
add missing python3-dbus build-dependency
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:548
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Michael Vogt (mvo) wrote : | # |
> Overall this looks like nice work, but I think you could productively simplify
> some things.
Thanks a lot for your detailed review. I addressed all the points you raised now.
The only one left is the blocking/
Alternatively we can ditch some of the compatibility with the apt methods and move the progress into the method. The downside is that if/when we support parallel downloads its more difficult to present a overall progress when its done in the individual downloaders.
- 549. By Michael Vogt
-
improve error erporting
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:549
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 550. By Michael Vogt
-
merged lp:click/devel
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:550
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Unmerged revisions
- 550. By Michael Vogt
-
merged lp:click/devel
- 549. By Michael Vogt
-
improve error erporting
- 548. By Michael Vogt
-
add missing python3-dbus build-dependency
- 547. By Michael Vogt
-
rename read_messages() -> _read_messages()
- 546. By Michael Vogt
-
click/acquire.py: add note about ssl options for curl
- 545. By Michael Vogt
-
first round of addressing review points from Colin
- 544. By Michael Vogt
-
add python3-pycurl to b-d (needed during the tests
- 543. By Michael Vogt
-
add missing python3-pycurl depdendency
- 542. By Michael Vogt
-
click/acquire.py: make dbus optional
- 541. By Michael Vogt
-
remove debug message
Preview Diff
1 | === added directory 'acquire' | |||
2 | === added symlink 'acquire/file' | |||
3 | === target is u'pycurl' | |||
4 | === added symlink 'acquire/ftp' | |||
5 | === target is u'pycurl' | |||
6 | === added symlink 'acquire/http' | |||
7 | === target is u'pycurl' | |||
8 | === added file 'acquire/http-udm' | |||
9 | --- acquire/http-udm 1970-01-01 00:00:00 +0000 | |||
10 | +++ acquire/http-udm 2014-10-14 13:55:44 +0000 | |||
11 | @@ -0,0 +1,28 @@ | |||
12 | 1 | #!/usr/bin/python3 | ||
13 | 2 | |||
14 | 3 | # Copyright (C) 2014 Canonical Ltd. | ||
15 | 4 | # Author: Michael Vogt <michael.vogt@ubuntu.com> | ||
16 | 5 | |||
17 | 6 | # This program is free software: you can redistribute it and/or modify | ||
18 | 7 | # it under the terms of the GNU General Public License as published by | ||
19 | 8 | # the Free Software Foundation; version 3 of the License. | ||
20 | 9 | # | ||
21 | 10 | # This program is distributed in the hope that it will be useful, | ||
22 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
23 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
24 | 13 | # GNU General Public License for more details. | ||
25 | 14 | # | ||
26 | 15 | # You should have received a copy of the GNU General Public License | ||
27 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
28 | 17 | |||
29 | 18 | """Acquire method for ubuntu-download-manager.""" | ||
30 | 19 | |||
31 | 20 | from click.acquire import ClickAcquireMethodUbuntuDownloadManager | ||
32 | 21 | |||
33 | 22 | from dbus.mainloop.glib import DBusGMainLoop | ||
34 | 23 | DBusGMainLoop(set_as_default=True) | ||
35 | 24 | |||
36 | 25 | |||
37 | 26 | if __name__ == "__main__": | ||
38 | 27 | m = ClickAcquireMethodUbuntuDownloadManager() | ||
39 | 28 | m.run() | ||
40 | 0 | 29 | ||
41 | === added symlink 'acquire/https' | |||
42 | === target is u'pycurl' | |||
43 | === added file 'acquire/pycurl' | |||
44 | --- acquire/pycurl 1970-01-01 00:00:00 +0000 | |||
45 | +++ acquire/pycurl 2014-10-14 13:55:44 +0000 | |||
46 | @@ -0,0 +1,30 @@ | |||
47 | 1 | #!/usr/bin/python3 | ||
48 | 2 | |||
49 | 3 | # Copyright (C) 2014 Canonical Ltd. | ||
50 | 4 | # Author: Michael Vogt <michael.vogt@ubuntu.com> | ||
51 | 5 | |||
52 | 6 | # This program is free software: you can redistribute it and/or modify | ||
53 | 7 | # it under the terms of the GNU General Public License as published by | ||
54 | 8 | # the Free Software Foundation; version 3 of the License. | ||
55 | 9 | # | ||
56 | 10 | # This program is distributed in the hope that it will be useful, | ||
57 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
58 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
59 | 13 | # GNU General Public License for more details. | ||
60 | 14 | # | ||
61 | 15 | # You should have received a copy of the GNU General Public License | ||
62 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
63 | 17 | |||
64 | 18 | """Acquire pycurl method click.""" | ||
65 | 19 | |||
66 | 20 | import signal | ||
67 | 21 | import sys | ||
68 | 22 | |||
69 | 23 | from click.acquire import ClickAcquireMethodPycurl | ||
70 | 24 | |||
71 | 25 | |||
72 | 26 | if __name__ == "__main__": | ||
73 | 27 | # apt will send a SIGINT to all methods that do not send "Need-Cleanup: 1" | ||
74 | 28 | signal.signal(signal.SIGINT, lambda *args: sys.exit()) | ||
75 | 29 | m = ClickAcquireMethodPycurl() | ||
76 | 30 | m.run() | ||
77 | 0 | 31 | ||
78 | === added file 'click/acquire.py' | |||
79 | --- click/acquire.py 1970-01-01 00:00:00 +0000 | |||
80 | +++ click/acquire.py 2014-10-14 13:55:44 +0000 | |||
81 | @@ -0,0 +1,369 @@ | |||
82 | 1 | # Copyright (C) 2014 Canonical Ltd. | ||
83 | 2 | # Author: Michael Vogt <michael.vogt@ubuntu.com> | ||
84 | 3 | |||
85 | 4 | # This program is free software: you can redistribute it and/or modify | ||
86 | 5 | # it under the terms of the GNU General Public License as published by | ||
87 | 6 | # the Free Software Foundation; version 3 of the License. | ||
88 | 7 | # | ||
89 | 8 | # This program is distributed in the hope that it will be useful, | ||
90 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
91 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
92 | 11 | # GNU General Public License for more details. | ||
93 | 12 | # | ||
94 | 13 | # You should have received a copy of the GNU General Public License | ||
95 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
96 | 15 | |||
97 | 16 | """Acquire of Click packages.""" | ||
98 | 17 | |||
99 | 18 | from __future__ import print_function | ||
100 | 19 | |||
101 | 20 | __metaclass__ = type | ||
102 | 21 | __all__ = [ | ||
103 | 22 | 'ClickAcquire', | ||
104 | 23 | 'ClickAcquireError', | ||
105 | 24 | 'ClickAcquireMethod', | ||
106 | 25 | 'ClickAcquireMethodPycurl', | ||
107 | 26 | 'ClickAcquireMethodUbuntuDownloadManager', | ||
108 | 27 | 'ClickAcquireStatus', | ||
109 | 28 | 'ClickAcquireStatusText', | ||
110 | 29 | ] | ||
111 | 30 | |||
112 | 31 | import os | ||
113 | 32 | import select | ||
114 | 33 | import subprocess | ||
115 | 34 | import sys | ||
116 | 35 | from textwrap import dedent | ||
117 | 36 | |||
118 | 37 | import apt_pkg | ||
119 | 38 | from debian.deb822 import Deb822 | ||
120 | 39 | import dbus | ||
121 | 40 | from gi.repository import GLib | ||
122 | 41 | import pycurl | ||
123 | 42 | from six.moves.urllib.parse import urlparse | ||
124 | 43 | |||
125 | 44 | from click.paths import acquire_methods_dir | ||
126 | 45 | |||
127 | 46 | |||
128 | 47 | class ClickAcquireError(Exception): | ||
129 | 48 | """Error during acquire""" | ||
130 | 49 | pass | ||
131 | 50 | |||
132 | 51 | |||
133 | 52 | def extract_one_message(lines): | ||
134 | 53 | msg = [] | ||
135 | 54 | while True: | ||
136 | 55 | try: | ||
137 | 56 | line = lines.pop(0) | ||
138 | 57 | except IndexError: | ||
139 | 58 | break | ||
140 | 59 | if os.environ.get("CLICK_DEBUG_ACQUIRE", ""): | ||
141 | 60 | sys.stderr.write("[%s] raw_line: '%s'\n" % ( | ||
142 | 61 | os.getpid(), line.replace("\n", "\\n"))) | ||
143 | 62 | msg.append(line) | ||
144 | 63 | if len(msg) > 0 and line == "": | ||
145 | 64 | # we are done, collect all remaining "\n" and stop | ||
146 | 65 | while len(lines) > 0 and lines[0] == "": | ||
147 | 66 | lines.pop(0) | ||
148 | 67 | break | ||
149 | 68 | if len(msg) < 2: | ||
150 | 69 | return -1 , "" | ||
151 | 70 | if os.environ.get("CLICK_DEBUG_ACQUIRE", ""): | ||
152 | 71 | sys.stderr.write("[%s] msg: '%s'\n" % (os.getpid(), str(msg))) | ||
153 | 72 | number = int(msg[0].split()[0]) | ||
154 | 73 | return number, "\n".join(msg) | ||
155 | 74 | |||
156 | 75 | |||
157 | 76 | def _read_messages(input_file, timeout=None): | ||
158 | 77 | infd = input_file.fileno() | ||
159 | 78 | msgs = [] | ||
160 | 79 | rl, wl, xl = select.select([infd], [], [], timeout) | ||
161 | 80 | if rl: | ||
162 | 81 | # FIXME: 16k message limit is arbitrary | ||
163 | 82 | buf = os.read(infd, 16*1024).decode("utf-8") | ||
164 | 83 | if os.environ.get("CLICK_DEBUG_ACQUIRE", ""): | ||
165 | 84 | sys.stderr.write("[%s] read buf: '%s'\n" % ( | ||
166 | 85 | os.getpid(), buf)) | ||
167 | 86 | lines = buf.split("\n") | ||
168 | 87 | while True: | ||
169 | 88 | number, msg = extract_one_message(lines) | ||
170 | 89 | if number < 0: | ||
171 | 90 | break | ||
172 | 91 | msgs.append( (number, msg) ) | ||
173 | 92 | return msgs | ||
174 | 93 | |||
175 | 94 | |||
176 | 95 | class ClickAcquireStatus: | ||
177 | 96 | """Base class for the status reporting """ | ||
178 | 97 | |||
179 | 98 | def __init__(self): | ||
180 | 99 | self.fetched_bytes = 0 | ||
181 | 100 | self.total_bytes = 1.0 | ||
182 | 101 | self.uri = "" | ||
183 | 102 | |||
184 | 103 | def pulse(self): | ||
185 | 104 | pass | ||
186 | 105 | |||
187 | 106 | def done(self): | ||
188 | 107 | pass | ||
189 | 108 | |||
190 | 109 | |||
191 | 110 | class ClickAcquireStatusText(ClickAcquireStatus): | ||
192 | 111 | """Text based progress reporting for the acquire progress""" | ||
193 | 112 | |||
194 | 113 | def pulse(self): | ||
195 | 114 | sys.stdout.write("\r") | ||
196 | 115 | sys.stdout.write("[%3.2f %%] Fetching %s" % ( | ||
197 | 116 | (self.fetched_bytes/self.total_bytes)*100.0, | ||
198 | 117 | os.path.basename(self.uri))) | ||
199 | 118 | sys.stdout.flush() | ||
200 | 119 | |||
201 | 120 | def done(self): | ||
202 | 121 | self.pulse() | ||
203 | 122 | sys.stdout.write("\n") | ||
204 | 123 | sys.stdout.write("Done fetching %s\n" % self.uri) | ||
205 | 124 | |||
206 | 125 | |||
207 | 126 | # similar to Acquire/AcquireWorker | ||
208 | 127 | class ClickAcquire: | ||
209 | 128 | """Acquire from remote locations""" | ||
210 | 129 | |||
211 | 130 | # default status reporting timeout | ||
212 | 131 | TIMEOUT = 0.05 | ||
213 | 132 | |||
214 | 133 | # acquire method status codecs | ||
215 | 134 | M_CAPABILITIES = 100 | ||
216 | 135 | M_STATUS = 102 | ||
217 | 136 | M_REDIRECT = 103 | ||
218 | 137 | URI_START = 200 | ||
219 | 138 | URI_SUCCESS = 201 | ||
220 | 139 | URI_FAILURE = 400 | ||
221 | 140 | |||
222 | 141 | def __init__(self, status=ClickAcquireStatus()): | ||
223 | 142 | self._fetch_queue = [] | ||
224 | 143 | self._status = status | ||
225 | 144 | |||
226 | 145 | def _uri_acquire(self, pipe, uri, destfile): | ||
227 | 146 | cmd = dedent("""\ | ||
228 | 147 | 600 URI Acquire | ||
229 | 148 | URI: {uri} | ||
230 | 149 | Filename: {destfile} | ||
231 | 150 | |||
232 | 151 | """).format(uri=uri, destfile=destfile) | ||
233 | 152 | pipe.write(cmd) | ||
234 | 153 | pipe.flush() | ||
235 | 154 | |||
236 | 155 | def _redirect(self, pipe, new_uri, destfile): | ||
237 | 156 | self._uri_acquire(pipe, new_uri, destfile) | ||
238 | 157 | |||
239 | 158 | def _run_acquire_method(self, uri, destfile): | ||
240 | 159 | parsed_uri = urlparse(uri) | ||
241 | 160 | cmd = os.path.join(acquire_methods_dir, parsed_uri.scheme) | ||
242 | 161 | p = subprocess.Popen([cmd], | ||
243 | 162 | stdout=subprocess.PIPE, stdin=subprocess.PIPE, | ||
244 | 163 | universal_newlines=True) | ||
245 | 164 | while True: | ||
246 | 165 | for number, raw_message in _read_messages(p.stdout, self.TIMEOUT): | ||
247 | 166 | message = Deb822(raw_message) | ||
248 | 167 | if number == self.M_CAPABILITIES: | ||
249 | 168 | self._uri_acquire(p.stdin, uri, destfile) | ||
250 | 169 | elif number == self.M_STATUS: | ||
251 | 170 | pass | ||
252 | 171 | elif number == self.M_REDIRECT: | ||
253 | 172 | self._redirect(p.stdin, message.get("New-URI"), destfile) | ||
254 | 173 | elif number == self.URI_START: | ||
255 | 174 | self._status.uri = message.get("URI", 0) | ||
256 | 175 | self._status.total_bytes = int(message.get("Size", 0)) | ||
257 | 176 | elif number == self.URI_SUCCESS: | ||
258 | 177 | self._status.fetched_bytes = int(message.get("Size")) | ||
259 | 178 | self._status.done() | ||
260 | 179 | p.stdout.close() | ||
261 | 180 | p.stdin.close() | ||
262 | 181 | return True | ||
263 | 182 | elif number == self.URI_FAILURE: | ||
264 | 183 | p.stdout.close() | ||
265 | 184 | p.stdin.close() | ||
266 | 185 | raise ClickAcquireError("Uri failure for %s: %s" % ( | ||
267 | 186 | message.get("uri"), message.get("Message"))) | ||
268 | 187 | # update progress | ||
269 | 188 | if os.path.exists(destfile): | ||
270 | 189 | self._status.fetched_bytes = os.path.getsize(destfile) | ||
271 | 190 | self._status.pulse() | ||
272 | 191 | return False | ||
273 | 192 | |||
274 | 193 | def fetch(self, uri, destfile): | ||
275 | 194 | self._run_acquire_method(uri, destfile) | ||
276 | 195 | |||
277 | 196 | |||
278 | 197 | # similar to the apt AcquireMethod | ||
279 | 198 | class ClickAcquireMethod: | ||
280 | 199 | |||
281 | 200 | M_CONFIGURATION = 601 | ||
282 | 201 | M_FETCH = 600 | ||
283 | 202 | |||
284 | 203 | VERSION = 0.1 | ||
285 | 204 | |||
286 | 205 | def __init__(self): | ||
287 | 206 | s = dedent("""\ | ||
288 | 207 | 100 Capabilities | ||
289 | 208 | Version: {version} | ||
290 | 209 | Single-Instance: true | ||
291 | 210 | |||
292 | 211 | """).format(version=self.VERSION) | ||
293 | 212 | sys.stdout.write(s) | ||
294 | 213 | sys.stdout.flush() | ||
295 | 214 | |||
296 | 215 | def run(self): | ||
297 | 216 | while True: | ||
298 | 217 | msgs = _read_messages(sys.stdin) | ||
299 | 218 | if not msgs: | ||
300 | 219 | break | ||
301 | 220 | for number, raw_message in msgs: | ||
302 | 221 | message = Deb822(raw_message) | ||
303 | 222 | if number == self.M_CONFIGURATION: | ||
304 | 223 | pass | ||
305 | 224 | elif number == self.M_FETCH: | ||
306 | 225 | self.fetch(message.get("URI"), message.get("FileName")) | ||
307 | 226 | |||
308 | 227 | def uri_start(self, uri, filename, size): | ||
309 | 228 | # note that apt itself does not use "Filename" here because it | ||
310 | 229 | # will set a filename and expects the method to use it, however | ||
311 | 230 | # this will not work with UbuntuDownloadManager as it downloads | ||
312 | 231 | # to its own location | ||
313 | 232 | sys.stdout.write(dedent("""\ | ||
314 | 233 | 200 URI Start | ||
315 | 234 | URI: {uri} | ||
316 | 235 | Filename: {filename} | ||
317 | 236 | Size: {size} | ||
318 | 237 | |||
319 | 238 | """).format(uri=uri, filename=filename, size=size)) | ||
320 | 239 | sys.stdout.flush() | ||
321 | 240 | |||
322 | 241 | def uri_done(self, uri, filename): | ||
323 | 242 | # bug in python-apt | ||
324 | 243 | hashes = apt_pkg.Hashes(filename.encode("utf-8")) | ||
325 | 244 | sys.stdout.write(dedent("""\ | ||
326 | 245 | 201 URI Done | ||
327 | 246 | URI: {uri} | ||
328 | 247 | Filename: {filename} | ||
329 | 248 | Size: {size} | ||
330 | 249 | Sha256-Hash: {sha256hash} | ||
331 | 250 | |||
332 | 251 | """).format(uri=uri, filename=filename, size=os.path.getsize(filename), | ||
333 | 252 | sha256hash=hashes.sha256)) | ||
334 | 253 | sys.stdout.flush() | ||
335 | 254 | |||
336 | 255 | def fail(self, uri, err="unknown error"): | ||
337 | 256 | sys.stdout.write(dedent("""\ | ||
338 | 257 | 400 URI Failure | ||
339 | 258 | URI: {uri} | ||
340 | 259 | Message: {err} | ||
341 | 260 | |||
342 | 261 | """).format(uri=uri, err=err)) | ||
343 | 262 | sys.stdout.flush() | ||
344 | 263 | |||
345 | 264 | def fetch(self, uri, destfile): | ||
346 | 265 | pass | ||
347 | 266 | |||
348 | 267 | |||
349 | 268 | class ClickAcquireMethodPycurl(ClickAcquireMethod): | ||
350 | 269 | |||
351 | 270 | def _write_to_file_callback(self, data): | ||
352 | 271 | self._destfile_fp.write(data) | ||
353 | 272 | return len(data) | ||
354 | 273 | |||
355 | 274 | def _progress_info(self, dltotal, dlnow, ultotal, ulnow): | ||
356 | 275 | if dltotal > 0 and not self._uri_start_reported: | ||
357 | 276 | self.uri_start( | ||
358 | 277 | self.uri, self._destfile_fp.name, int(dltotal)) | ||
359 | 278 | self._uri_start_reported = True | ||
360 | 279 | |||
361 | 280 | def fetch(self, uri, destfile): | ||
362 | 281 | self._uri_start_reported = False | ||
363 | 282 | self.uri = uri | ||
364 | 283 | self._destfile_fp = open(destfile, "wb") | ||
365 | 284 | curl = pycurl.Curl() | ||
366 | 285 | curl.setopt(pycurl.URL, uri) | ||
367 | 286 | curl.setopt(pycurl.WRITEFUNCTION, self._write_to_file_callback) | ||
368 | 287 | curl.setopt(pycurl.NOPROGRESS, 0) | ||
369 | 288 | curl.setopt(pycurl.PROGRESSFUNCTION, self._progress_info) | ||
370 | 289 | curl.setopt(pycurl.FOLLOWLOCATION, 1) | ||
371 | 290 | curl.setopt(pycurl.MAXREDIRS, 5) | ||
372 | 291 | curl.setopt(pycurl.FAILONERROR, 1) | ||
373 | 292 | # timeout 120s for conenction | ||
374 | 293 | curl.setopt(pycurl.CONNECTTIMEOUT, 120) | ||
375 | 294 | # timeout if the speed is 120s below 10 bytes/sec | ||
376 | 295 | curl.setopt(pycurl.LOW_SPEED_LIMIT, 10); | ||
377 | 296 | curl.setopt(pycurl.LOW_SPEED_TIME, 120); | ||
378 | 297 | # ssl: no need to set any option here, | ||
379 | 298 | # SSL_VERIFYPEER=1, SSL_VERIFYHOST=2, | ||
380 | 299 | # CAINFO=/etc/ssl/certs/ca-certificates.crt | ||
381 | 300 | # by default in libcurl3 these days | ||
382 | 301 | try: | ||
383 | 302 | curl.perform() | ||
384 | 303 | self._destfile_fp.close() | ||
385 | 304 | self.uri_done(uri, destfile) | ||
386 | 305 | except pycurl.error as e: | ||
387 | 306 | self.fail(self.uri, e.args) | ||
388 | 307 | self._destfile_fp.close() | ||
389 | 308 | |||
390 | 309 | |||
391 | 310 | class ClickAcquireMethodUbuntuDownloadManager(ClickAcquireMethod): | ||
392 | 311 | |||
393 | 312 | MANAGER_PATH = '/' | ||
394 | 313 | MANAGER_IFACE = 'com.canonical.applications.DownloadManager' | ||
395 | 314 | DOWNLOAD_IFACE = 'com.canonical.applications.Download' | ||
396 | 315 | |||
397 | 316 | def __init__(self): | ||
398 | 317 | super(ClickAcquireMethodUbuntuDownloadManager, self).__init__() | ||
399 | 318 | self.bus = dbus.SessionBus() | ||
400 | 319 | self.loop = GLib.MainLoop() | ||
401 | 320 | |||
402 | 321 | def _created_callback(self, dbus_path): | ||
403 | 322 | pass | ||
404 | 323 | |||
405 | 324 | def _finished_callback(self, path, loop): | ||
406 | 325 | self.down_path = path | ||
407 | 326 | loop.quit() | ||
408 | 327 | |||
409 | 328 | def _progress_callback(self, total, progress): | ||
410 | 329 | #print('Progress is %s/%s' % (progress, total)) | ||
411 | 330 | if not self._started: | ||
412 | 331 | # FIXME: we need the tmpfile path from udm | ||
413 | 332 | tmpfile = "meep" | ||
414 | 333 | self.uri_start(self.uri, tmpfile, total) | ||
415 | 334 | self._started = True | ||
416 | 335 | |||
417 | 336 | def fetch(self, uri, destfile): | ||
418 | 337 | self.uri = uri | ||
419 | 338 | self._started = False | ||
420 | 339 | manager = self.bus.get_object( | ||
421 | 340 | 'com.canonical.applications.Downloader', self.MANAGER_PATH) | ||
422 | 341 | manager_dev_iface = dbus.Interface( | ||
423 | 342 | manager, dbus_interface=self.MANAGER_IFACE) | ||
424 | 343 | manager_dev_iface.connect_to_signal( | ||
425 | 344 | 'downloadCreated', self._created_callback) | ||
426 | 345 | down_path = manager_dev_iface.createDownload( | ||
427 | 346 | (uri, "", "", dbus.Dictionary({}, signature="sv"), | ||
428 | 347 | dbus.Dictionary({}, signature="ss"))) | ||
429 | 348 | download1 = self.bus.get_object('com.canonical.applications.Downloader', | ||
430 | 349 | down_path) | ||
431 | 350 | download_dev_iface1 = dbus.Interface( | ||
432 | 351 | download1, dbus_interface=self.DOWNLOAD_IFACE) | ||
433 | 352 | download_dev_iface1.connect_to_signal( | ||
434 | 353 | 'progress', self._progress_callback) | ||
435 | 354 | download_dev_iface1.connect_to_signal( | ||
436 | 355 | 'finished', | ||
437 | 356 | lambda path: self._finished_callback(path, self.loop)) | ||
438 | 357 | download_dev_iface1.start() | ||
439 | 358 | self.loop.run() | ||
440 | 359 | # FIXME error handling, i.e. send self.fail() | ||
441 | 360 | self.uri_done(uri, self.down_path) | ||
442 | 361 | |||
443 | 362 | |||
444 | 363 | if __name__ == "__main__": | ||
445 | 364 | log = ClickAcquireStatusText() | ||
446 | 365 | acq = ClickAcquire(log) | ||
447 | 366 | uri = sys.argv[1] | ||
448 | 367 | if not acq.fetch(uri, os.path.basename(uri)): | ||
449 | 368 | sys.exit(1) | ||
450 | 369 | sys.exit(0) | ||
451 | 0 | 370 | ||
452 | === modified file 'click/commands/install.py' | |||
453 | --- click/commands/install.py 2014-09-10 12:28:49 +0000 | |||
454 | +++ click/commands/install.py 2014-10-14 13:55:44 +0000 | |||
455 | @@ -19,10 +19,17 @@ | |||
456 | 19 | 19 | ||
457 | 20 | from optparse import OptionParser | 20 | from optparse import OptionParser |
458 | 21 | import sys | 21 | import sys |
459 | 22 | import tempfile | ||
460 | 22 | from textwrap import dedent | 23 | from textwrap import dedent |
461 | 23 | 24 | ||
462 | 25 | from six.moves.urllib.parse import urlparse | ||
463 | 26 | |||
464 | 24 | from gi.repository import Click | 27 | from gi.repository import Click |
465 | 25 | 28 | ||
466 | 29 | from click.acquire import ( | ||
467 | 30 | ClickAcquire, | ||
468 | 31 | ClickAcquireStatusText, | ||
469 | 32 | ) | ||
470 | 26 | from click.install import ClickInstaller, ClickInstallerError | 33 | from click.install import ClickInstaller, ClickInstallerError |
471 | 27 | 34 | ||
472 | 28 | 35 | ||
473 | @@ -53,10 +60,19 @@ | |||
474 | 53 | db.read(db_dir=None) | 60 | db.read(db_dir=None) |
475 | 54 | if options.root is not None: | 61 | if options.root is not None: |
476 | 55 | db.add(options.root) | 62 | db.add(options.root) |
478 | 56 | package_path = args[0] | 63 | package_uri = args[0] |
479 | 57 | installer = ClickInstaller( | 64 | installer = ClickInstaller( |
480 | 58 | db=db, force_missing_framework=options.force_missing_framework, | 65 | db=db, force_missing_framework=options.force_missing_framework, |
481 | 59 | allow_unauthenticated=options.allow_unauthenticated) | 66 | allow_unauthenticated=options.allow_unauthenticated) |
482 | 67 | parsed_uri = urlparse(package_uri) | ||
483 | 68 | if parsed_uri.scheme != "": | ||
484 | 69 | t = tempfile.NamedTemporaryFile() | ||
485 | 70 | package_path = t.name | ||
486 | 71 | log = ClickAcquireStatusText() | ||
487 | 72 | acq = ClickAcquire(log) | ||
488 | 73 | acq.fetch(package_uri, package_path) | ||
489 | 74 | else: | ||
490 | 75 | package_path = package_uri | ||
491 | 60 | try: | 76 | try: |
492 | 61 | installer.install( | 77 | installer.install( |
493 | 62 | package_path, user=options.user, all_users=options.all_users) | 78 | package_path, user=options.user, all_users=options.all_users) |
494 | 63 | 79 | ||
495 | === modified file 'click/paths.py.in' | |||
496 | --- click/paths.py.in 2014-05-08 15:48:01 +0000 | |||
497 | +++ click/paths.py.in 2014-10-14 13:55:44 +0000 | |||
498 | @@ -14,6 +14,9 @@ | |||
499 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
500 | 15 | 15 | ||
501 | 16 | """Click paths.""" | 16 | """Click paths.""" |
502 | 17 | import os | ||
503 | 17 | 18 | ||
504 | 18 | preload_path = "@pkglibdir@/libclickpreload.so" | 19 | preload_path = "@pkglibdir@/libclickpreload.so" |
505 | 19 | frameworks_dir = "@pkgdatadir@/frameworks" | 20 | frameworks_dir = "@pkgdatadir@/frameworks" |
506 | 21 | acquire_methods_dir = (os.environ.get("CLICK_ACQUIRE_METHODS_DIR", "") or | ||
507 | 22 | "@pkglibdir@/acquire") | ||
508 | 20 | 23 | ||
509 | === added file 'click/tests/test_acquire.py' | |||
510 | --- click/tests/test_acquire.py 1970-01-01 00:00:00 +0000 | |||
511 | +++ click/tests/test_acquire.py 2014-10-14 13:55:44 +0000 | |||
512 | @@ -0,0 +1,174 @@ | |||
513 | 1 | # Copyright (C) 2014 Canonical Ltd. | ||
514 | 2 | # Author: Michael Vogt <michael.vogt@ubuntu.com> | ||
515 | 3 | |||
516 | 4 | # This program is free software: you can redistribute it and/or modify | ||
517 | 5 | # it under the terms of the GNU General Public License as published by | ||
518 | 6 | # the Free Software Foundation; version 3 of the License. | ||
519 | 7 | # | ||
520 | 8 | # This program is distributed in the hope that it will be useful, | ||
521 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
522 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
523 | 11 | # GNU General Public License for more details. | ||
524 | 12 | # | ||
525 | 13 | # You should have received a copy of the GNU General Public License | ||
526 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
527 | 15 | |||
528 | 16 | """Unit tests for click.acquire.""" | ||
529 | 17 | |||
530 | 18 | from __future__ import print_function | ||
531 | 19 | |||
532 | 20 | __metaclass__ = type | ||
533 | 21 | __all__ = [ | ||
534 | 22 | 'TestClickAcquire', | ||
535 | 23 | ] | ||
536 | 24 | |||
537 | 25 | import multiprocessing | ||
538 | 26 | import os.path | ||
539 | 27 | import time | ||
540 | 28 | |||
541 | 29 | from six.moves import ( | ||
542 | 30 | SimpleHTTPServer, | ||
543 | 31 | socketserver, | ||
544 | 32 | ) | ||
545 | 33 | |||
546 | 34 | from click.acquire import ( | ||
547 | 35 | ClickAcquire, | ||
548 | 36 | ClickAcquireError, | ||
549 | 37 | _read_messages, | ||
550 | 38 | ) | ||
551 | 39 | |||
552 | 40 | from click.tests.helpers import ( | ||
553 | 41 | TestCase, | ||
554 | 42 | ) | ||
555 | 43 | |||
556 | 44 | import click.acquire | ||
557 | 45 | click.acquire.acquire_methods_dir = os.path.join( | ||
558 | 46 | os.path.dirname(__file__), "..", "..", "acquire") | ||
559 | 47 | os.environ["PYTHONPATH"] = os.path.abspath( | ||
560 | 48 | os.path.join(os.path.dirname(__file__), "..", "..")) | ||
561 | 49 | |||
562 | 50 | # local httpd | ||
563 | 51 | LOCALHOST = "localhost" | ||
564 | 52 | PORT = 8128 | ||
565 | 53 | |||
566 | 54 | |||
567 | 55 | class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): | ||
568 | 56 | def log_message(self, format, *args): | ||
569 | 57 | pass | ||
570 | 58 | |||
571 | 59 | |||
572 | 60 | class MySocketServer(socketserver.TCPServer): | ||
573 | 61 | allow_reuse_address = True | ||
574 | 62 | |||
575 | 63 | |||
576 | 64 | class Httpd(multiprocessing.Process): | ||
577 | 65 | |||
578 | 66 | def __init__(self, basedir): | ||
579 | 67 | super(Httpd, self).__init__() | ||
580 | 68 | self.basedir = basedir | ||
581 | 69 | |||
582 | 70 | def run(self): | ||
583 | 71 | os.chdir(self.basedir) | ||
584 | 72 | server = MySocketServer((LOCALHOST, PORT), MyHandler) | ||
585 | 73 | while True: | ||
586 | 74 | server.handle_request() | ||
587 | 75 | server.shutdown() | ||
588 | 76 | |||
589 | 77 | def stop(self): | ||
590 | 78 | self.terminate() | ||
591 | 79 | self.join() | ||
592 | 80 | |||
593 | 81 | |||
594 | 82 | class TestClickAcquire(TestCase): | ||
595 | 83 | |||
596 | 84 | def setUp(self): | ||
597 | 85 | super(TestClickAcquire, self).setUp() | ||
598 | 86 | self.use_temp_dir() | ||
599 | 87 | self.httpd = Httpd(self.temp_dir) | ||
600 | 88 | self.httpd.start() | ||
601 | 89 | |||
602 | 90 | def tearDown(self): | ||
603 | 91 | self.httpd.stop() | ||
604 | 92 | |||
605 | 93 | def test_acquire_fail(self): | ||
606 | 94 | acq = ClickAcquire() | ||
607 | 95 | destfile = os.path.join(self.temp_dir, "meep") | ||
608 | 96 | with self.assertRaises(ClickAcquireError): | ||
609 | 97 | acq.fetch("http://%s:%s/not-here" % (LOCALHOST, PORT), destfile) | ||
610 | 98 | |||
611 | 99 | def test_acquire_good(self): | ||
612 | 100 | acq = ClickAcquire() | ||
613 | 101 | destfile = os.path.join(self.temp_dir, "meep") | ||
614 | 102 | canary_str = "hello" | ||
615 | 103 | with open(os.path.join(self.temp_dir, "i-am-here"), "w") as f: | ||
616 | 104 | f.write(canary_str) | ||
617 | 105 | acq.fetch("http://%s:%s/i-am-here" % (LOCALHOST, PORT), destfile) | ||
618 | 106 | with open(destfile) as f: | ||
619 | 107 | data = f.read() | ||
620 | 108 | self.assertEqual(canary_str, data) | ||
621 | 109 | |||
622 | 110 | |||
623 | 111 | class TestClickAcquireReadMessages(TestCase): | ||
624 | 112 | |||
625 | 113 | def test_forked_read_message(self): | ||
626 | 114 | read_end, write_end = os.pipe() | ||
627 | 115 | pid = os.fork() | ||
628 | 116 | if pid == 0: | ||
629 | 117 | os.close(write_end) | ||
630 | 118 | with os.fdopen(read_end) as f: | ||
631 | 119 | number, msg = _read_messages(f)[0] | ||
632 | 120 | self.assertEqual(msg, "102 Status\nFoo: Bar\n") | ||
633 | 121 | self.assertEqual(number, 102) | ||
634 | 122 | os._exit(0) | ||
635 | 123 | os.close(read_end) | ||
636 | 124 | os.write(write_end, "102 Status\nFoo: Bar\n\n".encode("utf-8")) | ||
637 | 125 | os.waitpid(pid, 0) | ||
638 | 126 | os.close(write_end) | ||
639 | 127 | |||
640 | 128 | def test_forked_read_multiple_messages(self): | ||
641 | 129 | read_end, write_end = os.pipe() | ||
642 | 130 | pid = os.fork() | ||
643 | 131 | if pid == 0: | ||
644 | 132 | os.close(write_end) | ||
645 | 133 | with os.fdopen(read_end) as f: | ||
646 | 134 | msgs = _read_messages(f) | ||
647 | 135 | self.assertEqual(len(msgs), 2) | ||
648 | 136 | self.assertEqual(msgs[0][0], 100) | ||
649 | 137 | self.assertEqual(msgs[1][0], 200) | ||
650 | 138 | os._exit(0) | ||
651 | 139 | os.close(read_end) | ||
652 | 140 | os.write(write_end, "100 Status\n\n200 meep\n\n".encode("utf-8")) | ||
653 | 141 | os.waitpid(pid, 0) | ||
654 | 142 | os.close(write_end) | ||
655 | 143 | |||
656 | 144 | def test_forked_read_message_broken_pipe(self): | ||
657 | 145 | read_end, write_end = os.pipe() | ||
658 | 146 | pid = os.fork() | ||
659 | 147 | if pid == 0: | ||
660 | 148 | os.close(write_end) | ||
661 | 149 | with os.fdopen(read_end) as f: | ||
662 | 150 | self.assertEqual(_read_messages(f), []) | ||
663 | 151 | os._exit(0) | ||
664 | 152 | os.close(read_end) | ||
665 | 153 | os.write(write_end, "102 close-before-msg".encode("utf-8")) | ||
666 | 154 | os.close(write_end) | ||
667 | 155 | os.waitpid(pid, 0) | ||
668 | 156 | |||
669 | 157 | def test_forked_read_message_with_timeout(self): | ||
670 | 158 | read_end, write_end = os.pipe() | ||
671 | 159 | pid = os.fork() | ||
672 | 160 | if pid == 0: | ||
673 | 161 | os.close(write_end) | ||
674 | 162 | with os.fdopen(read_end) as f: | ||
675 | 163 | self.assertEqual(_read_messages(f, 0.1), []) | ||
676 | 164 | msgs = _read_messages(f) | ||
677 | 165 | self.assertEqual(len(msgs), 2) | ||
678 | 166 | self.assertEqual(msgs[0][1], "100 aaa\n") | ||
679 | 167 | self.assertEqual(msgs[1][1], "200 bbb\n") | ||
680 | 168 | os._exit(0) | ||
681 | 169 | os.close(read_end) | ||
682 | 170 | # simulate slow method | ||
683 | 171 | time.sleep(0.2) | ||
684 | 172 | os.write(write_end, "100 aaa\n\n".encode("utf-8")) | ||
685 | 173 | os.write(write_end, "200 bbb\n\n".encode("utf-8")) | ||
686 | 174 | os.waitpid(pid, 0) | ||
687 | 0 | 175 | ||
688 | === modified file 'debian/control' | |||
689 | --- debian/control 2014-10-10 07:10:23 +0000 | |||
690 | +++ debian/control 2014-10-14 13:55:44 +0000 | |||
691 | @@ -3,7 +3,7 @@ | |||
692 | 3 | Priority: optional | 3 | Priority: optional |
693 | 4 | Maintainer: Colin Watson <cjwatson@ubuntu.com> | 4 | Maintainer: Colin Watson <cjwatson@ubuntu.com> |
694 | 5 | Standards-Version: 3.9.5 | 5 | Standards-Version: 3.9.5 |
696 | 6 | Build-Depends: debhelper (>= 9~), dh-autoreconf, intltool, python3:any (>= 3.2), python3-all:any, python3-setuptools, python3-apt, python3-debian, python3-gi, python3:any (>= 3.3) | python3-mock, pep8, python3-pep8, pyflakes, python3-sphinx, pkg-config, valac, gobject-introspection (>= 0.6.7), libgirepository1.0-dev (>= 0.6.7), libglib2.0-dev (>= 2.34), gir1.2-glib-2.0, libjson-glib-dev (>= 0.10), libgee-0.8-dev, libpackagekit-glib2-dev (>= 0.7.2), python3-coverage, python3-six, dh-systemd (>= 1.3) | 6 | Build-Depends: debhelper (>= 9~), dh-autoreconf, intltool, python3:any (>= 3.2), python3-all:any, python3-setuptools, python3-apt, python3-debian, python3-gi, python3:any (>= 3.3) | python3-mock, pep8, python3-pep8, pyflakes, python3-sphinx, pkg-config, valac, gobject-introspection (>= 0.6.7), libgirepository1.0-dev (>= 0.6.7), libglib2.0-dev (>= 2.34), gir1.2-glib-2.0, libjson-glib-dev (>= 0.10), libgee-0.8-dev, libpackagekit-glib2-dev (>= 0.7.2), python3-coverage, python3-six, dh-systemd (>= 1.3), python3-pycurl, python3-dbus |
697 | 7 | Vcs-Bzr: https://code.launchpad.net/~ubuntu-managed-branches/click/click | 7 | Vcs-Bzr: https://code.launchpad.net/~ubuntu-managed-branches/click/click |
698 | 8 | Vcs-Browser: http://bazaar.launchpad.net/~ubuntu-managed-branches/click/click/files | 8 | Vcs-Browser: http://bazaar.launchpad.net/~ubuntu-managed-branches/click/click/files |
699 | 9 | X-Auto-Uploader: no-rewrite-version | 9 | X-Auto-Uploader: no-rewrite-version |
700 | @@ -14,7 +14,7 @@ | |||
701 | 14 | Package: click | 14 | Package: click |
702 | 15 | Architecture: any | 15 | Architecture: any |
703 | 16 | Pre-Depends: ${misc:Pre-Depends} | 16 | Pre-Depends: ${misc:Pre-Depends} |
705 | 17 | Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3-click (= ${binary:Version}), adduser | 17 | Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3-click (= ${binary:Version}), adduser, python3-pycurl, python3-dbus |
706 | 18 | Recommends: click-apparmor | 18 | Recommends: click-apparmor |
707 | 19 | Suggests: click-reviewers-tools (>= 0.9), ubuntu-app-launch-tools | upstart-app-launch-tools | 19 | Suggests: click-reviewers-tools (>= 0.9), ubuntu-app-launch-tools | upstart-app-launch-tools |
708 | 20 | Conflicts: click-package | 20 | Conflicts: click-package |
709 | 21 | 21 | ||
710 | === modified file 'setup.py.in' | |||
711 | --- setup.py.in 2014-06-30 16:48:31 +0000 | |||
712 | +++ setup.py.in 2014-10-14 13:55:44 +0000 | |||
713 | @@ -1,5 +1,6 @@ | |||
714 | 1 | #! /usr/bin/env python3 | 1 | #! /usr/bin/env python3 |
715 | 2 | 2 | ||
716 | 3 | import glob | ||
717 | 3 | import sys | 4 | import sys |
718 | 4 | 5 | ||
719 | 5 | from setuptools import find_packages, setup | 6 | from setuptools import find_packages, setup |
720 | @@ -18,6 +19,8 @@ | |||
721 | 18 | if sys.version < "3.3": | 19 | if sys.version < "3.3": |
722 | 19 | require('mock') | 20 | require('mock') |
723 | 20 | require('chardet') | 21 | require('chardet') |
724 | 22 | require('click.paths') | ||
725 | 23 | import click.paths | ||
726 | 21 | 24 | ||
727 | 22 | if "@GCOVR@": | 25 | if "@GCOVR@": |
728 | 23 | require('coverage') | 26 | require('coverage') |
729 | @@ -47,6 +50,11 @@ | |||
730 | 47 | license="GNU GPL", | 50 | license="GNU GPL", |
731 | 48 | packages=find_packages(), | 51 | packages=find_packages(), |
732 | 49 | scripts=['bin/click'], | 52 | scripts=['bin/click'], |
733 | 53 | data_files=[ | ||
734 | 54 | # we need to remove the prefix here or we add it twice | ||
735 | 55 | (click.paths.acquire_methods_dir[1:].lstrip(sys.prefix), | ||
736 | 56 | glob.glob("acquire/*")), | ||
737 | 57 | ], | ||
738 | 50 | install_requires=requirements, | 58 | install_requires=requirements, |
739 | 51 | cmdclass={"test": test_extra}, | 59 | cmdclass={"test": test_extra}, |
740 | 52 | test_suite="click.tests", | 60 | test_suite="click.tests", |
FAILED: Continuous integration, rev:541 /code.launchpad .net/~mvo/ click/acquire/ +merge/ 235753/ +edit-commit- message
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http:// jenkins. qa.ubuntu. com/job/ click-devel- ci/71/ jenkins. qa.ubuntu. com/job/ click-devel- utopic- amd64-ci/ 73/console jenkins. qa.ubuntu. com/job/ click-devel- utopic- armhf-ci/ 71/console jenkins. qa.ubuntu. com/job/ click-devel- utopic- i386-ci/ 71/console
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/click- devel-ci/ 71/rebuild
http://