Add SIGWINCH support so that the progress bar is resized if the xterm is resized.
[portage.git] / bin / emaint
1 #!/usr/bin/python -O
2
3 import sys, os, time, signal
4 from optparse import OptionParser, OptionValueError
5 if not hasattr(__builtins__, "set"):
6         from sets import Set as set
7 import re
8 try:
9         import portage
10 except ImportError:
11         from os import path as osp
12         sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
13         import portage
14
15 import portage.const, portage.exception, portage.output
16 class WorldHandler(object):
17
18         def name():
19                 return "world"
20         name = staticmethod(name)
21
22         def __init__(self):
23                 self.invalid = []
24                 self.not_installed = []
25                 self.invalid_category = []
26                 self.okay = []
27                 self.world_file = os.path.join("/", portage.const.WORLD_FILE)
28                 self.found = os.access(self.world_file, os.R_OK)
29
30         def _check_world(self, onProgress):
31                 categories = set(portage.settings.categories)
32                 myroot = portage.settings["ROOT"]
33                 vardb = portage.db[myroot]["vartree"].dbapi
34
35                 world_atoms = open(self.world_file).read().split()
36                 maxval = len(world_atoms)
37                 if onProgress:
38                         onProgress(maxval, 0)
39                 for i, atom in enumerate(world_atoms):
40                         if not portage.isvalidatom(atom):
41                                 self.invalid.append(atom)
42                                 if onProgress:
43                                         onProgress(maxval, i+1)
44                                 continue
45                         okay = True
46                         if not vardb.match(atom):
47                                 self.not_installed.append(atom)
48                                 okay = False
49                         if portage.catsplit(atom)[0] not in categories:
50                                 self.invalid_category.append(atom)
51                                 okay = False
52                         if okay:
53                                 self.okay.append(atom)
54                         if onProgress:
55                                 onProgress(maxval, i+1)
56
57         def check(self, onProgress=None):
58                 self._check_world(onProgress)
59                 errors = []
60                 if self.found:
61                         errors += map(lambda x: "'%s' is not a valid atom" % x, self.invalid)
62                         errors += map(lambda x: "'%s' is not installed" % x, self.not_installed)
63                         errors += map(lambda x: "'%s' has a category that is not listed in /etc/portage/categories" % x, self.invalid_category)
64                 else:
65                         errors.append(self.world_file + " could not be opened for reading")
66                 return errors
67
68         def fix(self, onProgress=None):
69                 self._check_world(onProgress)
70                 errors = []
71                 try:
72                         portage.write_atomic(self.world_file, "\n".join(self.okay))
73                 except portage.exception.PortageException:
74                         errors.append(self.world_file + " could not be opened for writing")
75                 return errors
76
77 class VdbKeyHandler(object):
78         def name():
79                 return "vdbkeys"
80         name = staticmethod(name)
81
82         def __init__(self):
83                 self.list = portage.db["/"]["vartree"].dbapi.cpv_all()
84                 self.missing = []
85                 self.keys = ["HOMEPAGE", "SRC_URI", "KEYWORDS", "DESCRIPTION"]
86                 
87                 for p in self.list:
88                         mydir = os.path.join(os.sep, portage.settings["ROOT"], portage.const.VDB_PATH, p)+os.sep
89                         ismissing = True
90                         for k in self.keys:
91                                 if os.path.exists(mydir+k):
92                                         ismissing = False
93                                         break
94                         if ismissing:
95                                 self.missing.append(p)
96                 
97         def check(self):
98                 return ["%s has missing keys" % x for x in self.missing]
99         
100         def fix(self):
101         
102                 errors = []
103         
104                 for p in self.missing:
105                         mydir = os.path.join(os.sep, portage.settings["ROOT"], portage.const.VDB_PATH, p)+os.sep
106                         if not os.access(mydir+"environment.bz2", os.R_OK):
107                                 errors.append("Can't access %s" % (mydir+"environment.bz2"))
108                         elif not os.access(mydir, os.W_OK):
109                                 errors.append("Can't create files in %s" % mydir)
110                         else:
111                                 env = os.popen("bzip2 -dcq "+mydir+"environment.bz2", "r")
112                                 envlines = env.read().split("\n")
113                                 env.close()
114                                 for k in self.keys:
115                                         s = [l for l in envlines if l.startswith(k+"=")]
116                                         if len(s) > 1:
117                                                 errors.append("multiple matches for %s found in %senvironment.bz2" % (k, mydir))
118                                         elif len(s) == 0:
119                                                 s = ""
120                                         else:
121                                                 s = s[0].split("=",1)[1]
122                                                 s = s.lstrip("$").strip("\'\"")
123                                                 s = re.sub("(\\\\[nrt])+", " ", s)
124                                                 s = " ".join(s.split()).strip()
125                                                 if s != "":
126                                                         try:
127                                                                 keyfile = open(mydir+os.sep+k, "w")
128                                                                 keyfile.write(s+"\n")
129                                                                 keyfile.close()
130                                                         except (IOError, OSError), e:
131                                                                 errors.append("Could not write %s, reason was: %s" % (mydir+k, e))
132                 
133                 return errors
134
135 class ProgressHandler(object):
136         def __init__(self):
137                 self.curval = 0
138                 self.maxval = 0
139                 self.last_update = 0
140                 self.min_display_latency = 0.2
141
142         def onProgress(self, maxval, curval):
143                 self.maxval = maxval
144                 self.curval = curval
145                 cur_time = time.time()
146                 if cur_time - self.last_update >= self.min_display_latency:
147                         self.last_update = cur_time
148                         self.display()
149
150         def display(self):
151                 raise NotImplementedError(self)
152
153 def emaint_main(myargv):
154
155         # TODO: Create a system that allows external modules to be added without
156         #       the need for hard coding.
157         modules = {"world" : WorldHandler}
158
159         module_names = modules.keys()
160         module_names.sort()
161         module_names.insert(0, "all")
162
163         def exclusive(option, *args, **kw):
164                 var = kw.get("var", None)
165                 if var is None:
166                         raise ValueError("var not specified to exclusive()")
167                 if getattr(parser, var, ""):
168                         raise OptionValueError("%s and %s are exclusive options" % (getattr(parser, var), option))
169                 setattr(parser, var, str(option))
170
171
172         usage = "usage: emaint [options] " + " | ".join(module_names)
173
174         usage+= "\n\nCurrently emaint can only check and fix problems with one's world\n"
175         usage+= "file.  Future versions will integrate other portage check-and-fix\n"
176         usage+= "tools and provide a single interface to system health checks."
177
178
179         parser = OptionParser(usage=usage)
180         parser.add_option("-c", "--check", help="check for problems",
181                 action="callback", callback=exclusive, callback_kwargs={"var":"action"})
182         parser.add_option("-f", "--fix", help="attempt to fix problems",
183                 action="callback", callback=exclusive, callback_kwargs={"var":"action"})
184         parser.action = None
185
186
187         (options, args) = parser.parse_args(args=myargv)
188         if len(args) != 1:
189                 parser.error("Incorrect number of arguments")
190         if args[0] not in module_names:
191                 parser.error("%s target is not a known target" % args[0])
192
193         if parser.action:
194                 action = parser.action
195         else:
196                 print "Defaulting to --check"
197                 action = "-c/--check"
198
199         if args[0] == "all":
200                 tasks = modules.values()
201         else:
202                 tasks = [modules[args[0]]]
203
204
205         if action == "-c/--check":
206                 status = "Checking %s for problems"
207                 func = "check"
208         else:
209                 status = "Attempting to fix %s"
210                 func = "fix"
211
212         isatty = sys.stdout.isatty()
213         for task in tasks:
214                 print status % task.name()
215                 inst = task()
216                 onProgress = None
217                 if isatty:
218                         progressBar = portage.output.TermProgressBar()
219                         progressHandler = ProgressHandler()
220                         def display():
221                                 progressBar.set(progressHandler.curval, progressHandler.maxval)
222                         progressHandler.display = display
223                         def sigwinch_handler(signum, frame):
224                                 lines, progressBar.term_columns = \
225                                         portage.output.get_term_size()
226                         signal.signal(signal.SIGWINCH, sigwinch_handler)
227                 result = getattr(inst, func)(onProgress=progressHandler.onProgress)
228                 if isatty:
229                         # make sure the final progress is displayed
230                         progressHandler.display()
231                         print
232                         signal.signal(signal.SIGWINCH, signal.SIG_DFL)
233                 if result:
234                         print
235                         print "\n".join(result)
236                         print "\n"
237
238         print "Finished"
239
240 if __name__ == "__main__":
241         emaint_main(sys.argv[1:])