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