Merge ~azzar1/software-properties:ubuntu/bionic-livepatch into software-properties: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)
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.

To post a comment you must log in.
Revision history for this message
Sebastien Bacher (seb128) wrote :

Thanks, the code changes look fine it works as expected (tested with CVE applied, they were correctly listed)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/data/gtkbuilder/dialog-auth.ui b/data/gtkbuilder/dialog-auth.ui
2index 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">&lt;b&gt;Use another account...&lt;/b&gt;</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+
213diff --git a/data/gtkbuilder/dialog-livepatch-error.ui b/data/gtkbuilder/dialog-livepatch-error.ui
214index 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>
372diff --git a/data/gtkbuilder/main.ui b/data/gtkbuilder/main.ui
373index 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. &lt;a href="https://www.ubuntu.com/livepatch"&gt;Learn More&lt;/a&gt;</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>
719diff --git a/data/icons/16x16/apps/livepatch.svg b/data/icons/16x16/apps/livepatch.svg
720new file mode 100644
721index 0000000..6d82412
722Binary files /dev/null and b/data/icons/16x16/apps/livepatch.svg differ
723diff --git a/data/icons/24x24/apps/livepatch.svg b/data/icons/24x24/apps/livepatch.svg
724new file mode 100644
725index 0000000..8c13c57
726Binary files /dev/null and b/data/icons/24x24/apps/livepatch.svg differ
727diff --git a/data/icons/48x48/apps/livepatch.svg b/data/icons/48x48/apps/livepatch.svg
728new file mode 100644
729index 0000000..65a3049
730Binary files /dev/null and b/data/icons/48x48/apps/livepatch.svg differ
731diff --git a/data/icons/64x64/apps/livepatch.svg b/data/icons/64x64/apps/livepatch.svg
732new file mode 100644
733index 0000000..46c3dea
734Binary files /dev/null and b/data/icons/64x64/apps/livepatch.svg differ
735diff --git a/data/icons/scalable/apps/livepatch.svg b/data/icons/scalable/apps/livepatch.svg
736new file mode 100644
737index 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
743diff --git a/data/software-properties-livepatch.desktop.in b/data/software-properties-livepatch.desktop.in
744new file mode 100644
745index 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;
761diff --git a/debian/changelog b/debian/changelog
762index 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
781diff --git a/debian/control b/debian/control
782index 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,
825diff --git a/debian/gbp.conf b/debian/gbp.conf
826new file mode 100644
827index 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
834diff --git a/debian/software-properties-gtk.install b/debian/software-properties-gtk.install
835index 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
846diff --git a/po/POTFILES.in b/po/POTFILES.in
847index 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
876diff --git a/setup.cfg b/setup.cfg
877index 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 ]
888diff --git a/softwareproperties/GoaAuth.py b/softwareproperties/GoaAuth.py
889index 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:
945diff --git a/softwareproperties/LivepatchService.py b/softwareproperties/LivepatchService.py
946new file mode 100644
947index 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')
1205diff --git a/softwareproperties/LivepatchSnap.py b/softwareproperties/LivepatchSnap.py
1206new file mode 100644
1207index 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, ''
1346diff --git a/softwareproperties/SoftwareProperties.py b/softwareproperties/SoftwareProperties.py
1347index 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)
1532diff --git a/softwareproperties/dbus/SoftwarePropertiesDBus.py b/softwareproperties/dbus/SoftwarePropertiesDBus.py
1533index 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):
1573diff --git a/softwareproperties/gtk/DialogAuth.py b/softwareproperties/gtk/DialogAuth.py
1574index 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)
1898diff --git a/softwareproperties/gtk/DialogLivepatchError.py b/softwareproperties/gtk/DialogLivepatchError.py
1899index 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()
1951diff --git a/softwareproperties/gtk/LivepatchPage.py b/softwareproperties/gtk/LivepatchPage.py
1952new file mode 100644
1953index 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))
2332diff --git a/softwareproperties/gtk/SimpleGtkbuilderApp.py b/softwareproperties/gtk/SimpleGtkbuilderApp.py
2333index 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.
2346diff --git a/softwareproperties/gtk/SoftwarePropertiesGtk.py b/softwareproperties/gtk/SoftwarePropertiesGtk.py
2347index 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)
2558diff --git a/softwareproperties/gtk/utils.py b/softwareproperties/gtk/utils.py
2559index 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
2636diff --git a/tests/aptroot/etc/apt/apt.conf.d/.keep b/tests/aptroot/etc/apt/apt.conf.d/.keep
2637new file mode 100644
2638index 0000000..e69de29
2639--- /dev/null
2640+++ b/tests/aptroot/etc/apt/apt.conf.d/.keep

Subscribers

People subscribed via source and target branches