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 |
> |