????
Current Path : /scripts/ |
Current File : //scripts/retry_rpm |
#!/usr/local/cpanel/bin/python-packman import argparse import fcntl import os import select import signal import subprocess import sys import time class Script(object): rpm_bin = '/usr/bin/rpm' yum_bin = '/usr/bin/yum' lock_error_text = b'''error: can't create transaction lock on''' def __init__(self) -> None: self.max_wait = 300 self.saw_lock_error = False self.lockfile_name = subprocess.check_output('/usr/bin/rpm -E %_rpmlock_path'.split()).strip() if not self.lockfile_name: raise RuntimeError('get rpm lock path: empty') self.lockfile = open(self.lockfile_name, 'wb+') self.stdout = open(os.dup(sys.stdout.fileno()), 'wb') self.stderr = open(os.dup(sys.stderr.fileno()), 'wb', buffering=0) def run(self, args=None): parser = argparse.ArgumentParser() parser.add_argument('rpm_or_yum', choices=[Script.rpm_bin, Script.yum_bin]) args, wrapped_args = parser.parse_known_args(args=args) cmd = [args.rpm_or_yum] + wrapped_args start = time.time() try: while True: time_elapsed = time.time() - start if self.max_wait < time_elapsed: raise TimeExceeded() rc = self.try_cmd(cmd) if 0 == rc: return 0 if self.saw_lock_error: alarm_timeout = int(self.max_wait - time_elapsed) self.err(b'lock error detected; waiting on rpm lock\n') def handle_alarm(signo, frame): raise TimeExceeded() signal.signal(signal.SIGALRM, handle_alarm) signal.alarm(alarm_timeout) fcntl.lockf(self.lockfile, fcntl.LOCK_EX) signal.alarm(0) self.err(b'lock acquired\n') # We waited on lock as a cheap way to know when it was available. We have to give it up so that the # subprocess can acquire it, but it's possible some other process will get the lock between now and # then so that's why we're in this loop. You might be thinking you could carry this lock across an # execve call and that _is_ possible, however there is no guarantee that this lock is the first one # the subprocess will acquire and so you might end up deadlocking. fcntl.lockf(self.lockfile, fcntl.LOCK_UN) else: return rc except TimeExceeded as e: self.err(b'error: max wait time has elapsed; returning error result\n') return 99 def err(self, msg): self.stderr.write(msg) # self.stderr.flush() def try_cmd(self, args): self.saw_lock_error = False devnull = open(os.devnull, 'rb') proc = subprocess.Popen(args, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def out_cb(line, script): script.stdout.write(line) script.stdout.flush() def error_cb(line, script): if Script.lock_error_text in line: script.saw_lock_error = True # some Cpanel code looks for '^error:' text, so we prefix retryable errors with our name so that a # successful retry will not be detected as an error that needs review. line = b'retry_rpm.py: ' + line script.err(line) readers = [ LineReader(proc.stdout, out_cb, self), LineReader(proc.stderr, error_cb, self), ] while readers: readable, _, _ = select.select(readers, [], [], 1.0) for w in readable: if not w.read(): readers.remove(w) return proc.wait() class LineReader(object): def __init__(self, fl, cb, arg): self.file = fl os.set_blocking(self.file.fileno(), False) self.cb = cb self.arg = arg def fileno(self): return self.file.fileno() def read(self): lines = 0 for line in self.file: lines += 1 self.cb(line, self.arg) any_lines = 0 < lines return any_lines class TimeExceeded(Exception): pass if __name__ == '__main__': sys.exit(Script().run())