Merge ~azzar1/software-properties:ubuntu/bionic-livepatch into software-properties:ubuntu/bionic
- Git
- lp:~azzar1/software-properties
- ubuntu/bionic-livepatch
- Merge into ubuntu/bionic
Proposed by
Andrea Azzarone
Status: | Merged |
---|---|
Merged at revision: | 03978fc2a7159c878eb2fb33c79ed7ca45f4b08c |
Proposed branch: | ~azzar1/software-properties:ubuntu/bionic-livepatch |
Merge into: | software-properties:ubuntu/bionic |
Diff against target: |
2640 lines (+1452/-626) 23 files modified
data/gtkbuilder/dialog-auth.ui (+93/-77) data/gtkbuilder/dialog-livepatch-error.ui (+96/-15) data/gtkbuilder/main.ui (+221/-81) data/icons/scalable/apps/livepatch.svg (+1/-0) data/software-properties-livepatch.desktop.in (+12/-0) debian/changelog (+12/-0) debian/control (+7/-2) debian/gbp.conf (+3/-0) debian/software-properties-gtk.install (+1/-0) po/POTFILES.in (+6/-0) setup.cfg (+1/-0) softwareproperties/GoaAuth.py (+16/-5) softwareproperties/LivepatchService.py (+254/-0) softwareproperties/LivepatchSnap.py (+135/-0) softwareproperties/SoftwareProperties.py (+0/-152) softwareproperties/dbus/SoftwarePropertiesDBus.py (+8/-2) softwareproperties/gtk/DialogAuth.py (+132/-117) softwareproperties/gtk/DialogLivepatchError.py (+15/-6) softwareproperties/gtk/LivepatchPage.py (+375/-0) softwareproperties/gtk/SimpleGtkbuilderApp.py (+3/-0) softwareproperties/gtk/SoftwarePropertiesGtk.py (+5/-168) softwareproperties/gtk/utils.py (+56/-1) tests/aptroot/etc/apt/apt.conf.d/.keep (+0/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sebastien Bacher | Approve | ||
Review via email: mp+365684@code.launchpad.net |
Commit message
* Backport Livepatch changes from Disco (LP: #1823761):
- Implement new design for authentication dialog.
- Add livepatch desktop file and icon.
- Move Livepatch UI in a diffrent tab.
Description of the change
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 | diff --git a/data/gtkbuilder/dialog-auth.ui b/data/gtkbuilder/dialog-auth.ui |
2 | index b630fff..ab664ce 100644 |
3 | --- a/data/gtkbuilder/dialog-auth.ui |
4 | +++ b/data/gtkbuilder/dialog-auth.ui |
5 | @@ -1,114 +1,130 @@ |
6 | <?xml version="1.0" encoding="UTF-8"?> |
7 | -<!-- Generated with glade 3.20.4 --> |
8 | +<!-- Generated with glade 3.22.1 --> |
9 | <interface> |
10 | <requires lib="gtk+" version="3.10"/> |
11 | + <object class="GtkListStore" id="liststore_account"> |
12 | + <columns> |
13 | + <!-- column-name Id --> |
14 | + <column type="gchararray"/> |
15 | + <!-- column-name Email --> |
16 | + <column type="gchararray"/> |
17 | + <!-- column-name Account --> |
18 | + <column type="GObject"/> |
19 | + </columns> |
20 | + </object> |
21 | <object class="GtkDialog" id="dialog_auth"> |
22 | - <property name="can_focus">False</property> |
23 | <property name="resizable">False</property> |
24 | - <property name="modal">True</property> |
25 | - <property name="destroy_with_parent">True</property> |
26 | <property name="type_hint">dialog</property> |
27 | - <property name="deletable">False</property> |
28 | + <child> |
29 | + <placeholder/> |
30 | + </child> |
31 | <child internal-child="vbox"> |
32 | - <object class="GtkBox" id="box_dialog"> |
33 | - <property name="can_focus">False</property> |
34 | + <object class="GtkBox"> |
35 | + <property name="border_width">6</property> |
36 | <property name="orientation">vertical</property> |
37 | <property name="spacing">2</property> |
38 | <child internal-child="action_area"> |
39 | - <object class="GtkButtonBox" id="dialog-action_area1"> |
40 | - <property name="can_focus">False</property> |
41 | + <object class="GtkButtonBox"> |
42 | + <child> |
43 | + <object class="GtkButton" id="button_add_another"> |
44 | + <property name="label" translatable="yes">Add another…</property> |
45 | + <property name="visible">True</property> |
46 | + <property name="can_focus">True</property> |
47 | + <property name="receives_default">True</property> |
48 | + <signal name="clicked" handler="_button_add_another_clicked_cb" swapped="no"/> |
49 | + </object> |
50 | + <packing> |
51 | + <property name="expand">True</property> |
52 | + <property name="secondary">True</property> |
53 | + <property name="non_homogeneous">True</property> |
54 | + </packing> |
55 | + </child> |
56 | + <child> |
57 | + <object class="GtkButton" id="button_cancel"> |
58 | + <property name="label" translatable="yes">Cancel</property> |
59 | + <property name="visible">True</property> |
60 | + <property name="can_focus">True</property> |
61 | + <property name="receives_default">True</property> |
62 | + <signal name="clicked" handler="_button_cancel_clicked_cb" swapped="no"/> |
63 | + </object> |
64 | + <packing> |
65 | + <property name="expand">True</property> |
66 | + <property name="non_homogeneous">True</property> |
67 | + </packing> |
68 | + </child> |
69 | + <child> |
70 | + <object class="GtkButton" id="button_continue"> |
71 | + <property name="visible">True</property> |
72 | + <property name="can_focus">True</property> |
73 | + <property name="receives_default">True</property> |
74 | + <signal name="clicked" handler="_button_continue_clicked_cb" swapped="no"/> |
75 | + </object> |
76 | + <packing> |
77 | + <property name="expand">True</property> |
78 | + <property name="non_homogeneous">True</property> |
79 | + </packing> |
80 | + </child> |
81 | </object> |
82 | <packing> |
83 | - <property name="expand">False</property> |
84 | - <property name="fill">False</property> |
85 | - <property name="position">0</property> |
86 | + <property name="expand">True</property> |
87 | </packing> |
88 | </child> |
89 | <child> |
90 | - <object class="GtkGrid" id="main_grid"> |
91 | + <object class="GtkBox"> |
92 | <property name="visible">True</property> |
93 | - <property name="can_focus">False</property> |
94 | + <property name="halign">start</property> |
95 | <property name="border_width">12</property> |
96 | - <property name="row_spacing">12</property> |
97 | - <property name="column_spacing">12</property> |
98 | + <property name="spacing">18</property> |
99 | <child> |
100 | - <object class="GtkLabel" id="label_title"> |
101 | + <object class="GtkImage"> |
102 | <property name="visible">True</property> |
103 | - <property name="can_focus">False</property> |
104 | - <property name="hexpand">True</property> |
105 | - <property name="label" translatable="yes">To enable Livepatch choose an Ubuntu Single Sign-on account.</property> |
106 | - <property name="wrap">True</property> |
107 | - <property name="xalign">0</property> |
108 | + <property name="halign">start</property> |
109 | + <property name="valign">start</property> |
110 | + <property name="icon_name">software-properties</property> |
111 | + <property name="icon_size">6</property> |
112 | </object> |
113 | - <packing> |
114 | - <property name="left_attach">0</property> |
115 | - <property name="top_attach">0</property> |
116 | - </packing> |
117 | </child> |
118 | <child> |
119 | - <object class="GtkFrame" id="main_frame"> |
120 | + <object class="GtkBox" id="box_auth"> |
121 | <property name="visible">True</property> |
122 | - <property name="can_focus">False</property> |
123 | - <property name="label_xalign">0</property> |
124 | + <property name="halign">start</property> |
125 | + <property name="orientation">vertical</property> |
126 | + <property name="spacing">12</property> |
127 | <child> |
128 | - <object class="GtkListBox" id="listbox_accounts"> |
129 | + <object class="GtkLabel" id="label_header"> |
130 | + <property name="name">label_header</property> |
131 | <property name="visible">True</property> |
132 | - <property name="can_focus">False</property> |
133 | - <property name="selection_mode">none</property> |
134 | + <property name="halign">start</property> |
135 | + <property name="valign">start</property> |
136 | + <property name="use_markup">True</property> |
137 | + <property name="justify">fill</property> |
138 | + <property name="wrap">True</property> |
139 | + <property name="max_width_chars">40</property> |
140 | + </object> |
141 | + </child> |
142 | + <child> |
143 | + <object class="GtkComboBox" id="combobox_account"> |
144 | + <property name="halign">start</property> |
145 | + <property name="model">liststore_account</property> |
146 | <child> |
147 | - <object class="GtkListBoxRow" id="listboxrow_new_account"> |
148 | - <property name="visible">True</property> |
149 | - <property name="can_focus">False</property> |
150 | - <child> |
151 | - <object class="GtkLabel" id="label_new_account"> |
152 | - <property name="height_request">48</property> |
153 | - <property name="visible">True</property> |
154 | - <property name="can_focus">False</property> |
155 | - <property name="halign">center</property> |
156 | - <property name="valign">center</property> |
157 | - <property name="label" translatable="yes"><b>Use another account...</b></property> |
158 | - <property name="use_markup">True</property> |
159 | - <property name="justify">center</property> |
160 | - </object> |
161 | - </child> |
162 | - </object> |
163 | + <object class="GtkCellRendererText"/> |
164 | + <attributes> |
165 | + <attribute name="text">1</attribute> |
166 | + </attributes> |
167 | </child> |
168 | </object> |
169 | </child> |
170 | + <child> |
171 | + <object class="GtkLabel" id="label_account"> |
172 | + <property name="halign">start</property> |
173 | + </object> |
174 | + </child> |
175 | </object> |
176 | - <packing> |
177 | - <property name="left_attach">0</property> |
178 | - <property name="top_attach">1</property> |
179 | - <property name="width">2</property> |
180 | - </packing> |
181 | </child> |
182 | </object> |
183 | - <packing> |
184 | - <property name="expand">False</property> |
185 | - <property name="fill">True</property> |
186 | - <property name="position">1</property> |
187 | - </packing> |
188 | - </child> |
189 | - </object> |
190 | - </child> |
191 | - <child type="titlebar"> |
192 | - <object class="GtkHeaderBar"> |
193 | - <property name="visible">True</property> |
194 | - <property name="can_focus">False</property> |
195 | - <property name="title" translatable="yes">Choose an account</property> |
196 | - <child> |
197 | - <object class="GtkButton" id="button_cancel"> |
198 | - <property name="label">gtk-cancel</property> |
199 | - <property name="visible">True</property> |
200 | - <property name="can_focus">True</property> |
201 | - <property name="receives_default">True</property> |
202 | - <property name="use_stock">True</property> |
203 | - </object> |
204 | </child> |
205 | </object> |
206 | </child> |
207 | - <action-widgets> |
208 | - <action-widget response="-6">button_cancel</action-widget> |
209 | - </action-widgets> |
210 | </object> |
211 | </interface> |
212 | + |
213 | diff --git a/data/gtkbuilder/dialog-livepatch-error.ui b/data/gtkbuilder/dialog-livepatch-error.ui |
214 | index 9d64306..6ee1f44 100644 |
215 | --- a/data/gtkbuilder/dialog-livepatch-error.ui |
216 | +++ b/data/gtkbuilder/dialog-livepatch-error.ui |
217 | @@ -1,58 +1,139 @@ |
218 | <?xml version="1.0" encoding="UTF-8"?> |
219 | -<!-- Generated with glade 3.18.3 --> |
220 | +<!-- Generated with glade 3.22.0 --> |
221 | <interface> |
222 | <requires lib="gtk+" version="3.12"/> |
223 | - <object class="GtkMessageDialog" id="messagedialog_livepatch"> |
224 | - <property name="can_focus">False</property> |
225 | + <object class="GtkTextBuffer" id="textbuffer_message"/> |
226 | + <object class="GtkDialog" id="messagedialog_livepatch"> |
227 | + <property name="title">Livepatch</property> |
228 | + <property name="resizable">False</property> |
229 | + <property name="modal">True</property> |
230 | <property name="type_hint">dialog</property> |
231 | - <property name="message_type">error</property> |
232 | - <property name="text" translatable="yes">Sorry, there’s been a problem in setting up Canonical Livepatch.</property> |
233 | + <property name="urgency_hint">True</property> |
234 | + <property name="deletable">False</property> |
235 | + <property name="skip_taskbar_hint">True</property> |
236 | + <property name="skip_pager_hint">True</property> |
237 | <child internal-child="vbox"> |
238 | - <object class="GtkBox" id="messagedialog-vbox1"> |
239 | - <property name="can_focus">False</property> |
240 | + <object class="GtkBox"> |
241 | <property name="orientation">vertical</property> |
242 | <property name="spacing">2</property> |
243 | <child internal-child="action_area"> |
244 | - <object class="GtkButtonBox" id="messagedialog-action_area1"> |
245 | - <property name="can_focus">False</property> |
246 | + <object class="GtkButtonBox"> |
247 | <property name="layout_style">end</property> |
248 | <child> |
249 | <object class="GtkButton" id="button_settings"> |
250 | <property name="label" translatable="yes">Settings…</property> |
251 | - <property name="visible">True</property> |
252 | <property name="can_focus">True</property> |
253 | - <property name="receives_default">True</property> |
254 | + <property name="receives_default">False</property> |
255 | <signal name="clicked" handler="on_button_settings_clicked" swapped="no"/> |
256 | </object> |
257 | <packing> |
258 | <property name="expand">True</property> |
259 | <property name="fill">True</property> |
260 | - <property name="position">0</property> |
261 | </packing> |
262 | </child> |
263 | <child> |
264 | <object class="GtkButton" id="button_ignore"> |
265 | <property name="label" translatable="yes">Ignore</property> |
266 | <property name="visible">True</property> |
267 | + <property name="has_focus">True</property> |
268 | <property name="can_focus">True</property> |
269 | <property name="receives_default">True</property> |
270 | - <property name="yalign">0.51999998092651367</property> |
271 | <signal name="clicked" handler="on_button_ignore_clicked" swapped="no"/> |
272 | </object> |
273 | <packing> |
274 | <property name="expand">True</property> |
275 | <property name="fill">True</property> |
276 | - <property name="position">1</property> |
277 | </packing> |
278 | </child> |
279 | </object> |
280 | <packing> |
281 | <property name="expand">False</property> |
282 | <property name="fill">False</property> |
283 | - <property name="position">0</property> |
284 | + </packing> |
285 | + </child> |
286 | + <child> |
287 | + <object class="GtkBox"> |
288 | + <property name="visible">True</property> |
289 | + <property name="border_width">12</property> |
290 | + <property name="orientation">vertical</property> |
291 | + <child> |
292 | + <object class="GtkGrid"> |
293 | + <property name="visible">True</property> |
294 | + <property name="row_spacing">12</property> |
295 | + <property name="column_spacing">12</property> |
296 | + <child> |
297 | + <object class="GtkLabel" id="label_primary"> |
298 | + <property name="visible">True</property> |
299 | + <property name="xalign">0</property> |
300 | + </object> |
301 | + <packing> |
302 | + <property name="left_attach">1</property> |
303 | + <property name="top_attach">0</property> |
304 | + </packing> |
305 | + </child> |
306 | + <child> |
307 | + <object class="GtkImage"> |
308 | + <property name="visible">True</property> |
309 | + <property name="halign">center</property> |
310 | + <property name="valign">start</property> |
311 | + <property name="stock">gtk-dialog-error</property> |
312 | + <property name="use_fallback">True</property> |
313 | + <property name="icon_size">6</property> |
314 | + </object> |
315 | + <packing> |
316 | + <property name="left_attach">0</property> |
317 | + <property name="top_attach">0</property> |
318 | + <property name="height">3</property> |
319 | + </packing> |
320 | + </child> |
321 | + <child> |
322 | + <object class="GtkLabel"> |
323 | + <property name="visible">True</property> |
324 | + <property name="label" translatable="yes">The error was:</property> |
325 | + <property name="xalign">0</property> |
326 | + </object> |
327 | + <packing> |
328 | + <property name="left_attach">1</property> |
329 | + <property name="top_attach">1</property> |
330 | + </packing> |
331 | + </child> |
332 | + <child> |
333 | + <object class="GtkTextView" id="treeview_message"> |
334 | + <property name="height_request">100</property> |
335 | + <property name="visible">True</property> |
336 | + <property name="hexpand">True</property> |
337 | + <property name="vexpand">True</property> |
338 | + <property name="pixels_above_lines">6</property> |
339 | + <property name="pixels_below_lines">6</property> |
340 | + <property name="editable">False</property> |
341 | + <property name="wrap_mode">word</property> |
342 | + <property name="left_margin">6</property> |
343 | + <property name="right_margin">6</property> |
344 | + <property name="cursor_visible">False</property> |
345 | + <property name="buffer">textbuffer_message</property> |
346 | + <property name="accepts_tab">False</property> |
347 | + </object> |
348 | + <packing> |
349 | + <property name="left_attach">1</property> |
350 | + <property name="top_attach">2</property> |
351 | + </packing> |
352 | + </child> |
353 | + </object> |
354 | + <packing> |
355 | + <property name="expand">True</property> |
356 | + <property name="fill">True</property> |
357 | + </packing> |
358 | + </child> |
359 | + </object> |
360 | + <packing> |
361 | + <property name="expand">True</property> |
362 | + <property name="fill">True</property> |
363 | </packing> |
364 | </child> |
365 | </object> |
366 | </child> |
367 | + <child type="titlebar"> |
368 | + <placeholder/> |
369 | + </child> |
370 | </object> |
371 | </interface> |
372 | diff --git a/data/gtkbuilder/main.ui b/data/gtkbuilder/main.ui |
373 | index 146a9ed..557e844 100644 |
374 | --- a/data/gtkbuilder/main.ui |
375 | +++ b/data/gtkbuilder/main.ui |
376 | @@ -1,7 +1,13 @@ |
377 | <?xml version="1.0" encoding="UTF-8"?> |
378 | -<!-- Generated with glade 3.18.3 --> |
379 | +<!-- Generated with glade 3.22.0 --> |
380 | <interface> |
381 | - <requires lib="gtk+" version="3.0"/> |
382 | + <requires lib="gtk+" version="3.22"/> |
383 | + <object class="GtkListStore" id="model_livepatch_fixes"> |
384 | + <columns> |
385 | + <!-- column-name fix --> |
386 | + <column type="gchararray"/> |
387 | + </columns> |
388 | + </object> |
389 | <object class="GtkListStore" id="model_normal_updates_display"> |
390 | <columns> |
391 | <!-- column-name text --> |
392 | @@ -91,6 +97,7 @@ |
393 | <object class="GtkTextBuffer" id="textbuffer1"> |
394 | <property name="text" translatable="yes">To install from a CD-ROM or DVD, insert the medium into the drive.</property> |
395 | </object> |
396 | + <object class="GtkTextBuffer" id="textbuffer_livepatch"/> |
397 | <object class="GtkWindow" id="window_main"> |
398 | <property name="can_focus">False</property> |
399 | <property name="border_width">6</property> |
400 | @@ -696,84 +703,6 @@ |
401 | </packing> |
402 | </child> |
403 | <child> |
404 | - <object class="GtkAlignment" id="alignment3"> |
405 | - <property name="visible">True</property> |
406 | - <property name="can_focus">False</property> |
407 | - <property name="left_padding">12</property> |
408 | - <child> |
409 | - <object class="GtkGrid" id="grid_livepatch"> |
410 | - <property name="visible">True</property> |
411 | - <property name="can_focus">False</property> |
412 | - <property name="halign">center</property> |
413 | - <property name="hexpand">False</property> |
414 | - <property name="vexpand">False</property> |
415 | - <property name="row_spacing">6</property> |
416 | - <property name="column_spacing">6</property> |
417 | - <child> |
418 | - <object class="GtkBox" id="hbox_livepatch"> |
419 | - <property name="visible">True</property> |
420 | - <property name="can_focus">False</property> |
421 | - <property name="halign">center</property> |
422 | - <property name="spacing">6</property> |
423 | - <child> |
424 | - <object class="GtkLabel" id="label_livepatch_login"> |
425 | - <property name="visible">True</property> |
426 | - <property name="can_focus">False</property> |
427 | - </object> |
428 | - <packing> |
429 | - <property name="expand">False</property> |
430 | - <property name="fill">True</property> |
431 | - <property name="position">0</property> |
432 | - </packing> |
433 | - </child> |
434 | - <child> |
435 | - <object class="GtkButton" id="button_ubuntuone"> |
436 | - <property name="visible">True</property> |
437 | - <property name="can_focus">True</property> |
438 | - <property name="receives_default">True</property> |
439 | - <property name="xalign">0</property> |
440 | - </object> |
441 | - <packing> |
442 | - <property name="expand">False</property> |
443 | - <property name="fill">True</property> |
444 | - <property name="position">2</property> |
445 | - </packing> |
446 | - </child> |
447 | - </object> |
448 | - <packing> |
449 | - <property name="left_attach">1</property> |
450 | - <property name="top_attach">1</property> |
451 | - </packing> |
452 | - </child> |
453 | - <child> |
454 | - <object class="GtkCheckButton" id="checkbutton_livepatch"> |
455 | - <property name="label" translatable="yes">Use Canonical Livepatch to increase security between restarts</property> |
456 | - <property name="use_action_appearance">False</property> |
457 | - <property name="visible">True</property> |
458 | - <property name="can_focus">True</property> |
459 | - <property name="receives_default">False</property> |
460 | - <property name="use_underline">True</property> |
461 | - <property name="xalign">0</property> |
462 | - <property name="draw_indicator">True</property> |
463 | - </object> |
464 | - <packing> |
465 | - <property name="left_attach">1</property> |
466 | - <property name="top_attach">0</property> |
467 | - </packing> |
468 | - </child> |
469 | - <child> |
470 | - <placeholder/> |
471 | - </child> |
472 | - </object> |
473 | - </child> |
474 | - </object> |
475 | - <packing> |
476 | - <property name="expand">True</property> |
477 | - <property name="fill">True</property> |
478 | - <property name="position">2</property> |
479 | - </packing> |
480 | - </child> |
481 | - <child> |
482 | <object class="GtkAlignment" id="alignment15"> |
483 | <property name="visible">True</property> |
484 | <property name="can_focus">False</property> |
485 | @@ -820,7 +749,7 @@ |
486 | <packing> |
487 | <property name="expand">False</property> |
488 | <property name="fill">False</property> |
489 | - <property name="position">3</property> |
490 | + <property name="position">2</property> |
491 | </packing> |
492 | </child> |
493 | </object> |
494 | @@ -1187,6 +1116,214 @@ |
495 | <property name="tab_fill">False</property> |
496 | </packing> |
497 | </child> |
498 | + <child> |
499 | + <object class="GtkBox" id="vbox_livepatch"> |
500 | + <property name="visible">True</property> |
501 | + <property name="can_focus">False</property> |
502 | + <property name="border_width">12</property> |
503 | + <property name="orientation">vertical</property> |
504 | + <property name="spacing">12</property> |
505 | + <child> |
506 | + <object class="GtkLabel" id="label_livepatch_description"> |
507 | + <property name="visible">True</property> |
508 | + <property name="can_focus">False</property> |
509 | + <property name="label" translatable="yes">Canonical Livepatch helps keep your system secure by applying security updates that don't require a restart. <a href="https://www.ubuntu.com/livepatch">Learn More</a></property> |
510 | + <property name="use_markup">True</property> |
511 | + <property name="wrap">True</property> |
512 | + <property name="max_width_chars">1</property> |
513 | + <property name="xalign">0</property> |
514 | + </object> |
515 | + <packing> |
516 | + <property name="expand">False</property> |
517 | + <property name="fill">True</property> |
518 | + <property name="position">0</property> |
519 | + </packing> |
520 | + </child> |
521 | + <child> |
522 | + <object class="GtkBox" id="hbox_switch"> |
523 | + <property name="visible">True</property> |
524 | + <property name="can_focus">False</property> |
525 | + <property name="spacing">6</property> |
526 | + <child> |
527 | + <object class="GtkSwitch" id="switch_livepatch"> |
528 | + <property name="visible">True</property> |
529 | + <property name="sensitive">False</property> |
530 | + <property name="can_focus">True</property> |
531 | + </object> |
532 | + <packing> |
533 | + <property name="expand">False</property> |
534 | + <property name="fill">True</property> |
535 | + <property name="position">0</property> |
536 | + </packing> |
537 | + </child> |
538 | + <child> |
539 | + <object class="GtkSpinner" id="spinner_livepatch"> |
540 | + <property name="can_focus">False</property> |
541 | + </object> |
542 | + <packing> |
543 | + <property name="expand">False</property> |
544 | + <property name="fill">True</property> |
545 | + <property name="position">1</property> |
546 | + </packing> |
547 | + </child> |
548 | + <child> |
549 | + <object class="GtkLabel" id="label_livepatch_switch"> |
550 | + <property name="visible">True</property> |
551 | + <property name="can_focus">False</property> |
552 | + </object> |
553 | + <packing> |
554 | + <property name="expand">False</property> |
555 | + <property name="fill">True</property> |
556 | + <property name="position">2</property> |
557 | + </packing> |
558 | + </child> |
559 | + <child> |
560 | + <object class="GtkButton" id="button_livepatch_login"> |
561 | + <property name="can_focus">True</property> |
562 | + <property name="receives_default">True</property> |
563 | + </object> |
564 | + <packing> |
565 | + <property name="expand">False</property> |
566 | + <property name="fill">True</property> |
567 | + <property name="pack_type">end</property> |
568 | + <property name="position">3</property> |
569 | + </packing> |
570 | + </child> |
571 | + </object> |
572 | + <packing> |
573 | + <property name="expand">False</property> |
574 | + <property name="fill">True</property> |
575 | + <property name="position">1</property> |
576 | + </packing> |
577 | + </child> |
578 | + <child> |
579 | + <object class="GtkStack" id="stack_livepatch"> |
580 | + <property name="can_focus">False</property> |
581 | + <property name="transition_type">crossfade</property> |
582 | + <property name="interpolate_size">True</property> |
583 | + <child> |
584 | + <object class="GtkScrolledWindow"> |
585 | + <property name="visible">True</property> |
586 | + <property name="can_focus">True</property> |
587 | + <property name="shadow_type">in</property> |
588 | + <child> |
589 | + <object class="GtkTextView" id="textview_livepatch"> |
590 | + <property name="visible">True</property> |
591 | + <property name="can_focus">True</property> |
592 | + <property name="pixels_above_lines">6</property> |
593 | + <property name="editable">False</property> |
594 | + <property name="wrap_mode">word</property> |
595 | + <property name="left_margin">6</property> |
596 | + <property name="right_margin">6</property> |
597 | + <property name="cursor_visible">False</property> |
598 | + <property name="buffer">textbuffer_livepatch</property> |
599 | + <property name="accepts_tab">False</property> |
600 | + </object> |
601 | + </child> |
602 | + </object> |
603 | + <packing> |
604 | + <property name="name">page_livepatch_message</property> |
605 | + </packing> |
606 | + </child> |
607 | + <child> |
608 | + <object class="GtkBox"> |
609 | + <property name="visible">True</property> |
610 | + <property name="can_focus">False</property> |
611 | + <property name="orientation">vertical</property> |
612 | + <property name="spacing">12</property> |
613 | + <child> |
614 | + <object class="GtkLabel" id="label_livepatch_last_update"> |
615 | + <property name="visible">True</property> |
616 | + <property name="can_focus">False</property> |
617 | + <property name="xalign">0</property> |
618 | + </object> |
619 | + <packing> |
620 | + <property name="expand">False</property> |
621 | + <property name="fill">True</property> |
622 | + <property name="position">0</property> |
623 | + </packing> |
624 | + </child> |
625 | + <child> |
626 | + <object class="GtkLabel" id="label_livepatch_header"> |
627 | + <property name="visible">True</property> |
628 | + <property name="can_focus">False</property> |
629 | + <property name="xalign">0</property> |
630 | + </object> |
631 | + <packing> |
632 | + <property name="expand">False</property> |
633 | + <property name="fill">True</property> |
634 | + <property name="position">1</property> |
635 | + </packing> |
636 | + </child> |
637 | + <child> |
638 | + <object class="GtkScrolledWindow" id="scrolledwindow_livepatch_fixes"> |
639 | + <property name="visible">True</property> |
640 | + <property name="can_focus">True</property> |
641 | + <property name="shadow_type">in</property> |
642 | + <child> |
643 | + <object class="GtkTreeView" id="treeview_livepatch"> |
644 | + <property name="visible">True</property> |
645 | + <property name="can_focus">True</property> |
646 | + <property name="model">model_livepatch_fixes</property> |
647 | + <property name="headers_visible">False</property> |
648 | + <property name="enable_search">False</property> |
649 | + <property name="show_expanders">False</property> |
650 | + <child internal-child="selection"> |
651 | + <object class="GtkTreeSelection"/> |
652 | + </child> |
653 | + <child> |
654 | + <object class="GtkTreeViewColumn"> |
655 | + <property name="title" translatable="yes">column</property> |
656 | + <child> |
657 | + <object class="GtkCellRendererText"> |
658 | + <property name="width_chars">100</property> |
659 | + <property name="wrap_mode">word</property> |
660 | + <property name="wrap_width">100</property> |
661 | + </object> |
662 | + <attributes> |
663 | + <attribute name="markup">0</attribute> |
664 | + </attributes> |
665 | + </child> |
666 | + </object> |
667 | + </child> |
668 | + </object> |
669 | + </child> |
670 | + </object> |
671 | + <packing> |
672 | + <property name="expand">True</property> |
673 | + <property name="fill">True</property> |
674 | + <property name="position">2</property> |
675 | + </packing> |
676 | + </child> |
677 | + </object> |
678 | + <packing> |
679 | + <property name="name">page_livepatch_status</property> |
680 | + <property name="position">1</property> |
681 | + </packing> |
682 | + </child> |
683 | + </object> |
684 | + <packing> |
685 | + <property name="expand">True</property> |
686 | + <property name="fill">True</property> |
687 | + <property name="position">2</property> |
688 | + </packing> |
689 | + </child> |
690 | + </object> |
691 | + <packing> |
692 | + <property name="position">6</property> |
693 | + </packing> |
694 | + </child> |
695 | + <child type="tab"> |
696 | + <object class="GtkLabel" id="label_livepatch"> |
697 | + <property name="visible">True</property> |
698 | + <property name="can_focus">False</property> |
699 | + <property name="label">Livepatch</property> |
700 | + </object> |
701 | + <packing> |
702 | + <property name="position">6</property> |
703 | + <property name="tab_fill">False</property> |
704 | + </packing> |
705 | + </child> |
706 | </object> |
707 | <packing> |
708 | <property name="expand">True</property> |
709 | @@ -1246,6 +1383,9 @@ |
710 | </child> |
711 | </object> |
712 | </child> |
713 | + <child type="titlebar"> |
714 | + <placeholder/> |
715 | + </child> |
716 | </object> |
717 | <object class="GtkSizeGroup" id="sizegroup1"> |
718 | <widgets> |
719 | diff --git a/data/icons/16x16/apps/livepatch.svg b/data/icons/16x16/apps/livepatch.svg |
720 | new file mode 100644 |
721 | index 0000000..6d82412 |
722 | Binary files /dev/null and b/data/icons/16x16/apps/livepatch.svg differ |
723 | diff --git a/data/icons/24x24/apps/livepatch.svg b/data/icons/24x24/apps/livepatch.svg |
724 | new file mode 100644 |
725 | index 0000000..8c13c57 |
726 | Binary files /dev/null and b/data/icons/24x24/apps/livepatch.svg differ |
727 | diff --git a/data/icons/48x48/apps/livepatch.svg b/data/icons/48x48/apps/livepatch.svg |
728 | new file mode 100644 |
729 | index 0000000..65a3049 |
730 | Binary files /dev/null and b/data/icons/48x48/apps/livepatch.svg differ |
731 | diff --git a/data/icons/64x64/apps/livepatch.svg b/data/icons/64x64/apps/livepatch.svg |
732 | new file mode 100644 |
733 | index 0000000..46c3dea |
734 | Binary files /dev/null and b/data/icons/64x64/apps/livepatch.svg differ |
735 | diff --git a/data/icons/scalable/apps/livepatch.svg b/data/icons/scalable/apps/livepatch.svg |
736 | new file mode 100644 |
737 | index 0000000..8bdc8b5 |
738 | --- /dev/null |
739 | +++ b/data/icons/scalable/apps/livepatch.svg |
740 | @@ -0,0 +1 @@ |
741 | +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0px" y="0px" width="400px" height="400px" viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve"> <style type="text/css"> .st0{fill:#E95420;} </style> <path class="st0" d="M200,0C89.7,0,0,89.7,0,200s89.7,200,200,200s200-89.7,200-200S310.3,0,200,0L200,0z M200,325 c-64.6,0-117.9-49.3-124.4-112.2v-0.1H38.8c-5.1,0-9.6-3-11.5-7.7s-0.9-10.1,2.7-13.6l61.2-61.3c4.9-4.9,12.8-4.9,17.7,0l60.6,60.6 c2.6,2.3,4.3,5.7,4.3,9.5c0,6.9-5.6,12.5-12.5,12.5h-35.2c6,35.4,36.9,62.4,73.9,62.4c41.4,0,75-33.6,75-75c0-41.3-33.6-75-75-75 c-13.8,0-25-11.2-25-25s11.2-25,25-25c68.9,0,125,56.1,125,125C325,268.9,268.9,325,200,325z"/> </svg> |
742 | \ No newline at end of file |
743 | diff --git a/data/software-properties-livepatch.desktop.in b/data/software-properties-livepatch.desktop.in |
744 | new file mode 100644 |
745 | index 0000000..c9f168a |
746 | --- /dev/null |
747 | +++ b/data/software-properties-livepatch.desktop.in |
748 | @@ -0,0 +1,12 @@ |
749 | +[Desktop Entry] |
750 | +Keywords=Livepatch; |
751 | +Exec=/usr/bin/software-properties-gtk --open-tab=6 |
752 | +Icon=livepatch |
753 | +Terminal=false |
754 | +Type=Application |
755 | +OnlyShowIn=GNOME; |
756 | +Categories=GTK;Settings;HardwareSettings |
757 | +X-AppStream-Ignore=true |
758 | +Name=Livepatch |
759 | +_Comment=Manage Canonical Livepatch |
760 | +_Keywords=Security;Update; |
761 | diff --git a/debian/changelog b/debian/changelog |
762 | index a1449b1..1d7a3e8 100644 |
763 | --- a/debian/changelog |
764 | +++ b/debian/changelog |
765 | @@ -1,3 +1,15 @@ |
766 | +software-properties (0.96.24.32.8) UNRELEASED; urgency=medium |
767 | + |
768 | + * debian/control: Update Vcs-*: URLs |
769 | + * debian/gbp.conf: Add default configuration |
770 | + * Add .keep files to preserve empty directories |
771 | + * Backport Livepatch changes from Disco (LP: #1823761): |
772 | + - Implement new design for authentication dialog. |
773 | + - Add livepatch desktop file and icon. |
774 | + - Move Livepatch UI in a diffrent tab. |
775 | + |
776 | + -- Andrea Azzarone <andrea.azzarone@canonical.com> Mon, 08 Apr 2019 16:52:15 +0100 |
777 | + |
778 | software-properties (0.96.24.32.7) bionic; urgency=medium |
779 | |
780 | * SoftwarePropertiesGtk.py: when checking a package's depends for DKMS also |
781 | diff --git a/debian/control b/debian/control |
782 | index fd8d887..cd54cef 100644 |
783 | --- a/debian/control |
784 | +++ b/debian/control |
785 | @@ -7,13 +7,17 @@ Build-Depends: debhelper (>= 9), |
786 | libxml-parser-perl, |
787 | intltool, |
788 | dbus-x11 <!nocheck>, |
789 | + gir1.2-gtk-3.0 <!nocheck>, |
790 | gir1.2-snapd-1 <!nocheck>, |
791 | lsb-release <!nocheck>, |
792 | pyflakes3 <!nocheck>, |
793 | python3-apt <!nocheck>, |
794 | + python3-dateutil <!nocheck>, |
795 | python3-dbus <!nocheck>, |
796 | + python3-distro-info <!nocheck>, |
797 | python3-gi <!nocheck>, |
798 | python3-mock <!nocheck>, |
799 | + python3-requests-unixsocket <!nocheck>, |
800 | xauth <!nocheck>, |
801 | xvfb <!nocheck>, |
802 | python3-all, |
803 | @@ -22,7 +26,7 @@ Build-Depends: debhelper (>= 9), |
804 | dh-migrations, |
805 | dh-translations |
806 | Standards-Version: 3.9.6 |
807 | -Vcs-Bzr: http://code.launchpad.net/~ubuntu-core-dev/software-properties/main |
808 | +Vcs-Git: https://git.launchpad.net/software-properties -b ubuntu/bionic |
809 | XS-Testsuite: autopkgtest |
810 | |
811 | Package: python3-software-properties |
812 | @@ -58,10 +62,11 @@ Depends: ${python3:Depends}, ${misc:Depends}, python3, |
813 | python3-gi, |
814 | gir1.2-gtk-3.0, |
815 | gir1.2-goa-1.0 (>= 3.27.92-1ubuntu1), |
816 | - gir1.2-secret-1, |
817 | gir1.2-snapd-1, |
818 | python3-aptdaemon.gtk3widgets, |
819 | + python3-dateutil, |
820 | python3-distro-info, |
821 | + python3-requests-unixsocket, |
822 | software-properties-common, |
823 | ubuntu-drivers-common (>= 1:0.2.75), |
824 | python3-gi, |
825 | diff --git a/debian/gbp.conf b/debian/gbp.conf |
826 | new file mode 100644 |
827 | index 0000000..0bb5f1a |
828 | --- /dev/null |
829 | +++ b/debian/gbp.conf |
830 | @@ -0,0 +1,3 @@ |
831 | +[DEFAULT] |
832 | +debian-branch = ubuntu/bionic |
833 | +debian-tag = %(version)s |
834 | diff --git a/debian/software-properties-gtk.install b/debian/software-properties-gtk.install |
835 | index c71d9d2..cfc4d82 100644 |
836 | --- a/debian/software-properties-gtk.install |
837 | +++ b/debian/software-properties-gtk.install |
838 | @@ -5,6 +5,7 @@ debian/tmp/usr/share/mime/packages |
839 | debian/tmp/usr/share/icons |
840 | debian/tmp/usr/share/applications/software-properties-gtk.desktop |
841 | debian/tmp/usr/share/applications/software-properties-drivers.desktop |
842 | +debian/tmp/usr/share/applications/software-properties-livepatch.desktop |
843 | debian/tmp/usr/share/metainfo/software-properties-gtk.appdata.xml |
844 | debian/tmp/usr/share/glib-2.0/schemas |
845 | #debian/tmp/usr/share/gnome/help/software-properties |
846 | diff --git a/po/POTFILES.in b/po/POTFILES.in |
847 | index d1917fd..47830e4 100644 |
848 | --- a/po/POTFILES.in |
849 | +++ b/po/POTFILES.in |
850 | @@ -5,6 +5,7 @@ data/software-properties-gtk.desktop.in |
851 | data/software-properties-gtk.appdata.xml.in |
852 | data/software-properties-kde.desktop.in |
853 | data/software-properties-drivers.desktop.in |
854 | +data/software-properties-livepatch.desktop.in |
855 | software-properties-gtk |
856 | software-properties-kde |
857 | add-apt-repository |
858 | @@ -28,8 +29,12 @@ softwareproperties/gtk/dialogs.py |
859 | softwareproperties/gtk/DialogEdit.py |
860 | softwareproperties/gtk/DialogAdd.py |
861 | softwareproperties/gtk/DialogCacheOutdated.py |
862 | +softwareproperties/gtk/DialogLivepatchError.py |
863 | +softwareproperties/gtk/LivepatchPage.py |
864 | softwareproperties/CountryInformation.py |
865 | softwareproperties/AptAuth.py |
866 | +softwareproperties/LivepatchService.py |
867 | +softwareproperties/LivepatchSnap.py |
868 | [type: gettext/glade]data/designer/dialog_mirror.ui |
869 | [type: gettext/glade]data/designer/dialog_edit.ui |
870 | [type: gettext/glade]data/designer/main.ui |
871 | @@ -41,3 +46,4 @@ softwareproperties/AptAuth.py |
872 | [type: gettext/glade]data/gtkbuilder/dialog-mirror.ui |
873 | [type: gettext/glade]data/gtkbuilder/dialog-add.ui |
874 | [type: gettext/glade]data/gtkbuilder/dialog-auth.ui |
875 | +[type: gettext/glade]data/gtkbuilder/dialog-livepatch-error.ui |
876 | diff --git a/setup.cfg b/setup.cfg |
877 | index ef8c2a0..9b28335 100644 |
878 | --- a/setup.cfg |
879 | +++ b/setup.cfg |
880 | @@ -4,6 +4,7 @@ domain=software-properties |
881 | desktop_files=[("share/applications", |
882 | ("data/software-properties-gtk.desktop.in", |
883 | "data/software-properties-drivers.desktop.in", |
884 | + "data/software-properties-livepatch.desktop.in", |
885 | "data/software-properties-kde.desktop.in",), |
886 | ) |
887 | ] |
888 | diff --git a/softwareproperties/GoaAuth.py b/softwareproperties/GoaAuth.py |
889 | index 6583a4a..eb45bf7 100644 |
890 | --- a/softwareproperties/GoaAuth.py |
891 | +++ b/softwareproperties/GoaAuth.py |
892 | @@ -21,7 +21,9 @@ |
893 | |
894 | import gi |
895 | gi.require_version('Goa', '1.0') |
896 | -from gi.repository import Gio, Goa, GObject |
897 | +from gi.repository import Gio, GLib, Goa, GObject |
898 | + |
899 | +import logging |
900 | |
901 | class GoaAuth(GObject.GObject): |
902 | |
903 | @@ -32,12 +34,21 @@ class GoaAuth(GObject.GObject): |
904 | def __init__(self): |
905 | GObject.GObject.__init__(self) |
906 | |
907 | - self.goa_client = Goa.Client.new_sync(None) |
908 | self.account = None |
909 | + self.cancellable = Gio.Cancellable() |
910 | + Goa.Client.new(self.cancellable, self._on_goa_client_ready) |
911 | |
912 | self.settings = Gio.Settings.new('com.ubuntu.SoftwareProperties') |
913 | self.settings.connect('changed::goa-account-id', self._on_settings_changed) |
914 | - self._load() |
915 | + |
916 | + def _on_goa_client_ready(self, source, res): |
917 | + try: |
918 | + self.goa_client = Goa.Client.new_finish(res) |
919 | + except GLib.Error as e: |
920 | + logging.error('Failed to get a Gnome Online Account: {}'.format(e.message)) |
921 | + self.goa_client = None |
922 | + else: |
923 | + self._load() |
924 | |
925 | def login(self, account): |
926 | assert(account) |
927 | @@ -50,7 +61,7 @@ class GoaAuth(GObject.GObject): |
928 | |
929 | @GObject.Property |
930 | def token(self): |
931 | - if self.account is None: |
932 | + if self.account is None or self.goa_client is None: |
933 | return None |
934 | |
935 | obj = self.goa_client.lookup_by_id(self.account.props.id) |
936 | @@ -64,7 +75,7 @@ class GoaAuth(GObject.GObject): |
937 | return pbased.call_get_password_sync('livepatch') |
938 | |
939 | def _update_state_from_account_id(self, account_id): |
940 | - if account_id: |
941 | + if account_id and self.goa_client is not None: |
942 | # Make sure the account-id is valid |
943 | obj = self.goa_client.lookup_by_id(account_id) |
944 | if obj is None: |
945 | diff --git a/softwareproperties/LivepatchService.py b/softwareproperties/LivepatchService.py |
946 | new file mode 100644 |
947 | index 0000000..694e64c |
948 | --- /dev/null |
949 | +++ b/softwareproperties/LivepatchService.py |
950 | @@ -0,0 +1,254 @@ |
951 | +# |
952 | +# Copyright (c) 2019 Canonical |
953 | +# |
954 | +# Authors: |
955 | +# Andrea Azzarone <andrea.azzarone@canonical.com> |
956 | +# |
957 | +# This program is free software; you can redistribute it and/or |
958 | +# modify it under the terms of the GNU General Public License as |
959 | +# published by the Free Software Foundation; either version 2 of the |
960 | +# License, or (at your option) any later version. |
961 | +# |
962 | +# This program is distributed in the hope that it will be useful, |
963 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
964 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
965 | +# GNU General Public License for more details. |
966 | +# |
967 | +# You should have received a copy of the GNU General Public License |
968 | +# along with this program; if not, write to the Free Software |
969 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
970 | +# USA |
971 | + |
972 | +from gettext import gettext as _ |
973 | +import logging |
974 | + |
975 | +import gi |
976 | +from gi.repository import Gio, GLib, GObject |
977 | + |
978 | +try: |
979 | + import dateutil.parser |
980 | + import requests_unixsocket |
981 | + |
982 | + gi.require_version('Snapd', '1') |
983 | + from gi.repository import Snapd |
984 | +except(ImportError, ValueError): |
985 | + pass |
986 | + |
987 | +from softwareproperties.gtk.utils import ( |
988 | + has_gnome_online_accounts, |
989 | + is_current_distro_lts, |
990 | + is_current_distro_supported, |
991 | + retry |
992 | +) |
993 | + |
994 | +from softwareproperties.LivepatchSnap import LivepatchSnap |
995 | + |
996 | + |
997 | +def datetime_parser(json_dict): |
998 | + for (key, value) in json_dict.items(): |
999 | + try: |
1000 | + json_dict[key] = dateutil.parser.parse(value) |
1001 | + except (ValueError, TypeError): |
1002 | + pass |
1003 | + return json_dict |
1004 | + |
1005 | +class LivepatchAvailability: |
1006 | + FALSE = 0 |
1007 | + TRUE = 1 |
1008 | + NO_CONNECTIVITY=3 |
1009 | + CHECKING = 2 |
1010 | + |
1011 | + |
1012 | +class LivepatchService(GObject.GObject): |
1013 | + |
1014 | + # Constants |
1015 | + STATUS_ENDPOINT = 'http+unix://%2Fvar%2Fsnap%2Fcanonical-livepatch%2Fcurrent%2Flivepatchd.sock/status' |
1016 | + ENABLE_ENDPOINT = 'http+unix://%2Fvar%2Fsnap%2Fcanonical-livepatch%2Fcurrent%2Flivepatchd-priv.sock/enable' |
1017 | + DISABLE_ENDPOINT = 'http+unix://%2Fvar%2Fsnap%2Fcanonical-livepatch%2Fcurrent%2Flivepatchd-priv.sock/disable' |
1018 | + LIVEPATCH_RUNNING_FILE = '/var/snap/canonical-livepatch/common/machine-token' |
1019 | + |
1020 | + ENABLE_ERROR_MSG = _('Failed to enable Livepatch: {}') |
1021 | + DISABLE_ERROR_MSG = _('Failed to disable Livepatch: {}') |
1022 | + |
1023 | + # GObject.GObject |
1024 | + __gproperties__ = { |
1025 | + 'availability': ( |
1026 | + int, None, None, |
1027 | + LivepatchAvailability.FALSE, |
1028 | + LivepatchAvailability.CHECKING, |
1029 | + LivepatchAvailability.FALSE, |
1030 | + GObject.PARAM_READABLE), |
1031 | + 'availability-message': ( |
1032 | + str, None, None, None, GObject.PARAM_READABLE), |
1033 | + 'enabled': ( |
1034 | + bool, None, None, False, GObject.PARAM_READABLE), |
1035 | + } |
1036 | + |
1037 | + def __init__(self): |
1038 | + GObject.GObject.__init__(self) |
1039 | + |
1040 | + self._timeout_id = 0 |
1041 | + |
1042 | + self._snap = LivepatchSnap() |
1043 | + self._session = requests_unixsocket.Session() |
1044 | + |
1045 | + # Init Properties |
1046 | + self._availability = LivepatchAvailability.FALSE |
1047 | + self._availability_message = None |
1048 | + lp_file = Gio.File.new_for_path(path=self.LIVEPATCH_RUNNING_FILE) |
1049 | + self._enabled = lp_file.query_exists() |
1050 | + |
1051 | + # Monitor connectivity status |
1052 | + self._nm = Gio.NetworkMonitor.get_default() |
1053 | + self._nm.connect('notify::connectivity', self._network_changed_cb) |
1054 | + |
1055 | + # Monitor status of canonical-livepatch |
1056 | + self._lp_monitor = lp_file.monitor_file(Gio.FileMonitorFlags.NONE) |
1057 | + self._lp_monitor.connect('changed', self._livepatch_enabled_changed_cb) |
1058 | + |
1059 | + def do_get_property(self, pspec): |
1060 | + if pspec.name == 'availability': |
1061 | + return self._availability |
1062 | + elif pspec.name == 'availability-message': |
1063 | + return self._availability_message |
1064 | + elif pspec.name == 'enabled': |
1065 | + return self._enabled |
1066 | + else: |
1067 | + raise AssertionError |
1068 | + |
1069 | + # Public API |
1070 | + def trigger_availability_check(self): |
1071 | + """Trigger a Livepatch availability check to be executed after a short |
1072 | + timeout. Multiple triggers will result in a single request. |
1073 | + |
1074 | + A notify::availability will be emitted when the check starts, and |
1075 | + another one when the check ends. |
1076 | + """ |
1077 | + def _update_availability(): |
1078 | + # each rule is a tuple of two elements, a callable and a string. The |
1079 | + # string rapresents the error message that needs to be shown if the |
1080 | + # callable returns false. |
1081 | + rules = [ |
1082 | + (lambda: self._snap.get_status() != Snapd.SnapStatus.UNKNOWN, |
1083 | + _('Canonical Livepatch snap is not available.')), |
1084 | + (has_gnome_online_accounts, |
1085 | + _('Gnome Online Accounts is required to enable Livepatch.')), |
1086 | + (is_current_distro_lts, |
1087 | + _('Livepatch is not available for this release.')), |
1088 | + (is_current_distro_supported, |
1089 | + _('The current release is no longer supported.'))] |
1090 | + |
1091 | + if self._nm.props.connectivity != Gio.NetworkConnectivity.FULL: |
1092 | + self._availability = LivepatchAvailability.NO_CONNECTIVITY |
1093 | + self._availability_message = None |
1094 | + else: |
1095 | + for func, message in rules: |
1096 | + if not func(): |
1097 | + self._availability = LivepatchAvailability.FALSE |
1098 | + self._availability_message = message |
1099 | + break |
1100 | + else: |
1101 | + self._availability = LivepatchAvailability.TRUE |
1102 | + self._availability_message = None |
1103 | + |
1104 | + self.notify('availability') |
1105 | + self.notify('availability-message') |
1106 | + |
1107 | + self._timeout_id = 0 |
1108 | + return False |
1109 | + |
1110 | + self._availability = LivepatchAvailability.CHECKING |
1111 | + self._availability_message = None |
1112 | + self.notify('availability') |
1113 | + self.notify('availability-message') |
1114 | + |
1115 | + if self._timeout_id == 0: |
1116 | + self._timeout_id = GLib.timeout_add_seconds(3, _update_availability) |
1117 | + |
1118 | + def set_enabled(self, enabled, token): |
1119 | + """Enable or disable Canonical Livepatch in the current system. This |
1120 | + function will return once the operation succeeded or failed. |
1121 | + |
1122 | + Args: |
1123 | + enabled(bool): wheater to enable or disable the service. |
1124 | + token(str): the authentication token to be used to enable Canonical |
1125 | + Livepatch service. |
1126 | + |
1127 | + Returns: |
1128 | + (False, '') if successful, (True, error_message) otherwise. |
1129 | + """ |
1130 | + if self._enabled == enabled: |
1131 | + return False, '' |
1132 | + |
1133 | + if not enabled: |
1134 | + return self._disable_service() |
1135 | + elif self._snap.get_status() == Snapd.SnapStatus.ACTIVE: |
1136 | + return self._enable_service(token) |
1137 | + else: |
1138 | + success, msg = self._snap.enable_or_install() |
1139 | + return self._enable_service(token) if success else (True, msg) |
1140 | + |
1141 | + def get_status(self): |
1142 | + """Synchronously retrieve the status of Canonical Livepatch. |
1143 | + |
1144 | + Returns: |
1145 | + str: The status. A valid string for success, None otherwise. |
1146 | + """ |
1147 | + try: |
1148 | + params = {'verbosity': 3, 'format': 'json'} |
1149 | + r = self._session.get(self.STATUS_ENDPOINT, params=params) |
1150 | + return r.json(object_hook=datetime_parser) |
1151 | + except Exception as e: |
1152 | + logging.debug('Failed to get Livepatch status: {}'.format(str(e))) |
1153 | + return None |
1154 | + |
1155 | + # Private methods |
1156 | + def _enable_service(self, token): |
1157 | + """Enable Canonical Livepatch in the current system. This function will |
1158 | + return once the operation succeeded or failed. |
1159 | + |
1160 | + Args: |
1161 | + token(str): the authentication token to be used to enable Canonical |
1162 | + Livepatch service. |
1163 | + |
1164 | + Returns: |
1165 | + (False, '') if successful, (True, error_message) otherwise. |
1166 | + """ |
1167 | + try: |
1168 | + return self._enable_service_with_retry(token) |
1169 | + except Exception as e: |
1170 | + return True, self.ENABLE_ERROR_MSG.format(str(e)) |
1171 | + |
1172 | + @retry(Exception) |
1173 | + def _enable_service_with_retry(self, token): |
1174 | + params = {'auth-token': token} |
1175 | + r = self._session.put(self.ENABLE_ENDPOINT, params=params) |
1176 | + return not r.ok, '' if r.ok else self.ENABLE_ERROR_MSG.format(r.text) |
1177 | + |
1178 | + def _disable_service(self): |
1179 | + """Disable Canonical Livepatch in the current system. This function will |
1180 | + return once the operation succeeded or failed. |
1181 | + |
1182 | + Returns: |
1183 | + (False, '') if successful, (True, error_message) otherwise. |
1184 | + """ |
1185 | + try: |
1186 | + return self._disable_service_with_retry() |
1187 | + except Exception as e: |
1188 | + return True, self.DISABLE_ERROR_MSG.format(str(e)) |
1189 | + |
1190 | + |
1191 | + @retry(Exception) |
1192 | + def _disable_service_with_retry(self): |
1193 | + r = self._session.put(self.DISABLE_ENDPOINT) |
1194 | + return not r.ok, '' if r.ok else self.DISABLE_ERROR_MSG.format(r.text) |
1195 | + |
1196 | + # Signals handlers |
1197 | + def _network_changed_cb(self, monitor, network_available): |
1198 | + self.trigger_availability_check() |
1199 | + |
1200 | + def _livepatch_enabled_changed_cb(self, fm, file, other_file, event_type): |
1201 | + enabled = file.query_exists() |
1202 | + if self._enabled != enabled: |
1203 | + self._enabled = enabled |
1204 | + self.notify('enabled') |
1205 | diff --git a/softwareproperties/LivepatchSnap.py b/softwareproperties/LivepatchSnap.py |
1206 | new file mode 100644 |
1207 | index 0000000..32b76be |
1208 | --- /dev/null |
1209 | +++ b/softwareproperties/LivepatchSnap.py |
1210 | @@ -0,0 +1,135 @@ |
1211 | +# |
1212 | +# Copyright (c) 2019 Canonical |
1213 | +# |
1214 | +# Authors: |
1215 | +# Andrea Azzarone <andrea.azzarone@canonical.com> |
1216 | +# |
1217 | +# This program is free software; you can redistribute it and/or |
1218 | +# modify it under the terms of the GNU General Public License as |
1219 | +# published by the Free Software Foundation; either version 2 of the |
1220 | +# License, or (at your option) any later version. |
1221 | +# |
1222 | +# This program is distributed in the hope that it will be useful, |
1223 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1224 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1225 | +# GNU General Public License for more details. |
1226 | +# |
1227 | +# You should have received a copy of the GNU General Public License |
1228 | +# along with this program; if not, write to the Free Software |
1229 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
1230 | +# USA |
1231 | + |
1232 | +from gettext import gettext as _ |
1233 | +import logging |
1234 | + |
1235 | +import gi |
1236 | +from gi.repository import Gio, GLib |
1237 | + |
1238 | +try: |
1239 | + gi.require_version('Snapd', '1') |
1240 | + from gi.repository import Snapd |
1241 | +except(ImportError, ValueError): |
1242 | + pass |
1243 | + |
1244 | + |
1245 | +class LivepatchSnap(object): |
1246 | + |
1247 | + # Constants |
1248 | + SNAP_NAME = 'canonical-livepatch' |
1249 | + |
1250 | + # Public API |
1251 | + def __init__(self): |
1252 | + self._snapd_client = Snapd.Client() |
1253 | + self._cancellable = Gio.Cancellable() |
1254 | + |
1255 | + def get_status(self): |
1256 | + """ Get the status of canonical-livepatch snap. |
1257 | + |
1258 | + Returns: |
1259 | + Snapd.SnapStatus.Enun: An enum indicating the status of the snap. |
1260 | + """ |
1261 | + snap = self._get_raw_snap() |
1262 | + return snap.get_status() if snap else Snapd.SnapStatus.UNKNOWN |
1263 | + |
1264 | + def enable_or_install(self): |
1265 | + """Enable or install canonical-livepatch snap. |
1266 | + |
1267 | + Returns: |
1268 | + (True, '') if successful, (False, error_message) otherwise. |
1269 | + """ |
1270 | + status = self.get_status() |
1271 | + |
1272 | + if status == Snapd.SnapStatus.ACTIVE: |
1273 | + logging.warning('{} snap is already active'.format(self.SNAP_NAME)) |
1274 | + return True, '' |
1275 | + elif status == Snapd.SnapStatus.AVAILABLE: |
1276 | + return self._install() |
1277 | + elif status == Snapd.SnapStatus.INSTALLED: |
1278 | + return self._enable() |
1279 | + else: |
1280 | + logging.warning('{} snap is in an unknown state'.format(self.SNAP_NAME)) |
1281 | + return False, _('Canonical Livepatch snap cannot be installed.') |
1282 | + |
1283 | + # Private methods |
1284 | + def _get_raw_snap(self): |
1285 | + """Get the Sanpd.Snap raw object of the canonical-livepatch snapd. |
1286 | + |
1287 | + Returns: |
1288 | + Sanpd.Snap if successful, None otherwise. |
1289 | + """ |
1290 | + try: |
1291 | + snap = self._snapd_client.get_snap_sync( |
1292 | + name=self.SNAP_NAME, |
1293 | + cancellable=self._cancellable) |
1294 | + except GLib.Error as e: |
1295 | + logging.debug('Snapd.Client.get_snap_sync failed: {}'.format(e.message)) |
1296 | + snap = None |
1297 | + |
1298 | + if snap: |
1299 | + return snap |
1300 | + |
1301 | + try: |
1302 | + (snaps, ignored) = self._snapd_client.find_sync( |
1303 | + flags=Snapd.FindFlags.MATCH_NAME, |
1304 | + query=self.SNAP_NAME, |
1305 | + cancellable=self._cancellable) |
1306 | + snap = snaps[0] |
1307 | + except GLib.Error as e: |
1308 | + logging.debug('Snapd.Client.find_sync failed: {}'.format(e.message)) |
1309 | + |
1310 | + return snap |
1311 | + |
1312 | + def _install(self): |
1313 | + """Install canonical-livepatch snap. |
1314 | + |
1315 | + Returns: |
1316 | + (True, '') if successful, (False, error_message) otherwise. |
1317 | + """ |
1318 | + assert self.get_status() == Snapd.SnapStatus.AVAILABLE |
1319 | + |
1320 | + try: |
1321 | + self._snapd_client.install2_sync( |
1322 | + flags=Snapd.InstallFlags.NONE, |
1323 | + name=self.SNAP_NAME, |
1324 | + cancellable=self._cancellable) |
1325 | + except GLib.Error as e: |
1326 | + return False, _('Canonical Livepatch snap cannot be installed: {}'.format(e.message)) |
1327 | + else: |
1328 | + return True, '' |
1329 | + |
1330 | + def _enable(self): |
1331 | + """Enable the canonical-livepatch snap. |
1332 | + |
1333 | + Returns: |
1334 | + (True, '') if successful, (False, error_message) otherwise. |
1335 | + """ |
1336 | + assert self.get_status() == Snapd.SnapStatus.INSTALLED |
1337 | + |
1338 | + try: |
1339 | + self._snapd_client.enable_sync( |
1340 | + name=self.SNAP_NAME, |
1341 | + cancellable=self._cancellable) |
1342 | + except GLib.Error as e: |
1343 | + return False, _('Canonical Livepatch snap cannot be enabled: {}'.format(e.message)) |
1344 | + else: |
1345 | + return True, '' |
1346 | diff --git a/softwareproperties/SoftwareProperties.py b/softwareproperties/SoftwareProperties.py |
1347 | index 20d20e9..46ed68c 100644 |
1348 | --- a/softwareproperties/SoftwareProperties.py |
1349 | +++ b/softwareproperties/SoftwareProperties.py |
1350 | @@ -32,7 +32,6 @@ import re |
1351 | import os |
1352 | import glob |
1353 | import shutil |
1354 | -import subprocess |
1355 | import threading |
1356 | import atexit |
1357 | import tempfile |
1358 | @@ -65,15 +64,8 @@ from . import shortcuts |
1359 | from . import ppa |
1360 | from . import cloudarchive |
1361 | |
1362 | -import gi |
1363 | from gi.repository import Gio |
1364 | |
1365 | -try: |
1366 | - gi.require_version('Snapd', '1') |
1367 | - from gi.repository import Snapd |
1368 | -except (ImportError, ValueError): |
1369 | - pass |
1370 | - |
1371 | _SHORTCUT_FACTORIES = [ |
1372 | ppa.shortcut_handler, |
1373 | cloudarchive.shortcut_handler, |
1374 | @@ -100,9 +92,6 @@ class SoftwareProperties(object): |
1375 | RELEASE_UPGRADES_NEVER : 'never', |
1376 | } |
1377 | |
1378 | - # file to monitor canonical-livepatch status |
1379 | - LIVEPATCH_RUNNING_FILE = '/var/snap/canonical-livepatch/common/machine-token' |
1380 | - |
1381 | def __init__(self, datadir=None, options=None, rootdir="/"): |
1382 | """ Provides the core functionality to configure the used software |
1383 | repositories, the corresponding authentication keys and |
1384 | @@ -874,147 +863,6 @@ class SoftwareProperties(object): |
1385 | except: |
1386 | return False |
1387 | |
1388 | - # |
1389 | - # Livepatch |
1390 | - # |
1391 | - def init_snapd(self): |
1392 | - self.snapd_client = Snapd.Client() |
1393 | - |
1394 | - def get_livepatch_snap_async(self, callback): |
1395 | - assert self.snapd_client |
1396 | - self.snapd_client.list_one_async('canonical-livepatch', |
1397 | - self.cancellable, |
1398 | - self.on_list_one_ready_cb, |
1399 | - callback) |
1400 | - |
1401 | - def on_list_one_ready_cb(self, source_object, result, user_data): |
1402 | - callback = user_data |
1403 | - try: |
1404 | - snap = source_object.list_one_finish(result) |
1405 | - except: |
1406 | - snap = None |
1407 | - if snap: |
1408 | - if callback: |
1409 | - callback(snap) |
1410 | - return |
1411 | - else: |
1412 | - assert self.snapd_client |
1413 | - self.snapd_client.find_async(Snapd.FindFlags.MATCH_NAME, |
1414 | - 'canonical-livepatch', |
1415 | - self.cancellable, |
1416 | - self.on_find_ready_cb, |
1417 | - callback) |
1418 | - |
1419 | - def on_find_ready_cb(self, source_object, result, user_data): |
1420 | - callback = user_data |
1421 | - try: |
1422 | - snaps = source_object.find_finish(result)[0] |
1423 | - except: |
1424 | - snaps = list() |
1425 | - snap = snaps[0] if len(snaps) else None |
1426 | - if callback: |
1427 | - callback(snap) |
1428 | - |
1429 | - def get_livepatch_snap_status(self, snap): |
1430 | - if snap is None: |
1431 | - return Snapd.SnapStatus.UNKNOWN |
1432 | - return snap.get_status() |
1433 | - |
1434 | - # glib-snapd does not keep track of the status of the snap. Use this decorator |
1435 | - # to make it easy to write async functions that will always have an updated |
1436 | - # snap object. |
1437 | - def require_livepatch_snap(func): |
1438 | - def get_livepatch_snap_and_call(*args, **kwargs): |
1439 | - return args[0].get_livepatch_snap_async(lambda snap: func(snap=snap, *args, **kwargs)) |
1440 | - return get_livepatch_snap_and_call |
1441 | - |
1442 | - def is_livepatch_enabled(self): |
1443 | - file = Gio.File.new_for_path(path=self.LIVEPATCH_RUNNING_FILE) |
1444 | - return file.query_exists(None) |
1445 | - |
1446 | - @require_livepatch_snap |
1447 | - def set_livepatch_enabled_async(self, enabled, token, callback, snap=None): |
1448 | - status = self.get_livepatch_snap_status(snap) |
1449 | - if status == Snapd.SnapStatus.UNKNOWN: |
1450 | - if callback: |
1451 | - callback(True, _("Canonical Livepatch snap cannot be installed.")) |
1452 | - elif status == Snapd.SnapStatus.ACTIVE: |
1453 | - if enabled: |
1454 | - error = self.enable_livepatch_service(token) |
1455 | - else: |
1456 | - error = self.disable_livepatch_service() |
1457 | - if callback: |
1458 | - callback(len(error) > 0, error) |
1459 | - elif status == Snapd.SnapStatus.INSTALLED: |
1460 | - if enabled: |
1461 | - self.snapd_client.enable_async(name='canonical-livepatch', |
1462 | - cancellable=self.cancellable, |
1463 | - callback=self.livepatch_enable_snap_cb, |
1464 | - user_data=(callback, token)) |
1465 | - else: |
1466 | - if callback: |
1467 | - callback(False, "") |
1468 | - elif status == Snapd.SnapStatus.AVAILABLE: |
1469 | - if enabled: |
1470 | - self.snapd_client.install_async(name='canonical-livepatch', |
1471 | - cancellable=self.cancellable, |
1472 | - callback=self.livepatch_install_snap_cb, |
1473 | - user_data=(callback, token)) |
1474 | - else: |
1475 | - if callback: |
1476 | - callback(False, "") |
1477 | - |
1478 | - def livepatch_enable_snap_cb(self, source_object, result, user_data): |
1479 | - (callback, token) = user_data |
1480 | - try: |
1481 | - if source_object.enable_finish(result): |
1482 | - error = self.enable_livepatch_service(token) |
1483 | - if callback: |
1484 | - callback(len(error) > 0, error) |
1485 | - except Exception: |
1486 | - if callback: |
1487 | - callback(True, _("Canonical Livepatch snap cannot be enabled.")) |
1488 | - |
1489 | - def livepatch_install_snap_cb(self, source_object, result, user_data): |
1490 | - (callback, token) = user_data |
1491 | - try: |
1492 | - if source_object.install_finish(result): |
1493 | - error = self.enable_livepatch_service(token) |
1494 | - if callback: |
1495 | - callback(len(error) > 0, error) |
1496 | - except Exception: |
1497 | - if callback: |
1498 | - callback(True, _("Canonical Livepatch snap cannot be installed.")) |
1499 | - |
1500 | - def enable_livepatch_service(self, token): |
1501 | - generic_error = _("Canonical Livepatch cannot be enabled.") |
1502 | - |
1503 | - if self.is_livepatch_enabled(): |
1504 | - return "" |
1505 | - |
1506 | - try: |
1507 | - subprocess.check_output(['/snap/bin/canonical-livepatch', 'enable', token], stderr=subprocess.STDOUT) |
1508 | - return "" |
1509 | - except subprocess.CalledProcessError as e: |
1510 | - return e.output if e.output else generic_error |
1511 | - except: |
1512 | - return generic_error |
1513 | - |
1514 | - |
1515 | - def disable_livepatch_service(self): |
1516 | - generic_error = _("Canonical Livepatch cannot be disabled.") |
1517 | - |
1518 | - if not self.is_livepatch_enabled(): |
1519 | - return "" |
1520 | - |
1521 | - try: |
1522 | - subprocess.check_output(['/snap/bin/canonical-livepatch', 'disable'], stderr=subprocess.STDOUT) |
1523 | - return "" |
1524 | - except subprocess.CalledProcessError as e: |
1525 | - return e.output if e.output else generic_error |
1526 | - except: |
1527 | - return generic_error |
1528 | - |
1529 | def shortcut_handler(shortcut): |
1530 | for factory in _SHORTCUT_FACTORIES: |
1531 | ret = factory(shortcut) |
1532 | diff --git a/softwareproperties/dbus/SoftwarePropertiesDBus.py b/softwareproperties/dbus/SoftwarePropertiesDBus.py |
1533 | index ace8733..2653cd8 100644 |
1534 | --- a/softwareproperties/dbus/SoftwarePropertiesDBus.py |
1535 | +++ b/softwareproperties/dbus/SoftwarePropertiesDBus.py |
1536 | @@ -25,10 +25,12 @@ import logging |
1537 | import subprocess |
1538 | import tempfile |
1539 | import sys |
1540 | +import threading |
1541 | |
1542 | from aptsources.sourceslist import SourceEntry |
1543 | |
1544 | from dbus.mainloop.glib import DBusGMainLoop |
1545 | +from softwareproperties.LivepatchService import LivepatchService |
1546 | from softwareproperties.SoftwareProperties import SoftwareProperties |
1547 | |
1548 | DBUS_BUS_NAME = 'com.ubuntu.SoftwareProperties' |
1549 | @@ -61,7 +63,7 @@ class SoftwarePropertiesDBus(dbus.service.Object, SoftwareProperties): |
1550 | self.enforce_polkit = True |
1551 | logging.debug("waiting for connections") |
1552 | |
1553 | - self.init_snapd() |
1554 | + self._livepatch_service = LivepatchService() |
1555 | |
1556 | # override set_modified_sourceslist to emit a signal |
1557 | def save_sourceslist(self): |
1558 | @@ -324,9 +326,13 @@ class SoftwarePropertiesDBus(dbus.service.Object, SoftwareProperties): |
1559 | sender_keyword="sender", connection_keyword="conn", |
1560 | in_signature='bs', out_signature='bs', async_callbacks=('reply_handler', 'error_handler')) |
1561 | def SetLivepatchEnabled(self, enabled, token, reply_handler, error_handler, sender=None, conn=None): |
1562 | + def enable_thread_func(): |
1563 | + ret = self._livepatch_service.set_enabled(enabled, token) |
1564 | + GLib.idle_add(lambda: reply_handler(*ret)) |
1565 | + |
1566 | self._check_policykit_privilege( |
1567 | sender, conn, "com.ubuntu.softwareproperties.applychanges") |
1568 | - self.set_livepatch_enabled_async(enabled, token, reply_handler) |
1569 | + threading.Thread(target=enable_thread_func).start() |
1570 | |
1571 | # helper from jockey |
1572 | def _check_policykit_privilege(self, sender, conn, privilege): |
1573 | diff --git a/softwareproperties/gtk/DialogAuth.py b/softwareproperties/gtk/DialogAuth.py |
1574 | index d62e518..421daef 100644 |
1575 | --- a/softwareproperties/gtk/DialogAuth.py |
1576 | +++ b/softwareproperties/gtk/DialogAuth.py |
1577 | @@ -19,6 +19,7 @@ |
1578 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
1579 | # USA |
1580 | |
1581 | +from enum import IntEnum |
1582 | import os |
1583 | |
1584 | from gettext import gettext as _ |
1585 | @@ -28,9 +29,13 @@ from softwareproperties.gtk.utils import ( |
1586 | |
1587 | import gi |
1588 | gi.require_version('Goa', '1.0') |
1589 | -from gi.repository import Gio, GLib, Goa, GObject, Gtk |
1590 | +from gi.repository import Gio, GLib, Goa, Gtk |
1591 | import logging |
1592 | |
1593 | +class Column(IntEnum): |
1594 | + ID = 0 |
1595 | + MAIL = 1 |
1596 | + ACCOUNT = 2 |
1597 | |
1598 | class DialogAuth: |
1599 | |
1600 | @@ -38,68 +43,128 @@ class DialogAuth: |
1601 | """setup up the gtk dialog""" |
1602 | self.parent = parent |
1603 | |
1604 | - setup_ui(self, os.path.join(datadir, "gtkbuilder", |
1605 | - "dialog-auth.ui"), domain="software-properties") |
1606 | - self.label_title.set_max_width_chars(50) |
1607 | + setup_ui(self, os.path.join(datadir, "gtkbuilder", "dialog-auth.ui"), |
1608 | + domain="software-properties") |
1609 | |
1610 | self.dialog = self.dialog_auth |
1611 | - self.dialog.use_header_bar = True |
1612 | + self.dialog.set_title('') |
1613 | + self.dialog.set_deletable(False) |
1614 | self.dialog.set_transient_for(parent) |
1615 | |
1616 | - self.listboxrow_new_account.account = None |
1617 | + self.button_continue.grab_focus() |
1618 | |
1619 | self.account = None |
1620 | self.dispose_on_new_account = False |
1621 | self.goa_client = Goa.Client.new_sync(None) |
1622 | |
1623 | - self.listbox_accounts.connect('row-activated', self._listbox_accounts_row_activated_cb) |
1624 | + self._setup_model() |
1625 | + self._check_ui(select=False) |
1626 | |
1627 | # Be ready to other accounts |
1628 | self.goa_client.connect('account-added', self._account_added_cb) |
1629 | self.goa_client.connect('account-removed', self._account_removed_cb) |
1630 | |
1631 | - self._setup_listbox_accounts() |
1632 | - self._check_ui() |
1633 | - |
1634 | def run(self): |
1635 | res = self.dialog.run() |
1636 | self.dialog.hide() |
1637 | return res |
1638 | |
1639 | - def _check_ui(self): |
1640 | - rows = self.listbox_accounts.get_children() |
1641 | - has_accounts = len(rows) > 1 |
1642 | - |
1643 | - if has_accounts: |
1644 | - title = _('To continue choose an Ubuntu Single Sign-On account.') |
1645 | - new_account = _('Use another account…') |
1646 | + def _setup_model(self): |
1647 | + for obj in self.goa_client.get_accounts(): |
1648 | + self._add_account(obj.get_account(), select=False) |
1649 | + |
1650 | + def _set_header(self, label): |
1651 | + self.label_header.set_markup( |
1652 | + "<span size='larger' weight='bold'>%s</span>" % label) |
1653 | + |
1654 | + def _check_ui(self, select): |
1655 | + naccounts = len(self.liststore_account) |
1656 | + |
1657 | + if naccounts == 0: |
1658 | + self._set_header( |
1659 | + _('To use Livepatch, you need to use an Ubuntu One account.')) |
1660 | + self.combobox_account.set_visible(False) |
1661 | + self.label_account.set_visible(False) |
1662 | + self.button_add_another.set_visible(False) |
1663 | + self.button_continue.set_label(_('Sign In / Register…')) |
1664 | + elif naccounts == 1: |
1665 | + self._set_header( |
1666 | + _('To use Livepatch, you need to use your Ubuntu One account.')) |
1667 | + self.combobox_account.set_visible(False) |
1668 | + self.label_account.set_visible(True) |
1669 | + self.label_account.set_text(self.liststore_account[0][Column.MAIL]) |
1670 | + self.button_add_another.set_visible(True) |
1671 | + self.button_continue.set_label(_('Continue')) |
1672 | else: |
1673 | - title = _('To continue you need an Ubuntu Single Sign-On account.') |
1674 | - new_account = _('Sign In…') |
1675 | + self._set_header( |
1676 | + _('To use Livepatch, you need to use an Ubuntu One account.')) |
1677 | + self.button_add_another.set_visible(True) |
1678 | + self.combobox_account.set_visible(True) |
1679 | + self.label_account.set_visible(False) |
1680 | + self.button_continue.set_label(_('Use')) |
1681 | + if select: |
1682 | + self.combobox_account.set_active(naccounts-1) |
1683 | + elif self.combobox_account.get_active() == -1: |
1684 | + self.combobox_account.set_active(0) |
1685 | + |
1686 | + def _ignore_account(self, account): |
1687 | + return account.props.provider_type != 'ubuntusso' |
1688 | + |
1689 | + def _get_account_iter(self, account): |
1690 | + row = self.liststore_account.get_iter_first() |
1691 | + while row is not None: |
1692 | + account_id = self.liststore_account.get_value(row, Column.ID) |
1693 | + if account_id == account.props.id: |
1694 | + return row |
1695 | + row = self.liststore_account.iter_next(row) |
1696 | + return None |
1697 | + |
1698 | + def _add_account(self, account, select): |
1699 | + if self._ignore_account(account): |
1700 | + return |
1701 | |
1702 | - self.label_title.set_text(title) |
1703 | - self.label_new_account.set_markup('<b>{}</b>'.format(new_account)) |
1704 | + account_iter = self._get_account_iter(account) |
1705 | + if account_iter is not None: |
1706 | + return |
1707 | |
1708 | - def _setup_listbox_accounts(self): |
1709 | - for obj in self.goa_client.get_accounts(): |
1710 | - account = obj.get_account() |
1711 | - if self._is_account_supported(account): |
1712 | - self._add_account(account) |
1713 | + account_iter = self.liststore_account.append() |
1714 | + self.liststore_account.set(account_iter, |
1715 | + [Column.ID, Column.MAIL, Column.ACCOUNT], |
1716 | + [account.props.id, account.props.presentation_identity, account]) |
1717 | + self._check_ui(select) |
1718 | |
1719 | - def _is_account_supported(self, account): |
1720 | - return account.props.provider_type == 'ubuntusso' |
1721 | + def _remove_account(self, account): |
1722 | + if self._ignore_account(account): |
1723 | + return |
1724 | + |
1725 | + account_iter = self._get_account_iter(account) |
1726 | + if account_iter is None: |
1727 | + return |
1728 | |
1729 | - def _add_account(self, account): |
1730 | - row = self._create_row(account) |
1731 | - self.listbox_accounts.prepend(row) |
1732 | - self._check_ui() |
1733 | + self.liststore_account.remove(account_iter) |
1734 | + self._check_ui(select=False) |
1735 | |
1736 | - def _remove_account(self, account): |
1737 | - for row in self.listbox_accounts.get_children(): |
1738 | - if row.account == account: |
1739 | - row.destroy() |
1740 | - self._check_ui() |
1741 | - break |
1742 | + def _response_if_valid(self, account): |
1743 | + def cb(source, res, data): |
1744 | + try: |
1745 | + source.call_ensure_credentials_finish(res) |
1746 | + valid = True |
1747 | + except GLib.Error as e: |
1748 | + logging.warning("call_ensure_credentials_finish exception: %s", |
1749 | + e.message) |
1750 | + valid = False |
1751 | + |
1752 | + if not valid: |
1753 | + try: |
1754 | + self._spawn_goa_with_args(account.props.id, None) |
1755 | + except GLib.Error as e: |
1756 | + logging.warning ('Failed to spawn gnome-control-center: %s', |
1757 | + e.message) |
1758 | + else: |
1759 | + self.account = account |
1760 | + self.dialog.response(Gtk.ResponseType.OK) |
1761 | + |
1762 | + account.call_ensure_credentials(None, cb, None) |
1763 | |
1764 | def _build_dbus_params(self, action, arg): |
1765 | builder = GLib.VariantBuilder.new(GLib.VariantType.new('av')) |
1766 | @@ -118,7 +183,8 @@ class DialogAuth: |
1767 | v = GLib.Variant.new_variant(s) |
1768 | builder.add_value(v) |
1769 | |
1770 | - array = GLib.Variant.new_tuple(GLib.Variant.new_string('online-accounts'), builder.end()) |
1771 | + array = GLib.Variant.new_tuple( |
1772 | + GLib.Variant.new_string('online-accounts'), builder.end()) |
1773 | array = GLib.Variant.new_variant(array) |
1774 | |
1775 | param = GLib.Variant.new_tuple( |
1776 | @@ -135,94 +201,43 @@ class DialogAuth: |
1777 | 'org.gtk.Actions', None) |
1778 | |
1779 | param = self._build_dbus_params(action, arg) |
1780 | - timeout = 10*60*1000 # 10 minutes should be enough to create an account |
1781 | - proxy.call_sync('Activate', param, Gio.DBusCallFlags.NONE, timeout, None) |
1782 | - |
1783 | - def _create_row(self, account): |
1784 | - identity = account.props.presentation_identity |
1785 | - provider_name = account.props.provider_name |
1786 | - |
1787 | - row = Gtk.ListBoxRow.new() |
1788 | - row.show() |
1789 | - |
1790 | - hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 6) |
1791 | - hbox.set_hexpand(True) |
1792 | - hbox.show() |
1793 | - |
1794 | - image = Gtk.Image.new_from_icon_name('avatar-default', Gtk.IconSize.DIALOG) |
1795 | - image.set_pixel_size(48) |
1796 | - image.show() |
1797 | - hbox.pack_start(image, False, False, 0) |
1798 | - |
1799 | - vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 2) |
1800 | - vbox.set_valign(Gtk.Align.CENTER) |
1801 | - vbox.show() |
1802 | - hbox.pack_start(vbox, False, False, 0) |
1803 | - |
1804 | - if identity: |
1805 | - ilabel = Gtk.Label.new() |
1806 | - ilabel.set_halign(Gtk.Align.START) |
1807 | - ilabel.set_markup('<b>{}</b>'.format(identity)) |
1808 | - ilabel.show() |
1809 | - vbox.pack_start(ilabel, True, True, 0) |
1810 | - |
1811 | - if provider_name: |
1812 | - plabel = Gtk.Label.new() |
1813 | - plabel.set_halign(Gtk.Align.START) |
1814 | - plabel.set_markup('<small><span foreground=\"#555555\">{}</span></small>'.format(provider_name)) |
1815 | - plabel.show() |
1816 | - vbox.pack_start(plabel, True, True, 0) |
1817 | - |
1818 | - warning_icon = Gtk.Image.new_from_icon_name('dialog-warning-symbolic', Gtk.IconSize.BUTTON) |
1819 | - warning_icon.set_no_show_all(True) |
1820 | - warning_icon.set_margin_end (15) |
1821 | - hbox.pack_end(warning_icon, False, False, 0) |
1822 | - |
1823 | - row.add(hbox) |
1824 | - |
1825 | - account.bind_property('attention-needed', warning_icon, 'visible', |
1826 | - GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE) |
1827 | - |
1828 | - row.account = account |
1829 | - return row |
1830 | - |
1831 | - # Signals handlers |
1832 | - def _listbox_accounts_row_activated_cb(self, listbox, row): |
1833 | - account = row.account |
1834 | - |
1835 | - if account is None: |
1836 | - # TODO (azzar1): there is no easy way to put this to false |
1837 | - # if the user close the windows without adding an account. |
1838 | - # We need to discuss with goa's upstream to support such usercases |
1839 | - try: |
1840 | - self._spawn_goa_with_args('add', 'ubuntusso') |
1841 | - self.dispose_on_new_account = True |
1842 | - except GLib.Error as e: |
1843 | - logging.warning ('Failed to spawing gnome-control-center: %s', e.message) |
1844 | - else: |
1845 | - if account.props.attention_needed: |
1846 | - try: |
1847 | - self._spawn_goa_with_args(account.props.id, None) |
1848 | - except GLib.Error as e: |
1849 | - logging.warning ('Failed to spawing gnome-control-center: %s', e.message) |
1850 | - else: |
1851 | - self.account = account |
1852 | - self.dialog.response(Gtk.ResponseType.OK) |
1853 | + proxy.call_sync('Activate', param, Gio.DBusCallFlags.NONE, -1, None) |
1854 | |
1855 | def _account_added_cb(self, goa_client, goa_object): |
1856 | account = goa_object.get_account() |
1857 | - if not self._is_account_supported(account): |
1858 | + if self._ignore_account(account): |
1859 | return |
1860 | if not self.dispose_on_new_account: |
1861 | - self._add_account(account) |
1862 | + self._add_account(account, True) |
1863 | else: |
1864 | - self.account = account |
1865 | - self.dialog.response(Gtk.ResponseType.OK) |
1866 | + self._response_if_valid(account) |
1867 | |
1868 | def _account_removed_cb(self, goa_client, goa_object): |
1869 | account = goa_object.get_account() |
1870 | - if self._is_account_supported(account): |
1871 | + if not self._ignore_account(account): |
1872 | self._remove_account(account) |
1873 | |
1874 | + def _button_add_another_clicked_cb(self, button): |
1875 | + try: |
1876 | + # There is no easy way to put this to false if the user close the |
1877 | + # windows without adding an account. |
1878 | + self._spawn_goa_with_args('add', 'ubuntusso') |
1879 | + self.dispose_on_new_account = True |
1880 | + except GLib.Error as e: |
1881 | + logging.warning ('Failed to spawn control-center: %s', e.message) |
1882 | + |
1883 | + def _button_cancel_clicked_cb(self, button): |
1884 | + self.dialog.response(Gtk.ResponseType.CANCEL) |
1885 | + |
1886 | + def _button_continue_clicked_cb(self, button): |
1887 | + naccounts = len(self.liststore_account) |
1888 | |
1889 | + account = None |
1890 | + if naccounts >= 1: |
1891 | + active_index = self.combobox_account.get_active() |
1892 | + account = self.liststore_account[active_index][Column.ACCOUNT] |
1893 | |
1894 | + if account is None: |
1895 | + self._button_add_another_clicked_cb(self.button_add_another) |
1896 | + else: |
1897 | + self._response_if_valid(account) |
1898 | diff --git a/softwareproperties/gtk/DialogLivepatchError.py b/softwareproperties/gtk/DialogLivepatchError.py |
1899 | index 2d6688c..e27bb69 100644 |
1900 | --- a/softwareproperties/gtk/DialogLivepatchError.py |
1901 | +++ b/softwareproperties/gtk/DialogLivepatchError.py |
1902 | @@ -1,5 +1,5 @@ |
1903 | # |
1904 | -# Copyright (c) 2017-2018 Canonical |
1905 | +# Copyright (c) 2017-2019 Canonical |
1906 | # |
1907 | # Authors: |
1908 | # Andrea Azzarone <andrea.azzarone@canonical.com> |
1909 | @@ -21,6 +21,8 @@ |
1910 | |
1911 | import os |
1912 | |
1913 | +from gettext import gettext as _ |
1914 | + |
1915 | from softwareproperties.gtk.utils import ( |
1916 | setup_ui, |
1917 | ) |
1918 | @@ -31,20 +33,27 @@ class DialogLivepatchError: |
1919 | RESPONSE_SETTINGS = 100 |
1920 | RESPONSE_IGNORE = 101 |
1921 | |
1922 | + primary = _("Sorry, there's been a problem with setting up Canonical Livepatch.") |
1923 | + |
1924 | def __init__(self, parent, datadir): |
1925 | """setup up the gtk dialog""" |
1926 | self.parent = parent |
1927 | |
1928 | - setup_ui(self, os.path.join(datadir, "gtkbuilder", |
1929 | - "dialog-livepatch-error.ui"), domain="software-properties") |
1930 | + setup_ui( |
1931 | + self, |
1932 | + os.path.join(datadir, "gtkbuilder", "dialog-livepatch-error.ui"), |
1933 | + domain="software-properties") |
1934 | |
1935 | self.dialog = self.messagedialog_livepatch |
1936 | - self.dialog.use_header_bar = True |
1937 | self.dialog.set_transient_for(parent) |
1938 | |
1939 | def run(self, error, show_settings_button): |
1940 | - self.dialog.format_secondary_markup( |
1941 | - "The error was: \"%s\"" % error.strip()) |
1942 | + p = "<span weight=\"bold\" size=\"larger\">{}</span>".format(self.primary) |
1943 | + self.label_primary.set_markup(p) |
1944 | + |
1945 | + textbuffer = self.treeview_message.get_buffer() |
1946 | + textbuffer.set_text(error) |
1947 | + |
1948 | self.button_settings.set_visible(show_settings_button) |
1949 | res = self.dialog.run() |
1950 | self.dialog.hide() |
1951 | diff --git a/softwareproperties/gtk/LivepatchPage.py b/softwareproperties/gtk/LivepatchPage.py |
1952 | new file mode 100644 |
1953 | index 0000000..6c3ace9 |
1954 | --- /dev/null |
1955 | +++ b/softwareproperties/gtk/LivepatchPage.py |
1956 | @@ -0,0 +1,375 @@ |
1957 | +# |
1958 | +# Copyright (c) 2019 Canonical |
1959 | +# |
1960 | +# Authors: |
1961 | +# Andrea Azzarone <andrea.azzarone@canonical.com> |
1962 | +# |
1963 | +# This program is free software; you can redistribute it and/or |
1964 | +# modify it under the terms of the GNU General Public License as |
1965 | +# published by the Free Software Foundation; either version 2 of the |
1966 | +# License, or (at your option) any later version. |
1967 | +# |
1968 | +# This program is distributed in the hope that it will be useful, |
1969 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1970 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1971 | +# GNU General Public License for more details. |
1972 | +# |
1973 | +# You should have received a copy of the GNU General Public License |
1974 | +# along with this program; if not, write to the Free Software |
1975 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
1976 | +# USA |
1977 | + |
1978 | +import datetime |
1979 | +import gettext |
1980 | +from gettext import gettext as _ |
1981 | +import gi |
1982 | +gi.require_version("Gtk", "3.0") |
1983 | +from gi.repository import GLib, GObject, Gtk |
1984 | +import logging |
1985 | + |
1986 | +from softwareproperties.GoaAuth import GoaAuth |
1987 | +from softwareproperties.LivepatchService import ( |
1988 | + LivepatchService, |
1989 | + LivepatchAvailability) |
1990 | +from .DialogAuth import DialogAuth |
1991 | +from .DialogLivepatchError import DialogLivepatchError |
1992 | + |
1993 | + |
1994 | +class LivepatchPage(object): |
1995 | + |
1996 | + # Constants |
1997 | + COMMON_ISSUE_URL = 'https://wiki.ubuntu.com/Kernel/Livepatch#CommonIssues' |
1998 | + GENERIC_ERR_MSG = _('Canonical Livepatch has experienced an internal error.' |
1999 | + ' Please refer to {} for further information.'.format(COMMON_ISSUE_URL)) |
2000 | + |
2001 | + def __init__(self, parent): |
2002 | + self._parent = parent |
2003 | + |
2004 | + self._timeout_handler = -1 |
2005 | + self._waiting_livepatch_response = False |
2006 | + |
2007 | + self._lps = LivepatchService() |
2008 | + self._auth = GoaAuth() |
2009 | + |
2010 | + # Connect signals |
2011 | + self._lps.connect( |
2012 | + 'notify::availability', self._lps_availability_changed_cb) |
2013 | + self._lps.connect( |
2014 | + 'notify::enabled', self._lps_enabled_changed_cb) |
2015 | + self._auth.connect( |
2016 | + 'notify', self._auth_changed_cb) |
2017 | + self._state_set_handler = self._parent.switch_livepatch.connect( |
2018 | + 'state-set', self._switch_state_set_cb) |
2019 | + self._parent.button_livepatch_login.connect( |
2020 | + 'clicked', self._button_livepatch_login_clicked_cb) |
2021 | + |
2022 | + self._lps.trigger_availability_check() |
2023 | + |
2024 | + @property |
2025 | + def waiting_livepatch_response(self): |
2026 | + return self._waiting_livepatch_response |
2027 | + |
2028 | + # Private methods |
2029 | + def _trigger_ui_update(self, skip=False, error_message=None): |
2030 | + """Trigger the update of every single user interface component according |
2031 | + to the current state. |
2032 | + |
2033 | + Args: |
2034 | + skip (bool): whether to trigger the update after a small timeout. |
2035 | + Defaults to False. |
2036 | + error_message (str): error message to display. Defaults to None. |
2037 | + """ |
2038 | + def do_ui_update(): |
2039 | + self._timeout_handler = -1 |
2040 | + |
2041 | + self._update_switch() |
2042 | + self._update_spinner() |
2043 | + self._update_switch_label() |
2044 | + self._update_auth_button() |
2045 | + self._update_stack(error_message) |
2046 | + |
2047 | + return False |
2048 | + |
2049 | + if self._timeout_handler > 0: |
2050 | + GObject.source_remove(self._timeout_handler) |
2051 | + self._timeout_handler = -1 |
2052 | + |
2053 | + if skip: |
2054 | + do_ui_update() |
2055 | + else: |
2056 | + self._timeout_handler = GLib.timeout_add_seconds(2, do_ui_update) |
2057 | + |
2058 | + def _update_switch(self): |
2059 | + """Update the state of the on/off switch.""" |
2060 | + switch = self._parent.switch_livepatch |
2061 | + |
2062 | + availability = self._lps.props.availability |
2063 | + enabled = self._lps.props.enabled |
2064 | + logged = self._auth.logged |
2065 | + |
2066 | + switch.set_sensitive( |
2067 | + availability == LivepatchAvailability.TRUE and |
2068 | + (enabled or logged)) |
2069 | + |
2070 | + if self._waiting_livepatch_response: |
2071 | + return |
2072 | + |
2073 | + self._parent.switch_livepatch.handler_block(self._state_set_handler) |
2074 | + switch.set_state(switch.get_sensitive() and enabled) |
2075 | + self._parent.switch_livepatch.handler_unblock(self._state_set_handler) |
2076 | + |
2077 | + def _update_spinner(self): |
2078 | + """Update the state of the in-progress spinner.""" |
2079 | + spinner = self._parent.spinner_livepatch |
2080 | + availability = self._lps.props.availability |
2081 | + |
2082 | + spinner.set_visible(availability == LivepatchAvailability.CHECKING) |
2083 | + spinner.props.active = (availability == LivepatchAvailability.CHECKING) |
2084 | + |
2085 | + def _update_switch_label(self): |
2086 | + """Update the text of the label next to the on/off switch.""" |
2087 | + availability = self._lps.props.availability |
2088 | + logged = self._auth.logged |
2089 | + |
2090 | + if availability == LivepatchAvailability.CHECKING: |
2091 | + msg = _('Checking availability…') |
2092 | + elif availability == LivepatchAvailability.NO_CONNECTIVITY: |
2093 | + msg = _('Livepatch requires an Internet connection.') |
2094 | + elif availability == LivepatchAvailability.FALSE: |
2095 | + msg = _('Livepatch is not available for this system.') |
2096 | + else: |
2097 | + if self._parent.switch_livepatch.get_active(): |
2098 | + msg = _("Livepatch is on.") |
2099 | + elif not logged: |
2100 | + msg = _("To use Livepatch you need to sign in.") |
2101 | + else: |
2102 | + msg = _("Livepatch is off.") |
2103 | + |
2104 | + self._parent.label_livepatch_switch.set_label(msg) |
2105 | + |
2106 | + def _update_auth_button(self): |
2107 | + """Update the state and the label of the authentication button.""" |
2108 | + button = self._parent.button_livepatch_login |
2109 | + |
2110 | + availability = self._lps.props.availability |
2111 | + logged = self._auth.logged |
2112 | + |
2113 | + button.set_visible( |
2114 | + availability == LivepatchAvailability.TRUE and |
2115 | + not self._parent.switch_livepatch.get_active()) |
2116 | + button.set_label(_('Sign Out') if logged else _('Sign In…')) |
2117 | + |
2118 | + def _update_stack(self, error_message): |
2119 | + """Update the state of the stack. |
2120 | + |
2121 | + If livepatch is not available nothing will be shown, if an error |
2122 | + occurred an error message will be shown in a text view, otherwise the |
2123 | + current livepatch status (e.g. a list of CVE fixes) will be shown. |
2124 | + |
2125 | + Args: |
2126 | + error_message (str): error message to display. |
2127 | + """ |
2128 | + availability = self._lps.props.availability |
2129 | + availability_message = self._lps.props.availability_message |
2130 | + |
2131 | + has_error = ( |
2132 | + error_message is not None or |
2133 | + (availability == LivepatchAvailability.FALSE and |
2134 | + availability_message is not None)) |
2135 | + |
2136 | + if has_error: |
2137 | + self._parent.stack_livepatch.set_visible_child_name('page_livepatch_message') |
2138 | + self._parent.stack_livepatch.set_visible(True) |
2139 | + text_buffer = self._parent.textview_livepatch.get_buffer() |
2140 | + text_buffer.delete( |
2141 | + text_buffer.get_start_iter(), text_buffer.get_end_iter()) |
2142 | + text_buffer.insert_markup( |
2143 | + text_buffer.get_end_iter(), |
2144 | + error_message or availability_message, -1) |
2145 | + return |
2146 | + |
2147 | + if availability == LivepatchAvailability.CHECKING or not self._parent.switch_livepatch.get_active(): |
2148 | + self._parent.stack_livepatch.set_visible(False) |
2149 | + else: |
2150 | + self._update_status() |
2151 | + |
2152 | + def _format_timedelta(self, td): |
2153 | + days = td.days |
2154 | + hours = td.seconds // 3600 |
2155 | + minutes = td.seconds // 60 |
2156 | + |
2157 | + if days > 0: |
2158 | + return gettext.ngettext( |
2159 | + '({} day ago)', |
2160 | + '({} days ago)', |
2161 | + days).format(days) |
2162 | + elif hours > 0: |
2163 | + return gettext.ngettext( |
2164 | + '({} hour ago)', |
2165 | + '({} hours ago)', |
2166 | + hours).format(hours) |
2167 | + elif minutes > 0: |
2168 | + return gettext.ngettext( |
2169 | + '({} minute ago)', |
2170 | + '({} minutes ago)', |
2171 | + minutes).format(minutes) |
2172 | + else: |
2173 | + return '' |
2174 | + |
2175 | + def _datetime_to_str(self, dt): |
2176 | + gdt = GLib.DateTime.new_from_unix_utc(dt.timestamp()) |
2177 | + td = datetime.datetime.now(dt.tzinfo) - dt |
2178 | + return '{} {}'.format( |
2179 | + gdt.to_local().format('%x %H:%M'), |
2180 | + self._format_timedelta(td)) |
2181 | + |
2182 | + def _update_status(self): |
2183 | + """Populate the UI to reflect the Livepatch status""" |
2184 | + status = self._lps.get_status() |
2185 | + |
2186 | + if status is None: |
2187 | + if not self._waiting_livepatch_response: |
2188 | + self._trigger_ui_update(skip=True, error_message=_('Failed to retrieve Livepatch status.')) |
2189 | + return |
2190 | + |
2191 | + self._parent.stack_livepatch.set_visible_child_name('page_livepatch_status') |
2192 | + self._parent.stack_livepatch.set_visible(True) |
2193 | + |
2194 | + check_state = status['Status'][0]['Livepatch']['CheckState'] if status else None |
2195 | + state = status['Status'][0]['Livepatch']['State'] if status else None |
2196 | + |
2197 | + if check_state == 'check-failed': |
2198 | + self._trigger_ui_update(skip=True, error_message=self.GENERIC_ERR_MSG) |
2199 | + return |
2200 | + |
2201 | + if state in ['applied-with-bug', 'apply-failed', 'unknown']: |
2202 | + self._trigger_ui_update(skip=True, error_message=self.GENERIC_ERR_MSG) |
2203 | + return |
2204 | + |
2205 | + self._parent.label_livepatch_last_update.set_label( |
2206 | + _("Last check for updates: {}").format( |
2207 | + self._datetime_to_str(status['Last-Check']) if status else _('None yet'))) |
2208 | + |
2209 | + if state in ['unapplied', 'nothing-to-apply'] or state == None: |
2210 | + self._parent.label_livepatch_header.set_label(_('No updates currently applied.')) |
2211 | + self._parent.scrolledwindow_livepatch_fixes.set_visible(False) |
2212 | + elif state == 'applied': |
2213 | + self._parent.label_livepatch_header.set_label(_('Updates currently applied:')) |
2214 | + self._parent.scrolledwindow_livepatch_fixes.set_visible(True) |
2215 | + self._update_fixes(status['Status'][0]['Livepatch']['Fixes']) |
2216 | + else: |
2217 | + logging.warning('Livepatch status contains an invalid state: {}'.format(state)) |
2218 | + |
2219 | + if check_state == 'needs-check' or state == 'unapplied': |
2220 | + self._trigger_ui_update() |
2221 | + |
2222 | + def _update_fixes(self, fixes): |
2223 | + """Populate the UI to show the list of applied CVE fixes.""" |
2224 | + treeview = self._parent.treeview_livepatch |
2225 | + liststore = treeview.get_model() |
2226 | + liststore.clear() |
2227 | + |
2228 | + for fix in fixes: |
2229 | + fix_iter = liststore.append() |
2230 | + liststore.set(fix_iter, [0], [self._format_fix(fix)]) |
2231 | + |
2232 | + def _format_fix(self, fix): |
2233 | + """Format a fix in a UI friendly text.""" |
2234 | + return '<b>{}</b>\n{}'.format( |
2235 | + fix['Name'], fix['Description'].replace('\n', ' ')) |
2236 | + |
2237 | + def _do_login(self): |
2238 | + """Start the authentication flow to retrieve the livepatch token.""" |
2239 | + dialog = DialogAuth(self._parent.window_main, self._parent.datadir) |
2240 | + |
2241 | + if dialog.run() == Gtk.ResponseType.OK: |
2242 | + self._auth.login(dialog.account) |
2243 | + self._parent.switch_livepatch.set_state(True) |
2244 | + |
2245 | + def _do_logout(self): |
2246 | + """Start the de-authentication flow.""" |
2247 | + self._auth.logout() |
2248 | + |
2249 | + # Signals handler |
2250 | + def _lps_availability_changed_cb(self, o, v): |
2251 | + self._trigger_ui_update(skip=True) |
2252 | + |
2253 | + def _lps_enabled_changed_cb(self, o, v): |
2254 | + if self._waiting_livepatch_response: |
2255 | + return |
2256 | + self._trigger_ui_update(skip=False) |
2257 | + |
2258 | + def _auth_changed_cb(self, o, v): |
2259 | + self._trigger_ui_update(skip=True) |
2260 | + |
2261 | + def _switch_state_set_cb(self, widget, state): |
2262 | + if not self._waiting_livepatch_response: |
2263 | + self._waiting_livepatch_response = True |
2264 | + |
2265 | + token = self._auth.token or '' |
2266 | + self._parent.backend.SetLivepatchEnabled( |
2267 | + state, token, |
2268 | + reply_handler=self._enabled_reply_handler, |
2269 | + error_handler=self._enabled_error_handler, |
2270 | + timeout=1200) |
2271 | + |
2272 | + self._trigger_ui_update(skip=True) |
2273 | + self._parent.switch_livepatch.set_state(state) |
2274 | + |
2275 | + return False |
2276 | + |
2277 | + def _button_livepatch_login_clicked_cb(self, button): |
2278 | + if self._auth.logged: |
2279 | + self._do_logout() |
2280 | + else: |
2281 | + self._do_login() |
2282 | + |
2283 | + def _show_error_dialog(self, message): |
2284 | + dialog = DialogLivepatchError( |
2285 | + self._parent.window_main, |
2286 | + self._parent.datadir) |
2287 | + |
2288 | + response = dialog.run( |
2289 | + error=message, |
2290 | + show_settings_button=not self._parent.window_main.is_visible()) |
2291 | + |
2292 | + if response == DialogLivepatchError.RESPONSE_SETTINGS: |
2293 | + self._parent.window_main.show() |
2294 | + self._parent.notebook_main.set_current_page(6) |
2295 | + elif not self._parent.window_main.is_visible(): |
2296 | + self._parent.on_close_button(None) |
2297 | + |
2298 | + # DBus replay handlers |
2299 | + def _enabled_reply_handler(self, is_error, prompt): |
2300 | + if self._parent.switch_livepatch.get_active() == self._lps.props.enabled: |
2301 | + self._waiting_livepatch_response = False |
2302 | + self._trigger_ui_update(skip=True) |
2303 | + |
2304 | + if not self._parent.window_main.is_visible(): |
2305 | + self._parent.on_close_button(None) |
2306 | + else: |
2307 | + if is_error: |
2308 | + self._waiting_livepatch_response = False |
2309 | + self._trigger_ui_update(skip=True) |
2310 | + self._show_error_dialog(prompt) |
2311 | + else: |
2312 | + # The user tooggled on/off the switch while we were waiting |
2313 | + # livepatch to respond back. |
2314 | + self._parent.backend.SetLivepatchEnabled( |
2315 | + self._parent.switch_livepatch.get_active(), |
2316 | + self._auth.token, |
2317 | + reply_handler=self._enabled_reply_handler, |
2318 | + error_handler=self._enabled_error_handler, |
2319 | + timeout=1200) |
2320 | + |
2321 | + def _enabled_error_handler(self, e): |
2322 | + self._waiting_livepatch_response = False |
2323 | + self._trigger_ui_update(skip=True) |
2324 | + |
2325 | + if e._dbus_error_name == 'com.ubuntu.SoftwareProperties.PermissionDeniedByPolicy': |
2326 | + logging.warning("Authentication canceled, changes have not been saved") |
2327 | + |
2328 | + if not self._parent.window_main.is_visible(): |
2329 | + self._parent.on_close_button(None) |
2330 | + else: |
2331 | + self._show_error_dialog(str(e)) |
2332 | diff --git a/softwareproperties/gtk/SimpleGtkbuilderApp.py b/softwareproperties/gtk/SimpleGtkbuilderApp.py |
2333 | index 06375ff..e14f840 100644 |
2334 | --- a/softwareproperties/gtk/SimpleGtkbuilderApp.py |
2335 | +++ b/softwareproperties/gtk/SimpleGtkbuilderApp.py |
2336 | @@ -38,6 +38,9 @@ class SimpleGtkbuilderApp(Gtk.Application): |
2337 | def on_activate(self, data=None): |
2338 | self.add_window(self.window_main) |
2339 | |
2340 | + if not self.window_main.is_visible(): |
2341 | + self.window_main.show() |
2342 | + |
2343 | def run(self): |
2344 | """ |
2345 | Starts the main loop of processing events checking for Control-C. |
2346 | diff --git a/softwareproperties/gtk/SoftwarePropertiesGtk.py b/softwareproperties/gtk/SoftwarePropertiesGtk.py |
2347 | index 75ff85d..9271f54 100644 |
2348 | --- a/softwareproperties/gtk/SoftwarePropertiesGtk.py |
2349 | +++ b/softwareproperties/gtk/SoftwarePropertiesGtk.py |
2350 | @@ -27,9 +27,6 @@ from __future__ import absolute_import, print_function |
2351 | |
2352 | import apt |
2353 | import apt_pkg |
2354 | -import aptsources.distro |
2355 | -from datetime import datetime |
2356 | -import distro_info |
2357 | import dbus |
2358 | from gettext import gettext as _ |
2359 | import gettext |
2360 | @@ -52,11 +49,9 @@ from .DialogMirror import DialogMirror |
2361 | from .DialogEdit import DialogEdit |
2362 | from .DialogCacheOutdated import DialogCacheOutdated |
2363 | from .DialogAddSourcesList import DialogAddSourcesList |
2364 | -from .DialogLivepatchError import DialogLivepatchError |
2365 | -from .DialogAuth import DialogAuth |
2366 | +from .LivepatchPage import LivepatchPage |
2367 | |
2368 | import softwareproperties |
2369 | -from softwareproperties.GoaAuth import GoaAuth |
2370 | import softwareproperties.distro |
2371 | from softwareproperties.SoftwareProperties import SoftwareProperties |
2372 | import softwareproperties.SoftwareProperties |
2373 | @@ -85,8 +80,6 @@ RESPONSE_ADD = 2 |
2374 | STORE_VISIBLE |
2375 | ) = list(range(5)) |
2376 | |
2377 | -LIVEPATCH_TIMEOUT = 1200 |
2378 | - |
2379 | |
2380 | def error(parent_window, summary, msg): |
2381 | """ show a error dialog """ |
2382 | @@ -1045,7 +1038,9 @@ class SoftwarePropertiesGtk(SoftwareProperties, SimpleGtkbuilderApp): |
2383 | d = DialogCacheOutdated(self.window_main, |
2384 | self.datadir) |
2385 | d.run() |
2386 | - if self.waiting_livepatch_response: |
2387 | + |
2388 | + self.quit_when_livepatch_responds = False |
2389 | + if self.livepatch_page.waiting_livepatch_response: |
2390 | self.quit_when_livepatch_responds = True |
2391 | self.hide() |
2392 | else: |
2393 | @@ -1483,163 +1478,5 @@ class SoftwarePropertiesGtk(SoftwareProperties, SimpleGtkbuilderApp): |
2394 | else: |
2395 | self.label_driver_action.set_label(_("No proprietary drivers are in use.")) |
2396 | |
2397 | - # |
2398 | - # Livepatch |
2399 | - # |
2400 | def init_livepatch(self): |
2401 | - self.goa_auth = GoaAuth() |
2402 | - self.waiting_livepatch_response = False |
2403 | - self.quit_when_livepatch_responds = False |
2404 | - |
2405 | - if not self.is_livepatch_supported(): |
2406 | - self.grid_livepatch.set_visible(False) |
2407 | - return |
2408 | - |
2409 | - self.checkbutton_livepatch.set_active(self.is_livepatch_enabled()) |
2410 | - self.on_goa_auth_changed() |
2411 | - |
2412 | - # hacky way to monitor if livepatch is enabled or not |
2413 | - file = Gio.File.new_for_path(path=self.LIVEPATCH_RUNNING_FILE) |
2414 | - self.lp_monitor = file.monitor_file(Gio.FileMonitorFlags.NONE) |
2415 | - |
2416 | - # connect to signals |
2417 | - self.handlers[self.goa_auth] = \ |
2418 | - self.goa_auth.connect('notify', lambda o, p: self.on_goa_auth_changed()) |
2419 | - self.handlers[self.checkbutton_livepatch] = \ |
2420 | - self.checkbutton_livepatch.connect('toggled', self.on_checkbutton_livepatch_toggled) |
2421 | - self.handlers[self.button_ubuntuone] = \ |
2422 | - self.button_ubuntuone.connect('clicked', self.on_button_ubuntuone_clicked) |
2423 | - self.handlers[self.lp_monitor] = \ |
2424 | - self.lp_monitor.connect('changed', self.on_livepatch_status_changed) |
2425 | - |
2426 | - def has_online_accounts(self): |
2427 | - try: |
2428 | - d = Gio.DesktopAppInfo.new('gnome-online-accounts-panel.desktop') |
2429 | - return d != None |
2430 | - except Exception: |
2431 | - return False |
2432 | - |
2433 | - def is_livepatch_supported(self): |
2434 | - distro = aptsources.distro.get_distro() |
2435 | - di = distro_info.UbuntuDistroInfo() |
2436 | - return self.has_online_accounts() and \ |
2437 | - di.is_lts(distro.codename) and \ |
2438 | - distro.codename in di.supported(datetime.now().date()) |
2439 | - |
2440 | - def on_goa_auth_changed(self): |
2441 | - if self.goa_auth.logged: |
2442 | - self.button_ubuntuone.set_label(_('Sign Out')) |
2443 | - |
2444 | - if self.goa_auth.token: |
2445 | - self.checkbutton_livepatch.set_sensitive(True) |
2446 | - self.label_livepatch_login.set_label(_('Signed in as %s' % self.goa_auth.username)) |
2447 | - else: |
2448 | - self.checkbutton_livepatch.set_sensitive(False) |
2449 | - text = _('%s isn\'t authorized to use Livepatch.' % self.goa_auth.username) |
2450 | - text = "<span color='red'>" + text + "</span>" |
2451 | - self.label_livepatch_login.set_markup(text) |
2452 | - else: |
2453 | - if self.is_livepatch_enabled() and not self.waiting_livepatch_response: |
2454 | - # Allow the user to disable livepatch even if |
2455 | - # the account expired (see LP: #1768797) |
2456 | - self.checkbutton_livepatch.set_sensitive(True) |
2457 | - self.label_livepatch_login.set_label(_('Livepatch is active.')) |
2458 | - else: |
2459 | - self.checkbutton_livepatch.set_sensitive(False) |
2460 | - self.label_livepatch_login.set_label(_('To use Livepatch you need to sign in.')) |
2461 | - |
2462 | - self.button_ubuntuone.set_label(_('Sign In…')) |
2463 | - |
2464 | - def on_livepatch_status_changed(self, file_monitor, file, other_file, event_type): |
2465 | - if not self.waiting_livepatch_response: |
2466 | - self.checkbutton_livepatch.set_active(self.is_livepatch_enabled()) |
2467 | - self.on_goa_auth_changed() |
2468 | - |
2469 | - def on_button_ubuntuone_clicked(self, button): |
2470 | - if self.goa_auth.logged: |
2471 | - self.do_logout() |
2472 | - else: |
2473 | - self.do_login() |
2474 | - |
2475 | - def do_login(self): |
2476 | - try: |
2477 | - # Show login dialog! |
2478 | - dialog = DialogAuth(self.window_main, self.datadir) |
2479 | - response = dialog.run() |
2480 | - except Exception as e: |
2481 | - logging.error(e) |
2482 | - error(self.window_main, |
2483 | - _("Error enabling Canonical Livepatch"), |
2484 | - _("Please check your Internet connection.")) |
2485 | - else: |
2486 | - if response == Gtk.ResponseType.OK: |
2487 | - self.goa_auth.login(dialog.account) |
2488 | - if self.goa_auth.logged: |
2489 | - self.checkbutton_livepatch.set_active(True) |
2490 | - |
2491 | - def do_logout(self): |
2492 | - self.checkbutton_livepatch.set_active(False) |
2493 | - self.goa_auth.logout() |
2494 | - |
2495 | - def on_checkbutton_livepatch_toggled(self, checkbutton): |
2496 | - if self.waiting_livepatch_response: |
2497 | - return |
2498 | - |
2499 | - self.waiting_livepatch_response = True |
2500 | - |
2501 | - token = '' |
2502 | - enabled = False |
2503 | - if self.checkbutton_livepatch.get_active(): |
2504 | - enabled = True |
2505 | - token = self.goa_auth.token if self.goa_auth.token else '' |
2506 | - self.backend.SetLivepatchEnabled(enabled, token, |
2507 | - reply_handler=self.livepatch_enabled_reply_handler, |
2508 | - error_handler=self.livepatch_enabled_error_handler, |
2509 | - timeout=LIVEPATCH_TIMEOUT) |
2510 | - |
2511 | - def livepatch_enabled_reply_handler(self, is_error, prompt): |
2512 | - self.sync_checkbutton_livepatch(is_error, prompt) |
2513 | - |
2514 | - def livepatch_enabled_error_handler(self, e): |
2515 | - if e._dbus_error_name == 'com.ubuntu.SoftwareProperties.PermissionDeniedByPolicy': |
2516 | - logging.error("Authentication canceled, changes have not been saved") |
2517 | - self.sync_checkbutton_livepatch(is_error=True, prompt=None) |
2518 | - else: |
2519 | - self.sync_checkbutton_livepatch(is_error=True, prompt=str(e)) |
2520 | - |
2521 | - def sync_checkbutton_livepatch(self, is_error, prompt): |
2522 | - if is_error: |
2523 | - self.waiting_livepatch_response = False |
2524 | - self.checkbutton_livepatch.handler_block(self.handlers[self.checkbutton_livepatch]) |
2525 | - self.checkbutton_livepatch.set_active(self.is_livepatch_enabled()) |
2526 | - self.checkbutton_livepatch.handler_unblock(self.handlers[self.checkbutton_livepatch]) |
2527 | - |
2528 | - if prompt: |
2529 | - dialog = DialogLivepatchError(self.window_main, self.datadir) |
2530 | - response = dialog.run(prompt, show_settings_button=self.quit_when_livepatch_responds) |
2531 | - if response == DialogLivepatchError.RESPONSE_SETTINGS: |
2532 | - self.window_main.show() |
2533 | - self.quit_when_livepatch_responds = False |
2534 | - else: |
2535 | - do_dbus_call = False |
2536 | - if self.is_livepatch_enabled() and not self.checkbutton_livepatch.get_active(): |
2537 | - do_dbus_call = True |
2538 | - enabled = False |
2539 | - token = '' |
2540 | - elif not self.is_livepatch_enabled() and self.checkbutton_livepatch.get_active(): |
2541 | - do_dbus_call = True |
2542 | - enabled = True |
2543 | - token = self.goa_auth.token if self.goa_auth.token else '' |
2544 | - else: |
2545 | - self.waiting_livepatch_response = False |
2546 | - |
2547 | - if do_dbus_call: |
2548 | - self.backend.SetLivepatchEnabled(enabled, token, |
2549 | - reply_handler=self.livepatch_enabled_reply_handler, |
2550 | - error_handler=self.livepatch_enabled_error_handler, |
2551 | - timeout=LIVEPATCH_TIMEOUT) |
2552 | - |
2553 | - self.on_goa_auth_changed() |
2554 | - |
2555 | - if self.quit_when_livepatch_responds: |
2556 | - self.on_close_button(self.button_close) |
2557 | + self.livepatch_page = LivepatchPage(self) |
2558 | diff --git a/softwareproperties/gtk/utils.py b/softwareproperties/gtk/utils.py |
2559 | index 37f0c10..e0ddca9 100644 |
2560 | --- a/softwareproperties/gtk/utils.py |
2561 | +++ b/softwareproperties/gtk/utils.py |
2562 | @@ -18,13 +18,19 @@ |
2563 | |
2564 | from __future__ import print_function |
2565 | |
2566 | +import aptsources.distro |
2567 | +from datetime import datetime |
2568 | +import distro_info |
2569 | +from functools import wraps |
2570 | import gi |
2571 | gi.require_version("Gtk", "3.0") |
2572 | -from gi.repository import Gtk |
2573 | +from gi.repository import Gio, Gtk |
2574 | |
2575 | import logging |
2576 | LOG=logging.getLogger(__name__) |
2577 | |
2578 | +import time |
2579 | + |
2580 | def setup_ui(self, path, domain): |
2581 | # setup ui |
2582 | self.builder = Gtk.Builder() |
2583 | @@ -37,3 +43,52 @@ def setup_ui(self, path, domain): |
2584 | setattr(self, name, o) |
2585 | else: |
2586 | logging.debug("can not get name for object '%s'" % o) |
2587 | + |
2588 | +def has_gnome_online_accounts(): |
2589 | + try: |
2590 | + d = Gio.DesktopAppInfo.new('gnome-online-accounts-panel.desktop') |
2591 | + return d != None |
2592 | + except Exception: |
2593 | + return False |
2594 | + |
2595 | +def is_current_distro_lts(): |
2596 | + distro = aptsources.distro.get_distro() |
2597 | + di = distro_info.UbuntuDistroInfo() |
2598 | + return di.is_lts(distro.codename) |
2599 | + |
2600 | +def is_current_distro_supported(): |
2601 | + distro = aptsources.distro.get_distro() |
2602 | + di = distro_info.UbuntuDistroInfo() |
2603 | + return distro.codename in di.supported(datetime.now().date()) |
2604 | + |
2605 | +def retry(exceptions, tries=10, delay=0.1, backoff=2): |
2606 | + """ |
2607 | + Retry calling the decorated function using an exponential backoff. |
2608 | + |
2609 | + Args: |
2610 | + exceptions: The exception to check. may be a tuple of |
2611 | + exceptions to check. |
2612 | + tries: Number of times to try (not retry) before giving up. |
2613 | + delay: Initial delay between retries in seconds. |
2614 | + backoff: Backoff multiplier (e.g. value of 2 will double the delay |
2615 | + each retry). |
2616 | + """ |
2617 | + def deco_retry(f): |
2618 | + |
2619 | + @wraps(f) |
2620 | + def f_retry(*args, **kwargs): |
2621 | + mtries, mdelay = tries, delay |
2622 | + while mtries > 1: |
2623 | + try: |
2624 | + return f(*args, **kwargs) |
2625 | + except exceptions as e: |
2626 | + msg = '{}, Retrying in {} seconds...'.format(e, mdelay) |
2627 | + logging.warning(msg) |
2628 | + time.sleep(mdelay) |
2629 | + mtries -= 1 |
2630 | + mdelay *= backoff |
2631 | + return f(*args, **kwargs) |
2632 | + |
2633 | + return f_retry # true decorator |
2634 | + |
2635 | + return deco_retry |
2636 | diff --git a/tests/aptroot/etc/apt/apt.conf.d/.keep b/tests/aptroot/etc/apt/apt.conf.d/.keep |
2637 | new file mode 100644 |
2638 | index 0000000..e69de29 |
2639 | --- /dev/null |
2640 | +++ b/tests/aptroot/etc/apt/apt.conf.d/.keep |
Thanks, the code changes look fine it works as expected (tested with CVE applied, they were correctly listed)