~ubuntu-core-dev/update-notifier/ubuntu

« back to all changes in this revision

Viewing changes to data/package-data-downloader

  • Committer: Jeremy Bicha
  • Date: 2019-02-09 14:25:07 UTC
  • Revision ID: jbicha@ubuntu.com-20190209142507-45fc0sqnn4uqdl7j
Try adding gnome-shell as alternate dependency of notification-daemon

as part of an effort to drop notification-daemon to universe

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python3
 
2
# -*- coding: utf-8 -*-
 
3
"""Process new requests to download per-package data"""
 
4
# Copyright (C) 2012 Canonical Ltd
 
5
#
 
6
# This program is free software; you can redistribute it and/or modify
 
7
# it under the terms of version 3 of the GNU General Public License as
 
8
# published by the Free Software Foundation.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License along
 
16
# with this program; if not, write to the Free Software Foundation, Inc.,
 
17
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
18
 
 
19
import glob
 
20
import os
 
21
import sys
 
22
import subprocess
 
23
import traceback
 
24
import debian.deb822
 
25
import string
 
26
import debconf
 
27
from datetime import datetime
 
28
 
 
29
# avoid hanging forever (LP: #1243090)
 
30
import socket
 
31
socket.setdefaulttimeout(60)
 
32
 
 
33
 
 
34
DATADIR = "/usr/share/package-data-downloads/"
 
35
STAMPDIR = "/var/lib/update-notifier/package-data-downloads/"
 
36
NOTIFIER_SOURCE_FILE = \
 
37
    "/usr/share/update-notifier/package-data-downloads-failed"
 
38
NOTIFIER_FILE = "/var/lib/update-notifier/user.d/data-downloads-failed"
 
39
NOTIFIER_PERMANENT_SOURCE_FILE = NOTIFIER_SOURCE_FILE + '-permanently'
 
40
NOTIFIER_PERMANENT_FILE = NOTIFIER_FILE + '-permanently'
 
41
 
 
42
failures = []
 
43
permanent_failures = []
 
44
 
 
45
 
 
46
def create_or_update_stampfile(file):
 
47
    """Create or update the indicated stampfile, and remove failure flags"""
 
48
 
 
49
    try:
 
50
        with open(file, 'w'):
 
51
            pass
 
52
    # Ignore errors
 
53
    except Exception:
 
54
        traceback.print_exc(file=sys.stderr)
 
55
 
 
56
    os.utime(file, None)
 
57
 
 
58
    for ext in ('.failed', '.permanent-failure'):
 
59
        if os.path.exists(file + ext):
 
60
            os.unlink(file + ext)
 
61
 
 
62
 
 
63
def mark_hook_failed(hook_name, permanent=False):
 
64
    """Create a stampfile recording that a hook failed
 
65
 
 
66
    We create separate stampfiles for failed hooks so we can
 
67
    keep track of how long the hook has been failing and if the failure
 
68
    should be considered permanent."""
 
69
 
 
70
    if permanent:
 
71
        filename = hook_name + '.permanent-failure'
 
72
    else:
 
73
        filename = hook_name + '.failed'
 
74
 
 
75
    failure_file = os.path.join(STAMPDIR, filename)
 
76
    try:
 
77
        with open(failure_file, 'w'):
 
78
            pass
 
79
 
 
80
    # Ignore errors
 
81
    except Exception:
 
82
        traceback.print_exc(file=sys.stderr)
 
83
 
 
84
    for ext in ('', '.failed', '.permanent-failure'):
 
85
        stampfile = hook_name + ext
 
86
        if filename != stampfile \
 
87
           and os.path.exists(os.path.join(STAMPDIR, stampfile)):
 
88
            os.unlink(os.path.join(STAMPDIR, stampfile))
 
89
 
 
90
 
 
91
def hook_is_permanently_failed(hook_name):
 
92
    """Check if this hook has been marked as permanently failing.
 
93
 
 
94
    If so, don't raise any more errors about it."""
 
95
 
 
96
    failure_file = os.path.join(STAMPDIR, hook_name + '.permanent-failure')
 
97
    return os.path.exists(failure_file)
 
98
 
 
99
 
 
100
def hook_aged_out(hook_name):
 
101
    """Check if this hook has been failing consistently for >= 3 days"""
 
102
 
 
103
    failure_file = os.path.join(STAMPDIR, hook_name + '.failed')
 
104
    try:
 
105
        hook_date = datetime.fromtimestamp(os.stat(failure_file).st_ctime)
 
106
        cur_time = datetime.now()
 
107
        d = cur_time - hook_date
 
108
        if d.days >= 3:
 
109
            return True
 
110
    except OSError:
 
111
        pass
 
112
    except Exception:
 
113
        traceback.print_exc(file=sys.stderr)
 
114
    return False
 
115
 
 
116
 
 
117
def record_failure(hook):
 
118
    """Record that the named hook has failed"""
 
119
    if hook_aged_out(hook):
 
120
        permanent_failures.append(hook)
 
121
    else:
 
122
        failures.append(hook)
 
123
 
 
124
 
 
125
def existing_permanent_failures():
 
126
    """Return the list of all previously recorded permanent failures"""
 
127
 
 
128
    files = glob.glob(os.path.join(STAMPDIR, "*.permanent-failure"))
 
129
    return [os.path.splitext(os.path.basename(path))[0] for path in files]
 
130
 
 
131
 
 
132
def trigger_update_notifier(failures, permanent=False):
 
133
    """Tell update-notifier that there were failed packages"""
 
134
 
 
135
    try:
 
136
        if permanent:
 
137
            with open(NOTIFIER_PERMANENT_SOURCE_FILE, 'r',
 
138
                      encoding='utf-8') as f:
 
139
                input = f.read()
 
140
            output_file = open(NOTIFIER_PERMANENT_FILE, 'w', encoding='utf-8')
 
141
        else:
 
142
            with open(NOTIFIER_SOURCE_FILE, 'r', encoding='utf-8') as f:
 
143
                input = f.read()
 
144
            output_file = open(NOTIFIER_FILE, 'w', encoding='utf-8')
 
145
    except Exception:
 
146
        # Things failed and we can't even notify about it.  Break the
 
147
        # trigger so that there's some error propagation, even if not
 
148
        # the most pleasant sort.
 
149
        traceback.print_exc(file=sys.stderr)
 
150
        sys.exit(1)
 
151
 
 
152
    packages = [os.path.basename(failure) for failure in failures]
 
153
    output_file.write(
 
154
        string.Template(input).substitute(
 
155
            {'packages': ", ".join(packages)}))
 
156
    output_file.close()
 
157
 
 
158
 
 
159
def get_hook_file_names():
 
160
    res = []
 
161
    for relfile in os.listdir(DATADIR):
 
162
        # ignore files ending in .dpkg-*
 
163
        if (os.path.splitext(relfile)[1]
 
164
                and os.path.splitext(relfile)[1].startswith(".dpkg")):
 
165
            continue
 
166
        res.append(relfile)
 
167
    return res
 
168
 
 
169
 
 
170
# we use apt-helper here as this gives us the exact same proxy behavior
 
171
# as apt-get itself (environment/apt-config proxy settings/autodiscover)
 
172
def download_file(uri, sha256_hashsum):
 
173
    """Download a URI and checks the given hashsum using apt-helper
 
174
 
 
175
    Returns: path to the downloaded file or None
 
176
    """
 
177
    download_dir = os.path.join(STAMPDIR, "partial")
 
178
    dest_file = os.path.join(download_dir, os.path.basename(uri))
 
179
    ret = subprocess.call(
 
180
        ["/usr/lib/apt/apt-helper",
 
181
         "download-file", uri, dest_file, "SHA256:" + sha256_hashsum])
 
182
    if ret != 0:
 
183
        if os.path.exists(dest_file):
 
184
            os.remove(dest_file)
 
185
        return None
 
186
    return dest_file
 
187
 
 
188
 
 
189
def print_maybe(*args, **kwargs):
 
190
    """Version of print() that ignores failure"""
 
191
    try:
 
192
        print(*args, **kwargs)
 
193
    except OSError:
 
194
        pass
 
195
 
 
196
 
 
197
def process_download_requests():
 
198
    """Process requests to download package data files
 
199
 
 
200
    Iterate over /usr/share/package-data-downloads and download any
 
201
    package data specified in the contained file, then hand off to
 
202
    the indicated handler for further processing.
 
203
 
 
204
    Successful downloads are recorded in
 
205
    /var/lib/update-notifier/package-data-downloads to avoid unnecessary
 
206
    repeat handling.
 
207
 
 
208
    Failed downloads are reported to the user via the
 
209
    update-notifier interface."""
 
210
 
 
211
    # Iterate through all the available hooks.  If we get a failure
 
212
    # processing any of them (download failure, checksum failure, or
 
213
    # failure to run the hook script), record it but continue processing
 
214
    # the rest of the hooks since some of them may succeed.
 
215
    for relfile in get_hook_file_names():
 
216
 
 
217
        stampfile = os.path.join(STAMPDIR, relfile)
 
218
        file = os.path.join(DATADIR, relfile)
 
219
        try:
 
220
            if not os.path.exists(NOTIFIER_FILE) and \
 
221
                    not os.path.exists(NOTIFIER_PERMANENT_FILE):
 
222
                hook_date = os.stat(file).st_mtime
 
223
                stamp_date = os.stat(stampfile).st_mtime
 
224
                if hook_date < stamp_date:
 
225
                    continue
 
226
            elif os.path.exists(stampfile):
 
227
                continue
 
228
 
 
229
        except Exception as e:
 
230
            if not isinstance(e, OSError):
 
231
                traceback.print_exc(file=sys.stderr)
 
232
 
 
233
        hook = debian.deb822.Deb822()
 
234
        files = []
 
235
        sums = []
 
236
        for para in hook.iter_paragraphs(open(file)):
 
237
            if 'Script' in para:
 
238
                if not files:
 
239
                    record_failure(relfile)
 
240
                    break
 
241
                command = [para['Script']]
 
242
 
 
243
                if 'Should-Download' in para:
 
244
                    db = debconf.DebconfCommunicator('update-notifier')
 
245
                    try:
 
246
                        should = db.get(para['Should-Download'])
 
247
                        if should == "false":
 
248
                            # Do nothing with this file.
 
249
                            break
 
250
                    except (DebconfError, KeyError):
 
251
                        pass
 
252
                    finally:
 
253
                        db.shutdown()
 
254
 
 
255
                print_maybe("%s: processing..." % (relfile))
 
256
 
 
257
                # Download each file and verify the sum
 
258
                try:
 
259
                    downloaded = set()
 
260
                    for i in range(len(files)):
 
261
                        print_maybe("%s: downloading %s" % (relfile, files[i]))
 
262
                        dest_file = download_file(files[i], sums[i])
 
263
                        if dest_file:
 
264
                            command.append(dest_file)
 
265
                            downloaded.add(dest_file)
 
266
                        else:
 
267
                            record_failure(relfile)
 
268
                            break
 
269
                    if relfile in failures + permanent_failures:
 
270
                        break
 
271
 
 
272
                    sys.stdout.flush()
 
273
                    result = subprocess.call(command)
 
274
                    if result:
 
275
                        # There's no sense redownloading if the script fails
 
276
                        permanent_failures.append(relfile)
 
277
                    else:
 
278
                        create_or_update_stampfile(stampfile)
 
279
                    # cleanup
 
280
                    for f in downloaded:
 
281
                        os.remove(f)
 
282
                    break
 
283
                except Exception:
 
284
                    traceback.print_exc(file=sys.stderr)
 
285
 
 
286
                record_failure(relfile)
 
287
                # The 'script' is always the last stanza
 
288
                break
 
289
 
 
290
            # Not in a 'script' stanza, so we should have some urls
 
291
            try:
 
292
                files.append(para['Url'])
 
293
                sums.append(para['Sha256'])
 
294
            except Exception as e:
 
295
                print_maybe("%s: Error processing!" % (relfile))
 
296
                if not isinstance(e, KeyError):
 
297
                    traceback.print_exc(file=sys.stderr)
 
298
                record_failure(relfile)
 
299
                break
 
300
 
 
301
    previous_failures = existing_permanent_failures()
 
302
 
 
303
    # We only report about "permanent" failures when there are new ones,
 
304
    # but we want the whole list of permanently-failing hooks so when
 
305
    # we clobber the update-notifier file we don't lose information the
 
306
    # user may not have seen yet
 
307
    if permanent_failures:
 
308
        new_failures = False
 
309
        for failure in permanent_failures:
 
310
            if failure not in previous_failures:
 
311
                mark_hook_failed(failure, permanent=True)
 
312
                previous_failures.append(failure)
 
313
                new_failures = True
 
314
        if new_failures:
 
315
            trigger_update_notifier(previous_failures, permanent=True)
 
316
        # 2016-09-19 14:36 reset the list of permanent_failures as it caused
 
317
        # tests not to be idempotent
 
318
        permanent_failures.clear()
 
319
    if not previous_failures and os.path.exists(NOTIFIER_PERMANENT_FILE):
 
320
        os.unlink(NOTIFIER_PERMANENT_FILE)
 
321
 
 
322
    # Filter out new failure reports for permanently-failed packages
 
323
    our_failures = [x for x in failures if x not in previous_failures]
 
324
    # 2016-09-19 14:36 reset the list of permanent_failures as it caused
 
325
    # tests not to be idempotent
 
326
    failures.clear()
 
327
 
 
328
    if our_failures:
 
329
        for failure in our_failures:
 
330
            mark_hook_failed(failure)
 
331
        trigger_update_notifier(our_failures)
 
332
    elif os.path.exists(NOTIFIER_FILE):
 
333
        os.unlink(NOTIFIER_FILE)
 
334
 
 
335
 
 
336
if __name__ == "__main__":
 
337
    process_download_requests()