diff --git a/.gitignore b/.gitignore index df72d0d..cdc7c28 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ test_extract.py test_paths.py test_resources.py manager_fixed.py +test_simple.sh +test_container.sh # Docker-Build docker_build/ @@ -28,3 +30,5 @@ debug_docker.sh Dockerfile.nuitka DOCKER_BUILD_ANLEITUNG.md nuitka_builder.py +Dockerfile.test +Dockerfile.simple diff --git a/Changelog b/Changelog index c5f082a..e22ee2a 100644 --- a/Changelog +++ b/Changelog @@ -2,8 +2,42 @@ Changelog for LXTools installer ## [Unreleased] - - +- In the future, lxtools_installer will use the /tmp working + directory for extracting necessary files from the AppImage. Currently, + required files are extracted into separate folders using additional + methods, which would then be removed. Depending on how the installer + is called, examples include being invoked in the home directory where + needed folders and files are unpacked there, and upon closing the app, + these unpacked files will be automatically deleted. Additionally, + a folder is created in the /tmp directory. This folder is used + when the installer is called from an installed program to avoid + permission issues and to prevent extracting into a bin folder. + ### Added +09.07.2025 + +- gpg check, download and import public_key.asc from two sources + automatically and check if the signature is valid + +- Check checksumm and signature of the AppImage-File + +- Methods for checking pkexec and NetworkManager extended for Fedora + and Open Suse if it is displayed incorrectly, the appimage can be started + in terminal to see where the problem is + + ### Added +02.07.2025 + +- build dockercontainer (ubuntu 22.04) for build appimage + the app installer is now running on Debian 12 + +- first complete test runs on Debian12, Linux Mint 22.1, + Open Suse (Leap and Thumbleweed) and Fedora, + at Arch linux the installer starts only if xorg-xrandr is missing + +- the installable programs also run on all systems mentioned + and should also be running on other derivatives. + ### Added 29.06.2025 @@ -16,6 +50,7 @@ Changelog for LXTools installer - add methods check polkit and check Networkmanager is installed and view in header is result false + ### Added 23-06-2025 @@ -37,6 +72,7 @@ Changelog for LXTools installer version ud the respective recognized system to run with it on all supported systems. + ### Added 21-06-2025 @@ -57,6 +93,7 @@ Changelog for LXTools installer - Installer divided into several modules and added new MessageDialog module + ### Added 4-06-2025 @@ -65,6 +102,7 @@ Changelog for LXTools installer - add ensure_shared_libs_pth_exists Script to install + ### Added 4-06-2025 diff --git a/README.md b/README.md index 8a102b3..ee68f05 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,36 @@ LX Tools Installer is a GUI for simple install, update, and remove Apps from ilunix.de +## Build + +Create a compatible AppImage: +```bash +./build_compatible.sh +``` + +This creates: +- `lxtools_installer{VERSION}-x86_64.AppImage` - The executable +- `lxtools_installer{VERSION}-x86_64.AppImage.sha256` - SHA256 checksum +- `lxtools_installer{VERSION}-x86_64.AppImage.asc` - GPG signature (if configured) + +## GPG Setup + +To enable GPG signing: +```bash +./gpg_setup.sh +``` + +## Verification + +Verify the downloaded AppImage: +```bash +# Check SHA256 +sha256sum -c lxtools_installer{VERSION}-x86_64.AppImage.sha256 + +# Verify GPG signature (requires public key) +gpg --import public_key.asc +gpg --verify lxtools_installer{VERSION}-x86_64.AppImage.asc lxtools_installer{VERSION}-x86_64.AppImage +``` # Screenshots [![wire-py.png](https://fb.ilunix.de/api/public/dl/ZnfG9gxv?inline=true)](https://fb.ilunix.de/share/ZnfG9gxv) \ No newline at end of file diff --git a/gpg_setup.sh b/gpg_setup.sh new file mode 100755 index 0000000..9fc6ffa --- /dev/null +++ b/gpg_setup.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +echo "🔐 GPG Setup für lxtools_installer" +echo "==================================" + +# Check if GPG is installed +if ! command -v gpg &> /dev/null; then + echo "❌ GPG ist nicht installiert" + echo "Installation: sudo apt install gnupg" + exit 1 +fi + +# Check if GPG key exists +if ! gpg --list-secret-keys | grep -q "sec"; then + echo "📋 Kein GPG-Schlüssel gefunden. Erstelle einen neuen..." + echo "Verwende diese Einstellungen:" + echo "- Typ: RSA" + echo "- Größe: 4096" + echo "- Gültigkeitsdauer: 2y (2 Jahre)" + echo "- Email: deine@email.com" + echo "" + gpg --full-generate-key +else + echo "✅ GPG-Schlüssel bereits vorhanden" +fi + +# List available keys +echo "" +echo "📋 Verfügbare Schlüssel:" +gpg --list-secret-keys --keyid-format SHORT + +# Ask for key selection +echo "" +read -p "Welchen Key-ID möchtest du verwenden? (oder Enter für Standard): " KEY_ID + +if [ -z "$KEY_ID" ]; then + # Use first available key + KEY_ID=$(gpg --list-secret-keys --keyid-format SHORT | grep "sec" | head -1 | sed 's/.*\///' | cut -d' ' -f1) +fi + +echo "🔑 Verwende Key-ID: $KEY_ID" + +# Export public key +gpg --export --armor "$KEY_ID" > public_key.asc + +echo "✅ Öffentlicher Schlüssel exportiert: public_key.asc" +echo "" +echo "📋 Nächste Schritte:" +echo "1. Committen Sie public_key.asc ins Repository" +echo "2. Testen Sie den Build mit: ./build_compatible.sh" +echo "3. Verifizieren Sie die Signatur mit: gpg --verify file.asc file" diff --git a/gpg_simple_setup.sh b/gpg_simple_setup.sh new file mode 100755 index 0000000..d05c850 --- /dev/null +++ b/gpg_simple_setup.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +echo "🔐 Einfaches GPG Setup für lxtools_installer" +echo "===========================================" + +# Check if GPG is installed +if ! command -v gpg &> /dev/null; then + echo "❌ GPG ist nicht installiert" + echo "Installation: sudo apt install gnupg" + exit 1 +fi + +# Check if GPG key exists +if ! gpg --list-secret-keys | grep -q "sec"; then + echo "📋 Erstelle GPG-Schlüssel automatisch..." + echo "" + echo "Verwende diese Einstellungen:" + echo "- Name: Désiré Werner Menrath" + echo "- Email: polunga40@unity-mail.de" + echo "- Typ: RSA 4096" + echo "- Gültigkeitsdauer: 2 Jahre" + echo "" + + # Create GPG key non-interactively + cat > /tmp/gpg_batch < public_key.asc + +echo "✅ Öffentlicher Schlüssel exportiert: public_key.asc" + +# Test the signing +echo "" +echo "🧪 Teste Signierung..." +echo "test" > /tmp/test.txt +if gpg --armor --detach-sign --yes /tmp/test.txt 2>/dev/null; then + echo "✅ Signierung funktioniert!" + rm /tmp/test.txt /tmp/test.txt.asc +else + echo "❌ Signierung fehlgeschlagen" +fi + +echo "" +echo "📋 Setup abgeschlossen!" +echo "Jetzt kannst du ./build_compatible.sh ausführen" diff --git a/locale/de/LC_MESSAGES/lxtoolsinstaller.mo b/locale/de/LC_MESSAGES/lxtoolsinstaller.mo index 191cb60..2cc8b5f 100644 Binary files a/locale/de/LC_MESSAGES/lxtoolsinstaller.mo and b/locale/de/LC_MESSAGES/lxtoolsinstaller.mo differ diff --git a/lx-icons/16/settings.png b/lx-icons/16/settings.png index 5a09d74..8f49da2 100644 Binary files a/lx-icons/16/settings.png and b/lx-icons/16/settings.png differ diff --git a/lx-icons/32/lxtools_key.png b/lx-icons/32/lxtools_key.png new file mode 100644 index 0000000..01f50a6 Binary files /dev/null and b/lx-icons/32/lxtools_key.png differ diff --git a/lxtools_installer.py b/lxtools_installer.py index 4a30842..53afb25 100755 --- a/lxtools_installer.py +++ b/lxtools_installer.py @@ -4,6 +4,8 @@ from tkinter import ttk import os import sys import subprocess +from typing import Optional, List +import hashlib import getpass from datetime import datetime import tempfile @@ -18,8 +20,9 @@ from manager import ( Image, AppManager, LxTools, + Locale, ) -from network import NetworkChecker +from network import NetworkChecker, GiteaUpdater, GPGManager from message import MessageDialog @@ -34,6 +37,173 @@ class InstallationManager: self.system_manager = System() self.download_manager = DownloadManager() + def check_gpg(self) -> bool: + if GPGManager.is_key_already_imported( + LXToolsAppConfig.EXPECTED_FINGERPRINT[-8:] + ): + return True + + all_keys_valid: bool = True + + # 1. Check reachability of OpenPGP keyserver and process key if reachable + openpgp_reachable: bool = GPGManager.is_url_reachable( + LXToolsAppConfig.KEY_URL_OPENPGP + ) + if openpgp_reachable: + self.log(LocaleStrings.MSGGPG["keyserver_reachable"]) + self.update_progress(LocaleStrings.MSGGPG["keyserver_reachable"]) + if not GPGManager.import_key_from_url( + key_url=LXToolsAppConfig.KEY_URL_OPENPGP, + expected_fingerprint=LXToolsAppConfig.EXPECTED_FINGERPRINT, + filename="public_key1.asc", + ): + all_keys_valid = False + else: + self.log(LocaleStrings.MSGGPG["keyserver_unreachable"]) + self.update_progress(LocaleStrings.MSGGPG["keyserver_unreachable"]) + + # 2. Always download and verify Git.ilunix public key + if not GPGManager.import_key_from_url( + key_url=LXToolsAppConfig.KEY_URL_GITILUNIX, + expected_fingerprint=LXToolsAppConfig.EXPECTED_FINGERPRINT, + filename="public_key2.asc", + ): + all_keys_valid = False + + # Final step: Proceed only if all fingerprints match or OpenPGP was unreachable + if all_keys_valid: + self.log(LocaleStrings.MSGGPG["all_keys_valid"]) + self.update_progress(LocaleStrings.MSGGPG["all_keys_valid"]) + key_id: str = LXToolsAppConfig.EXPECTED_FINGERPRINT[-8:] + + if not GPGManager.update_gpg_trust_level(key_id, trust_level=5): + self.log(LocaleStrings.MSGGPG["error_updating_trust_level"], key_id) + self.update_progress( + LocaleStrings.MSGGPG["error_updating_trust_level"], key_id + ) + return False + else: + self.log(f"{LocaleStrings.MSGGPG['set_trust_level']}{key_id}.") + self.update_progress( + f"{LocaleStrings.MSGGPG['set_trust_level']}{key_id}." + ) + return True + + else: + self.log(LocaleStrings.MSGGPG["not_all_keys_are_valid"]) + self.update_progress(LocaleStrings.MSGGPG["not_all_keys_are_valid"]) + + def check_appimage(self) -> bool | None | str: + """ + Checks the current version of the lxtools_installer AppImage by: + + 1. Downloading it from Gitea (with checksum and signature verification). + 2. Renaming and making it executable. + 3. Comparing its SHA-256 hash with an existing reference file at `/usr/local/bin/lxtools_installer`. + 4. Returns `True` if the hashes match, otherwise `False`. + + Returns: + bool: True if AppImages are identical; False if they differ or an error occurred. + """ + appimage_path: Optional[str] = GiteaUpdater.download_appimage( + verify_checksum=True, verify_signature=True + ) + + tests: List[str] = [ + "Checksum file not found", + "Signature file not found", + "Checksum mismatch", + "Signature verification failed", + ] + + if appimage_path is None or appimage_path in tests: + print(f"{LocaleStrings.MSGA['error']}{appimage_path}") + return None + + else: + try: + if os.path.isfile(appimage_path): + + new_filename = "/tmp/portinstaller/lxtools_installer" + + result = subprocess.run( + ["mv", appimage_path, new_filename], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + self.update_progress(LocaleStrings.MSGA["appimage_renamed"]) + self.log(LocaleStrings.MSGA["appimage_renamed"]) + else: + self.update_progress( + f"{LocaleStrings.MSGA['appimage_rename_error']}{result.stderr}" + ) + self.log( + f"{LocaleStrings.MSGA['appimage_rename_error']}{result.stderr}" + ) + + result = subprocess.run( + ["chmod", "+x", new_filename], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + self.update_progress( + LocaleStrings.MSGA["appimage_executable_success"] + ) + self.log(LocaleStrings.MSGA["appimage_executable_success"]) + else: + self.update_progress( + f"{LocaleStrings.MSGA['appimage_executable_error']}{result.stderr}" + ) + self.log( + f"{LocaleStrings.MSGA['appimage_executable_error']}{result.stderr}" + ) + + # Define reference path + reference_appimage_path = "/usr/local/bin/lxtools_installer" + if os.path.isfile(reference_appimage_path): + + def calculate_sha256(file_path: str) -> str: + """ + Calculates the SHA-256 hash of a file. + + Args: + file_path (str): Path to the file. + + Returns: + str: Hexdigest of the SHA-256 hash. + """ + hash_obj = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_obj.update(chunk) + return hash_obj.hexdigest() + + # Calculate hashes + new_checksum = calculate_sha256(new_filename) + reference_checksum = calculate_sha256(reference_appimage_path) + + # Compare and decide action + if new_checksum == reference_checksum: + return "True" # String as a compatibility solution to network.py + + else: + return False + else: + return False + else: + self.update_progress(LocaleStrings.MSGA["appimage_not_exist"]) + self.log(LocaleStrings.MSGA["appimage_not_exist"]) + return False + + except Exception as e: + self.update_progress(f"Exception occurred: {str(e)}") + self.log(f"Exception occurred: {str(e)}") + return None + def install_project(self, project_key): """Install or update project""" project_info = self.app_manager.get_project_info(project_key) @@ -77,6 +247,13 @@ class InstallationManager: raise Exception(f"{LocaleStrings.MSGI['unknow_project']}{project_key}") def _create_wirepy_install_script(self): + gpg_checker = self.check_gpg() + result_appimage = self.check_appimage() + if not gpg_checker: + print(gpg_checker) + return + if result_appimage is True or result_appimage is None: + return detected_os = Detector.get_os() if detected_os == "Arch Linux": result_unzip = Detector.get_unzip() @@ -90,13 +267,14 @@ class InstallationManager: """Create Wire-Py installation script""" script = f"""#!/bin/bash set -e - -echo "=== Wire-Py Installation ===" +if [ "{result_appimage}" = "False" ]; then + cp -f /tmp/portinstaller/lxtools_installer /usr/local/bin/lxtools_installer +fi if [ "{detected_os}" = "Arch Linux" ]; then if [ "{result_unzip}" = "False" ]; then pacman -S --noconfirm unzip fi - + if [ "{result_wget}" = "False" ]; then pacman -S --noconfirm wget fi @@ -215,13 +393,21 @@ fi # Cleanup cd /tmp -rm -rf wirepy_install +rm -rf wirepy_install portinstaller echo "Wire-Py installation completed!" """ return script def _create_logviewer_install_script(self): + gpg_checker = self.check_gpg() + result_appimage = self.check_appimage() + if not gpg_checker: + print(gpg_checker) + return + + if result_appimage is True or result_appimage is None: + return detected_os = Detector.get_os() if detected_os == "Arch Linux": result_unzip = Detector.get_unzip() @@ -235,7 +421,10 @@ echo "Wire-Py installation completed!" script = f"""#!/bin/bash set -e {LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface" -echo "=== LogViewer Installation ===" + +if [ "{result_appimage}" = "False" ]; then + cp -f /tmp/portinstaller/lxtools_installer /usr/local/bin/lxtools_installer +fi if [ "{detected_os}" = "Arch Linux" ]; then if [ "{result_unzip}" = "False" ]; then pacman -S --noconfirm unzip @@ -273,12 +462,12 @@ if [ ! -d "/usr/share/TK-Themes" ] || [ -z "$(ls -A /usr/share/TK-Themes 2>/dev/ wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip unzip -q wirepy.zip WIREPY_DIR=$(find . -name "Wire-Py" -type d | head -1) - + if [ -d "$WIREPY_DIR/TK-Themes" ]; then echo "Installing TK-Themes..." cp -rf "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ fi - + # Also install icons from Wire-Py if not present if [ -d "$WIREPY_DIR/lx-icons" ] && [ ! -d "/usr/share/icons/lx-icons" ]; then echo "Installing icons from Wire-Py..." @@ -315,7 +504,7 @@ fi # Cleanup cd /tmp -rm -rf logviewer_install +rm -rf logviewer_install portinstaller echo "LogViewer installation completed!" """ @@ -344,7 +533,7 @@ echo "LogViewer installation completed!" # Log output if result.stdout: - self.log(f"STDOUT: {result.stdout}") + self.log((result.stdout).strip("Log\n")) if result.stderr: self.log(f"STDERR: {result.stderr}") @@ -358,6 +547,7 @@ echo "LogViewer installation completed!" except subprocess.TimeoutExpired: raise Exception(LocaleStrings.MSGI["install_timeout"]) + except subprocess.CalledProcessError as e: raise Exception(f"{LocaleStrings.MSGI['install_script_failed']}{e}") @@ -476,12 +666,13 @@ if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then echo "No other LX apps found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/TK-Themes - + rm -rf /usr/local/bin/lxtools_installer + # Remove shared libs for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py logviewer.py; do rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file done - + # Try to remove shared_libs directory if empty rmdir {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} 2>/dev/null || true else @@ -523,15 +714,16 @@ if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then echo "No other LX apps found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/TK-Themes - + rm -rf /usr/local/bin/lxtools_installer + # Remove shared libs (but keep logviewer.py if we're only uninstalling logviewer) for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file done - + # Remove logviewer.py last rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py - + # Try to remove shared_libs directory if empty rmdir LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os] 2>/dev/null || true else @@ -568,7 +760,7 @@ echo "LogViewer uninstallation completed!" # Log output if result.stdout: - self.log(f"STDOUT: {result.stdout}") + self.log(result.stdout) if result.stderr: self.log(f"STDERR: {result.stderr}") @@ -628,6 +820,8 @@ class LXToolsGUI: self.download_icon_label = None self.log_text = None self.selected_project = None + self.polkit_ok = Detector.get_polkit() + self.networkmanager_ok = Detector.get_networkmanager() self.project_frames = {} self.status_labels = {} self.version_labels = {} @@ -661,7 +855,7 @@ class LXToolsGUI: def create_gui(self): """Create the main GUI""" self.root = tk.Tk() - self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") + self.root.title(f"{Locale.APP_NAME} v{LXToolsAppConfig.VERSION}") self.root.geometry( f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}+100+100" ) @@ -677,6 +871,7 @@ class LXToolsGUI: self.root.iconphoto(False, icon) except: pass + self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT) Theme.apply_light_theme(self.root) # Create header @@ -719,10 +914,25 @@ class LXToolsGUI: icon_text_frame = tk.Frame(left_side, bg="#2c3e50") icon_text_frame.pack(anchor="w") - # Tool-Icon - tk.Label( - icon_text_frame, text="🔧", font=("Helvetica", 18), bg="#2c3e50", fg="white" - ).pack(side="left", padx=(0, 8)) + # Tool-Icon (versuche echtes Icon, dann Fallback) + try: + icon = self.image_manager.load_image("header_image") + if icon: + # Resize icon für Header + icon_label = tk.Label(icon_text_frame, image=icon, bg="#2c3e50") + icon_label.image = icon # Referenz behalten + icon_label.pack(side="left", padx=(0, 8)) + else: + raise Exception("Kein Icon gefunden") + except: + # Fallback: Unicode-Symbol statt Emoji + tk.Label( + icon_text_frame, + text="⚙", + font=("Helvetica", 18), + bg="#2c3e50", + fg="white", + ).pack(side="left", padx=(0, 8)) # App Name and Version text_frame = tk.Frame(icon_text_frame, bg="#2c3e50") @@ -774,40 +984,51 @@ class LXToolsGUI: def check_ready_status(self): """Check if system is ready for installation""" - # Checks: - polkit_ok = Detector.get_polkit() - networkmanager_ok = Detector.get_networkmanager() + + # Führe alle Checks durch internet_ok = NetworkChecker.check_internet_connection() repo_ok = NetworkChecker.check_repository_access() - result = Detector.get_host_python_version() + python_result = Detector.get_host_python_version() - if not polkit_ok: - self.update_header_status( - LocaleStrings.MSGO["polkit_check"], "#e74c3c" - ) # Red - if not networkmanager_ok: - self.update_header_status( - LocaleStrings.MSGO["networkmanager_check"], "#e74c3c" - ) # Red + # Sammle alle Probleme + issues = [] + if not self.polkit_ok: + issues.append(("PolicyKit", "#e74c3c", LocaleStrings.MSGO["polkit_check"])) + if not self.networkmanager_ok: + issues.append( + ( + "NetworkManager", + "#e74c3c", + LocaleStrings.MSGO["networkmanager_check"], + ) + ) + if not internet_ok: + issues.append(("Internet", "#e74c3c", LocaleStrings.MSGO["no_internet"])) + if not repo_ok: + issues.append( + ("Repository", "#f39c12", LocaleStrings.MSGO["repo_unavailable"]) + ) + if python_result is None: + issues.append(("Python", "#e74c3c", LocaleStrings.MSGO["python_check"])) - if internet_ok and repo_ok and result is not None: - self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green - elif not internet_ok: - self.update_header_status( - LocaleStrings.MSGO["no_internet"], "#e74c3c" - ) # Red - elif not repo_ok: - self.update_header_status( - LocaleStrings.MSGO["repo_unavailable"], "#f39c12" - ) # Orange - elif result is None: - self.update_header_status( - LocaleStrings.MSGO["python_check"], "#e74c3c" - ) # Red + # Bestimme Hauptstatus basierend auf Priorität + if issues: + # Zeige das wichtigste Problem (Internet > Python > Repository > Services) + for priority_check in [ + "Internet", + "Python", + "Repository", + "PolicyKit", + "NetworkManager", + ]: + for issue_name, color, message in issues: + if issue_name == priority_check: + print(f"DEBUG: Zeige Problem: {issue_name} - {message}") + self.update_header_status(message, color) + return else: - self.update_header_status( - LocaleStrings.MSGO["system_check"], "#3498db" - ) # Blue + + self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green def _create_projects_tab(self): """Create projects tab with project cards""" @@ -1046,14 +1267,16 @@ class LXToolsGUI: clear_log_btn.pack(side="right", pady=(0, 10)) # Initial log message - self.log_message( - f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===" - ) + self.log_message(f"=== {Locale.APP_NAME} v {LXToolsAppConfig.VERSION} ===") self.log_message(f"{LocaleStrings.MSGL['work_dir']}{LXToolsAppConfig.WORK_DIR}") self.log_message( f"{LocaleStrings.MSGL['icons_dir']}{LXToolsAppConfig.ICONS_DIR}" ) self.log_message(f"{LocaleStrings.MSGL['detected_os']}{self.detected_os}") + self.log_message(f"{LocaleStrings.MSGO['polkit_check_log']}{self.polkit_ok}") + self.log_message( + f"{LocaleStrings.MSGO['networkmanager_check_log']}{self.networkmanager_ok}" + ) self.log_message(f"{LocaleStrings.MSGO['ready']}...") def _create_progress_section(self): @@ -1089,58 +1312,87 @@ class LXToolsGUI: button_frame = tk.Frame(self.root, bg=self.colors["bg"]) button_frame.pack(fill="x", padx=15, pady=(5, 10)) - # Button style configuration - style = ttk.Style() + # Button style configuration (mit Error-Handling für Linux Mint) + try: + style = ttk.Style() - # Install button (green) - style.configure("Install.TButton", foreground="#27ae60", font=("Helvetica", 11)) - style.map( - "Install.TButton", - foreground=[("active", "#14542f"), ("pressed", "#1e8449")], - ) + # Install button (green) + style.configure( + "Install.TButton", foreground="#27ae60", font=("Helvetica", 11) + ) + style.map( + "Install.TButton", + foreground=[("active", "#14542f"), ("pressed", "#1e8449")], + ) - # Uninstall button (red) - style.configure( - "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 11) - ) - style.map( - "Uninstall.TButton", - foreground=[("active", "#7d3b34"), ("pressed", "#c0392b")], - ) + # Uninstall button (red) + style.configure( + "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 11) + ) + style.map( + "Uninstall.TButton", + foreground=[("active", "#7d3b34"), ("pressed", "#c0392b")], + ) + # Refresh button (blue) + style.configure( + "Refresh.TButton", foreground="#3498db", font=("Helvetica", 11) + ) + style.map( + "Refresh.TButton", + foreground=[("active", "#1e3747"), ("pressed", "#2980b9")], + ) + except Exception as e: + print(f"Style-Konfiguration fehlgeschlagen: {e}") + # Fallback: verwende Standard-Styles - # Refresh button (blue) - style.configure("Refresh.TButton", foreground="#3498db", font=("Helvetica", 11)) - style.map( - "Refresh.TButton", - foreground=[("active", "#1e3747"), ("pressed", "#2980b9")], - ) - - # Create buttons - install_btn = ttk.Button( - button_frame, - text=LocaleStrings.MSGB["install"], - command=self.install_selected, - style="Install.TButton", - padding=8, - ) + # Create buttons (mit Fallback für Style-Probleme) + try: + install_btn = ttk.Button( + button_frame, + text=LocaleStrings.MSGB["install"], + command=self.install_selected, + style="Install.TButton", + padding=8, + ) + except: + install_btn = ttk.Button( + button_frame, + text=LocaleStrings.MSGB["install"], + command=self.install_selected, + ) install_btn.pack(side="left", padx=(0, 10)) - uninstall_btn = ttk.Button( - button_frame, - text=LocaleStrings.MSGB["uninstall"], - command=self.uninstall_selected, - style="Uninstall.TButton", - padding=8, - ) + try: + uninstall_btn = ttk.Button( + button_frame, + text=LocaleStrings.MSGB["uninstall"], + command=self.uninstall_selected, + style="Uninstall.TButton", + padding=8, + ) + except: + uninstall_btn = ttk.Button( + button_frame, + text=LocaleStrings.MSGB["uninstall"], + command=self.uninstall_selected, + ) uninstall_btn.pack(side="left", padx=(0, 10)) - refresh_btn = ttk.Button( - button_frame, - text=LocaleStrings.MSGB["refresh"], - command=self.refresh_status, - style="Refresh.TButton", - padding=8, - ) + try: + refresh_btn = ttk.Button( + button_frame, + text=LocaleStrings.MSGB["refresh"], + command=self.refresh_status, + style="Refresh.TButton", + width=30, + padding=8, + ) + except: + refresh_btn = ttk.Button( + button_frame, + text=LocaleStrings.MSGB["refresh"], + command=self.refresh_status, + ) refresh_btn.pack(side="right") def update_download_icon(self, status): @@ -1211,16 +1463,16 @@ class LXToolsGUI: if latest_version != "Unknown": if installed_version != f"v. {latest_version}": version_label.config( - text=f"{LocaleStrings.MSGC['latest']}(v. {latest_version}) {LocaleStrings.MSGC['update_available']}", + text=f"{LocaleStrings.MSGC['update_on']}(v. {latest_version}) {LocaleStrings.MSGC['available_lower']}", fg="orange", ) self.log_message( - f"{project_info['name']}: {LocaleStrings.MSGC['update_available']}(v. {latest_version})" + f"{project_info['name']}: {LocaleStrings.MSGC['update_on']}(v. {latest_version} {LocaleStrings.MSGC['available_lower']})" ) else: version_label.config( - text=f"{LocaleStrings.MSGC['latest']}: (v. {latest_version}) {LocaleStrings.MSGC['up_to_date']}", + text=f"(v. {latest_version}) {LocaleStrings.MSGC['up_to_date']}", fg="green", ) self.log_message( @@ -1379,7 +1631,7 @@ class LXToolsGUI: def main(): """Main function to start the application""" - print(f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===") + print(f"=== {Locale.APP_NAME} v {LXToolsAppConfig.VERSION} ===") print(f"{LocaleStrings.MSGL['working_dir']}{os.getcwd()}") try: diff --git a/lxtoolsinstaller.pot b/lxtoolsinstaller.pot new file mode 100644 index 0000000..2d36eb2 --- /dev/null +++ b/lxtoolsinstaller.pot @@ -0,0 +1,487 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-07-09 08:01+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: manager.py:735 +msgid "WireGuard VPN Manager with GUI" +msgstr "WireGuard-VPN-Manager mit GUI" + +#: manager.py:748 +msgid "System Log Viewer with GUI" +msgstr "Systemprotokoll-Ansicht mit GUI" + +#: manager.py:812 +msgid "Refreshing status and checking versions..." +msgstr "Status aktualisieren und Versionen prüfen..." + +#: manager.py:813 +msgid "Starting installation of " +msgstr "Installation von " + +#: manager.py:814 +msgid "Installing " +msgstr "Installiere " + +#: manager.py:815 +msgid " installation successfully!" +msgstr " Installation erfolgreich!" + +#: manager.py:816 +msgid "Installation failed: " +msgstr "Installation fehlgeschlagen: " + +#: manager.py:817 +msgid "Created install script: " +msgstr "Installations-Skript erstellt: " + +#: manager.py:818 +msgid "Installation script failed: " +msgstr "Installations-Skript fehlgeschlagen: " + +#: manager.py:819 +msgid "Installation timed out" +msgstr "Installation abgelaufen" + +#: manager.py:820 +msgid "Installed " +msgstr "Installiert: " + +#: manager.py:824 +msgid "Uninstalling " +msgstr "Deinstalieren von " + +#: manager.py:825 +msgid " uninstalled successfully!" +msgstr " erfolgreich deinstalliert!" + +#: manager.py:826 +msgid "Uninstallation failed: " +msgstr "Deinstallation fehlgeschlagen: " + +#: manager.py:827 +msgid "Created uninstall script: " +msgstr "Deinstallations-Skript erstellt: " + +#: manager.py:828 +msgid "Uninstallation script failed: " +msgstr "Deinstallations-Skript fehlgeschlagen: " + +#: manager.py:829 +msgid "Uninstallation timed out" +msgstr "Deinstallation abgelaufen" + +#: manager.py:833 +msgid "Unknown project: " +msgstr "Unbekanntes Projekt: " + +#: manager.py:834 +msgid " is not installed." +msgstr " ist nicht installiert." + +#: manager.py:835 +msgid "Downloading from " +msgstr "Herunterladen von " + +#: manager.py:836 +msgid "Extracting files..." +msgstr "Dateien entpacken..." + +#: manager.py:837 +msgid "Download failed: " +msgstr "Herunterladen fehlgeschlagen: " + +#: manager.py:838 +msgid "System: " +msgstr "System: " + +#: manager.py:839 +msgid "Linux App Installer" +msgstr "Linux-App-Installer" + +#: manager.py:840 +msgid "Ready for installation" +msgstr "Bereit für Installation" + +#: manager.py:841 +msgid "No internet connection" +msgstr "Keine Internetverbindung" + +#: manager.py:842 +msgid "Repository unavailable" +msgstr "Repository nicht verfügbar" + +#: manager.py:843 +msgid "System checking..." +msgstr "Systemprüfung..." + +#: manager.py:844 +msgid "Applications" +msgstr "Anwendungen" + +#: manager.py:845 +msgid "Progress" +msgstr "Fortschritt" + +#: manager.py:846 +msgid "Status refresh completed" +msgstr "Statusaktualisierung abgeschlossen" + +#: manager.py:847 +msgid "Python not installed" +msgstr "Python nicht installiert" + +#: manager.py:848 +msgid "Please install Polkit!" +msgstr "Bitte installieren Sie Polkit!" + +#: manager.py:849 +msgid "Please install Networkmanager!" +msgstr "Bitte installieren Sie Networkmanager!" + +#: manager.py:850 +msgid "Polkit check: " +msgstr "Polkit-Prüfung: " + +#: manager.py:851 +msgid "Networkmanager check: " +msgstr "Networkmanager-Prüfung: " + +#: manager.py:856 +msgid "Checking..." +msgstr "Prüfen..." + +#: manager.py:857 +msgid "Version: Checking..." +msgstr "Version: Prüfen..." + +#: manager.py:858 +msgid "Update on " +msgstr "Aktualisierung am " + +#: manager.py:859 +msgid "available" +msgstr "verfügbar" + +#: manager.py:860 +msgid "Up to date" +msgstr "Auf dem neuesten Stand" + +#: manager.py:861 +msgid "Latest unknown" +msgstr "Neueste Version unbekannt" + +#: manager.py:862 +msgid "Could not check latest version" +msgstr "Kann aktuelle Version nicht prüfen" + +#: manager.py:863 +msgid "Latest: Check failed" +msgstr "Neueste: Prüfung fehlgeschlagen" + +#: manager.py:864 +msgid "Version check failed" +msgstr "Versionsprüfung fehlgeschlagen" + +#: manager.py:865 +msgid "Not installed" +msgstr "Nicht installiert" + +#: manager.py:866 +msgid "Available " +msgstr "Verfügbar " + +#: manager.py:867 +msgid "Available unknown" +msgstr "Verfügbarkeit unbekannt" + +#: manager.py:868 +msgid "Available: Check failed" +msgstr "Verfügbar: Prüfung fehlgeschlagen" + +#: manager.py:873 +msgid "Selected project: " +msgstr "Ausgewähltes Projekt: " + +#: manager.py:874 +msgid "Installation Log" +msgstr "Installationsprotokoll" + +#: manager.py:875 manager.py:879 +msgid "Working directory: " +msgstr "Arbeitsverzeichnis: " + +#: manager.py:876 +msgid "Icons directory: " +msgstr "Icons-Verzeichnis: " + +#: manager.py:877 +msgid "Detected OS: " +msgstr "Erkanntes Betriebssystem: " + +#: manager.py:878 +msgid "Log cleared" +msgstr "Protokoll geleert" + +#: manager.py:880 +msgid "" +"\n" +"Application interrupted by user." +msgstr "" +"\n" +"Anwendung durch Benutzer unterbrochen." + +#: manager.py:881 +msgid "Fatal error: " +msgstr "Kritischer Fehler: " + +#: manager.py:882 +msgid "Fatal Error Application failed to start: " +msgstr "Kritischer Fehler: Anwendung konnte nicht gestartet werden: " + +#: manager.py:887 +msgid "Clear Log" +msgstr "Protokoll leeren" + +#: manager.py:888 +msgid "Install/Update" +msgstr "Installieren/Aktualisieren" + +#: manager.py:889 +msgid "Uninstall" +msgstr "Deinstallieren" + +#: manager.py:890 +msgid "Refresh Status" +msgstr "Status aktualisieren" + +#: manager.py:895 +msgid "Please select a project to install." +msgstr "Bitte ein Projekt zum Installieren auswählen." + +#: manager.py:897 +msgid "" +"No internet connection available.\n" +"Please check your network connection." +msgstr "" +"Keine Internetverbindung vorhanden.\n" +"Überprüfen Sie Ihre Netzwerkverbindung." + +#: manager.py:900 +msgid "" +"Cannot access repository.\n" +"Please try again later." +msgstr "" +"Repository nicht erreichbar.\n" +Bitte später erneut versuchen. + +#: manager.py:902 +msgid "has been successfully installed/updated." +msgstr "wurde erfolgreich installiert/aktualisiert." + +#: manager.py:903 +msgid "Please select a project to uninstall." +msgstr "Bitte ein Projekt zur Deinstallation auswählen." + +#: manager.py:908 +msgid "Installing tkinter for " +msgstr "Installation von tkinter für " + +#: manager.py:909 +msgid "Command: " +msgstr "Befehl: " + +#: manager.py:910 +msgid "TKinter installation completed successfully!" +msgstr "TKinter-Installation erfolgreich abgeschlossen!" + +#: manager.py:911 +msgid "TKinter installation failed: " +msgstr "TKinter-Installation fehlgeschlagen: " + +#: manager.py:912 +msgid "TKinter installation timed out" +msgstr "TKinter-Installation abgelaufen" + +#: manager.py:913 +msgid "Error installing tkinter: " +msgstr "Fehler bei der Installation von tkinter: " + +#: manager.py:914 +msgid "No tkinter installation command defined for " +msgstr "Kein Installationsbefehl für tkinter definiert für " + +#: manager.py:915 +msgid "Failed to load image from " +msgstr "Bild konnte nicht geladen werden aus " + +#: manager.py:916 +msgid "LogViewer installation check:" +msgstr "Prüfung der Installation von LogViewer:" + +#: manager.py:917 +msgid " Symlink exists: " +msgstr " Symbolischer Link existiert: " + +#: manager.py:918 +msgid " Executable exists: " +msgstr " Ausführbarer Datei existiert: " + +#: manager.py:919 +msgid " Is executable: " +msgstr " Ist ausführbar: " + +#: manager.py:920 +msgid " Final result: " +msgstr " Endergebnis: " + +#: manager.py:921 +msgid "Error getting version for " +msgstr "Fehler beim Abrufen der Version für " + +#: manager.py:926 +msgid "Error verifying signature: " +msgstr "Fehler bei der Prüfung der Signatur: " + +#: manager.py:927 +msgid "Could not find GPG signature: " +msgstr "GPG-Signatur nicht gefunden: " + +#: manager.py:928 +msgid "SHA256-File not found: " +msgstr "SHA256-Datei nicht gefunden: " + +#: manager.py:929 +msgid "Would you like to continue?" +msgstr "Möchten Sie fortfahren?" + +#: manager.py:930 +msgid "SHA256 hash mismatch. File might be corrupted!" +msgstr "SHA256-Hash passt nicht überein. Datei könnte beschädigt sein!" + +#: manager.py:931 +msgid "Failed to retrieve version from Gitea API" +msgstr "Konnte Version über die Gitea-API nicht abrufen" + +#: manager.py:932 +msgid "Error retrieving latest version: " +msgstr "Fehler beim Abrufen der neuesten Version: " + +#: manager.py:933 +msgid "GPG verification successful. Signature is valid." +msgstr "GPG-Prüfung erfolgreich. Die Signatur ist gültig." + +#: manager.py:934 +msgid "Error GPG check: " +msgstr "Fehler bei der GPG-Prüfung: " + +#: manager.py:935 +msgid "Error downloading or verifying AppImage: " +msgstr "Fehler beim Herunterladen oder Verifizieren der AppImage: " + +#: manager.py:936 +msgid "Error accessing Gitea repository: " +msgstr "Fehler beim Zugreifen auf das Gitea-Repository: " + +#: manager.py:937 +msgid "Error: " +msgstr "Fehler: " + +#: manager.py:938 +msgid "The AppImage renamed..." +msgstr "Die AppImage umbenannt..." + +#: manager.py:939 +msgid "Error renaming the AppImage: " +msgstr "Fehler beim Umbenennen der AppImage: " + +#: manager.py:940 +msgid "Error making the AppImage executable: " +msgstr "Fehler beim Markieren der AppImage als ausführbar: " + +#: manager.py:941 +msgid "The AppImage has been made executable" +msgstr "Die AppImage wurde als ausführbar markiert" + +#: manager.py:942 +msgid "Error: The AppImage file does not exist." +msgstr "Fehler: Die AppImage-Datei existiert nicht." + +#: manager.py:947 +msgid "" +"Warning: 'gpg' is not installed. Please install it to verify the AppImage " +"signature." +msgstr "" +"Warnung: 'gpg' ist nicht installiert. Bitte installieren Sie es, um die " +"AppImage-Signatur zu überprüfen." + +#: manager.py:949 +msgid "URL not reachable: " +msgstr "URL nicht erreichbar: " + +#: manager.py:951 +msgid "No fingerprint found in the key. File might be corrupted or empty." +msgstr "Kein Fingerabdruck in dem Schlüssel gefunden. Datei könnte beschädigt oder leer sein." + +#: manager.py:953 +msgid "Fingerprint mismatch: Expected " +msgstr "Fingerabdruck nicht übereinstimmend: Erwartet " + +#: manager.py:954 +msgid "but got: " +msgstr "aber erhalten: " + +#: manager.py:955 +msgid "Failed to import public key: " +msgstr "Fehler beim Importieren des öffentlichen Schlüssels: " + +#: manager.py:956 +msgid "GPG fingerprint extraction failed: " +msgstr "Extrahierung von GPG-Fingerabdruck fehlgeschlagen: " + +#: manager.py:957 +msgid "Error importing GPG key from " +msgstr "Fehler beim Importieren des GPG-Schlüssels aus " + +#: manager.py:958 +msgid "Error updating trust level: " +msgstr "Fehler beim Aktualisieren des Vertrauensniveaus: " + +#: manager.py:959 +msgid "Error executing script to update trust level: " +msgstr "Fehler beim Ausführen des Skripts zur Aktualisierung des Vertrauensniveaus: " + +#: manager.py:961 +msgid "OpenPGP keyserver is reachable. Proceeding with download." +msgstr "Der OpenPGP-Schlüsselserver ist erreichbar. Herunterladen wird fortgesetzt." + +#: manager.py:964 +msgid "OpenPGP keyserver unreachable. Skipping this source." +msgstr "Der OpenPGP-Schlüsselserver ist nicht erreichbar. Dieser Quelle wird übersprungen." + +#: manager.py:967 +msgid "All keys have valid fingerprints matching the expected value." +msgstr "Alle Schlüssel haben gültige Fingerabdrücke, die mit dem erwarteten Wert übereinstimmen." + +#: manager.py:969 +msgid "Trust level 5 successfully applied to key " +msgstr "Vertrauensniveau 5 erfolgreich auf Schlüssel angewendet " + +#: manager.py:970 +msgid "Not all keys are valid." +msgstr "Nicht alle Schlüssel sind gültig." + diff --git a/manager.py b/manager.py index 19c4915..4fa9b06 100644 --- a/manager.py +++ b/manager.py @@ -11,7 +11,28 @@ import sys import shutil import subprocess import stat -from network import GiteaUpdate +from network import GiteaUpdater + + +class Locale: + APP_NAME = "lxtoolsinstaller" + # Locale settings + LOCALE_DIR = "./locale/" + + @staticmethod + def setup_translations() -> gettext.gettext: + """Initialize translations and set the translation function""" + try: + locale.bindtextdomain(Locale.APP_NAME, Locale.LOCALE_DIR) + gettext.bindtextdomain(Locale.APP_NAME, Locale.LOCALE_DIR) + gettext.textdomain(Locale.APP_NAME) + except: + pass + return gettext.gettext + + +# Initialize translations +_ = Locale.setup_translations() class Detector: @@ -107,14 +128,16 @@ class Detector: arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"] if os_system in deb: result = subprocess.run( - ["apt", "list", "--installed", "|", "grep", "polkit"], + ["apt list --installed | grep polkit"], capture_output=True, + shell=True, text=True, check=False, ) if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False elif os_system in arch: @@ -127,30 +150,35 @@ class Detector: if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False elif os_system == "Fedora": result = subprocess.run( - ["dnf", "list", "--installed", "|", "grep", "polkit"], + ["systemctl --type=service | grep polkit"], capture_output=True, + shell=True, text=True, check=False, ) if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False elif os_system == "SUSE Tumbleweed" or os_system == "SUSE Leap": result = subprocess.run( - ["zypper", "search", "--installed-only", "|", "grep", "polkit"], + ["zypper search --installed-only | grep pkexec"], capture_output=True, + shell=True, text=True, check=False, ) if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False @staticmethod @@ -161,14 +189,17 @@ class Detector: arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"] if os_system in deb: result = subprocess.run( - ["apt", "list", "--installed", "|", "grep", "network-manager"], + ["apt list --installed | grep network-manager"], capture_output=True, + shell=True, text=True, check=False, ) + if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False elif os_system in arch: @@ -178,33 +209,40 @@ class Detector: text=True, check=False, ) + if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False elif os_system == "Fedora": + result = subprocess.run( - ["dnf", "list", "--installed", "|", "grep", "NetworkManager"], + ["which NetworkManager"], capture_output=True, + shell=True, text=True, - check=False, + check=True, ) if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False elif os_system == "SUSE Tumbleweed" or os_system == "SUSE Leap": result = subprocess.run( - ["zypper", "search", "--installed-only", "|", "grep", "networkmanager"], + ["zypper search --installed-only | grep NetworkManager"], capture_output=True, + shell=True, text=True, - check=False, + check=True, ) if result.returncode == 0: return True else: + print(f"STDERR: {result.stderr}") return False @@ -322,6 +360,10 @@ class Image: "./lx-icons/48/log.png", "/usr/share/icons/lx-icons/48/log.png", ], + "header_image": [ + "./lx-icons/32/lxtools_key.png", + "/usr/share/icons/lx-icons/32/lxtools_key.png", + ], } # Get paths to try @@ -430,7 +472,7 @@ class AppManager: if not project_info: return "Unknown" - return GiteaUpdate.api_down(project_info["api_url"]) + return GiteaUpdater.get_latest_version_from_api(project_info["api_url"]) def check_other_apps_installed(self, exclude_key) -> bool: """Check if other apps are still installed""" @@ -658,24 +700,8 @@ class LXToolsAppConfig: os.path.dirname(os.path.abspath(__file__)), "certs", "cacert.pem" ) - @staticmethod - def setup_translations() -> gettext.gettext: - """Initialize translations and set the translation function""" - try: - locale.bindtextdomain( - LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR - ) - gettext.bindtextdomain( - LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR - ) - gettext.textdomain(LXToolsAppConfig.APP_NAME) - except: - pass - return gettext.gettext - - VERSION = "1.1.7" - APP_NAME = "lxtoolsinstaller" - WINDOW_WIDTH = 450 + VERSION = "1.1.8" + WINDOW_WIDTH = 460 WINDOW_HEIGHT = 580 # Working directory WORK_DIR = os.getcwd() @@ -683,9 +709,6 @@ class LXToolsAppConfig: THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") TEMP_DIR = "/tmp/lxtools" - # Locale settings - LOCALE_DIR = "./locale/" - # Download URLs WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip" SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" @@ -696,11 +719,20 @@ class LXToolsAppConfig: "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" ) + # GPG required + EXPECTED_FINGERPRINT = "743745087C6414E00F1EF84D4CCF06B6CE2A4C7F" + KEY_URL_OPENPGP = ( + f"https://keys.openpgp.org/vks/v1/by-fingerprint/{EXPECTED_FINGERPRINT}" + ) + KEY_URL_GITILUNIX = ( + "https://git.ilunix.de/punix/lxtools_installer/raw/branch/main/public_key.asc" + ) + # Project configurations PROJECTS = { "wirepy": { "name": "Wire-Py", - "description": "WireGuard VPN Manager with GUI", + "description": _("WireGuard VPN Manager with GUI"), "download_url": WIREPY_URL, "api_url": WIREPY_API_URL, "icon_key": "icon_vpn", @@ -713,7 +745,7 @@ class LXToolsAppConfig: }, "logviewer": { "name": "LogViewer", - "description": "System Log Viewer with GUI", + "description": _("System Log Viewer with GUI"), "download_url": SHARED_LIBS_URL, "api_url": SHARED_LIBS_API_URL, "icon_key": "icon_log", @@ -772,8 +804,6 @@ class LXToolsAppConfig: LXToolsAppConfig.extract_data_files() -# Initialize translations -_ = LXToolsAppConfig.setup_translations() class LocaleStrings: @@ -817,14 +847,16 @@ class LocaleStrings: "python_check": _("Python not installed"), "polkit_check": _("Please install Polkit!"), "networkmanager_check": _("Please install Networkmanager!"), + "polkit_check_log": _("Polkit check: "), + "networkmanager_check_log": _("Networkmanager check: "), } # MSGC = Strings on Cards MSGC = { "checking": _("Checking..."), "version_check": _("Version: Checking..."), - "latest": _("Latest: "), - "update_available": _("Update available "), + "update_on": _("Update on "), + "available_lower": _("available"), "up_to_date": _("Up to date"), "latest_unknown": _("Latest unknown"), "could_not_check": _("Could not check latest version"), @@ -873,7 +905,7 @@ class LocaleStrings: # MSGP = Others print strings MSGP = { - "tk_install": _("Installing tkinter for )"), + "tk_install": _("Installing tkinter for "), "command_string": _("Command: "), "tk_success": _("TKinter installation completed successfully!"), "tk_failed": _("TKinter installation failed: "), @@ -888,3 +920,52 @@ class LocaleStrings: "final_result": _(" Final result: "), "get_version_error": _("Error getting version for "), } + + # MSGG = String on AppImageMessagDialogs and Strings + MSGA = { + "gitea_gpg_error": _("Error verifying signature: "), + "not_gpg_found": _("Could not find GPG signature: "), + "SHA256_File_not_found": _("SHA256-File not found: "), + "SHA256_File_not_found1": _("Would you like to continue?"), + "SHA256_hash mismatch": _("SHA256 hash mismatch. File might be corrupted!"), + "Failed_retrieving": _("Failed to retrieve version from Gitea API"), + "Error_retrieving": _("Error retrieving latest version: "), + "gpg_verify_success": _("GPG verification successful. Signature is valid."), + "error_gpg_check": _("Error GPG check: "), + "error_gpg_download": _("Error downloading or verifying AppImage: "), + "error_repo": _("Error accessing Gitea repository: "), + "error": _("Error: "), + "appimage_renamed": _("The AppImage renamed..."), + "appimage_rename_error": _("Error renaming the AppImage: "), + "appimage_executable_error": _("Error making the AppImage executable: "), + "appimage_executable_success": _("The AppImage has been made executable"), + "appimage_not_exist": _("Error: The AppImage file does not exist."), + } + + MSGGPG = { + "gpg_missing": _( + "Warning: 'gpg' is not installed. Please install it to verify the AppImage signature." + ), + "url_not_reachable": _("URL not reachable: "), + "corrupted_file": _( + "No fingerprint found in the key. File might be corrupted or empty." + ), + "mismatch": _("Fingerprint mismatch: Expected "), + "but_got": _("but got: "), + "failed_import": _("Failed to import public key: "), + "fingerprint_extract": _("GPG fingerprint extraction failed: "), + "error_import_key": _("Error importing GPG key from "), + "error_updating_trust_level": _("Error updating trust level: "), + "error_executing_script": _("Error executing script to update trust level: "), + "keyserver_reachable": _( + "OpenPGP keyserver is reachable. Proceeding with download." + ), + "keyserver_unreachable": _( + "OpenPGP keyserver unreachable. Skipping this source." + ), + "all_keys_valid": _( + "All keys have valid fingerprints matching the expected value." + ), + "set_trust_level": _("Trust level 5 successfully applied to key "), + "not_all_keys_are_valid": _("Not all keys are valid."), + } diff --git a/network.py b/network.py index ba60639..7e032d7 100644 --- a/network.py +++ b/network.py @@ -1,28 +1,428 @@ import socket +import os import urllib.request import json +import hashlib +import subprocess +from typing import Union, List +import re +import tempfile -class GiteaUpdate: +class GPGManager: + @staticmethod - def api_down(url, current_version=""): - """Get latest version from Gitea API""" + def get_gpg() -> bool: + """ + Check if gpg is installed. + + Returns: + bool: True if `gpg` is installed, False otherwise. + """ + result = subprocess.run( + ["which gpg || command -v gpg"], + capture_output=True, + shell=True, + text=True, + check=False, + ) + if result.returncode == 0: + return True + else: + return False + + @staticmethod + def is_key_already_imported(key_id: str) -> bool: + """ + Prüft, ob der Schlüssel bereits im Keyring importiert ist. + + Args: + key_id (str): ID des öffentlichen Schlüssels (z. B. '7D8A6E1F9B4C3A5D...') + + Returns: + bool: True, wenn der Schlüssel vorhanden ist. + """ + from message import MessageDialog + from manager import LocaleStrings + + # Check if `gpg` is installed + if not GPGManager.get_gpg(): + + MessageDialog("warning", LocaleStrings.MSGGPG["gpg_missing"]).show() + return False + + try: + result = subprocess.run( + ["gpg", "--list-keys", key_id], + capture_output=True, + text=True, + check=True, + ) + if key_id in result.stdout: + return True + else: + return False + except Exception as e: + if e.returncode == 2: + return False + else: + MessageDialog( + "error", f"{LocaleStrings.MSGA['error_gpg_check']}{e}" + ).show() + return False + + @staticmethod + def is_url_reachable(url: str, timeout: int = 5) -> bool: + """ + Checks if a given URL is reachable. + + Args: + url (str): The URL to check. + timeout (int): Timeout in seconds for the connection attempt. + + Returns: + bool: True if the URL is reachable, False otherwise. + """ + try: + urllib.request.urlopen(url, timeout=timeout) + return True + except Exception as e: + from message import MessageDialog + from manager import LocaleStrings + + MessageDialog( + "error", + f"{LocaleStrings.MSGGPG['url_not_reachable']}{url} - {LocaleStrings.MSGA['error']}{e}", + ) + return False + + @staticmethod + def import_key_from_url( + key_url: str, expected_fingerprint: str, filename: str + ) -> bool: + """ + Downloads a GPG public key from the given URL, verifies its fingerprint matches + the expected value, and imports it into the local GPG keyring. + + Args: + key_url (str): URL to the `.asc` public key file. + expected_fingerprint (str): Expected 40-character hexadecimal fingerprint of the key. + filename (str): Destination filename for saving the downloaded key in `PUBLIC_KEYS_DIR`. + + Returns: + bool: True if the key was downloaded, verified, and successfully imported. False otherwise. + """ + from message import MessageDialog + from manager import LocaleStrings + + # Configuration: Define the directory for storing public key files + PUBLIC_KEYS_DIR = "/tmp/public_keys" + if not os.path.exists(PUBLIC_KEYS_DIR): + os.makedirs(PUBLIC_KEYS_DIR) + + try: + # Construct full path to save the key file + key_path = os.path.join(PUBLIC_KEYS_DIR, filename) + print(f"Downloading public key from {key_url} to {key_path}") + urllib.request.urlretrieve(key_url, key_path) + + result = subprocess.run( + ["gpg", "-fingerprint", key_path], + capture_output=True, + text=True, + check=True, + ) + + if result.returncode == 0: + # Extract all fingerprints from GPG output (40-character hexadecimal) + fingerprints_from_key: List[str] = [] + for line in result.stdout.splitlines(): + matches = re.findall(r"\b[0-9A-Fa-f]{40}\b", line) + fingerprints_from_key.extend(matches) + + if not fingerprints_from_key: + MessageDialog( + "warning", LocaleStrings.MSGGPG["corrupted_file"] + ).show() + return False + + # Check if any of the extracted fingerprints match the expected one + found = any(fp == expected_fingerprint for fp in fingerprints_from_key) + if not found: + MessageDialog( + "warning", + f"{LocaleStrings.MSGGPG['mismatch']}\n{expected_fingerprint}\n{LocaleStrings.MSGGPG['but_got']}\n{fingerprints_from_key}", + wraplength=450, + ).show() + + return False + + else: + # Import key into GPG keyring + result_import = subprocess.run( + ["gpg", "--import", key_path], + capture_output=True, + text=True, + check=True, + ) + if result_import.returncode == 0: + print(f"Public key from {key_url} successfully imported.") + return True + else: + MessageDialog( + "error", + f"{LocaleStrings.MSGGPG['failed_import']}{result_import.stderr}", + ).show() + return False + + else: + MessageDialog( + "error", + f"{LocaleStrings.MSGGPG['fingerprint_extract']}{result.stderr}", + ) + return False + + except Exception as e: + MessageDialog( + "error", f"{LocaleStrings.MSGGPG['error_import_key']}{key_url}: {e}" + ) + return False + + @staticmethod + def update_gpg_trust_level(key_id: str, trust_level: int = 5) -> bool: + """ + Sets the trust level for a specified GPG public key. + + Args: + key_id (str): The hexadecimal ID of the key to update. + trust_level (int): Trust level (1-5). Default is 5 (fully trusted). + + Returns: + bool: True if the trust level was successfully updated, False otherwise. + """ + script = f"""#!/bin/bash + # Set required environment variables + export GPG_TTY=$(tty) + export GNUPG_STATUS="1" + export GNUPGAGENT_INFO_FILE="/dev/null" + gpg --batch \ + --no-tty \ + --command-fd 0 \ + --edit-key {key_id} << EOF + trust + {trust_level} + quit + EOF + """ + + try: + + from message import MessageDialog + from manager import LocaleStrings + + # Create temporary script file + temp_script = tempfile.NamedTemporaryFile( + mode="w", suffix=".sh", delete=False, encoding="utf-8" + ) + temp_script.write(script) + temp_script.close() + + print(f"Setting trust level {trust_level} for key {key_id}") + result = subprocess.run( + ["bash", temp_script.name], capture_output=True, text=True, check=True + ) + + if result.returncode != 0: + + MessageDialog( + "error", + f"{LocaleStrings.MSGGPG['error_updating_trust_level']}{result.stderr}", + ).show() + + except (subprocess.CalledProcessError, FileNotFoundError) as e: + MessageDialog( + "error", f"{LocaleStrings.MSGGPG['error_executing_script']}{e}" + ).show() + return False + + finally: + try: + os.unlink(temp_script.name) + except OSError: + pass + + return True + + +class GiteaUpdater: + @staticmethod + def get_latest_version_from_api(url: str, current_version: str = "") -> str: + """ + Fetches the latest version of a project from the Gitea API. + + Args: + url (str): The URL to query the Gitea API for releases. + current_version (str, optional): Not used in this implementation. Defaults to "". + + Returns: + str: Latest version tag name without the 'v' prefix. + If an error occurs, returns "Unknown". + """ try: with urllib.request.urlopen(url, timeout=10) as response: data = json.loads(response.read().decode()) if data and len(data) > 0: latest_version = data[0].get("tag_name", "Unknown") - return latest_version.lstrip("v") # Remove 'v' prefix if present + return latest_version.lstrip("v") # Remove 'v' prefix return "Unknown" except Exception as e: print(f"API Error: {e}") return "Unknown" + @staticmethod + def download_appimage( + version: str = "latest", + verify_checksum: bool = False, + verify_signature: bool = False, + ) -> Union[str, None]: + """ + Downloads the AppImage file to /tmp/portinstaller and performs verification checks. + + Args: + version (str): 'latest' for the latest version or a specific version. + verify_checksum (bool): Whether to perform SHA256 checksum verification. + verify_signature (bool): Whether to validate GPG signature. + + Returns: + str: Full path to the AppImage if all checks pass. Otherwise, returns an error message. + None: If an exception occurs during download or verification. + """ + base_url = "https://git.ilunix.de/punix/lxtools_installer/releases/download/" + + from message import MessageDialog + from manager import LocaleStrings + + # Get latest version from API if not provided + if version == "latest": + try: + + with urllib.request.urlopen( + "https://git.ilunix.de/api/v1/repos/punix/lxtools_installer/releases?limit=1", + timeout=10, + ) as response: + data = json.loads(response.read().decode()) + latest_version = data[0].get("tag_name") + if not latest_version: + print(LocaleStrings.MSGA["Failed_retrieving"]) + return None + except Exception as e: + print(f"{LocaleStrings.MSGA['Error_retrieving']}{e}") + return None + + else: + latest_version = version + + # Create /tmp/portinstaller directory if it doesn't exist + download_dir = "/tmp/portinstaller" + os.makedirs(download_dir, exist_ok=True) + + filename = f"{download_dir}/lxtools_installer{latest_version}-x86_64.AppImage" + appimage_url = f"{base_url}{latest_version}/{filename.split('/')[-1]}" + checksum_url = f"{base_url}{latest_version}/{filename.split('/')[-1]}.sha256" + signature_url = f"{base_url}{latest_version}/{filename.split('/')[-1]}.asc" + + try: + print("Downloading AppImage Updater...") + urllib.request.urlretrieve(appimage_url, filename) + + # SHA256 checksum verification + result = None + if verify_checksum: + try: + with urllib.request.urlopen(checksum_url, timeout=10) as response: + checksum_content = response.read().decode() + expected_hash, _ = checksum_content.strip().split(" ") + except Exception as e: + + result = MessageDialog( + "ask", + f"{LocaleStrings.MSGA['SHA256_File_not_found']}{e} {LocaleStrings.MSGA['SHA256_File_not_found1']}", + buttons=["Yes", "No"], + ).show() + + if not result: + return "Checksum file not found" + + if result: + pass + else: + with open(filename, "rb") as f: + file_hash = hashlib.sha256(f.read()).hexdigest() + + if expected_hash != file_hash: + + MessageDialog( + "warning", LocaleStrings.MSGA["SHA256 hash mismatch"] + ).show() + return "Checksum mismatch" + + # GPG signature verification + if verify_signature: + signature_path = f"{download_dir}/{filename.split('/')[-1]}.asc" + try: + urllib.request.urlretrieve(signature_url, signature_path) + except Exception as e: + + MessageDialog( + "error", f"{LocaleStrings.MSGA['not_gpg_found']}{e}" + ).show() + return "Signature file not found" + + try: + result = subprocess.run( + ["gpg", "--verify", signature_path, filename], + capture_output=True, + text=True, + check=True, + ) + if result.returncode == 0: + print(LocaleStrings.MSGA["gpg_verify_success"]) + except Exception as e: + from message import MessageDialog + + MessageDialog( + "error", f"{LocaleStrings.MSGA['error_gpg_check']}{e.stderr}" + ).show() + return "Signature verification failed" + + # Return the full path to the AppImage + return filename + + except Exception as e: + from message import MessageDialog + + MessageDialog( + "error", f"{LocaleStrings.MSGA['error_gpg_download']}{e}" + ).show() + return None + class NetworkChecker: @staticmethod - def check_internet_connection(host="8.8.8.8", port=53, timeout=3): - """Check if internet connection is available""" + def check_internet_connection( + host: str = "8.8.8.8", port: int = 53, timeout: float = 3 + ) -> bool: + """ + Checks if an internet connection is available. + + Args: + host (str): Host to connect to for testing. Defaults to "8.8.8.8" (Google DNS). + port (int): Port number to use for the test. Defaults to 53. + timeout (float): Timeout in seconds for the connection attempt. Defaults to 3. + + Returns: + bool: True if internet is available, False otherwise. + """ try: socket.setdefaulttimeout(timeout) socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) @@ -31,10 +431,22 @@ class NetworkChecker: return False @staticmethod - def check_repository_access(url="https://git.ilunix.de", timeout=5): - """Check if repository is accessible""" + def check_repository_access( + url: str = "https://git.ilunix.de", timeout: float = 5 + ) -> bool: + """ + Checks if the Gitea repository is accessible. + + Args: + url (str): The URL of the Gitea repository. Defaults to "https://git.ilunix.de". + timeout (float): Timeout in seconds for the connection attempt. Defaults to 5. + + Returns: + bool: True if the repository is accessible, False otherwise. + """ try: urllib.request.urlopen(url, timeout=timeout) return True - except: + except Exception as e: + print(f"Error accessing Gitea repository: {e}") return False diff --git a/public_key.asc b/public_key.asc new file mode 100644 index 0000000..a3e1ae1 --- /dev/null +++ b/public_key.asc @@ -0,0 +1,64 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +40WrGrMw8RQJRui1bNFIz4BT0w== +=G2Bo +-----END PGP PUBLIC KEY BLOCK-----