Gentoo Archives: gentoo-portage-dev

From: Alexandru Elisei <alexandru.elisei@×××××.com>
To: gentoo-portage-dev@l.g.o
Subject: Re: [gentoo-portage-dev] [PATCH] emaint: exit with non-zero status code when module fails (bug 567478)
Date: Wed, 18 Jan 2017 15:52:52
Message-Id: CAB-4s4kdp6pEKGWTvCJFh2Yx-YnXmTBye_MuuTh18mky9iXbng@mail.gmail.com
In Reply to: Re: [gentoo-portage-dev] [PATCH] emaint: exit with non-zero status code when module fails (bug 567478) by Alexandru Elisei
1 I've modified the patch as per Brian's suggestions:
2 - the modules return True on success and False on failure.
3 - TaskHandler.run_tasks() returns a list of all the module return
4 codes, then emaint_main() exits.
5 - the portage/pym/portage/tests/emerge/test_simple.py change wasn't
6 present in the first version of the patch because of a subtle bug that
7 I introduced: rval was None when the variable PORT_LOGDIR wasn't set,
8 it was different from os.EX_OK so TaskHandler.run_tasks() was exiting
9 with sys.exit(None), which is zero - success.
10
11 Below is the modified commit message and the diff.
12
13 Module functions currently return a message to emaint after invocation.
14 Emaint prints this message then exits normally (with a success return
15 code) even if the module encountered an error. This patch aims to
16 change this by having each module public function return a tuple of
17 (returncode, message), where returncode is boolean True if the function
18 was successful or False otherwise. Emaint will inspect the return codes
19 and exit unsuccessfully if necessary.
20 ---
21 pym/portage/emaint/main.py | 12 +++++++++---
22 pym/portage/emaint/modules/binhost/binhost.py | 6 ++++--
23 pym/portage/emaint/modules/config/config.py | 6 ++++--
24 pym/portage/emaint/modules/logs/logs.py | 9 +++++----
25 pym/portage/emaint/modules/merges/merges.py | 18 +++++++++++-------
26 pym/portage/emaint/modules/move/move.py | 9 +++++++--
27 pym/portage/emaint/modules/resume/resume.py | 4 +++-
28 pym/portage/emaint/modules/sync/sync.py | 20 +++++++++++++-------
29 pym/portage/emaint/modules/world/world.py | 8 ++++++--
30 pym/portage/tests/emerge/test_simple.py | 1 +
31 10 files changed, 63 insertions(+), 30 deletions(-)
32
33 diff --git a/pym/portage/emaint/main.py b/pym/portage/emaint/main.py
34 index 65e3545..f448d6b 100644
35 --- a/pym/portage/emaint/main.py
36 +++ b/pym/portage/emaint/main.py
37 @@ -115,6 +115,7 @@ class TaskHandler(object):
38 """Runs the module tasks"""
39 if tasks is None or func is None:
40 return
41 + returncodes = []
42 for task in tasks:
43 inst = task()
44 show_progress = self.show_progress_bar and self.isatty
45 @@ -135,14 +136,17 @@ class TaskHandler(object):
46 # them for other tasks if there is more to do.
47 'options': options.copy()
48 }
49 - result = getattr(inst, func)(**kwargs)
50 + returncode, msgs = getattr(inst, func)(**kwargs)
51 + returncodes.append(returncode)
52 if show_progress:
53 # make sure the final progress is displayed
54 self.progress_bar.display()
55 print()
56 self.progress_bar.stop()
57 if self.callback:
58 - self.callback(result)
59 + self.callback(msgs)
60 +
61 + return returncodes
62
63
64 def print_results(results):
65 @@ -237,4 +241,6 @@ def emaint_main(myargv):
66 task_opts = options.__dict__
67 task_opts['return-messages'] = True
68 taskmaster = TaskHandler(callback=print_results, module_output=sys.stdout)
69 - taskmaster.run_tasks(tasks, func, status, options=task_opts)
70 + returncodes = taskmaster.run_tasks(tasks, func, status, options=task_opts)
71 +
72 + sys.exit(False in returncodes)
73 diff --git a/pym/portage/emaint/modules/binhost/binhost.py
74 b/pym/portage/emaint/modules/binhost/binhost.py
75 index cf1213e..527b02f 100644
76 --- a/pym/portage/emaint/modules/binhost/binhost.py
77 +++ b/pym/portage/emaint/modules/binhost/binhost.py
78 @@ -86,7 +86,9 @@ class BinhostHandler(object):
79 stale = set(metadata).difference(cpv_all)
80 for cpv in stale:
81 errors.append("'%s' is not in the repository" % cpv)
82 - return errors
83 + if errors:
84 + return (False, errors)
85 + return (True, None)
86
87 def fix(self, **kwargs):
88 onProgress = kwargs.get('onProgress', None)
89 @@ -177,4 +179,4 @@ class BinhostHandler(object):
90 if maxval == 0:
91 maxval = 1
92 onProgress(maxval, maxval)
93 - return None
94 + return (True, None)
95 diff --git a/pym/portage/emaint/modules/config/config.py
96 b/pym/portage/emaint/modules/config/config.py
97 index dad024b..a05a3c2 100644
98 --- a/pym/portage/emaint/modules/config/config.py
99 +++ b/pym/portage/emaint/modules/config/config.py
100 @@ -36,7 +36,8 @@ class CleanConfig(object):
101 if onProgress:
102 onProgress(maxval, i+1)
103 i += 1
104 - return self._format_output(messages)
105 + msgs = self._format_output(messages)
106 + return (True, msgs)
107
108 def fix(self, **kwargs):
109 onProgress = kwargs.get('onProgress', None)
110 @@ -65,7 +66,8 @@ class CleanConfig(object):
111 i += 1
112 if modified:
113 writedict(configs, self.target)
114 - return self._format_output(messages, True)
115 + msgs = self._format_output(messages, True)
116 + return (True, msgs)
117
118 def _format_output(self, messages=[], cleaned=False):
119 output = []
120 diff --git a/pym/portage/emaint/modules/logs/logs.py
121 b/pym/portage/emaint/modules/logs/logs.py
122 index fe65cf5..028084a 100644
123 --- a/pym/portage/emaint/modules/logs/logs.py
124 +++ b/pym/portage/emaint/modules/logs/logs.py
125 @@ -40,7 +40,6 @@ class CleanLogs(object):
126 'NUM': int: number of days
127 'pretend': boolean
128 """
129 - messages = []
130 num_of_days = None
131 pretend = False
132 if kwargs:
133 @@ -70,10 +69,12 @@ class CleanLogs(object):
134 clean_cmd.remove("-delete")
135
136 if not clean_cmd:
137 - return []
138 + return (True, None)
139 rval = self._clean_logs(clean_cmd, settings)
140 - messages += self._convert_errors(rval)
141 - return messages
142 + errors = self._convert_errors(rval)
143 + if errors:
144 + return (False, errors)
145 + return (True, None)
146
147
148 @staticmethod
149 diff --git a/pym/portage/emaint/modules/merges/merges.py
150 b/pym/portage/emaint/modules/merges/merges.py
151 index 8f677c2..416a725 100644
152 --- a/pym/portage/emaint/modules/merges/merges.py
153 +++ b/pym/portage/emaint/modules/merges/merges.py
154 @@ -239,7 +239,9 @@ class MergesHandler(object):
155 for pkg, mtime in failed_pkgs.items():
156 mtime_str = time.ctime(int(mtime))
157 errors.append("'%s' failed to merge on '%s'" % (pkg, mtime_str))
158 - return errors
159 + if errors:
160 + return (False, errors)
161 + return (True, None)
162
163
164 def fix(self, **kwargs):
165 @@ -247,13 +249,13 @@ class MergesHandler(object):
166 module_output = kwargs.get('module_output', None)
167 failed_pkgs = self._failed_pkgs()
168 if not failed_pkgs:
169 - return ['No failed merges found.']
170 + return (True, ['No failed merges found.'])
171
172 pkg_invalid_entries = set()
173 pkg_atoms = set()
174 self._get_pkg_atoms(failed_pkgs, pkg_atoms, pkg_invalid_entries)
175 if pkg_invalid_entries:
176 - return pkg_invalid_entries
177 + return (False, pkg_invalid_entries)
178
179 try:
180 self._tracking_file.save(failed_pkgs)
181 @@ -261,7 +263,7 @@ class MergesHandler(object):
182 errors = ['Unable to save failed merges to tracking file: %s\n'
183 % str(ex)]
184 errors.append(', '.join(sorted(failed_pkgs)))
185 - return errors
186 + return (False, errors)
187 self._remove_failed_dirs(failed_pkgs)
188 results = self._emerge_pkg_atoms(module_output, pkg_atoms)
189 # list any new failed merges
190 @@ -276,12 +278,14 @@ class MergesHandler(object):
191 if not vardb.cpv_exists(pkg_name):
192 still_failed_pkgs[pkg] = mtime
193 self._tracking_file.save(still_failed_pkgs)
194 - return results
195 + if still_failed_pkgs:
196 + return (False, results)
197 + return (True, results)
198
199
200 def purge(self, **kwargs):
201 """Attempt to remove previously saved tracking file."""
202 if not self._tracking_file.exists():
203 - return ['Tracking file not found.']
204 + return (True, ['Tracking file not found.'])
205 self._tracking_file.purge()
206 - return ['Removed tracking file.']
207 + return (True, ['Removed tracking file.'])
208 diff --git a/pym/portage/emaint/modules/move/move.py
209 b/pym/portage/emaint/modules/move/move.py
210 index 41ca167..1c2636d 100644
211 --- a/pym/portage/emaint/modules/move/move.py
212 +++ b/pym/portage/emaint/modules/move/move.py
213 @@ -123,7 +123,10 @@ class MoveHandler(object):
214 errors.append("'%s' has outdated metadata" % cpv)
215 if onProgress:
216 onProgress(maxval, i+1)
217 - return errors
218 +
219 + if errors:
220 + return (False, errors)
221 + return (True, None)
222
223 def fix(self, **kwargs):
224 onProgress = kwargs.get('onProgress', None)
225 @@ -156,7 +159,9 @@ class MoveHandler(object):
226 # Searching for updates in all the metadata is relatively slow, so this
227 # is where the progress bar comes out of indeterminate mode.
228 self._tree.dbapi.update_ents(allupdates, onProgress=onProgress)
229 - return errors
230 + if errors:
231 + return (False, errors)
232 + return (True, None)
233
234 class MoveInstalled(MoveHandler):
235
236 diff --git a/pym/portage/emaint/modules/resume/resume.py
237 b/pym/portage/emaint/modules/resume/resume.py
238 index 1bada52..1d14275 100644
239 --- a/pym/portage/emaint/modules/resume/resume.py
240 +++ b/pym/portage/emaint/modules/resume/resume.py
241 @@ -2,6 +2,7 @@
242 # Distributed under the terms of the GNU General Public License v2
243
244 import portage
245 +import os
246
247
248 class CleanResume(object):
249 @@ -37,7 +38,7 @@ class CleanResume(object):
250 finally:
251 if onProgress:
252 onProgress(maxval, i+1)
253 - return messages
254 + return (True, messages)
255
256 def fix(self, **kwargs):
257 onProgress = kwargs.get('onProgress', None)
258 @@ -56,3 +57,4 @@ class CleanResume(object):
259 onProgress(maxval, i+1)
260 if delete_count:
261 mtimedb.commit()
262 + return (True, None)
263 diff --git a/pym/portage/emaint/modules/sync/sync.py
264 b/pym/portage/emaint/modules/sync/sync.py
265 index 15d63e2..d867699 100644
266 --- a/pym/portage/emaint/modules/sync/sync.py
267 +++ b/pym/portage/emaint/modules/sync/sync.py
268 @@ -127,8 +127,8 @@ class SyncRepos(object):
269 % (bold(", ".join(repos))) + "\n ...returning"
270 ]
271 if return_messages:
272 - return msgs
273 - return
274 + return (False, msgs)
275 + return (False, None)
276 return self._sync(selected, return_messages,
277 emaint_opts=options)
278
279 @@ -211,8 +211,8 @@ class SyncRepos(object):
280 msgs.append("Emaint sync, nothing to sync... returning")
281 if return_messages:
282 msgs.extend(self.rmessage([('None', os.EX_OK)], 'sync'))
283 - return msgs
284 - return
285 + return (True, msgs)
286 + return (True, None)
287 # Portage needs to ensure a sane umask for the files it creates.
288 os.umask(0o22)
289
290 @@ -232,9 +232,14 @@ class SyncRepos(object):
291 sync_scheduler.wait()
292 retvals = sync_scheduler.retvals
293 msgs.extend(sync_scheduler.msgs)
294 + returncode = True
295
296 if retvals:
297 msgs.extend(self.rmessage(retvals, 'sync'))
298 + for repo, returncode in retvals:
299 + if returncode != os.EX_OK:
300 + returncode = False
301 + break
302 else:
303 msgs.extend(self.rmessage([('None', os.EX_OK)], 'sync'))
304
305 @@ -244,6 +249,8 @@ class SyncRepos(object):
306 rcode = sync_manager.perform_post_sync_hook('')
307 if rcode:
308 msgs.extend(self.rmessage([('None', rcode)], 'post-sync'))
309 + if rcode != os.EX_OK:
310 + returncode = False
311
312 # Reload the whole config.
313 portage._sync_mode = False
314 @@ -254,8 +261,8 @@ class SyncRepos(object):
315 self.emerge_config.opts)
316
317 if return_messages:
318 - return msgs
319 - return
320 + return (returncode, msgs)
321 + return (returncode, None)
322
323
324 def _do_pkg_moves(self):
325 @@ -355,7 +362,6 @@ class SyncScheduler(AsyncScheduler):
326 # that hooks will be called in a backward-compatible manner
327 # even if all sync tasks have failed.
328 hooks_enabled = True
329 - returncode = task.returncode
330 if task.returncode == os.EX_OK:
331 returncode, message, updatecache_flg, hooks_enabled = task.result
332 if message:
333 diff --git a/pym/portage/emaint/modules/world/world.py
334 b/pym/portage/emaint/modules/world/world.py
335 index 2c9dbff..ebc3adc 100644
336 --- a/pym/portage/emaint/modules/world/world.py
337 +++ b/pym/portage/emaint/modules/world/world.py
338 @@ -65,7 +65,9 @@ class WorldHandler(object):
339 errors += ["'%s' is not installed" % x for x in self.not_installed]
340 else:
341 errors.append(self.world_file + " could not be opened for reading")
342 - return errors
343 + if errors:
344 + return (False, errors)
345 + return (True, None)
346
347 def fix(self, **kwargs):
348 onProgress = kwargs.get('onProgress', None)
349 @@ -83,7 +85,9 @@ class WorldHandler(object):
350 except portage.exception.PortageException:
351 errors.append("%s could not be opened for writing" % \
352 self.world_file)
353 - return errors
354 + if errors:
355 + return (False, errors)
356 + return (True, None)
357 finally:
358 world_set.unlock()
359
360 diff --git a/pym/portage/tests/emerge/test_simple.py
361 b/pym/portage/tests/emerge/test_simple.py
362 index b1a2af5..5930f6c 100644
363 --- a/pym/portage/tests/emerge/test_simple.py
364 +++ b/pym/portage/tests/emerge/test_simple.py
365 @@ -382,6 +382,7 @@ pkg_preinst() {
366 "PORTAGE_PYTHON" : portage_python,
367 "PORTAGE_REPOSITORIES" : settings.repositories.config_string(),
368 "PORTAGE_TMPDIR" : portage_tmpdir,
369 + "PORT_LOGDIR" : portage_tmpdir,
370 "PYTHONDONTWRITEBYTECODE" : os.environ.get("PYTHONDONTWRITEBYTECODE", ""),
371 "PYTHONPATH" : pythonpath,
372 "__PORTAGE_TEST_PATH_OVERRIDE" : fake_bin,
373 --
374 2.10.2
375
376 On 1/17/17, Alexandru Elisei <alexandru.elisei@×××××.com> wrote:
377 >>
378 >> Please move this sys.exit to the main(). So this should just return
379 >> a list of the rcodes to match the tasks it was passed.
380 >> TaskHandler can be used easily via api, so we don't want it to exit out
381 >> of someone else's code. That will also keep main() as the primary cli
382 >> interface. There process the list of rcodes and exit appropriately.
383 >
384 >
385 > This was an oversight on my part, I will make the necessary changes.
386 >
387 > Alexandru, I tend to not prefer using numbers to indicate success/fail.
388 >> They can be confusing at times. In this case 1 means fail, 0 means
389 >> success. Which is the opposite of True/False. It would be different
390 >> if there were multiple return code values. Then they would be
391 >> meaningful.
392 >
393 >
394 > Sure, I understand your point, we are only interested in success of
395 > failure of the modules, so having True or False as return values makes
396 > sense. I got the idea to use 1 as a failure return code because that's how
397 > the portage sync modules work (for example:
398 > https://gitweb.gentoo.org/proj/portage.git/tree/pym/portage/sync/modules/git/git.py#n46).
399 > That's also why I chose to return (returncode, message) and not (message,
400 > returncode).
401 >
402 >
403 >
404 > On Tue, Jan 17, 2017 at 8:23 PM, Brian Dolbec <dolsen@g.o> wrote:
405 >
406 >> On Tue, 17 Jan 2017 19:48:16 +0200
407 >> Alexandru Elisei <alexandru.elisei@×××××.com> wrote:
408 >>
409 >> > Currently module functions return a message to emaint after
410 >> > invocation. Emaint prints this message then exits normally (with a
411 >> > success return code) even if the module encountered an error. This
412 >> > patch aims to change this by having each module public function
413 >> > return a tuple of (returncode, message). Emaint will inspect the
414 >> > return code and will exit unsuccessfully if necessary.
415 >> > ---
416 >> > pym/portage/emaint/main.py | 11 +++++++++--
417 >> > pym/portage/emaint/modules/binhost/binhost.py | 6 ++++--
418 >> > pym/portage/emaint/modules/config/config.py | 8 ++++++--
419 >> > pym/portage/emaint/modules/logs/logs.py | 9 +++++----
420 >> > pym/portage/emaint/modules/merges/merges.py | 18 +++++++++++-------
421 >> > pym/portage/emaint/modules/move/move.py | 9 +++++++--
422 >> > pym/portage/emaint/modules/resume/resume.py | 4 +++-
423 >> > pym/portage/emaint/modules/sync/sync.py | 19
424 >> > +++++++++++++------ pym/portage/emaint/modules/world/world.py |
425 >> > 8 ++++++-- 9 files changed, 64 insertions(+), 28 deletions(-)
426 >> >
427 >> > diff --git a/pym/portage/emaint/main.py b/pym/portage/emaint/main.py
428 >> > index 65e3545..ef4383a 100644
429 >> > --- a/pym/portage/emaint/main.py
430 >> > +++ b/pym/portage/emaint/main.py
431 >> > @@ -115,6 +115,7 @@ class TaskHandler(object):
432 >> > """Runs the module tasks"""
433 >> > if tasks is None or func is None:
434 >> > return
435 >> > + returncode = os.EX_OK
436 >> > for task in tasks:
437 >> > inst = task()
438 >> > show_progress = self.show_progress_bar and
439 >> > self.isatty @@ -135,14 +136,20 @@ class TaskHandler(object):
440 >> > # them for other tasks if there is
441 >> > more to do. 'options': options.copy()
442 >> > }
443 >> > - result = getattr(inst, func)(**kwargs)
444 >> > + rcode, msgs = getattr(inst, func)(**kwargs)
445 >> > if show_progress:
446 >> > # make sure the final progress is
447 >> > displayed self.progress_bar.display()
448 >> > print()
449 >> > self.progress_bar.stop()
450 >> > if self.callback:
451 >> > - self.callback(result)
452 >> > + self.callback(msgs)
453 >> > + # Keep the last error code when using the
454 >> > 'all' command.
455 >> > + if rcode != os.EX_OK:
456 >> > + returncode = rcode
457 >> > +
458 >> > + if returncode != os.EX_OK:
459 >> > + sys.exit(returncode)
460 >> >
461 >>
462 >>
463 >> Please move this sys.exit to the main(). So this should just return
464 >> a list of the rcodes to match the tasks it was passed.
465 >> TaskHandler can be used easily via api, so we don't want it to exit out
466 >> of someone else's code. That will also keep main() as the primary cli
467 >> interface. There process the list of rcodes and exit appropriately.
468 >>
469 >>
470 >>
471 >> >
472 >> > def print_results(results):
473 >> > diff --git a/pym/portage/emaint/modules/binhost/binhost.py
474 >> > b/pym/portage/emaint/modules/binhost/binhost.py
475 >> > index cf1213e..8cf3da6 100644
476 >> > --- a/pym/portage/emaint/modules/binhost/binhost.py
477 >> > +++ b/pym/portage/emaint/modules/binhost/binhost.py
478 >> > @@ -86,7 +86,9 @@ class BinhostHandler(object):
479 >> > stale = set(metadata).difference(cpv_all)
480 >> > for cpv in stale:
481 >> > errors.append("'%s' is not in the
482 >> > repository" % cpv)
483 >> > - return errors
484 >> > + if errors:
485 >> > + return (1, errors)
486 >> > + return (os.EX_OK, None)
487 >> >
488 >>
489 >> Alexandru, I tend to not prefer using numbers to indicate success/fail.
490 >> They can be confusing at times. In this case 1 means fail, 0 means
491 >> success. Which is the opposite of True/False. It would be different
492 >> if there were multiple return code values. Then they would be
493 >> meaningful. I want to keep these modules pythonic in the sense that
494 >> they return standard True/False for API use. In this case returning
495 >> True/False would also make it easy to process the list of rcodes
496 >> returned to main()
497 >>
498 >> sys.exit(False in rcodes)
499 >>
500 >> which will reverse the True/False to 0/1 automatically for us and make
501 >> it super simple to process a variable length list success/fail results.
502 >>
503 >> --
504 >> Brian Dolbec <dolsen>
505 >>
506 >>
507 >>
508 >