import os import glob import shutil import logging import threading from collections import OrderedDict from gi.repository import GObject, Gtk, Gdk, Pango from ubuntucleaner.gui import GuiBuilder from ubuntucleaner.gui.gtk import post_ui from ubuntucleaner.utils import icon from ubuntucleaner.utils.files import filesizeformat from ubuntucleaner.modules import ModuleLoader from ubuntucleaner.settings.debug import run_traceback, log_func log = logging.getLogger('Janitor') class CruftObject(object): def __init__(self, name, path=None, size=0): self.name = name self.path = path self.size = size def __str__(self): return self.get_name() def get_name(self): return self.name def get_size(self): return int(self.size) def get_size_display(self): return '' def get_icon(self): return None class PackageObject(CruftObject): def __init__(self, name, package_name, size): self.name = name self.package_name = package_name self.size = size def get_size_display(self): return filesizeformat(self.size) def get_icon(self): return icon.get_from_name('deb') def get_package_name(self): return self.package_name class CacheObject(CruftObject): def __init__(self, name, path, size): self.name = name self.path = path self.size = size def get_path(self): return self.path def get_size_display(self): return filesizeformat(self.size) def get_icon(self): return icon.guess_from_path(self.get_path()) def is_dir(self): return os.path.isdir(self.path) class JanitorPlugin(GObject.GObject): __title__ = '' __category__ = '' __utmodule__ = '' __desktop__ = '' __distro__ = '' __utactive__ = True __user_extension__ = False scan_finished = GObject.property(type=bool, default=False) clean_finished = GObject.property(type=bool, default=False) error = GObject.property(type=str, default='') __gsignals__ = { 'find_object': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT, GObject.TYPE_INT)), 'scan_finished': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_BOOLEAN, GObject.TYPE_INT, GObject.TYPE_LONG)), 'object_cleaned': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT, GObject.TYPE_INT)), 'all_cleaned': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_BOOLEAN,)), 'scan_error': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_STRING,)), 'clean_error': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_STRING,)), } @classmethod def is_active(cls): return cls.__utactive__ @classmethod def get_name(cls): return cls.__name__ @classmethod def get_title(cls): return cls.__title__ @classmethod def get_category(cls): return cls.__category__ @classmethod def is_user_extension(cls): return cls.__user_extension__ @classmethod def get_pixbuf(cls): #TODO return None def get_cruft(self): return () def get_summary(self, count): return self.get_title() def clean_cruft(self, parent=None, cruft_list=[]): '''Clean all the cruft, you must emit the "cleaned" signal to tell the main thread your task is finished :param parent: the toplevel window, use for transient :param cruft_list: a list contains all the cruft objects to be clean :param rescan_handler: the handler to rescan the result, must be called after the clean task is done ''' pass class JanitorCachePlugin(JanitorPlugin): root_path = '' pattern = '*' targets = [] def __str__(self): try: return self.__module__.split('.')[-1] except Exception: return "%s Plugin" % self.__title__ @classmethod def is_active(cls): return cls.__utactive__ and os.path.exists(cls.get_path()) def get_cruft(self): if self.pattern == '*': if self.targets: total_size = 0 count = 0 for target in self.targets: new_root_path = os.path.join(self.get_path(), target) if os.path.exists(new_root_path): if os.path.isdir(new_root_path): try: size = os.popen('du -bs "%s"' % new_root_path).read().split()[0] except Exception: size = 0 else: size = os.path.getsize(new_root_path) total_size += int(size) count += 1 self.emit('find_object', CacheObject(os.path.basename(new_root_path), new_root_path, size), count) self.emit('scan_finished', True, count, total_size) else: self.get_cruft_by_path() else: self.get_cruft_by_glob() def clean_cruft(self, cruft_list=[], parent=None): for index, cruft in enumerate(cruft_list): try: log.debug('Cleaning...%s' % cruft.get_name()) if cruft.is_dir(): shutil.rmtree(cruft.get_path()) else: os.remove(cruft.get_path()) self.emit('object_cleaned', cruft, index + 1) except Exception as e: log.error(run_traceback(e)) self.emit('clean_error', cruft.get_name()) break self.emit('all_cleaned', True) def on_done(self, widget): widget.destroy() def get_cruft_by_glob(self): cruft_list = glob.glob('%s/%s' % (self.get_path(), self.pattern)) cruft_list.sort() size = 0 count = 0 for full_path in cruft_list: current_size = os.path.getsize(full_path) size += current_size count += 1 self.emit('find_object', CacheObject(os.path.basename(full_path), full_path, current_size), count) self.emit('scan_finished', True, len(cruft_list), size) @classmethod def get_path(cls): if cls.root_path.startswith('~'): return os.path.expanduser(cls.root_path) else: return cls.root_path def get_cruft_by_path(self, root_path=None): if root_path is None: root_path = self.get_path() try: count = 0 total_size = 0 for root, dirs, files in os.walk(root_path): if root == root_path and dirs: dirs.sort() files.sort() to_deleted = dirs + files for path in to_deleted: full_path = os.path.join(root_path, path) try: size = os.popen('du -bs "%s"' % full_path).read().split()[0] except Exception: size = 0 count += 1 total_size += int(size) self.emit('find_object', CacheObject(path, full_path, size), count) else: continue self.emit('scan_finished', True, count, total_size) except Exception as e: log.error(e) self.emit('scan_error', e) def get_summary(self, count): if count: return '[%d] %s' % (count, self.__title__) else: return '%s (%s)' % (self.__title__, _('No cache to be cleaned')) class JanitorPage(Gtk.VBox, GuiBuilder): (JANITOR_CHECK, JANITOR_ICON, JANITOR_NAME, JANITOR_DISPLAY, JANITOR_PLUGIN, JANITOR_SPINNER_ACTIVE, JANITOR_SPINNER_PULSE) = range(7) (RESULT_CHECK, RESULT_ICON, RESULT_NAME, RESULT_DISPLAY, RESULT_DESC, RESULT_PLUGIN, RESULT_CRUFT) = range(7) max_janitor_view_width = 0 def __init__(self): GObject.GObject.__init__(self) self.scan_tasks = [] self.clean_tasks = [] self._total_count = 0 self.set_border_width(6) GuiBuilder.__init__(self, 'janitorpage.xml') self.pack_start(self.vbox1, True, True, 0) self.connect('realize', self.setup_ui_tasks) self.janitor_view.get_selection().connect('changed', self.on_janitor_selection_changed) self.show() def on_move_handle(self, widget, gproperty): log.debug("on_move_handle: %d", widget.get_property('position')) self.janitor_view.set_size_request(self.max_janitor_view_width, -1) def is_auto_scan(self): return True @log_func(log) def on_result_view_row_activated(self, treeview, path, column): iter = self.result_model.get_iter(path) cruft = self.result_model[iter][self.RESULT_CRUFT] display = self.result_model[iter][self.RESULT_DISPLAY] if 'red' in display: plugin = self.result_model[iter][self.RESULT_PLUGIN] error = plugin.get_property('error') self.result_model[iter][self.RESULT_DISPLAY] = '<span color="red"><b>%s</b></span>' % error elif hasattr(cruft, 'get_path'): path = cruft.get_path() if not os.path.isdir(path): path = os.path.dirname(path) os.system("xdg-open '%s' &" % path) def setup_ui_tasks(self, widget): self.janitor_model.set_sort_column_id(self.JANITOR_NAME, Gtk.SortType.ASCENDING) #add janitor columns janitor_column = Gtk.TreeViewColumn() renderer = Gtk.CellRendererToggle() renderer.connect('toggled', self.on_janitor_check_button_toggled) janitor_column.pack_start(renderer, False) janitor_column.add_attribute(renderer, 'active', self.JANITOR_CHECK) self.janitor_view.append_column(janitor_column) janitor_column = Gtk.TreeViewColumn() renderer = Gtk.CellRendererPixbuf() janitor_column.pack_start(renderer, False) janitor_column.add_attribute(renderer, 'pixbuf', self.JANITOR_ICON) janitor_column.set_cell_data_func(renderer, self.icon_column_view_func, self.JANITOR_ICON) renderer = Gtk.CellRendererText() renderer.set_property('ellipsize', Pango.EllipsizeMode.END) janitor_column.pack_start(renderer, True) janitor_column.add_attribute(renderer, 'markup', self.JANITOR_DISPLAY) renderer = Gtk.CellRendererSpinner() janitor_column.pack_start(renderer, False) janitor_column.add_attribute(renderer, 'active', self.JANITOR_SPINNER_ACTIVE) janitor_column.add_attribute(renderer, 'pulse', self.JANITOR_SPINNER_PULSE) self.janitor_view.append_column(janitor_column) #end janitor columns #new result columns result_display_renderer = self.builder.get_object('result_display_renderer') result_display_renderer.set_property('ellipsize', Pango.EllipsizeMode.END) result_icon_renderer= self.builder.get_object('result_icon_renderer') self.result_column.set_cell_data_func(result_icon_renderer, self.icon_column_view_func, self.RESULT_ICON) #end new result columns self.scan_button.set_visible(not self.is_auto_scan()) self.update_model() self._expand_janitor_view() self.hpaned1.connect('notify::position', self.on_move_handle) def _expand_janitor_view(self): self.janitor_view.expand_all() log.debug("max_janitor_view_width is: %d" % self.max_janitor_view_width) if self.max_janitor_view_width: self.janitor_view.set_size_request(self.max_janitor_view_width, -1) def set_busy(self): self.get_parent_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) def unset_busy(self): self.get_parent_window().set_cursor(None) def on_janitor_selection_changed(self, selection): model, iter = selection.get_selected() if iter: if self.janitor_model.iter_has_child(iter): iter = self.janitor_model.iter_children(iter) plugin = model[iter][self.JANITOR_PLUGIN] for row in self.result_model: if row[self.RESULT_PLUGIN] == plugin: self.result_view.get_selection().select_path(row.path) log.debug("scroll_to_cell: %s" % row.path) self.result_view.scroll_to_cell(row.path) def _is_scanning_or_cleaning(self): for row in self.janitor_model: for child_row in row.iterchildren(): if child_row[self.JANITOR_SPINNER_ACTIVE]: return True else: return False def on_janitor_check_button_toggled(self, cell, path): self.result_view.show() self.happy_box.hide() iter = self.janitor_model.get_iter(path) if self._is_scanning_or_cleaning(): return checked = not self.janitor_model[iter][self.JANITOR_CHECK] if self.janitor_model.iter_has_child(iter): child_iter = self.janitor_model.iter_children(iter) while child_iter: self.janitor_model[child_iter][self.JANITOR_CHECK] = checked child_iter = self.janitor_model.iter_next(child_iter) self.janitor_model[iter][self.JANITOR_CHECK] = checked self._check_child_is_all_the_same(self.janitor_model, iter, self.JANITOR_CHECK, checked) if self.is_auto_scan(): self._auto_scan_cruft(iter, checked) def _update_clean_button_sensitive(self): self.clean_button.set_sensitive(False) for row in self.result_model: for child_row in row.iterchildren(): if child_row[self.RESULT_CHECK]: self.clean_button.set_sensitive(True) break def on_result_check_renderer_toggled(self, cell, path): iter = self.result_model.get_iter(path) checked = self.result_model[iter][self.RESULT_CHECK] if self._is_scanning_or_cleaning(): return if self.result_model.iter_has_child(iter): child_iter = self.result_model.iter_children(iter) while child_iter: self.result_model[child_iter][self.RESULT_CHECK] = not checked child_iter = self.result_model.iter_next(child_iter) self.result_model[iter][self.RESULT_CHECK] = not checked self._check_child_is_all_the_same(self.result_model, iter, self.RESULT_CHECK, not checked) self._update_clean_button_sensitive() def _check_child_is_all_the_same(self, model, iter, column_id, status): iter = model.iter_parent(iter) if iter: child_iter = model.iter_children(iter) while child_iter: if status != model[child_iter][column_id]: model[iter][column_id] = False break child_iter = model.iter_next(child_iter) else: model[iter][column_id] = status def on_scan_button_clicked(self, widget=None): self.result_model.clear() self.clean_button.set_sensitive(False) scan_dict = OrderedDict() for row in self.janitor_model: for child_row in row.iterchildren(): checked = child_row[self.JANITOR_CHECK] scan_dict[child_row.iter] = checked self.scan_tasks = list(scan_dict.items()) self._total_count = 0 self.result_view.show() self.happy_box.hide() self.set_busy() self.do_scan_task() def _auto_scan_cruft(self, iter, checked): self.set_busy() scan_dict = OrderedDict() if self.janitor_model.iter_has_child(iter): log.info('Scan cruft for all plugins') #Scan cruft for children child_iter = self.janitor_model.iter_children(iter) while child_iter: scan_dict[child_iter] = checked child_iter = self.janitor_model.iter_next(child_iter) else: scan_dict[iter] = checked self.scan_tasks = list(scan_dict.items()) for plugin_iter, checked in self.scan_tasks: plugin = self.janitor_model[plugin_iter][self.JANITOR_PLUGIN] for row in self.result_model: if row[self.RESULT_PLUGIN] == plugin: self.result_model.remove(row.iter) self.do_scan_task() def do_scan_task(self): plugin_iter, checked = self.scan_tasks.pop(0) plugin = self.janitor_model[plugin_iter][self.JANITOR_PLUGIN] plugin.set_property('scan_finished', False) log.debug("do_scan_task for %s for status: %s" % (plugin, checked)) if checked: log.info('Scan cruft for plugin: %s' % plugin.get_name()) iter = self.result_model.append(None, (None, None, plugin.get_title(), '<b>%s</b>' % _('Scanning cruft for "%s"...') % plugin.get_title(), None, plugin, None)) self.janitor_model[plugin_iter][self.JANITOR_SPINNER_ACTIVE] = True self.janitor_model[plugin_iter][self.JANITOR_SPINNER_PULSE] = 0 self.janitor_view.scroll_to_cell(self.janitor_model.get_path(plugin_iter)) self._find_handler = plugin.connect('find_object', self.on_find_object, (plugin_iter, iter)) self._scan_handler = plugin.connect('scan_finished', self.on_scan_finished, (plugin_iter, iter)) self._error_handler = plugin.connect('scan_error', self.on_scan_error, (plugin_iter, iter)) t = threading.Thread(target=plugin.get_cruft) GObject.timeout_add(50, self._on_spinner_timeout, plugin_iter, t) t.start() else: # Update the janitor title for row in self.janitor_model: for child_row in row.iterchildren(): if child_row[self.JANITOR_PLUGIN] == plugin: child_row[self.JANITOR_DISPLAY] = plugin.get_title() if self.scan_tasks: self.do_scan_task() else: if self._total_count == 0: self.result_view.hide() self.happy_box.show() else: self.result_view.show() self.happy_box.hide() self.unset_busy() def _on_spinner_timeout(self, plugin_iter, thread): plugin = self.janitor_model[plugin_iter][self.JANITOR_PLUGIN] finished = plugin.get_property('scan_finished') self.janitor_model[plugin_iter][self.JANITOR_SPINNER_PULSE] += 1 if finished: for handler in (self._find_handler, self._scan_handler, self._error_handler): if plugin.handler_is_connected(handler): log.debug("Disconnect the cleaned signal, or it will clean many times: %s" % plugin) plugin.disconnect(handler) self.janitor_model[plugin_iter][self.JANITOR_SPINNER_ACTIVE] = False thread.join() if len(self.scan_tasks) != 0: log.debug("Pending scan tasks: %d" % len(self.scan_tasks)) self.do_scan_task() else: log.debug("total_count is: %d" % self._total_count) if self._total_count == 0: self.result_view.hide() self.happy_box.show() else: self.result_view.show() self.happy_box.hide() self.unset_busy() return not finished @post_ui def on_find_object(self, plugin, cruft, count, iters): while Gtk.events_pending(): Gtk.main_iteration() plugin_iter, result_iter = iters self.result_model.append(result_iter, (False, cruft.get_icon(), cruft.get_name(), cruft.get_name(), cruft.get_size_display(), plugin, cruft)) self.result_view.expand_row(self.result_model.get_path(result_iter), True) # Update the janitor title if count: self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] = "<b>[%d] %s</b>" % (count, plugin.get_title()) else: self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] = "[0] %s" % plugin.get_title() @post_ui def on_scan_finished(self, plugin, result, count, size, iters): plugin.disconnect(self._find_handler) plugin.disconnect(self._scan_handler) plugin.set_property('scan_finished', True) plugin_iter, result_iter = iters if count == 0: self.result_model.remove(result_iter) else: self.result_model[result_iter][self.RESULT_DISPLAY] = "<b>%s</b>" % plugin.get_summary(count) if size != 0: self.result_model[result_iter][self.RESULT_DESC] = "<b>%s</b>" % filesizeformat(size) # Update the janitor title self._total_count += count if count: self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] = "<b>[%d] %s</b>" % (count, plugin.get_title()) self.result_view.collapse_row(self.result_model.get_path(result_iter)) else: self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] = "[0] %s" % plugin.get_title() @post_ui def on_scan_error(self, plugin, error, iters): plugin_iter, result_iter = iters self.janitor_model[plugin_iter][self.JANITOR_ICON] = icon.get_from_name('error', size=16) self.result_model[result_iter][self.RESULT_DISPLAY] = '<span color="red"><b>%s</b></span>' % _('Scan error for "%s", double-click to see details') % plugin.get_title() plugin.set_property('scan_finished', True) plugin.set_property('error', error) def on_clean_button_clicked(self, widget): self.plugin_to_run = 0 self.set_busy() self.clean_button.set_sensitive(False) plugin_dict = OrderedDict() for row in self.result_model: plugin = row[self.RESULT_PLUGIN] cruft_dict = OrderedDict() for child_row in row.iterchildren(): checked = child_row[self.RESULT_CHECK] if checked: cruft_dict[child_row[self.RESULT_CRUFT]] = child_row.iter if cruft_dict: plugin_dict[plugin] = cruft_dict self.clean_tasks = list(plugin_dict.items()) self.do_real_clean_task() log.debug("All finished!") def do_real_clean_task(self): if len(self.clean_tasks) != 0: plugin, cruft_dict = self.clean_tasks.pop(0) plugin.set_property('clean_finished', False) for row in self.janitor_model: for child_row in row.iterchildren(): if child_row[self.JANITOR_PLUGIN] == plugin: plugin_iter = child_row.iter log.debug("Call %s to clean cruft" % plugin) self._object_clean_handler = plugin.connect('object_cleaned', self.on_plugin_object_cleaned, (plugin_iter, cruft_dict)) self._all_clean_handler = plugin.connect('all_cleaned', self.on_plugin_cleaned, plugin_iter) self._error_handler = plugin.connect('clean_error', self.on_clean_error, plugin_iter) self.janitor_view.scroll_to_cell(self.janitor_model.get_path(plugin_iter)) t = threading.Thread(target=plugin.clean_cruft, kwargs={'cruft_list': cruft_dict.keys(), 'parent': self.get_toplevel()}) for row in self.result_model: if row[self.RESULT_PLUGIN] == plugin: self.result_view.get_selection().select_path(row.path) self.result_view.scroll_to_cell(row.path) row[self.RESULT_DISPLAY] = '<b>%s</b>' % _('Cleaning cruft for "%s"...') % plugin.get_title() self.result_view.expand_row(self.result_model.get_path(row.iter), True) self.janitor_model[plugin_iter][self.JANITOR_SPINNER_ACTIVE] = True self.janitor_model[plugin_iter][self.JANITOR_SPINNER_PULSE] = 0 GObject.timeout_add(50, self._on_clean_spinner_timeout, plugin_iter, t) t.start() else: self.on_scan_button_clicked() self.unset_busy() def _on_clean_spinner_timeout(self, plugin_iter, thread): plugin = self.janitor_model[plugin_iter][self.JANITOR_PLUGIN] finished = plugin.get_property('clean_finished') self.janitor_model[plugin_iter][self.JANITOR_SPINNER_PULSE] += 1 if finished: log.debug("Disconnect the cleaned signal for %s, or it will clean many times" % plugin) for handler in (self._object_clean_handler, self._all_clean_handler, self._error_handler): if plugin.handler_is_connected(handler): plugin.disconnect(handler) self.janitor_model[plugin_iter][self.JANITOR_SPINNER_ACTIVE] = False thread.join() self.do_real_clean_task() return not finished @post_ui def on_plugin_object_cleaned(self, plugin, cruft, count, user_data): while Gtk.events_pending(): Gtk.main_iteration() plugin_iter, cruft_dict = user_data self.result_model.remove(cruft_dict[cruft]) self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] remain = len(cruft_dict) - count if remain: self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] = "<b>[%d] %s</b>" % (remain, plugin.get_title()) else: self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] = "[0] %s" % plugin.get_title() def on_plugin_cleaned(self, plugin, cleaned, plugin_iter): #TODO should accept the cruft_list plugin.set_property('clean_finished', True) self.janitor_model[plugin_iter][self.JANITOR_DISPLAY] = "[0] %s" % plugin.get_title() def on_clean_error(self, plugin, error, plugin_iter): #TODO response to user? self.janitor_model[plugin_iter][self.JANITOR_ICON] = icon.get_from_name('error', size=16) self.clean_tasks = [] plugin.set_property('clean_finished', True) def icon_column_view_func(self, cell_layout, renderer, model, iter, id): if model[iter][id] == None: renderer.set_property("visible", False) else: renderer.set_property("visible", True) def update_model(self, a=None, b=None, expand=False): self.janitor_model.clear() self.result_model.clear() size_list = [] loader = ModuleLoader('janitor') system_text = _('System') iter = self.janitor_model.append(None, (None, icon.get_from_name('distributor-logo'), system_text, "<b><big>%s</big></b>" % system_text, None, None, None)) for plugin in loader.get_modules_by_category('system'): size_list.append(Gtk.Label(label=plugin.get_title()).get_layout().get_pixel_size()[0]) self.janitor_model.append(iter, (False, None, plugin.get_title(), plugin.get_title(), plugin(), None, None)) personal_text = _('Personal') iter = self.janitor_model.append(None, (None, icon.get_from_name('system-users'), personal_text, "<b><big>%s</big></b>" % personal_text, None, None, None)) for plugin in loader.get_modules_by_category('personal'): size_list.append(Gtk.Label(label=plugin.get_title()).get_layout().get_pixel_size()[0]) self.janitor_model.append(iter, (False, None, plugin.get_title(), plugin.get_title(), plugin(), None, None)) app_text = _('Apps') iter = self.janitor_model.append(None, (None, icon.get_from_name('gnome-app-install'), app_text, "<b><big>%s</big></b>" % app_text, None, None, None)) for plugin in loader.get_modules_by_category('application'): size_list.append(Gtk.Label(label=plugin.get_title()).get_layout().get_pixel_size()[0]) self.janitor_model.append(iter, (False, None, plugin.get_title(), plugin.get_title(), plugin(), None, None)) if size_list: self.max_janitor_view_width = max(size_list) + 80 if expand: self._expand_janitor_view()