|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import subprocess |
| 4 | +import threading |
| 5 | +import queue |
| 6 | +import time |
| 7 | +import logging |
| 8 | +from rich.console import Console |
| 9 | +from rich.panel import Panel |
| 10 | +from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn |
| 11 | +from rich.text import Text |
| 12 | +from rich.style import Style |
| 13 | +from rich.live import Live |
| 14 | +from rich import box |
| 15 | + |
| 16 | +# Setup logging |
| 17 | +logging.basicConfig( |
| 18 | + filename='/tmp/hacker-update.log', |
| 19 | + level=logging.DEBUG, |
| 20 | + format='%(asctime)s - %(levelname)s - %(message)s', |
| 21 | + datefmt='%Y-%m-%d %H:%M:%S' |
| 22 | +) |
| 23 | + |
| 24 | +console = Console() |
| 25 | + |
| 26 | +# Constants for styling |
| 27 | +HEADER_WIDTH = 80 |
| 28 | +COLOR_MAP = { |
| 29 | + "APT System Update": "bright_magenta", |
| 30 | + "Flatpak Update": "bright_yellow", |
| 31 | + "Snap Update": "bright_blue", |
| 32 | + "Firmware Update": "bright_green", |
| 33 | +} |
| 34 | + |
| 35 | +# Structure for commands |
| 36 | +commands = [ |
| 37 | + { |
| 38 | + "name": "APT System Update", |
| 39 | + "commands": [ |
| 40 | + {"name": "APT Update", "cmd": "sudo apt update", "color": "bright_magenta", "list_cmd": None}, |
| 41 | + {"name": "APT Upgrade", "cmd": "sudo apt upgrade -y", "color": "bright_magenta", "list_cmd": "apt list --upgradable"}, |
| 42 | + {"name": "APT Autoremove", "cmd": "sudo apt autoremove -y", "color": "bright_magenta", "list_cmd": None}, |
| 43 | + {"name": "APT Autoclean", "cmd": "sudo apt autoclean", "color": "bright_magenta", "list_cmd": None}, |
| 44 | + ] |
| 45 | + }, |
| 46 | + { |
| 47 | + "name": "Flatpak Update", |
| 48 | + "commands": [ |
| 49 | + {"name": "Flatpak Update", "cmd": "flatpak update -y", "color": "bright_yellow", "list_cmd": "flatpak remote-ls --updates flathub"}, |
| 50 | + ] |
| 51 | + }, |
| 52 | + { |
| 53 | + "name": "Snap Update", |
| 54 | + "commands": [ |
| 55 | + {"name": "Snap Refresh", "cmd": "sudo snap refresh", "color": "bright_blue", "list_cmd": "snap refresh --list"}, |
| 56 | + ] |
| 57 | + }, |
| 58 | + { |
| 59 | + "name": "Firmware Update", |
| 60 | + "commands": [ |
| 61 | + {"name": "Firmware Refresh", "cmd": "sudo fwupdmgr refresh", "color": "bright_green", "list_cmd": None}, |
| 62 | + {"name": "Firmware Update", "cmd": "sudo fwupdmgr update", "color": "bright_green", "list_cmd": "fwupdmgr get-updates"}, |
| 63 | + ] |
| 64 | + }, |
| 65 | +] |
| 66 | + |
| 67 | +def print_header(): |
| 68 | + title = Text("HackerOS Update Utility v0.0.1", style="bold bright_cyan") |
| 69 | + panel = Panel(title, width=HEADER_WIDTH, box=box.HEAVY, expand=True, style="on black") |
| 70 | + console.print(panel) |
| 71 | + console.print(Text("Initializing updates...", style="italic bright_blue", justify="center")) |
| 72 | + console.print() |
| 73 | + |
| 74 | +def print_section_header(name): |
| 75 | + color = COLOR_MAP.get(name, "bright_white") |
| 76 | + header_text = Text(name, style=f"bold white on {color}") |
| 77 | + panel = Panel(header_text, width=HEADER_WIDTH, box=box.DOUBLE_EDGE, expand=True) |
| 78 | + console.print(panel) |
| 79 | + console.print() |
| 80 | + |
| 81 | +def run_command(cmd, color, is_list=False): |
| 82 | + process = subprocess.Popen( |
| 83 | + ["sh", "-c", cmd], |
| 84 | + stdout=subprocess.PIPE, |
| 85 | + stderr=subprocess.PIPE, |
| 86 | + text=True, |
| 87 | + bufsize=1 |
| 88 | + ) |
| 89 | + stdout_queue = queue.Queue() |
| 90 | + stderr_queue = queue.Queue() |
| 91 | + |
| 92 | + def read_stdout(): |
| 93 | + for line in iter(process.stdout.readline, ''): |
| 94 | + stdout_queue.put(line.strip()) |
| 95 | + process.stdout.close() |
| 96 | + |
| 97 | + def read_stderr(): |
| 98 | + for line in iter(process.stderr.readline, ''): |
| 99 | + stderr_queue.put(line.strip()) |
| 100 | + process.stderr.close() |
| 101 | + |
| 102 | + stdout_thread = threading.Thread(target=read_stdout) |
| 103 | + stderr_thread = threading.Thread(target=read_stderr) |
| 104 | + stdout_thread.start() |
| 105 | + stderr_thread.start() |
| 106 | + |
| 107 | + stdout_lines = [] |
| 108 | + stderr_lines = [] |
| 109 | + |
| 110 | + while process.poll() is None or not stdout_queue.empty() or not stderr_queue.empty(): |
| 111 | + try: |
| 112 | + line = stdout_queue.get_nowait() |
| 113 | + if not is_list: |
| 114 | + console.print(Text(line, style=color)) |
| 115 | + stdout_lines.append(line) |
| 116 | + logging.info(f"STDOUT: {line}") |
| 117 | + except queue.Empty: |
| 118 | + pass |
| 119 | + try: |
| 120 | + line = stderr_queue.get_nowait() |
| 121 | + if not is_list: |
| 122 | + console.print(Text(line, style="bright_red")) |
| 123 | + stderr_lines.append(line) |
| 124 | + logging.error(f"STDERR: {line}") |
| 125 | + except queue.Empty: |
| 126 | + pass |
| 127 | + time.sleep(0.01) |
| 128 | + |
| 129 | + stdout_thread.join() |
| 130 | + stderr_thread.join() |
| 131 | + return_code = process.wait() |
| 132 | + |
| 133 | + return '\n'.join(stdout_lines), '\n'.join(stderr_lines), return_code == 0 |
| 134 | + |
| 135 | +def list_updates(list_cmd, name, color): |
| 136 | + with console.status(f"[green]Listing updates for {name}...[/]", spinner="dots"): |
| 137 | + stdout, stderr, success = run_command(list_cmd, color, is_list=True) |
| 138 | + if success and stdout.strip(): |
| 139 | + console.print(Text(f"Updates available for {name}:", style=f"bold {color}")) |
| 140 | + lines = [line for line in stdout.splitlines() if line.strip() and "Listing..." not in line and "All snaps up to date." not in line] |
| 141 | + if lines: |
| 142 | + for i, line in enumerate(lines, 1): |
| 143 | + console.print(Text(f" {i:2}. {line}", style="bright_white")) |
| 144 | + else: |
| 145 | + console.print(Text(" None", style="bright_white")) |
| 146 | + elif stderr: |
| 147 | + console.print(Text(f"Error listing updates: {stderr}", style="bright_red")) |
| 148 | + else: |
| 149 | + console.print(Text(f"No updates available for {name}.", style="bright_green")) |
| 150 | + console.print() |
| 151 | + |
| 152 | +def print_menu(): |
| 153 | + menu_text = Text.assemble( |
| 154 | + Text("Update Process Completed\n", style="bold bright_green", justify="center"), |
| 155 | + Text("Choose an action:\n", style="bold white", justify="center"), |
| 156 | + Text("(E)xit (S)hutdown (R)eboot\n", style="bright_yellow", justify="center"), |
| 157 | + Text("(L)og Out (T)ry Again (H) Show Logs", style="bright_yellow", justify="center") |
| 158 | + ) |
| 159 | + panel = Panel(menu_text, width=HEADER_WIDTH, box=box.ROUNDED, expand=True, style="bright_cyan") |
| 160 | + console.print(panel) |
| 161 | + console.print(Text("Select an option:", style="italic white", justify="center")) |
| 162 | + |
| 163 | +def print_logs(logs): |
| 164 | + log_content = Text() |
| 165 | + for name, log, is_stdout in logs: |
| 166 | + if log.strip(): |
| 167 | + log_type = "Output" if is_stdout else "Error" |
| 168 | + log_color = "bright_white" if is_stdout else "bright_red" |
| 169 | + log_content.append(f"{log_type} for {name}:\n", style=f"bold {log_color}") |
| 170 | + for line in log.splitlines(): |
| 171 | + log_content.append(f" {line}\n", style=log_color) |
| 172 | + log_content.append("\n") |
| 173 | + panel = Panel(log_content, title="Update Logs", width=HEADER_WIDTH, box=box.SQUARE, style="on bright_cyan", expand=True) |
| 174 | + console.print(panel) |
| 175 | + console.print() |
| 176 | + |
| 177 | +def print_action(message, color): |
| 178 | + action_text = Text(message, style=f"white on {color}") |
| 179 | + panel = Panel(action_text, width=HEADER_WIDTH // 2, box=box.MINIMAL, expand=False, style="bold") |
| 180 | + console.print(panel, justify="center") |
| 181 | + time.sleep(0.2) |
| 182 | + |
| 183 | +def get_single_key(): |
| 184 | + import termios, tty |
| 185 | + fd = sys.stdin.fileno() |
| 186 | + old_attr = termios.tcgetattr(fd) |
| 187 | + try: |
| 188 | + tty.setraw(fd) |
| 189 | + return sys.stdin.read(1).lower() |
| 190 | + finally: |
| 191 | + termios.tcsetattr(fd, termios.TCDRAIN, old_attr) |
| 192 | + |
| 193 | +def main(): |
| 194 | + print_header() |
| 195 | + logs = [] |
| 196 | + |
| 197 | + for section in commands: |
| 198 | + print_section_header(section["name"]) |
| 199 | + total_steps = len(section["commands"]) |
| 200 | + progress = Progress( |
| 201 | + SpinnerColumn(), |
| 202 | + BarColumn(bar_width=None), |
| 203 | + TextColumn("[progress.description]{task.description}"), |
| 204 | + TextColumn("{task.completed}/{task.total}"), |
| 205 | + "|", |
| 206 | + TextColumn("{task.fields[msg]}"), |
| 207 | + "| ETA:", |
| 208 | + TimeRemainingColumn(), |
| 209 | + console=console, |
| 210 | + expand=True |
| 211 | + ) |
| 212 | + task_id = progress.add_task(f"[bright_cyan]{section['name']:<30}[/]", total=total_steps, msg="") |
| 213 | + |
| 214 | + with Live(progress, refresh_per_second=20): |
| 215 | + for cmd_info in section["commands"]: |
| 216 | + progress.update(task_id, description=f"[bright_cyan]{section['name']:<30}[/]", msg=f"{cmd_info['name']}") |
| 217 | + if cmd_info["list_cmd"]: |
| 218 | + list_updates(cmd_info["list_cmd"], cmd_info["name"], cmd_info["color"]) |
| 219 | + |
| 220 | + with console.status(f"[green]Executing: {cmd_info['name']}[/]", spinner="dots"): |
| 221 | + stdout, stderr, success = run_command(cmd_info["cmd"], cmd_info["color"]) |
| 222 | + |
| 223 | + logs.append((cmd_info["name"], stdout, True)) |
| 224 | + if stderr: |
| 225 | + logs.append((cmd_info["name"], stderr, False)) |
| 226 | + |
| 227 | + status_msg = Text(cmd_info["name"] + ": " + ("Completed" if success else "Failed"), style="bright_green bold" if success else "bright_red bold") |
| 228 | + console.print(status_msg) |
| 229 | + console.print() |
| 230 | + |
| 231 | + progress.advance(task_id) |
| 232 | + |
| 233 | + progress.update(task_id, description=f"[bright_cyan]{section['name']:<30}[/]", msg=f"{section['name']} completed", completed=total_steps) |
| 234 | + console.print(Text(f"{section['name']} completed", style="bold bright_green")) |
| 235 | + console.print() |
| 236 | + time.sleep(0.3) |
| 237 | + |
| 238 | + while True: |
| 239 | + print_menu() |
| 240 | + choice = get_single_key() |
| 241 | + if choice == 'e': |
| 242 | + print_action("Exiting Update Utility", "bright_blue") |
| 243 | + break |
| 244 | + elif choice == 's': |
| 245 | + print_action("Shutting Down System", "bright_blue") |
| 246 | + subprocess.run(["sudo", "poweroff"]) |
| 247 | + break |
| 248 | + elif choice == 'r': |
| 249 | + print_action("Rebooting System", "bright_blue") |
| 250 | + subprocess.run(["sudo", "reboot"]) |
| 251 | + break |
| 252 | + elif choice == 'l': |
| 253 | + print_action("Logging Out", "bright_blue") |
| 254 | + username = os.getlogin() |
| 255 | + subprocess.run(["pkill", "-u", username]) |
| 256 | + break |
| 257 | + elif choice == 't': |
| 258 | + print_action("Restarting Update Process", "bright_blue") |
| 259 | + main() |
| 260 | + return |
| 261 | + elif choice == 'h': |
| 262 | + print_logs(logs) |
| 263 | + else: |
| 264 | + print_action("Invalid Option", "bright_red") |
| 265 | + |
| 266 | +if __name__ == "__main__": |
| 267 | + main() |
0 commit comments