# portage: news management code # Copyright 2006 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Id$ import errno import os import re from portage.const import INCREMENTALS, PROFILE_PATH, NEWS_LIB_PATH from portage.util import ensure_dirs, apply_permissions, normalize_path, grabfile, write_atomic from portage.data import portage_gid from portage.locks import lockfile, unlockfile, lockdir, unlockdir from portage.exception import FileNotFound, OperationNotPermitted class NewsManager(object): """ This object manages GLEP 42 style news items. It will cache news items that have previously shown up and notify users when there are relevant news items that apply to their packages that the user has not previously read. Creating a news manager requires: root - typically ${ROOT} see man make.conf and man emerge for details news_path - path to news items; usually $REPODIR/metadata/news unread_path - path to the news.repoid.unread file; this helps us track news items """ def __init__(self, portdb, vardb, news_path, unread_path, language_id='en'): self.news_path = news_path self.unread_path = unread_path self.target_root = vardb.root self.language_id = language_id self.config = vardb.settings self.vdb = vardb self.portdb = portdb portdir = portdb.porttree_root profiles_base = os.path.join(portdir, 'profiles') + os.path.sep profile_path = None if portdb.mysettings.profile_path: profile_path = normalize_path( os.path.realpath(portdb.mysettings.profile_path)) if profile_path.startswith(profiles_base): profile_path = profile_path[len(profiles_base):] self._profile_path = profile_path # Ensure that the unread path exists and is writable. dirmode = 02070 modemask = 02 try: ensure_dirs(self.unread_path, mode=dirmode, mask=modemask, gid=portage_gid) except OperationNotPermitted: pass def updateItems(self, repoid): """ Figure out which news items from NEWS_PATH are both unread and relevant to the user (according to the GLEP 42 standards of relevancy). Then add these items into the news.repoid.unread file. """ repos = self.portdb.getRepositories() if repoid not in repos: raise ValueError("Invalid repoID: %s" % repoid) path = os.path.join(self.portdb.getRepositoryPath(repoid), self.news_path) # Skip reading news for repoid if the news dir does not exist. Requested by # NightMorph :) if not os.path.exists(path): return None news = os.listdir(path) skipfile = os.path.join(self.unread_path, "news-%s.skip" % repoid) skiplist = grabfile(skipfile) updates = [] for itemid in news: if itemid in skiplist: continue try: filename = os.path.join(path, itemid, itemid + "." + self.language_id + ".txt") item = NewsItem(filename, itemid) except (TypeError): continue if item.isRelevant(profile=self._profile_path, config=self.config, vardb=self.vdb): updates.append(item) del path path = os.path.join(self.unread_path, 'news-%s.unread' % repoid) unread_lock = None try: unread_lock = lockfile(path) if not os.path.exists(path): #create the file if it does not exist open(path, "w") # Ensure correct perms on the unread file. apply_permissions( filename=path, uid=int(self.config['PORTAGE_INST_UID']), gid=portage_gid, mode=0664) # Make sure we have the correct permissions when created unread_file = open(path, 'a') for item in updates: unread_file.write(item.name + "\n") skiplist.append(item.name) unread_file.close() finally: if unread_lock: unlockfile(unread_lock) write_atomic(skipfile, "\n".join(skiplist)+"\n") try: apply_permissions(filename=skipfile, uid=int(self.config["PORTAGE_INST_UID"]), gid=portage_gid, mode=0664) except OperationNotPermitted, e: import errno # skip "permission denied" errors as we're likely running in pretend mode # with reduced priviledges if e.errno == errno.EPERM: pass else: raise def getUnreadItems(self, repoid, update=False): """ Determine if there are unread relevant items in news.repoid.unread. If there are unread items return their number. If update is specified, updateNewsItems( repoid ) will be called to check for new items. """ if update: self.updateItems(repoid) unreadfile = os.path.join(self.unread_path, 'news-%s.unread' % repoid) unread_lock = None try: if os.access(os.path.dirname(unreadfile), os.W_OK): # TODO: implement shared readonly locks unread_lock = lockfile(unreadfile) try: f = open(unreadfile) try: unread = f.readlines() finally: f.close() except EnvironmentError, e: if e.errno != errno.ENOENT: raise del e return 0 if len(unread): return len(unread) finally: if unread_lock: unlockfile(unread_lock) _installedRE = re.compile("Display-If-Installed:(.*)\n") _profileRE = re.compile("Display-If-Profile:(.*)\n") _keywordRE = re.compile("Display-If-Keyword:(.*)\n") class NewsItem(object): """ This class encapsulates a GLEP 42 style news item. It's purpose is to wrap parsing of these news items such that portage can determine whether a particular item is 'relevant' or not. This requires parsing the item and determining 'relevancy restrictions'; these include "Display if Installed" or "display if arch: x86" and so forth. Creation of a news item involves passing in the path to the particular news item. """ def __init__(self, path, name): """ For a given news item we only want if it path is a file. """ if not os.path.isfile(path): raise TypeError("%s is no regular file" % path) self.path = path self.name = name self._parsed = False def isRelevant(self, vardb, config, profile): """ This function takes a dict of keyword arguments; one should pass in any objects need to do to lookups (like what keywords we are on, what profile, and a vardb so we can look at installed packages). Each restriction will pluck out the items that are required for it to match or raise a ValueError exception if the required object is not present. """ if not len(self.restrictions): return True # no restrictions to match means everyone should see it kwargs = \ { 'vardb' : vardb, 'config' : config, 'profile' : profile } for restriction in self.restrictions: if restriction.checkRestriction(**kwargs): return True return False # No restrictions were met; thus we aren't relevant :( def parse(self): lines = open(self.path).readlines() self.restrictions = [] for line in lines: #Optimization to ignore regex matchines on lines that #will never match if not line.startswith('D'): continue restricts = { _installedRE : DisplayInstalledRestriction, _profileRE : DisplayProfileRestriction, _keywordRE : DisplayKeywordRestriction } for regex, restriction in restricts.iteritems(): match = regex.match(line) if match: self.restrictions.append(restriction(match.groups()[0].strip())) continue self._parsed = True def __getattr__(self, attr): if not self._parsed: self.parse() return self.__dict__[attr] class DisplayRestriction(object): """ A base restriction object representing a restriction of display. news items may have 'relevancy restrictions' preventing them from being important. In this case we need a manner of figuring out if a particular item is relevant or not. If any of it's restrictions are met, then it is displayed """ def checkRestriction(self, **kwargs): raise NotImplementedError('Derived class should over-ride this method') class DisplayProfileRestriction(DisplayRestriction): """ A profile restriction where a particular item shall only be displayed if the user is running a specific profile. """ def __init__(self, profile): self.profile = profile def checkRestriction(self, **kwargs): if self.profile == kwargs['profile']: return True return False class DisplayKeywordRestriction(DisplayRestriction): """ A keyword restriction where a particular item shall only be displayed if the user is running a specific keyword. """ def __init__(self, keyword): self.keyword = keyword def checkRestriction(self, **kwargs): if kwargs['config']['ARCH'] == self.keyword: return True return False class DisplayInstalledRestriction(DisplayRestriction): """ An Installation restriction where a particular item shall only be displayed if the user has that item installed. """ def __init__(self, cpv): self.cpv = cpv def checkRestriction(self, **kwargs): vdb = kwargs['vardb'] if vdb.match(self.cpv): return True return False