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