Skip to content

Instantly share code, notes, and snippets.

@kidpixo
Last active December 19, 2025 13:58
Show Gist options
  • Select an option

  • Save kidpixo/5b4178b0ccd0c02b4cd554ab1e9ad43a to your computer and use it in GitHub Desktop.

Select an option

Save kidpixo/5b4178b0ccd0c02b4cd554ab1e9ad43a to your computer and use it in GitHub Desktop.
Health checkups for the sophisticated Arch user: sanity checks for your rolling-release setup written in pure python3 stdlib, no dependencies (hell).
#!/usr/bin/env python3
import subprocess
import os
import sys
import argparse
import shutil
import platform
# --- Configuration & Colors ---
BLUE = '\033[34m'
CYAN = '\033[36m'
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BOLD = '\033[1m'
RESET = '\033[0m'
# The high-detail ASCII logo provided
ARCH_LOGO = [
f"{CYAN}⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣷⣤⣙⢻⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⡿⠛⠛⠿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠙⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⠿⣆⠀⠀⠀⠀{RESET}",
f"{CYAN}⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣷⣦⡀⠀⠀⠀{RESET}",
f"{CYAN}⠀⢀⣾⣿⣿⠿⠟⠛⠋⠉⠉⠀⠀⠀⠀⠀⠀⠉⠉⠙⠛⠻⠿⣿⣿⣷⡀⠀{RESET}",
f"{CYAN}⣠⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⣄{RESET}",
]
# Shared counter for the final summary
ISSUE_COUNT = 0
def print_header(title):
print(f"\n{BOLD}{'='*10} {title} {'='*10}{RESET}")
# --- Helper: Device Origin ---
def get_device_origin(mount_point):
try:
# findmnt gets the source (e.g., /dev/mapper/volume-home)
result = subprocess.check_output(["findmnt", "-nno", "SOURCE", mount_point], text=True).strip()
dev_name = os.path.basename(result)
# lsblk gets the parent (e.g., cryptlvm or nvme0n1)
lineage = subprocess.check_output(["lsblk", "-no", "PKNAME", result], text=True).strip().split('\n')[-1]
return dev_name, lineage if lineage else dev_name
except:
return "unknown", "unknown"
# --- Main Check Functions ---
def print_logo_info():
# Gather System Info
info = {
'User': os.getlogin(),
'Host': platform.node(),
'OS': "Arch Linux",
'Kernel': platform.release(),
'Shell': os.environ.get('SHELL', 'N/A').split('/')[-1],
}
try:
with open('/proc/cpuinfo', 'r') as f:
cpu = [line.split(':')[1].strip() for line in f if "model name" in line][0]
info['CPU'] = cpu.split('@')[0].strip()
except: info['CPU'] = "Unknown"
try:
with open('/proc/meminfo', 'r') as f:
lines = f.readlines()
total = int(lines[0].split()[1]) // 1024
avail = int(lines[2].split()[1]) // 1024
info['Memory'] = f"{total - avail}MiB / {total}MiB"
except: info['Memory'] = "Unknown"
data_lines = [
f"{CYAN}{BOLD}{info['User']}@{info['Host']}{RESET}",
f"{'─' * (len(info['User']) + len(info['Host']) + 1)}",
f"{BOLD}OS:{RESET} {info['OS']}",
f"{BOLD}Kernel:{RESET} {info['Kernel']}",
f"{BOLD}Shell:{RESET} {info['Shell']}",
f"{BOLD}CPU:{RESET} {info['CPU']}",
f"{BOLD}Memory:{RESET} {info['Memory']}"
]
print("")
for i in range(max(len(ARCH_LOGO), len(data_lines))):
logo = ARCH_LOGO[i] if i < len(ARCH_LOGO) else " " * 20
text = data_lines[i] if i < len(data_lines) else ""
print(f" {logo} {text}")
def check_disk():
global ISSUE_COUNT
print_header("Disk Usage & Origins")
critical_mounts = ['/', '/boot', '/home', '/var']
print(f"{BOLD}{'Mount':<10} : {'Usage':<8} : {'Free':<10} : {'Type':<8} : {'Origin'}{RESET}")
print("─" * 85)
with open('/proc/mounts', 'r') as f:
mount_data = {l.split()[1]: l.split()[2] for l in f if l.split()[1] in critical_mounts}
for mount in critical_mounts:
if mount not in mount_data: continue
try:
total, used, free = shutil.disk_usage(mount)
percent = (used / total) * 100
dev, parent = get_device_origin(mount)
color = GREEN
if percent > 90:
color = RED
ISSUE_COUNT += 1
elif percent > 75: color = YELLOW
origin = f"{dev} ({parent})" if dev != parent else dev
print(f"{mount:<10} : {color}{percent:>6.1f}%{RESET} : {free/(2**30):>7.2f} GB : {mount_data[mount]:<8} : {origin}")
except: continue
def check_kernel():
global ISSUE_COUNT
print_header("Kernel Version Check")
try:
pac_out = subprocess.check_output(["pacman", "-Qi", "linux"], text=True)
installed = next(l.split(":")[1].strip() for l in pac_out.splitlines() if l.startswith("Version"))
running = subprocess.check_output(["uname", "-r"], text=True).strip()
p_v, r_v = installed.replace('-', '.').split('.'), running.replace('-', '.').split('.')
mismatch = False
print(f"{'Component':<12} : {'Installed':<12} : {'Running'}")
print("─" * 45)
for lbl, p, r in zip(['Major', 'Minor', 'Patch', 'Arch Rel'], p_v, r_v):
if p != r: mismatch = True
print(f"{GREEN if p==r else RED}{lbl:<12} : {p:<12} {'==' if p==r else '!='} {r}{RESET}")
if mismatch:
ISSUE_COUNT += 1
print(f"\n{RED}{BOLD}![REBOOT REQUIRED]: Running kernel mismatch.{RESET}")
except: print(f"{RED}Kernel check failed.{RESET}")
def check_pacnew():
global ISSUE_COUNT
print_header("Config Files (.pacnew)")
found = [os.path.join(r, f) for r, _, fs in os.walk('/etc') for f in fs if f.endswith(('.pacnew', '.pacsave'))]
if found:
ISSUE_COUNT += len(found)
for f in found: print(f"{YELLOW} -> {f}{RESET}")
else: print(f"{GREEN}No pending merges.{RESET}")
def check_failed_services():
global ISSUE_COUNT
print_header("Failed Services")
try:
out = subprocess.check_output(["systemctl", "list-units", "--state=failed", "--plain", "--no-legend"], text=True).strip()
if out:
lines = out.splitlines()
ISSUE_COUNT += len(lines)
for line in lines: print(f"{RED} -> {line.split()[0]}{RESET}")
else: print(f"{GREEN}All units OK.{RESET}")
except: pass
def check_orphans():
print_header("Orphaned Packages")
try:
out = subprocess.check_output(["pacman", "-Qdtq"], text=True).strip()
if out: print(f"{YELLOW}Orphans: {out.replace(chr(10), ', ')}{RESET}")
else: print(f"{GREEN}No orphans.{RESET}")
except: print(f"{GREEN}No orphans.{RESET}")
def check_stats():
print_header("Pacman Statistics")
try:
def get_count(flags):
try:
out = subprocess.check_output(["pacman", flags], text=True, stderr=subprocess.DEVNULL)
return len(out.strip().splitlines())
except subprocess.CalledProcessError:
return 0
total = get_count("-Q")
explicit = get_count("-Qe")
deps = get_count("-Qd")
foreign = get_count("-Qm")
native = total - foreign
# Calculate Pacman Cache Size
cache_path = "/var/cache/pacman/pkg"
cache_size_str = "Unknown"
if os.path.exists(cache_path):
# Using du -sh is the easiest way to get human-readable directory size
try:
cache_out = subprocess.check_output(["du", "-sh", cache_path], text=True).split()[0]
cache_size_str = cache_out
except: pass
print(f"{BOLD}{'Category':<18} : {'Count/Size'}{RESET}")
print("─" * 35)
print(f"{'Total Packages':<18} : {total}")
print(f"{' ┗━ Native':<18} : {native}")
print(f"{' ┗━ Foreign/AUR':<18} : {CYAN}{foreign}{RESET}")
print("-" * 35)
print(f"{'Explicitly Sourced':<18} : {explicit}")
print(f"{'As Dependencies':<18} : {deps}")
print("-" * 35)
print(f"{'Pacman Cache Size':<18} : {YELLOW}{cache_size_str}{RESET}")
except Exception as e:
print(f"{RED}Could not retrieve stats: {e}{RESET}")
# --- Main ---
def main():
# 1. Setup Parser
# Use RawDescriptionHelpFormatter to preserve newlines in descriptions
parser = argparse.ArgumentParser(
description=f"{CYAN}{BOLD}Arch Linux System Health Utility{RESET}",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
{BOLD}Usage Examples:{RESET}
arch-health -a Run every check available.
arch-health -k -d Check only kernel and disk status.
arch-health -p Scan for configuration merges.
{BOLD}Extended Descriptions:{RESET}
{BOLD}--kernel{RESET} Compares 'uname -r' with the version in the pacman DB.
If they differ, your system cannot load new modules until reboot.
{BOLD}--pacnew{RESET} Scans /etc for .pacnew and .pacsave files. These are created
when an update has a new default config but you've modified yours.
{BOLD}--services{RESET} Queries systemd for any units in a 'failed' state. Useful for
catching silent background daemon crashes.
{BOLD}--orphans{RESET} Lists packages installed as dependencies but no longer required
by any other package. Helps keep the system lean.
{BOLD}--disk{RESET} Analyzes usage for /, /boot, and /home. Specifically tracks
LVM/LUKS lineage to show you the physical origin of each mount.
{BOLD}--stats{RESET} Show pacman package statistics (Native vs AUR)
"""
)
parser.add_argument("-l", "--logo", action="store_true", help="Print the Arch logo and hardware summary")
parser.add_argument("-k", "--kernel", action="store_true", help="Check for kernel/running version mismatch")
parser.add_argument("-p", "--pacnew", action="store_true", help="Scan for unmerged .pacnew config files")
parser.add_argument("-s", "--services", action="store_true", help="List failed systemd services")
parser.add_argument("-o", "--orphans", action="store_true", help="List orphaned packages (unused dependencies)")
parser.add_argument("-d", "--disk", action="store_true", help="Show usage, filesystem type, and LVM/LUKS origin")
parser.add_argument("-a", "--all", action="store_true", help="Perform all health checks and show logo")
parser.add_argument("-t", "--stats", action="store_true", help="Show pacman package statistics (Native vs AUR)")
args = parser.parse_args()
# 2. Help/Early Exit Check
if len(sys.argv) == 1:
parser.print_help()
sys.exit(0)
# 3. THE ARCH CHECK (The Gatekeeper)
if not os.path.exists("/etc/arch-release"):
print(f"{RED}{BOLD}Error:{RESET} This script requires Arch Linux.")
print("Required file '/etc/arch-release' not found.")
sys.exit(1)
# 4. Proceed with checks...
checks = [
(args.logo, print_logo_info),
(args.kernel, check_kernel),
(args.pacnew, check_pacnew),
(args.services, check_failed_services),
(args.orphans, check_orphans),
(args.disk, check_disk),
(args.stats, check_stats),
]
for selected, func in checks:
if args.all or selected:
func()
print_header("Summary")
if ISSUE_COUNT == 0:
print(f"{GREEN}{BOLD}✔ System Healthy: No issues detected.{RESET}")
else:
print(f"{RED}{BOLD}✘ Attention Required: {ISSUE_COUNT} potential issue(s) found.{RESET}")
print("")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment