~paul-mcspadden/computer-janitor/bug-726616

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
# Copyright (C) 2008, 2009, 2010  Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""A cruft collector."""

from __future__ import absolute_import, unicode_literals

__metaclass__ = type
__all__ = [
    'Collector',
    ]


import os
import time
import logging

from computerjanitor import PluginManager
from computerjanitord.errors import DuplicateCruftError, PackageCleanupError
from computerjanitord.whitelist import Whitelist


log = logging.getLogger('computerjanitor')
MISSING = object()
DEFAULT_PLUGINS_DIRS = "/usr/share/computerjanitor/plugins"

# For testing purposes.
SLEEPY_TIME = float(os.environ.get('COMPUTER_JANITOR_SLEEPY_TIME', '1.0'))


class Collector:
    """A cruft collector."""

    def __init__(self, application, plugin_manager_class=None,
                 whitelist_dirs=None, service=None):
        """Create a cruft collector.

        :param application: The `Application` class.
        :type application: This object is used by plugins to access the apt
            database.  It must have an attribute named `apt_cache` and a
            method named `refresh_apt_cache()`.
        :param plugin_manager_class: The plugin manager class.  If None (the
            default), then `computerjanitor.PluginManager` is used.
        :type plugin_manager_class: callable accepting a single argument,
            which is a sequence of plugin directories.
        :param whitelist_dirs: Sequence of directories to search for
            '.whitelist' files.  Passed directly to
            `computerjanitord.whitelist.Whitelist`.
        :param service: The dbus service; when doing plugin post-cleanup, this
            will be used to emit a progress signal.
        """
        self.application = application
        self.service = service
        self.whitelist = Whitelist(whitelist_dirs)
        # Keep track of cruft and map between the cruft's name and its Cruft
        # instance.  We'll use the latter when cruft cleanup is requested
        # through the dbus API.
        self.cruft = None
        self.cruft_by_name = None
        # Set up the plugin manager.
        plugin_path = os.environ.get('COMPUTER_JANITOR_PLUGINS',
                                     DEFAULT_PLUGINS_DIRS)
        plugin_dirs = plugin_path.split(':')
        if plugin_manager_class is None:
            plugin_manager_class = PluginManager
        self.plugin_manager = plugin_manager_class(application, plugin_dirs)
        self.load()

    def load(self):
        """Reload all cruft."""
        self.cruft = []
        self.cruft_by_name = {}
        # Ask all the plugins to find their cruft, filtering out whitelisted
        # cruft.
        for plugin in self.plugin_manager.get_plugins():
            for cruft in plugin.get_cruft():
                if not self.whitelist.is_whitelisted(cruft):
                    # Different plugins can give us duplicate cruft names,
                    # however the Cruft class better be the same, otherwise we
                    # won't actually know how to map the name back to a cruft
                    # instance for proper cleanup.
                    if cruft.get_name() in self.cruft_by_name:
                        my_cruft = self.cruft_by_name[cruft.get_name()]
                        if cruft.__class__ is my_cruft.__class__:
                            # We only need one instance of this cruft.
                            continue
                        else:
                            raise DuplicateCruftError(cruft.get_name())
                    #print '    ', cruft.get_name()
                    self.cruft.append(cruft)
                    self.cruft_by_name[cruft.get_name()] = cruft

    def clean(self, names, dry_run=False):
        """Clean up the named cruft.

        :param names: The names of the cruft to clean up.
        :type names: list of strings
        :param dry_run: Flag indicating whether to do permanent changes.
        :type dry_run: bool
        """
        for name in names:
            # Ensure that all named cruft is known.
            cruft = self.cruft_by_name.get(name, MISSING)
            if cruft is MISSING:
                log.error('No such cruft: {0}'.format(name))
                raise NoSuchCruftError(name)
            # Ensure that the cruft is not being ignored.
            if cruft in self.service.state.ignore:
                log.error('Skipping ignored cruft: {0}'.format(name))
                continue
            log.info('cleaning cruft{1}: {0}'.format(
                cruft.get_name(), ('[X]' if dry_run else '[Y]')))
            if not dry_run:
                try:
                    cruft.cleanup()
                except:
                    log.exception('cruft.cleanup(): {0}'.format(name))
                    raise
        # Do plugin-specific post-cleanup.
        for plugin in self.plugin_manager.get_plugins():
            log.info('post-cleanup: {0}'.format(plugin))
            if self.service is not None:
                # Notify the client that we're not done yet.
                #
                # 2010-02-09 barry: this actually kind of sucks because the
                # granularity is too coarse.  Some plugins will post_cleanup()
                # very quickly, others will take a long time.  Unfortunately,
                # the computerjanitor.Plugin API doesn't support a more
                # granular feedback.  Plugin.get_plugin(..., callback=foo)
                # doesn't really cut it because that only gets called during
                # get_plugins().
                self.service.cleanup_status(plugin.__class__.__name__)
            if dry_run:
                # For testing purposes.
                time.sleep(SLEEPY_TIME)
            else:
                try:
                    plugin.post_cleanup()
                except SystemError as error:
                    # apt will raise a SystemError if some other package
                    # manager is already running.  Turn this into a dbus
                    # derived exception so the client will be properly
                    # informed.
                    log.exception(
                        'plugin.post_cleanup(): {0}'.format(plugin))
                    raise PackageCleanupError(str(error))
                except:
                    log.exception(
                        'plugin.post_cleanup(): {0}'.format(plugin))
                    raise
        # Now we're done.
        if self.service is not None:
            self.service.cleanup_status('')
        # Reload list of crufts.
        self.application.refresh_apt_cache()
        self.load()