Gentoo Archives: gentoo-portage-dev

From: Alec Warner <antarus@g.o>
To: gentoo-portage-dev@l.g.o
Cc: Pavel Kazakov <nullishzero@g.o>
Subject: Re: [gentoo-portage-dev] [PATCH 3/3] Add an emaint module that can scan for failed merges and that can fix failed merges.
Date: Thu, 13 Mar 2014 22:09:23
Message-Id: CAAr7Pr8x0LRAsDgkqFYKb=rs4tfnnVkJAAq-c+DNw-7ujcGC1w@mail.gmail.com
In Reply to: [gentoo-portage-dev] [PATCH 3/3] Add an emaint module that can scan for failed merges and that can fix failed merges. by Pavel Kazakov
1 On Wed, Mar 12, 2014 at 11:10 PM, Pavel Kazakov <nullishzero@g.o>wrote:
2
3 > ---
4 > pym/portage/emaint/main.py | 6 +-
5 > pym/portage/emaint/modules/merges/__init__.py | 30 +++
6 > pym/portage/emaint/modules/merges/merges.py | 281
7 > ++++++++++++++++++++++++++
8 > 3 files changed, 315 insertions(+), 2 deletions(-)
9 > create mode 100644 pym/portage/emaint/modules/merges/__init__.py
10 > create mode 100644 pym/portage/emaint/modules/merges/merges.py
11 >
12 > diff --git a/pym/portage/emaint/main.py b/pym/portage/emaint/main.py
13 > index 6a17027..646883d 100644
14 > --- a/pym/portage/emaint/main.py
15 > +++ b/pym/portage/emaint/main.py
16 > @@ -98,10 +98,11 @@ def module_opts(module_controller, module):
17 > class TaskHandler(object):
18 > """Handles the running of the tasks it is given"""
19 >
20 > - def __init__(self, show_progress_bar=True, verbose=True,
21 > callback=None):
22 > + def __init__(self, show_progress_bar=True, verbose=True,
23 > callback=None, module_output=None):
24 > self.show_progress_bar = show_progress_bar
25 > self.verbose = verbose
26 > self.callback = callback
27 > + self.module_output = module_output
28 > self.isatty = os.environ.get('TERM') != 'dumb' and
29 > sys.stdout.isatty()
30 > self.progress_bar = ProgressBar(self.isatty,
31 > title="Emaint", max_desc_length=27)
32 >
33 > @@ -124,6 +125,7 @@ class TaskHandler(object):
34 > onProgress = None
35 > kwargs = {
36 > 'onProgress': onProgress,
37 > + 'module_output': self.module_output,
38 > # pass in a copy of the options so a
39 > module can not pollute or change
40 > # them for other tasks if there is more to
41 > do.
42 > 'options': options.copy()
43 > @@ -219,5 +221,5 @@ def emaint_main(myargv):
44 > # need to pass the parser options dict to the modules
45 > # so they are available if needed.
46 > task_opts = options.__dict__
47 > - taskmaster = TaskHandler(callback=print_results)
48 > + taskmaster = TaskHandler(callback=print_results,
49 > module_output=sys.stdout)
50 > taskmaster.run_tasks(tasks, func, status, options=task_opts)
51 > diff --git a/pym/portage/emaint/modules/merges/__init__.py
52 > b/pym/portage/emaint/modules/merges/__init__.py
53 > new file mode 100644
54 > index 0000000..96ee71b
55 > --- /dev/null
56 > +++ b/pym/portage/emaint/modules/merges/__init__.py
57 > @@ -0,0 +1,30 @@
58 > +# Copyright 2005-2014 Gentoo Foundation
59 > +# Distributed under the terms of the GNU General Public License v2
60 > +
61 > +"""Scan for failed merges and fix them."""
62 > +
63 > +
64 > +module_spec = {
65 > + 'name': 'merges',
66 > + 'description': __doc__,
67 > + 'provides': {
68 > + 'merges': {
69 > + 'name': "merges",
70 > + 'class': "MergesHandler",
71 > + 'description': __doc__,
72 > + 'functions': ['check', 'fix', 'purge'],
73 > + 'func_desc': {
74 > + 'purge': {
75 > + 'short': '-P', 'long':
76 > '--purge-tracker',
77 > + 'help': 'Removes the list of
78 > previously failed merges.' +
79 > + ' WARNING: Only
80 > use this option if you plan on' +
81 > + ' manually fixing
82 > them or do not want them'
83 > + ' re-installed.',
84 > + 'status': "Removing %s",
85 > + 'action': 'store_true',
86 > + 'func': 'purge'
87 > + }
88 > + }
89 > + }
90 > + }
91 > +}
92 > diff --git a/pym/portage/emaint/modules/merges/merges.py
93 > b/pym/portage/emaint/modules/merges/merges.py
94 > new file mode 100644
95 > index 0000000..a99dad4
96 > --- /dev/null
97 > +++ b/pym/portage/emaint/modules/merges/merges.py
98 > @@ -0,0 +1,281 @@
99 > +# Copyright 2005-2014 Gentoo Foundation
100 > +# Distributed under the terms of the GNU General Public License v2
101 > +
102 > +from _emerge.actions import load_emerge_config
103 > +
104 > +import portage
105 > +from portage import os, _unicode_encode
106 > +from portage.const import MERGING_IDENTIFIER, PORTAGE_BIN_PATH,
107 > PRIVATE_PATH, \
108 > + VDB_PATH
109 > +from portage.dep import isvalidatom
110 > +
111 >
112
113 import shutil
114 import subprocess
115 import sys
116 import time
117
118 Alphabetical order.
119
120
121 > +import time
122 > +import shutil
123 > +import sys
124 > +import subprocess
125 > +
126 > +class TrackingFile(object):
127 > + """File for keeping track of failed merges."""
128 > +
129 > +
130 > + def __init__(self, tracking_path):
131 > + """
132 > + Create a TrackingFile object.
133 > +
134 > + @param tracking_path: file path used to keep track of
135 > failed merges
136 > + @type tracking_path: String
137 > + """
138 > + self._tracking_path = _unicode_encode(tracking_path)
139 > +
140 > +
141 > + def save(self, failed_pkgs):
142 > + """
143 > + Save the specified packages that failed to merge.
144 > +
145 > + @param failed_pkgs: dictionary of failed packages
146 > + @type failed_pkgs: dict
147 > + """
148 > + tracking_path = self._tracking_path
149 >
150
151 You don't appear to do any atomic operations or file locking here, what if
152 2 callers are trying to write to the same filename at once?
153 Normally we write to a temporary file and perform an atomic rename (which
154 prevents this sort of thing.)
155
156
157 > + with open(tracking_path, 'w') as tracking_file:
158 > + for pkg, mtime in failed_pkgs.items():
159 > + tracking_file.write('%s %s\n' % (pkg,
160 > mtime))
161 > +
162 > +
163 > + def load(self):
164 > + """
165 > + Load previously failed merges.
166 > +
167 > + @rtype: dict
168 > + @return: dictionary of packages that failed to merge
169 > + """
170 >
171 + tracking_path = self._tracking_path
172 > + if not os.path.exists(tracking_path):
173 > + return {}
174 >
175
176 if not self.exists():
177 return {}
178
179
180 > + failed_pkgs = {}
181 > + with open(tracking_path, 'r') as tracking_file:
182 > + for failed_merge in tracking_file:
183 > + pkg, mtime = failed_merge.strip().split()
184 > + failed_pkgs[pkg] = mtime
185 > + return failed_pkgs
186 > +
187 > +
188 > + def exists(self):
189 > + """
190 > + Check if tracking file exists.
191 > +
192 > + @rtype: bool
193 > + @return: true if tracking file exists, false otherwise
194 > + """
195 > + return os.path.exists(self._tracking_path)
196 > +
197 > +
198 > + def purge(self):
199 > + """Delete previously saved tracking file if one exists."""
200 > + if self.exists():
201 > + os.remove(self._tracking_path)
202 > +
203 > +
204 > +class MergesHandler(object):
205 > + """Handle failed package merges."""
206 > +
207 > + short_desc = "Remove failed merges"
208 > +
209 > + @staticmethod
210 > + def name():
211 > + return "merges"
212 > +
213 > +
214 > + def __init__(self):
215 > + """Create MergesHandler object."""
216 > + eroot = portage.settings['EROOT']
217 > + tracking_path = os.path.join(eroot, PRIVATE_PATH,
218 > 'failed-merges');
219 > + self._tracking_file = TrackingFile(tracking_path)
220 > + self._vardb_path = os.path.join(eroot, VDB_PATH)
221 > +
222 > +
223 > + def can_progressbar(self, func):
224 > + return func == 'check'
225 > +
226 > +
227 > + def _scan(self, onProgress=None):
228 > + """
229 > + Scan the file system for failed merges and return any
230 > found.
231 > +
232 > + @param onProgress: function to call for updating progress
233 > + @type onProgress: Function
234 > + @rtype: dict
235 > + @return: dictionary of packages that failed to merges
236 > + """
237 > + failed_pkgs = {}
238 > + for cat in os.listdir(self._vardb_path):
239 > + pkgs_path = os.path.join(self._vardb_path, cat)
240 > + if not os.path.isdir(pkgs_path):
241 > + continue
242 > + pkgs = os.listdir(pkgs_path)
243 > + maxval = len(pkgs)
244 > + for i, pkg in enumerate(pkgs):
245 > + if onProgress:
246 > + onProgress(maxval, i+1)
247 > + if MERGING_IDENTIFIER in pkg:
248 > + mtime =
249 > int(os.stat(os.path.join(pkgs_path, pkg)).st_mtime)
250 > + pkg = os.path.join(cat, pkg)
251 > + failed_pkgs[pkg] = mtime
252 > + return failed_pkgs
253 > +
254 > +
255 > + def _failed_pkgs(self, onProgress=None):
256 > + """
257 > + Return failed packages from both the file system and
258 > tracking file.
259 > +
260 > + @rtype: dict
261 > + @return: dictionary of packages that failed to merges
262 > + """
263 > + failed_pkgs = self._scan(onProgress)
264 >
265
266 Perhaps add an __iter__ function to TrackingFile, that implicitly calls
267 load?
268
269 Then this can be:
270 for pkg, mtime in self._tracking_file:
271
272 ?
273
274 Just a thought.
275
276
277 > + for pkg, mtime in self._tracking_file.load().items():
278 > + if pkg not in failed_pkgs:
279 > + failed_pkgs[pkg] = mtime
280 > + return failed_pkgs
281 > +
282 > +
283 > + def _remove_failed_dirs(self, failed_pkgs):
284 > + """
285 > + Remove the directories of packages that failed to merge.
286 > +
287 > + @param failed_pkgs: failed packages whose directories to
288 > remove
289 > + @type failed_pkg: dict
290 > + """
291 > + for failed_pkg in failed_pkgs:
292 > + pkg_path = os.path.join(self._vardb_path,
293 > failed_pkg)
294 > + # delete failed merge directory if it exists (it
295 > might not exist
296 > + # if loaded from tracking file)
297 > + if os.path.exists(pkg_path):
298 > + shutil.rmtree(pkg_path)
299 > + # TODO: try removing package contents to prevent
300 > orphaned
301 > + # files
302 >
303
304 I don't get this TODO, can you elaborate?
305
306
307 > +
308 > +
309 > + def _get_pkg_atoms(self, failed_pkgs, pkg_atoms, pkg_dne):
310 > + """
311 > + Get the package atoms for the specified failed packages.
312 > +
313 > + @param failed_pkgs: failed packages to iterate
314 > + @type failed_pkgs: dict
315 > + @param pkg_atoms: append package atoms to this set
316 > + @type pkg_atoms: set
317 > + @param pkg_dne: append any packages atoms that are invalid
318 > to this set
319 > + @type pkg_dne: set
320 >
321
322 Not following what dne means.
323
324
325 > + """
326 > +
327 > + emerge_config = load_emerge_config()
328 > + portdb =
329 > emerge_config.target_config.trees['porttree'].dbapi
330 > + for failed_pkg in failed_pkgs:
331 > + # validate pkg name
332 > + pkg_name = '%s' %
333 > failed_pkg.replace(MERGING_IDENTIFIER, '')
334 > + pkg_atom = '=%s' % pkg_name
335 > +
336 > + if not isvalidatom(pkg_atom):
337 > + pkg_dne.append( "'%s' is an invalid
338 > package atom." % pkg_atom)
339 > + if not portdb.cpv_exists(pkg_name):
340 > + pkg_dne.append( "'%s' does not exist in
341 > the portage tree."
342 > + % pkg_name)
343 > + pkg_atoms.add(pkg_atom)
344 > +
345 > +
346 > + def _emerge_pkg_atoms(self, module_output, pkg_atoms):
347 > + """
348 > + Emerge the specified packages atoms.
349 > +
350 > + @param module_output: output will be written to
351 > + @type module_output: Class
352 > + @param pkg_atoms: packages atoms to emerge
353 > + @type pkg_atoms: set
354 > + @rtype: list
355 > + @return: List of results
356 > + """
357 > + # TODO: rewrite code to use portage's APIs instead of a
358 > subprocess
359 > + env = {
360 > + "FEATURES" : "-collision-detect -protect-owned",
361 > + "PATH" : os.environ["PATH"]
362 > + }
363 > + emerge_cmd = (
364 > + portage._python_interpreter,
365 > + '-b',
366 > + os.path.join(PORTAGE_BIN_PATH, 'emerge'),
367 > + '--quiet',
368 > + '--oneshot',
369 > + '--complete-graph=y'
370 > + )
371 > + results = []
372 > + msg = 'Re-Emerging packages that failed to merge...\n'
373 > + if module_output:
374 > + module_output.write(msg)
375 > + else:
376 > + module_output = subprocess.PIPE
377 > + results.append(msg)
378 > + proc = subprocess.Popen(emerge_cmd + tuple(pkg_atoms),
379 > env=env,
380 > + stdout=module_output, stderr=sys.stderr)
381 > + output = proc.communicate()[0]
382 >
383
384 This seems sort of weird, perhaps:
385
386 output, _ = proc.communicate()
387
388
389 > + if output:
390 > + results.append(output)
391 > + if proc.returncode != os.EX_OK:
392 > + emerge_status = "Failed to emerge '%s'" % ('
393 > '.join(pkg_atoms))
394 > + else:
395 > + emerge_status = "Successfully emerged '%s'" % ('
396 > '.join(pkg_atoms))
397 > + results.append(emerge_status)
398 > + return results
399 > +
400 > +
401 > + def check(self, **kwargs):
402 > + """Check for failed merges."""
403 > + onProgress = kwargs.get('onProgress', None)
404 > + failed_pkgs = self._failed_pkgs(onProgress)
405 > + errors = []
406 > + for pkg, mtime in failed_pkgs.items():
407 > + mtime_str = time.ctime(int(mtime))
408 > + errors.append("'%s' failed to merge on '%s'" %
409 > (pkg, mtime_str))
410 > + return errors
411 > +
412 > +
413 > + def fix(self, **kwargs):
414 > + """Attempt to fix any failed merges."""
415 > + module_output = kwargs.get('module_output', None)
416 > + failed_pkgs = self._failed_pkgs()
417 > + if not failed_pkgs:
418 > + return [ 'No failed merges found. ' ]
419 >
420
421 Less spacing here:
422 return ['No failed merges found.']
423
424
425 > +
426 > + pkg_dne = set()
427 > + pkg_atoms = set()
428 > + self._get_pkg_atoms(failed_pkgs, pkg_atoms, pkg_dne)
429 > + if pkg_dne:
430 > + return pkg_dne
431 > +
432 > + try:
433 > + self._tracking_file.save(failed_pkgs)
434 > + except IOError as ex:
435 > + errors = [ 'Unable to save failed merges to
436 > tracking file: %s\n'
437 > + % str(ex) ]
438 >
439
440 Same here, no spaces before or after [ and ].
441
442
443 > + errors.append(', '.join(sorted(failed_pkgs)))
444 > + return errors
445 > + self._remove_failed_dirs(failed_pkgs)
446 > + results = self._emerge_pkg_atoms(module_output, pkg_atoms)
447 > + # list any new failed merges
448 > + for pkg in sorted(self._scan()):
449 > + results.append("'%s' still found as a failed
450 > merge." % pkg)
451 > + # reload config and remove successful packages from
452 > tracking file
453 > + emerge_config = load_emerge_config()
454 > + vardb = emerge_config.target_config.trees['vartree'].dbapi
455 > + still_failed_pkgs = {}
456 > + for pkg, mtime in failed_pkgs.items():
457 > + pkg_name = '%s' % pkg.replace(MERGING_IDENTIFIER,
458 > '')
459 > + if not vardb.cpv_exists(pkg_name):
460 > + still_failed_pkgs[pkg] = mtime
461 > + self._tracking_file.save(still_failed_pkgs)
462 > + return results
463 > +
464 > +
465 > + def purge(self, **kwargs):
466 > + """Attempt to remove previously saved tracking file."""
467 > + if not self._tracking_file.exists():
468 > + return ['Tracking file not found.']
469 > + self._tracking_file.purge()
470 > + return ['Removed tracking file.']
471 > --
472 > 1.8.3.2
473 >
474 >
475 >

Replies