diff --git a/src/pacroller/checker.py b/src/pacroller/checker.py index 1bcb83a..c69ae83 100644 --- a/src/pacroller/checker.py +++ b/src/pacroller/checker.py @@ -145,7 +145,7 @@ def _stdout_parser(stdout: List[str], report: checkReport) -> None: if _m := REGEX['s_upgrade_pkg'].match(line): logger.debug(f's_upgrade_pkg {line=}') else: - report.crit(f'{line=} is unknown') + logger.debug(f'stdout {line=} is unknown') else: logger.debug(f'skip {line=}') ln += 1 diff --git a/src/pacroller/config.json.example b/src/pacroller/config.json.example index 561902c..a797f8e 100644 --- a/src/pacroller/config.json.example +++ b/src/pacroller/config.json.example @@ -1,4 +1,5 @@ { + "timeout": 300, "upgrade_timeout": 3600, "network_retry": 5, "custom_sync": false, @@ -12,5 +13,8 @@ "ignored_pacnew": [ "/etc/locale.gen", "/etc/pacman.d/mirrorlist" - ] + ], + "need_restart": false, + "need_restart_cmd": ["needrestart", "-r", "a", "-m", "a", "-l"], + "systemd-check": true } diff --git a/src/pacroller/config.py b/src/pacroller/config.py index 5488205..9a02c16 100644 --- a/src/pacroller/config.py +++ b/src/pacroller/config.py @@ -14,9 +14,10 @@ if (cfg := (CONFIG_DIR / CONFIG_FILE)).exists(): else: _config = dict() +TIMEOUT = int(_config.get('timeout', 300)) UPGRADE_TIMEOUT = int(_config.get('upgrade_timeout', 3600)) NETWORK_RETRY = int(_config.get('network_retry', 5)) -assert UPGRADE_TIMEOUT > 0 and NETWORK_RETRY > 0 +assert TIMEOUT > 0 and UPGRADE_TIMEOUT > 0 and NETWORK_RETRY > 0 CUSTOM_SYNC = bool(_config.get('custom_sync', False)) SYNC_SH = CONFIG_DIR / str(_config.get('sync_shell', "sync.sh")) @@ -31,4 +32,12 @@ for (k, v) in HOLD.items(): assert isinstance(k, str) and isinstance(v, str) IGNORED_PACNEW = _config.get('ignored_pacnew', list()) -assert isinstance(IGNORED_PACNEW, list) +for i in IGNORED_PACNEW: + assert isinstance(i, str) + +NEEDRESTART = bool(_config.get('need_restart', False)) +NEEDRESTART_CMD = _config.get('need_restart_cmd', False) +for i in NEEDRESTART_CMD: + assert isinstance(i, str) + +SYSTEMD = bool(_config.get('systemd-check', True)) diff --git a/src/pacroller/main.py b/src/pacroller/main.py index 2b545f7..9dfeb7d 100644 --- a/src/pacroller/main.py +++ b/src/pacroller/main.py @@ -13,7 +13,8 @@ from typing import List, Iterator from pacroller.utils import execute_with_io, UnknownQuestionError, back_readline from pacroller.checker import log_checker, sync_err_is_net, upgrade_err_is_net, checkReport from pacroller.config import (CONFIG_DIR, CONFIG_FILE, LIB_DIR, DB_FILE, PACMAN_LOG, PACMAN_CONFIG, - UPGRADE_TIMEOUT, NETWORK_RETRY, CUSTOM_SYNC, SYNC_SH, EXTRA_SAFE, SHELL, HOLD) + TIMEOUT, UPGRADE_TIMEOUT, NETWORK_RETRY, CUSTOM_SYNC, SYNC_SH, + EXTRA_SAFE, SHELL, HOLD, NEEDRESTART, NEEDRESTART_CMD, SYSTEMD) logger = logging.getLogger() @@ -29,6 +30,10 @@ class PackageHold(Exception): pass class CheckFailed(Exception): pass +class NeedrestartFailed(Exception): + pass +class SystemdNotRunning(Exception): + pass def sync() -> None: logger.info('sync start') @@ -43,7 +48,7 @@ def sync() -> None: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', - timeout=300 + timeout=TIMEOUT ) except subprocess.CalledProcessError as e: if sync_err_is_net(e.output): @@ -99,7 +104,7 @@ def upgrade() -> List[str]: examine_upgrade(t.to_add, t.to_remove) finally: t.release() - pacman_output = execute_with_io(['pacman', '-Su', '--noprogressbar', '--color', 'never']) + pacman_output = execute_with_io(['pacman', '-Su', '--noprogressbar', '--color', 'never'], UPGRADE_TIMEOUT) logger.info('upgrade end') return pacman_output @@ -132,7 +137,13 @@ def do_system_upgrade(debug=False) -> checkReport: with open(PACMAN_LOG, 'r') as pacman_log: pacman_log.seek(log_anchor) log = pacman_log.read().split('\n') - report = log_checker(stdout, log, debug=debug) + try: + report = log_checker(stdout, log, debug=debug) + except Exception: + logger.exception('checker has crashed, here is the debug info') + _report = log_checker(stdout, log, debug=True) + raise + logger.info(report.summary(verbose=True, show_package=False)) return report @@ -156,6 +167,21 @@ def has_previous_error() -> Exception: else: return None +def is_system_failed() -> str: + try: + p = subprocess.run(["systemctl", "is-system-running"], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + timeout=20, encoding='utf-8') + except Exception: + ret = "exec fail" + else: + ret = p.stdout.strip() + if ret == 'running': + return None + else: + return ret + def main() -> None: import argparse parser = argparse.ArgumentParser(description='Pacman Automatic Rolling Helper') @@ -171,7 +197,12 @@ def main() -> None: if args.action == 'run': if getuid() != 0: - parser.error('you need to be root') + logger.error('you need to be root') + exit(1) + if SYSTEMD: + if _s := is_system_failed(): + logger.error(f'systemd is not in {_s} state, refused') + exit(11) if prev_err := has_previous_error(): logger.error(f'Cannot continue, a previous error {prev_err} is still present. Please resolve this issue and run fail-reset.') else: @@ -190,7 +221,28 @@ def main() -> None: else: if report._warn or report._crit: exc = CheckFailed('manual inspection required') + if exc: + write_db(report, exc) + exit(2) + if NEEDRESTART: + try: + p = subprocess.run( + NEEDRESTART_CMD, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding='utf-8', + timeout=TIMEOUT + ) + except subprocess.CalledProcessError as e: + logger.error(f'needrestart failed with {e.returncode=} {e.output=}') + exc = NeedrestartFailed(f'{e.returncode=}') + else: + logger.debug(f'needrestart {p.stdout=}') write_db(report, exc) + if exc: + exit(2) + elif args.action == 'status': count = 0 for entry in read_db(): @@ -202,8 +254,13 @@ def main() -> None: break print() elif args.action == 'fail-reset': + if SYSTEMD: + if _s := is_system_failed(): + logger.error(f'systemd is not in {_s} state, refused') + exit(11) if getuid() != 0: - parser.error('you need to be root') + logger.error('you need to be root') + exit(1) if prev_err := has_previous_error(): write_db(None) logger.info(f'reset previous error {prev_err}') diff --git a/src/pacroller/utils.py b/src/pacroller/utils.py index e2f7be9..bc0b8d2 100644 --- a/src/pacroller/utils.py +++ b/src/pacroller/utils.py @@ -14,7 +14,7 @@ class UnknownQuestionError(subprocess.SubprocessError): def __str__(self): return f"Pacman returned an unknown question {self.question}" -def execute_with_io(command: List[str]) -> List[str]: +def execute_with_io(command: List[str], timeout: int = 3600) -> List[str]: ''' captures stdout and stderr and automatically handles [y/n] questions of pacman @@ -41,7 +41,7 @@ def execute_with_io(command: List[str]) -> List[str]: stderr=subprocess.STDOUT, encoding='utf-8' ) - Thread(target=set_timeout, args=(p, 3600), daemon=True).start() # should be configurable + Thread(target=set_timeout, args=(p, timeout), daemon=True).start() # should be configurable line = '' output = '' while (r := p.stdout.read(1)) != '':