~ci-train-bot/ubuntu-system-settings/ubuntu-system-settings-ubuntu-yakkety-1966

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Software Updates Push Notifications helper.
#
# This helper is called with one of three things:
# a) regular push messages about updated click packages¹
# b) broadcast messages about system updates
# c) notifications you send yourself over dbus to actually notify the user that
#    an update is ready to install².
#
# Figuring out which of those is the case is also this helper's job.
#
# notes:
# 1. nobody is sending those at the time of writing.
# 2. yes, this is rather convoluted. Most push helpers don't have to deal with
#    this stuff.

import os
import json
import sys
import time
import gettext
import logging
import logging.handlers
from xdg.BaseDirectory import save_cache_path

_ = gettext.translation('ubuntu-system-settings', fallback=True).gettext

SYS_UPDATE = "system-image-update"


class SystemImage:
    def __init__(self):
        self.loop = None
        self.sysimg = None
        self.postal = None
        self.notify = False

    def setup(self):
        import dbus
        from dbus.mainloop.glib import DBusGMainLoop
        from gi.repository import GLib

        gloop = DBusGMainLoop()
        sys_bus = dbus.SystemBus(mainloop=gloop)
        ses_bus = dbus.SessionBus(mainloop=gloop)
        self.loop = GLib.MainLoop()
        self.sysimg = dbus.Interface(
            sys_bus.get_object("com.canonical.SystemImage", "/Service"),
            dbus_interface="com.canonical.SystemImage")
        self.postal = dbus.Interface(
            ses_bus.get_object("com.ubuntu.Postal", "/com/ubuntu/Postal/_"),
            dbus_interface="com.ubuntu.Postal")
        self.notify = False

    def quit(self):
        if self.notify:
            logging.debug("Notifying.")
            # remove any older notifications about this
            self.postal.ClearPersistent("_ubuntu-system-settings", SYS_UPDATE)
            # send ours. This will of course come back to this same script.
            self.postal.Post("_ubuntu-system-settings", json.dumps(SYS_UPDATE))
        self.loop.quit()

    def available_cb(self, available, downloading, *ignored):
        logging.debug("Available: %s; downloading: %s", available, downloading)
        if available:
            if downloading:
                # handled in the UpdateDownloaded or UpdateFailed handlers
                return
            # available and not downloading: auto downloads are turned
            # off; notify the user right now.
            self.notify = True
        else:
            # if not available, we were called spuriously. No notification.
            self.notify = False
        self.quit()

    def downloaded_cb(self):
        logging.debug("Downloaded.")
        self.notify = True
        self.quit()

    def failed_cb(self, *ignored):
        # give up
        logging.debug("Failed.")
        self.notify = False
        self.quit()

    def run(self):
        logging.debug("Checking for update.")
        self.sysimg.connect_to_signal("UpdateAvailableStatus",
                                      self.available_cb)
        self.sysimg.connect_to_signal("UpdateDownloaded", self.downloaded_cb)
        self.sysimg.connect_to_signal("UpdateFailed", self.failed_cb)
        self.sysimg.CheckForUpdate()
        self.loop.run()


def main():
    if len(sys.argv) != 3:
        print("File in and out expected via argv", file=sys.stderr)
        sys.exit(1)

    f1, f2 = sys.argv[1:3]

    # here you should look at the input (the contents of the file whose
    # name is in f1, which are guaranteed to be json). If it's a broadcast
    # it will be the most recent we've received, and will have passed a
    # minimum amount of sanity checking, but you can probably do more. As
    # per the design on https://wiki.ubuntu.com/SoftwareUpdates#Prompting
    # if things are set to auto-download you should go download them (in a
    # child process -- this helper process itself has 4 more seconds to
    # live).
    #
    # the broadcast payload will be a single json object looking like
    #   { image-channel/device-model": [build-number, channel-alias]}
    #
    # e.g.,
    #
    #   {"ubuntu-touch/utopic-proposed/hammerhead":[265,""]}
    #
    #
    # When the click server starts sending notifications of packages a user
    # can update, you should probably describe that payload here. What to
    # do with it is described in some detail in the wiki page above.
    #
    #
    # Once you've downloaded things and need to actually notify the user,
    # you'd send a notification as below, over dbus to Post. That's the
    # third payload this script will be called with; for that case you'd
    # just pass the payload through.
    #
    # For cases when you don't want to notify the user (yet), the correct
    # output (to be written to a file whose name is f2) is “{}”, i.e. an
    # empty json object.

    with open(f1) as f:
        arg = json.load(f)

    obj = {}
    if arg == "system-image-update":
        logging.debug("system-image-update; requesting regular notification.")
        icon = "/usr/share/ubuntu/settings/system/icons/" + \
               "settings-system-update.svg"
        obj = {
            "notification": {
                "tag": SYS_UPDATE,
                "emblem-counter": {
                    "count": 1,
                    "visible": True,
                },
                "vibrate": {
                    "pattern": [50, 150],
                    "repeat": 3,
                },
                "card": {
                    "summary": _("There's an updated system image."),
                    "body": _("Tap to open the system updater."),
                    "actions": ["settings:///system/system-update"],
                    "icon": icon,
                    "timestamp": int(time.time()),
                    "persist": True,
                },
            },
        }
    elif arg == "testing":
        # for tests
        logging.debug("Testing.")
        obj = {"testing": True}
    else:
        # assume it's a broadcast
        logging.debug("Broadcast; forking.")
        if os.fork() == 0:
            os.setsid()
            os.closerange(0, 3)
            logging.debug("Forked.")
            s = SystemImage()
            s.setup()
            s.run()
            return

    json.dump(obj, open(f2, "w"))

if __name__ == '__main__':
    logdir = save_cache_path("ubuntu-system-settings")
    logfile = os.path.join(logdir, "software_updates_helper.log")
    rothandler = logging.handlers.TimedRotatingFileHandler(logfile, when="D",
                                                           backupCount=10)
    logging.basicConfig(
        format="%(asctime)s %(levelname)8s [%(process)04x] %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
        level=logging.DEBUG,
        handlers=(rothandler,))
    logging.debug("Starting.")
    try:
        main()
    except Exception:
        logging.exception("Died with exception:")
    else:
        logging.debug("Done.")