9.07.2025-gpg-ipmort-public-key #1

Merged
punix merged 2 commits from 9.07.2025-gpg-ipmort-public-key into main 2025-07-09 08:54:23 +02:00
16 changed files with 1971 additions and 195 deletions

28
.gitignore vendored
View File

@ -4,3 +4,31 @@ debug.log
.idea .idea
.vscode .vscode
__pycache__ __pycache__
# Build-Artefakte
build/
dist/
certs/
*.spec.bak
lxtools_installer
lxtools_installer.AppImage
lxtools_installer_compat
build_compatible.sh
build_local.sh
clean_build.sh
start_builder.sh
test_extract.py
test_paths.py
test_resources.py
manager_fixed.py
test_simple.sh
test_container.sh
# Docker-Build
docker_build/
debug_docker.sh
Dockerfile.nuitka
DOCKER_BUILD_ANLEITUNG.md
nuitka_builder.py
Dockerfile.test
Dockerfile.simple

View File

@ -2,9 +2,54 @@ Changelog for LXTools installer
## [Unreleased] ## [Unreleased]
- language set auto detection - 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
- add methode sigi, clean_files and remove_lxtools_files
for remove files and dirs on close lxtools_installer
- fix message dialog on font and padding
- add methods check polkit and check Networkmanager is installed
and view in header is result false
### Added ### Added
23-06-2025 23-06-2025
@ -27,6 +72,7 @@ Changelog for LXTools installer
version ud the respective recognized system to run with version ud the respective recognized system to run with
it on all supported systems. it on all supported systems.
### Added ### Added
21-06-2025 21-06-2025
@ -47,6 +93,7 @@ Changelog for LXTools installer
- Installer divided into several modules and added new MessageDialog module - Installer divided into several modules and added new MessageDialog module
### Added ### Added
4-06-2025 4-06-2025
@ -55,6 +102,7 @@ Changelog for LXTools installer
- add ensure_shared_libs_pth_exists Script to install - add ensure_shared_libs_pth_exists Script to install
### Added ### Added
4-06-2025 4-06-2025

View File

@ -2,6 +2,36 @@
LX Tools Installer is a GUI for simple install, update, and remove Apps from ilunix.de 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 # Screenshots
[![wire-py.png](https://fb.ilunix.de/api/public/dl/ZnfG9gxv?inline=true)](https://fb.ilunix.de/share/ZnfG9gxv) [![wire-py.png](https://fb.ilunix.de/api/public/dl/ZnfG9gxv?inline=true)](https://fb.ilunix.de/share/ZnfG9gxv)

51
gpg_setup.sh Executable file
View File

@ -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"

70
gpg_simple_setup.sh Executable file
View File

@ -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 <<EOF
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: Désiré Werner Menrath
Name-Email: polunga40@unity-mail.de
Expire-Date: 2y
%no-protection
%commit
EOF
echo "🔑 Erstelle Schlüssel..."
gpg --batch --generate-key /tmp/gpg_batch
rm /tmp/gpg_batch
echo "✅ GPG-Schlüssel erstellt!"
else
echo "✅ GPG-Schlüssel bereits vorhanden"
fi
# Get the key ID
KEY_ID=$(gpg --list-secret-keys --keyid-format SHORT | grep "sec" | head -1 | sed 's/.*\///' | cut -d' ' -f1)
echo ""
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"
# 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"

BIN
lx-icons/16/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

BIN
lx-icons/16/wg_vpn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

BIN
lx-icons/32/lxtools_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -2,7 +2,10 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import os import os
import sys
import subprocess import subprocess
from typing import Optional, List
import hashlib
import getpass import getpass
from datetime import datetime from datetime import datetime
import tempfile import tempfile
@ -17,8 +20,9 @@ from manager import (
Image, Image,
AppManager, AppManager,
LxTools, LxTools,
Locale,
) )
from network import NetworkChecker from network import NetworkChecker, GiteaUpdater, GPGManager
from message import MessageDialog from message import MessageDialog
@ -33,16 +37,183 @@ class InstallationManager:
self.system_manager = System() self.system_manager = System()
self.download_manager = DownloadManager() 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): def install_project(self, project_key):
"""Install or update project""" """Install or update project"""
project_info = self.app_manager.get_project_info(project_key) project_info = self.app_manager.get_project_info(project_key)
if not project_info: if not project_info:
raise Exception(f"{LocaleStrings.MSG["unknow_project"]}{project_key}") raise Exception(f"{LocaleStrings.MSG['unknow_project']}{project_key}")
self.update_progress( self.update_progress(
f"{LocaleStrings.MSGI["start_install"]}{project_info['name']}..." f"{LocaleStrings.MSGI['start_install']}{project_info['name']}..."
) )
self.log(f"=== {LocaleStrings.MSGI["install"]}{project_info['name']} ===") self.log(f"=== {LocaleStrings.MSGI['install']}{project_info['name']} ===")
try: try:
# Create installation script # Create installation script
@ -52,19 +223,19 @@ class InstallationManager:
self._execute_install_script(script_content) self._execute_install_script(script_content)
self.update_progress( self.update_progress(
f"{project_info["name"]}{LocaleStrings.MSGI["install_success"]}" f"{project_info['name']}{LocaleStrings.MSGI['install_success']}"
) )
self.log( self.log(
f"=== {project_info["name"]}{LocaleStrings.MSGI["install_success"]} ===" f"=== {project_info['name']}{LocaleStrings.MSGI['install_success']} ==="
) )
# Set success icon # Set success icon
self.update_icon("success") self.update_icon("success")
except Exception as e: except Exception as e:
self.log(f"ERROR: {LocaleStrings.MSGI["install_failed"]}{e}") self.log(f"ERROR: {LocaleStrings.MSGI['install_failed']}{e}")
self.update_icon("error") self.update_icon("error")
raise Exception(f"{LocaleStrings.MSGI["install_failed"]}{e}") raise Exception(f"{LocaleStrings.MSGI['install_failed']}{e}")
def _create_install_script(self, project_key): def _create_install_script(self, project_key):
"""Create installation script based on project""" """Create installation script based on project"""
@ -73,9 +244,16 @@ class InstallationManager:
elif project_key == "logviewer": elif project_key == "logviewer":
return self._create_logviewer_install_script() return self._create_logviewer_install_script()
else: else:
raise Exception(f"{LocaleStrings.MSGI["unknow_project"]}{project_key}") raise Exception(f"{LocaleStrings.MSGI['unknow_project']}{project_key}")
def _create_wirepy_install_script(self): 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() detected_os = Detector.get_os()
if detected_os == "Arch Linux": if detected_os == "Arch Linux":
result_unzip = Detector.get_unzip() result_unzip = Detector.get_unzip()
@ -89,13 +267,14 @@ class InstallationManager:
"""Create Wire-Py installation script""" """Create Wire-Py installation script"""
script = f"""#!/bin/bash script = f"""#!/bin/bash
set -e set -e
if [ "{result_appimage}" = "False" ]; then
echo "=== Wire-Py Installation ===" cp -f /tmp/portinstaller/lxtools_installer /usr/local/bin/lxtools_installer
fi
if [ "{detected_os}" = "Arch Linux" ]; then if [ "{detected_os}" = "Arch Linux" ]; then
if [ "{result_unzip}" = "False" ]; then if [ "{result_unzip}" = "False" ]; then
pacman -S --noconfirm unzip pacman -S --noconfirm unzip
fi fi
if [ "{result_wget}" = "False" ]; then if [ "{result_wget}" = "False" ]; then
pacman -S --noconfirm wget pacman -S --noconfirm wget
fi fi
@ -214,13 +393,21 @@ fi
# Cleanup # Cleanup
cd /tmp cd /tmp
rm -rf wirepy_install rm -rf wirepy_install portinstaller
echo "Wire-Py installation completed!" echo "Wire-Py installation completed!"
""" """
return script return script
def _create_logviewer_install_script(self): 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() detected_os = Detector.get_os()
if detected_os == "Arch Linux": if detected_os == "Arch Linux":
result_unzip = Detector.get_unzip() result_unzip = Detector.get_unzip()
@ -234,7 +421,10 @@ echo "Wire-Py installation completed!"
script = f"""#!/bin/bash script = f"""#!/bin/bash
set -e set -e
{LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface" {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 [ "{detected_os}" = "Arch Linux" ]; then
if [ "{result_unzip}" = "False" ]; then if [ "{result_unzip}" = "False" ]; then
pacman -S --noconfirm unzip pacman -S --noconfirm unzip
@ -272,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 wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip
unzip -q wirepy.zip unzip -q wirepy.zip
WIREPY_DIR=$(find . -name "Wire-Py" -type d | head -1) WIREPY_DIR=$(find . -name "Wire-Py" -type d | head -1)
if [ -d "$WIREPY_DIR/TK-Themes" ]; then if [ -d "$WIREPY_DIR/TK-Themes" ]; then
echo "Installing TK-Themes..." echo "Installing TK-Themes..."
cp -rf "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ cp -rf "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/
fi fi
# Also install icons from Wire-Py if not present # Also install icons from Wire-Py if not present
if [ -d "$WIREPY_DIR/lx-icons" ] && [ ! -d "/usr/share/icons/lx-icons" ]; then if [ -d "$WIREPY_DIR/lx-icons" ] && [ ! -d "/usr/share/icons/lx-icons" ]; then
echo "Installing icons from Wire-Py..." echo "Installing icons from Wire-Py..."
@ -314,7 +504,7 @@ fi
# Cleanup # Cleanup
cd /tmp cd /tmp
rm -rf logviewer_install rm -rf logviewer_install portinstaller
echo "LogViewer installation completed!" echo "LogViewer installation completed!"
""" """
@ -331,7 +521,7 @@ echo "LogViewer installation completed!"
# Make script executable # Make script executable
os.chmod(script_file.name, 0o755) os.chmod(script_file.name, 0o755)
self.log(f"{LocaleStrings.MSGI["install_create"]}{script_file.name}") self.log(f"{LocaleStrings.MSGI['install_create']}{script_file.name}")
# Execute with pkexec # Execute with pkexec
result = subprocess.run( result = subprocess.run(
@ -343,7 +533,7 @@ echo "LogViewer installation completed!"
# Log output # Log output
if result.stdout: if result.stdout:
self.log(f"STDOUT: {result.stdout}") self.log((result.stdout).strip("Log\n"))
if result.stderr: if result.stderr:
self.log(f"STDERR: {result.stderr}") self.log(f"STDERR: {result.stderr}")
@ -352,13 +542,14 @@ echo "LogViewer installation completed!"
if result.returncode != 0: if result.returncode != 0:
raise Exception( raise Exception(
f"{LocaleStrings.MSGI["install_script_failed"]}{result.stderr}" f"{LocaleStrings.MSGI['install_script_failed']}{result.stderr}"
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise Exception(LocaleStrings.MSGI["install_timeout"]) raise Exception(LocaleStrings.MSGI["install_timeout"])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise Exception(f"{LocaleStrings.MSGI["install_script_failed"]}{e}") raise Exception(f"{LocaleStrings.MSGI['install_script_failed']}{e}")
def update_progress(self, message): def update_progress(self, message):
if self.progress_callback: if self.progress_callback:
@ -383,17 +574,17 @@ class UninstallationManager:
"""Uninstall project""" """Uninstall project"""
project_info = self.app_manager.get_project_info(project_key) project_info = self.app_manager.get_project_info(project_key)
if not project_info: if not project_info:
raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}") raise Exception(f"{LocaleStrings.MSGO['unknow_project']}{project_key}")
if not self.app_manager.is_installed(project_key): if not self.app_manager.is_installed(project_key):
raise Exception( raise Exception(
f"{project_info["name"]}{LocaleStrings.MSGO["not_installed"]}" f"{project_info['name']}{LocaleStrings.MSGO['not_installed']}"
) )
self.update_progress( self.update_progress(
f"{LocaleStrings.MSGU["uninstall"]}{project_info['name']}..." f"{LocaleStrings.MSGU['uninstall']}{project_info['name']}..."
) )
self.log(f"=== {LocaleStrings.MSGU["uninstall"]}{project_info['name']} ===") self.log(f"=== {LocaleStrings.MSGU['uninstall']}{project_info['name']} ===")
try: try:
# Create uninstallation script # Create uninstallation script
@ -403,15 +594,15 @@ class UninstallationManager:
self._execute_uninstall_script(script_content) self._execute_uninstall_script(script_content)
self.update_progress( self.update_progress(
f"{project_info['name']}{LocaleStrings.MSGU["uninstall_success"]}" f"{project_info['name']}{LocaleStrings.MSGU['uninstall_success']}"
) )
self.log( self.log(
f"=== {project_info['name']}{LocaleStrings.MSGU["uninstall_success"]} ===" f"=== {project_info['name']}{LocaleStrings.MSGU['uninstall_success']} ==="
) )
except Exception as e: except Exception as e:
self.log(f"ERROR: {LocaleStrings.MSGU['uninstall_failed']}{e}") self.log(f"ERROR: {LocaleStrings.MSGU['uninstall_failed']}{e}")
raise Exception(f"{LocaleStrings.MSGU["uninstall_failed"]}{e}") raise Exception(f"{LocaleStrings.MSGU['uninstall_failed']}{e}")
def _create_uninstall_script(self, project_key): def _create_uninstall_script(self, project_key):
"""Create uninstallation script based on project""" """Create uninstallation script based on project"""
@ -420,7 +611,7 @@ class UninstallationManager:
elif project_key == "logviewer": elif project_key == "logviewer":
return self._create_logviewer_uninstall_script() return self._create_logviewer_uninstall_script()
else: else:
raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}") raise Exception(f"{LocaleStrings.MSGO['unknow_project']}{project_key}")
def _create_wirepy_uninstall_script(self): def _create_wirepy_uninstall_script(self):
detected_os = Detector.get_os() detected_os = Detector.get_os()
@ -475,12 +666,13 @@ if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then
echo "No other LX apps found, removing shared resources..." echo "No other LX apps found, removing shared resources..."
rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/icons/lx-icons
rm -rf /usr/share/TK-Themes rm -rf /usr/share/TK-Themes
rm -rf /usr/local/bin/lxtools_installer
# Remove shared libs # 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 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 rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file
done done
# Try to remove shared_libs directory if empty # Try to remove shared_libs directory if empty
rmdir {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} 2>/dev/null || true rmdir {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} 2>/dev/null || true
else else
@ -522,15 +714,16 @@ if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then
echo "No other LX apps found, removing shared resources..." echo "No other LX apps found, removing shared resources..."
rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/icons/lx-icons
rm -rf /usr/share/TK-Themes 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) # 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 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 rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file
done done
# Remove logviewer.py last # Remove logviewer.py last
rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py
# Try to remove shared_libs directory if empty # Try to remove shared_libs directory if empty
rmdir LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os] 2>/dev/null || true rmdir LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os] 2>/dev/null || true
else else
@ -555,7 +748,7 @@ echo "LogViewer uninstallation completed!"
# Make script executable # Make script executable
os.chmod(script_file.name, 0o755) os.chmod(script_file.name, 0o755)
self.log(f"{LocaleStrings.MSGU["uninstall_create"]}{script_file.name}") self.log(f"{LocaleStrings.MSGU['uninstall_create']}{script_file.name}")
# Execute with pkexec # Execute with pkexec
result = subprocess.run( result = subprocess.run(
@ -567,7 +760,7 @@ echo "LogViewer uninstallation completed!"
# Log output # Log output
if result.stdout: if result.stdout:
self.log(f"STDOUT: {result.stdout}") self.log(result.stdout)
if result.stderr: if result.stderr:
self.log(f"STDERR: {result.stderr}") self.log(f"STDERR: {result.stderr}")
@ -576,13 +769,13 @@ echo "LogViewer uninstallation completed!"
if result.returncode != 0: if result.returncode != 0:
raise Exception( raise Exception(
f"{LocaleStrings.MSGU["uninstall_script_failed"]}{result.stderr}" f"{LocaleStrings.MSGU['uninstall_script_failed']}{result.stderr}"
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise Exception(LocaleStrings.MSGU["uninstall_timeout"]) raise Exception(LocaleStrings.MSGU["uninstall_timeout"])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise Exception(f"{LocaleStrings.MSGU["uninstall_script_failed"]}{e}") raise Exception(f"{LocaleStrings.MSGU['uninstall_script_failed']}{e}")
def update_progress(self, message): def update_progress(self, message):
if self.progress_callback: if self.progress_callback:
@ -599,7 +792,7 @@ class DownloadManager:
"""Download and extract ZIP file""" """Download and extract ZIP file"""
try: try:
if progress_callback: if progress_callback:
progress_callback(f"{LocaleStrings.MSGO["download_from"]}{url}...") progress_callback(f"{LocaleStrings.MSGO['download_from']}{url}...")
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
urllib.request.urlretrieve(url, tmp_file.name) urllib.request.urlretrieve(url, tmp_file.name)
@ -615,7 +808,7 @@ class DownloadManager:
except Exception as e: except Exception as e:
if progress_callback: if progress_callback:
progress_callback(f"{LocaleStrings.MSGO["download_failed"]}{e}") progress_callback(f"{LocaleStrings.MSGO['download_failed']}{e}")
return False return False
@ -627,6 +820,8 @@ class LXToolsGUI:
self.download_icon_label = None self.download_icon_label = None
self.log_text = None self.log_text = None
self.selected_project = None self.selected_project = None
self.polkit_ok = Detector.get_polkit()
self.networkmanager_ok = Detector.get_networkmanager()
self.project_frames = {} self.project_frames = {}
self.status_labels = {} self.status_labels = {}
self.version_labels = {} self.version_labels = {}
@ -660,7 +855,7 @@ class LXToolsGUI:
def create_gui(self): def create_gui(self):
"""Create the main GUI""" """Create the main GUI"""
self.root = tk.Tk() 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( self.root.geometry(
f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}+100+100" f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}+100+100"
) )
@ -676,6 +871,7 @@ class LXToolsGUI:
self.root.iconphoto(False, icon) self.root.iconphoto(False, icon)
except: except:
pass pass
self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT) self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT)
Theme.apply_light_theme(self.root) Theme.apply_light_theme(self.root)
# Create header # Create header
@ -718,10 +914,25 @@ class LXToolsGUI:
icon_text_frame = tk.Frame(left_side, bg="#2c3e50") icon_text_frame = tk.Frame(left_side, bg="#2c3e50")
icon_text_frame.pack(anchor="w") icon_text_frame.pack(anchor="w")
# Tool-Icon # Tool-Icon (versuche echtes Icon, dann Fallback)
tk.Label( try:
icon_text_frame, text="🔧", font=("Helvetica", 18), bg="#2c3e50", fg="white" icon = self.image_manager.load_image("header_image")
).pack(side="left", padx=(0, 8)) 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 # App Name and Version
text_frame = tk.Frame(icon_text_frame, bg="#2c3e50") text_frame = tk.Frame(icon_text_frame, bg="#2c3e50")
@ -738,7 +949,7 @@ class LXToolsGUI:
tk.Label( tk.Label(
text_frame, text_frame,
text=f"v {LXToolsAppConfig.VERSION}{LocaleStrings.MSGO["head_string3"]}", text=f"v {LXToolsAppConfig.VERSION}{LocaleStrings.MSGO['head_string3']}",
font=("Helvetica", 9), font=("Helvetica", 9),
fg="#bdc3c7", fg="#bdc3c7",
bg="#2c3e50", bg="#2c3e50",
@ -750,7 +961,7 @@ class LXToolsGUI:
tk.Label( tk.Label(
right_side, right_side,
text=f"{LocaleStrings.MSGO["head_string2"]}{self.detected_os}", text=f"{LocaleStrings.MSGO['head_string2']}{self.detected_os}",
font=("Helvetica", 11), font=("Helvetica", 11),
fg="#ecf0f1", fg="#ecf0f1",
bg="#2c3e50", bg="#2c3e50",
@ -773,29 +984,51 @@ class LXToolsGUI:
def check_ready_status(self): def check_ready_status(self):
"""Check if system is ready for installation""" """Check if system is ready for installation"""
# Checks:
# Führe alle Checks durch
internet_ok = NetworkChecker.check_internet_connection() internet_ok = NetworkChecker.check_internet_connection()
repo_ok = NetworkChecker.check_repository_access() repo_ok = NetworkChecker.check_repository_access()
result = Detector.get_host_python_version() python_result = Detector.get_host_python_version()
if internet_ok and repo_ok and result is not None: # Sammle alle Probleme
self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green issues = []
elif not internet_ok: if not self.polkit_ok:
self.update_header_status( issues.append(("PolicyKit", "#e74c3c", LocaleStrings.MSGO["polkit_check"]))
LocaleStrings.MSGO["no_internet"], "#e74c3c" if not self.networkmanager_ok:
) # Red issues.append(
elif not repo_ok: (
self.update_header_status( "NetworkManager",
LocaleStrings.MSGO["repo_unavailable"], "#f39c12" "#e74c3c",
) # Orange LocaleStrings.MSGO["networkmanager_check"],
elif result is None: )
self.update_header_status( )
LocaleStrings.MSGO["python_check"], "#e74c3c" if not internet_ok:
) # Red 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"]))
# 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: else:
self.update_header_status(
LocaleStrings.MSGO["system_check"], "#3498db" self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green
) # Blue
def _create_projects_tab(self): def _create_projects_tab(self):
"""Create projects tab with project cards""" """Create projects tab with project cards"""
@ -897,7 +1130,7 @@ class LXToolsGUI:
# Status label # Status label
status_label = tk.Label( status_label = tk.Label(
status_frame, status_frame,
text=f"{LocaleStrings.MSGC["checking"]}", text=f"{LocaleStrings.MSGC['checking']}",
font=("Helvetica", 10), font=("Helvetica", 10),
bg=self.colors["card_bg"], bg=self.colors["card_bg"],
fg="#95a5a6", fg="#95a5a6",
@ -991,7 +1224,7 @@ class LXToolsGUI:
self._update_frame_children_bg(new_content, self.colors["selected_bg"]) self._update_frame_children_bg(new_content, self.colors["selected_bg"])
project_info = self.app_manager.get_project_info(project_key) project_info = self.app_manager.get_project_info(project_key)
self.log_message(f"{LocaleStrings.MSGL["selected_app"]}{project_info["name"]}") self.log_message(f"{LocaleStrings.MSGL['selected_app']}{project_info['name']}")
def _create_log_tab(self): def _create_log_tab(self):
"""Create log tab""" """Create log tab"""
@ -1034,15 +1267,17 @@ class LXToolsGUI:
clear_log_btn.pack(side="right", pady=(0, 10)) clear_log_btn.pack(side="right", pady=(0, 10))
# Initial log message # Initial log message
self.log_message(f"=== {Locale.APP_NAME} v {LXToolsAppConfig.VERSION} ===")
self.log_message(f"{LocaleStrings.MSGL['work_dir']}{LXToolsAppConfig.WORK_DIR}")
self.log_message( self.log_message(
f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===" f"{LocaleStrings.MSGL['icons_dir']}{LXToolsAppConfig.ICONS_DIR}"
) )
self.log_message(f"{LocaleStrings.MSGL["work_dir"]}{LXToolsAppConfig.WORK_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( self.log_message(
f"{LocaleStrings.MSGL["icons_dir"]}{LXToolsAppConfig.ICONS_DIR}" f"{LocaleStrings.MSGO['networkmanager_check_log']}{self.networkmanager_ok}"
) )
self.log_message(f"{LocaleStrings.MSGL["detected_os"]}{self.detected_os}") self.log_message(f"{LocaleStrings.MSGO['ready']}...")
self.log_message(f"{LocaleStrings.MSGO["ready"]}...")
def _create_progress_section(self): def _create_progress_section(self):
"""Create progress section with download icon""" """Create progress section with download icon"""
@ -1062,7 +1297,7 @@ class LXToolsGUI:
# Progress Text (right, expandable) # Progress Text (right, expandable)
self.progress_label = tk.Label( self.progress_label = tk.Label(
progress_container, progress_container,
text=f"{LocaleStrings.MSGO["ready"]}...", text=f"{LocaleStrings.MSGO['ready']}...",
font=("Helvetica", 10), font=("Helvetica", 10),
fg="blue", fg="blue",
anchor="w", anchor="w",
@ -1077,58 +1312,87 @@ class LXToolsGUI:
button_frame = tk.Frame(self.root, bg=self.colors["bg"]) button_frame = tk.Frame(self.root, bg=self.colors["bg"])
button_frame.pack(fill="x", padx=15, pady=(5, 10)) button_frame.pack(fill="x", padx=15, pady=(5, 10))
# Button style configuration # Button style configuration (mit Error-Handling für Linux Mint)
style = ttk.Style() try:
style = ttk.Style()
# Install button (green) # Install button (green)
style.configure("Install.TButton", foreground="#27ae60", font=("Helvetica", 11)) style.configure(
style.map( "Install.TButton", foreground="#27ae60", font=("Helvetica", 11)
"Install.TButton", )
foreground=[("active", "#14542f"), ("pressed", "#1e8449")], style.map(
) "Install.TButton",
foreground=[("active", "#14542f"), ("pressed", "#1e8449")],
)
# Uninstall button (red) # Uninstall button (red)
style.configure( style.configure(
"Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 11) "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 11)
) )
style.map( style.map(
"Uninstall.TButton", "Uninstall.TButton",
foreground=[("active", "#7d3b34"), ("pressed", "#c0392b")], 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) # Create buttons (mit Fallback für Style-Probleme)
style.configure("Refresh.TButton", foreground="#3498db", font=("Helvetica", 11)) try:
style.map( install_btn = ttk.Button(
"Refresh.TButton", button_frame,
foreground=[("active", "#1e3747"), ("pressed", "#2980b9")], text=LocaleStrings.MSGB["install"],
) command=self.install_selected,
style="Install.TButton",
# Create buttons padding=8,
install_btn = ttk.Button( )
button_frame, except:
text=LocaleStrings.MSGB["install"], install_btn = ttk.Button(
command=self.install_selected, button_frame,
style="Install.TButton", text=LocaleStrings.MSGB["install"],
padding=8, command=self.install_selected,
) )
install_btn.pack(side="left", padx=(0, 10)) install_btn.pack(side="left", padx=(0, 10))
uninstall_btn = ttk.Button( try:
button_frame, uninstall_btn = ttk.Button(
text=LocaleStrings.MSGB["uninstall"], button_frame,
command=self.uninstall_selected, text=LocaleStrings.MSGB["uninstall"],
style="Uninstall.TButton", command=self.uninstall_selected,
padding=8, 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)) uninstall_btn.pack(side="left", padx=(0, 10))
refresh_btn = ttk.Button( try:
button_frame, refresh_btn = ttk.Button(
text=LocaleStrings.MSGB["refresh"], button_frame,
command=self.refresh_status, text=LocaleStrings.MSGB["refresh"],
style="Refresh.TButton", command=self.refresh_status,
padding=8, 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") refresh_btn.pack(side="right")
def update_download_icon(self, status): def update_download_icon(self, status):
@ -1175,22 +1439,22 @@ class LXToolsGUI:
"""Refresh application status and version information""" """Refresh application status and version information"""
self.update_progress(LocaleStrings.MSGI["refresh_and_check"]) self.update_progress(LocaleStrings.MSGI["refresh_and_check"])
self._reset_download_icon() self._reset_download_icon()
self.log_message(f"=== {LocaleStrings.MSGB["refresh"]} ===") self.log_message(f"=== {LocaleStrings.MSGB['refresh']} ===")
self.root.focus_set() self.root.focus_set()
for project_key, project_info in self.app_manager.get_all_projects().items(): for project_key, project_info in self.app_manager.get_all_projects().items():
status_label = self.status_labels[project_key] status_label = self.status_labels[project_key]
version_label = self.version_labels[project_key] version_label = self.version_labels[project_key]
self.log_message(f"{LocaleStrings.MSGC["checking"]} {project_info['name']}") self.log_message(f"{LocaleStrings.MSGC['checking']} {project_info['name']}")
if self.app_manager.is_installed(project_key): if self.app_manager.is_installed(project_key):
installed_version = self.app_manager.get_installed_version(project_key) installed_version = self.app_manager.get_installed_version(project_key)
status_label.config( status_label.config(
text=f"{LocaleStrings.MSGI["installed"]}({installed_version})", text=f"{LocaleStrings.MSGI['installed']}({installed_version})",
fg="green", fg="green",
) )
self.log_message( self.log_message(
f"{project_info['name']}: {LocaleStrings.MSGI["installed"]}({installed_version})" f"{project_info['name']}: {LocaleStrings.MSGI['installed']}({installed_version})"
) )
# Get latest version from API # Get latest version from API
@ -1199,41 +1463,41 @@ class LXToolsGUI:
if latest_version != "Unknown": if latest_version != "Unknown":
if installed_version != f"v. {latest_version}": if installed_version != f"v. {latest_version}":
version_label.config( 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", fg="orange",
) )
self.log_message( 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: else:
version_label.config( 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", fg="green",
) )
self.log_message( self.log_message(
f"{project_info['name']}: {LocaleStrings.MSGC["up_to_date"]}", f"{project_info['name']}: {LocaleStrings.MSGC['up_to_date']}",
) )
else: else:
version_label.config( version_label.config(
text=LocaleStrings.MSGC["latest_unknown"], fg="gray" text=LocaleStrings.MSGC["latest_unknown"], fg="gray"
) )
self.log_message( self.log_message(
f"{project_info['name']}: {LocaleStrings.MSGC["could_not_check"]}" f"{project_info['name']}: {LocaleStrings.MSGC['could_not_check']}"
) )
except Exception as e: except Exception as e:
version_label.config( version_label.config(
text=LocaleStrings.MSGC["check_last_failed"], fg="gray" text=LocaleStrings.MSGC["check_last_failed"], fg="gray"
) )
self.log_message( self.log_message(
f"{project_info['name']}: {LocaleStrings.MSGC["version_check_failed"]}: {e}" f"{project_info['name']}: {LocaleStrings.MSGC['version_check_failed']}: {e}"
) )
else: else:
status_label.config( status_label.config(
text=f"{LocaleStrings.MSGC["not_installed"]}", fg="red" text=f"{LocaleStrings.MSGC['not_installed']}", fg="red"
) )
self.log_message( self.log_message(
f"{project_info['name']}: {LocaleStrings.MSGC["not_installed"]}" f"{project_info['name']}: {LocaleStrings.MSGC['not_installed']}"
) )
# Still show latest available version # Still show latest available version
@ -1241,12 +1505,12 @@ class LXToolsGUI:
latest_version = self.app_manager.get_latest_version(project_key) latest_version = self.app_manager.get_latest_version(project_key)
if latest_version != "Unknown": if latest_version != "Unknown":
version_label.config( version_label.config(
text=f"{project_info['name']} {LocaleStrings.MSGC["available"]}: v {latest_version}", text=f"{project_info['name']} {LocaleStrings.MSGC['available']}: v {latest_version}",
fg="blue", fg="blue",
) )
self.log_message( self.log_message(
f"{project_info['name']} {LocaleStrings.MSGC["available"]}: v {latest_version}" f"{project_info['name']} {LocaleStrings.MSGC['available']}: v {latest_version}"
) )
else: else:
version_label.config( version_label.config(
@ -1257,11 +1521,11 @@ class LXToolsGUI:
text=LocaleStrings.MSGC["available_check_unknown"], fg="gray" text=LocaleStrings.MSGC["available_check_unknown"], fg="gray"
) )
self.log_message( self.log_message(
f" {project_info['name']}: {LocaleStrings.MSGC["version_check_failed"]}: {e}" f" {project_info['name']}: {LocaleStrings.MSGC['version_check_failed']}: {e}"
) )
self.update_progress(LocaleStrings.MSGO["refresh2"]) self.update_progress(LocaleStrings.MSGO["refresh2"])
self.log_message(f"=== {LocaleStrings.MSGO["refresh2"]} ===") self.log_message(f"=== {LocaleStrings.MSGO['refresh2']} ===")
self.check_ready_status() self.check_ready_status()
def install_selected(self): def install_selected(self):
@ -1294,7 +1558,7 @@ class LXToolsGUI:
self.update_download_icon("success") self.update_download_icon("success")
MessageDialog( MessageDialog(
"info", "info",
f"{project_info["name"]} {LocaleStrings.MSGM["has_success_update"]}", f"{project_info['name']} {LocaleStrings.MSGM['has_success_update']}",
wraplength=400, wraplength=400,
) )
@ -1317,7 +1581,7 @@ class LXToolsGUI:
if not self.app_manager.is_installed(self.selected_project): if not self.app_manager.is_installed(self.selected_project):
MessageDialog( MessageDialog(
"error", f"{project_info["name"]} {LocaleStrings.MSGO["not_installed"]}" "error", f"{project_info['name']} {LocaleStrings.MSGO['not_installed']}"
) )
self.root.focus_set() self.root.focus_set()
return return
@ -1326,14 +1590,14 @@ class LXToolsGUI:
self.uninstallation_manager.uninstall_project(self.selected_project) self.uninstallation_manager.uninstall_project(self.selected_project)
MessageDialog( MessageDialog(
"info", "info",
f"{project_info["name"]} {LocaleStrings.MSGU["uninstall_success"]}", f"{project_info['name']} {LocaleStrings.MSGU['uninstall_success']}",
wraplength=400, wraplength=400,
) )
self.refresh_status() self.refresh_status()
self.root.focus_set() self.root.focus_set()
except Exception as e: except Exception as e:
MessageDialog("error", f"{LocaleStrings.MSGU["uninstall_failed"]}: {e}") MessageDialog("error", f"{LocaleStrings.MSGU['uninstall_failed']}: {e}")
self.root.focus_set() self.root.focus_set()
def update_progress(self, message): def update_progress(self, message):
@ -1341,7 +1605,7 @@ class LXToolsGUI:
if self.progress_label: if self.progress_label:
self.progress_label.config(text=message) self.progress_label.config(text=message)
self.progress_label.update() self.progress_label.update()
print(f"{LocaleStrings.MSGO["progress"]}: {message}") print(f"{LocaleStrings.MSGO['progress']}: {message}")
def log_message(self, message): def log_message(self, message):
"""Add message to log""" """Add message to log"""
@ -1367,17 +1631,18 @@ class LXToolsGUI:
def main(): def main():
"""Main function to start the application""" """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()}") print(f"{LocaleStrings.MSGL['working_dir']}{os.getcwd()}")
try: try:
LxTools.sigi(LXToolsAppConfig.TEMP_DIR)
# Create and run the GUI # Create and run the GUI
app = LXToolsGUI() app = LXToolsGUI()
app.run() app.run()
except KeyboardInterrupt: except KeyboardInterrupt:
print(LocaleStrings.MSGL["user_interrupt"]) print(LocaleStrings.MSGL["user_interrupt"])
except Exception as e: except Exception as e:
print(f"{LocaleStrings.MSGL["fatal_error"]}: {e}") print(f"{LocaleStrings.MSGL['fatal_error']}: {e}")
try: try:
MessageDialog("error", f"{LocaleStrings.MSGL['fatal_app_error']}: {e}") MessageDialog("error", f"{LocaleStrings.MSGL['fatal_app_error']}: {e}")
@ -1387,3 +1652,6 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
LxTools.remove_lxtools_files()
LxTools.clean_files(LXToolsAppConfig.TEMP_DIR)
sys.exit(0)

38
lxtools_installer.spec Normal file
View File

@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['lxtools_installer.py'],
pathex=[],
binaries=[],
datas=[('locale/de/LC_MESSAGES/lxtoolsinstaller.mo', 'locale/de/LC_MESSAGES/'), ('manager.py', '.'), ('network.py', '.'), ('message.py', '.'), ('TK-Themes/theme/dark/*.png', 'TK-Themes/theme/dark'), ('TK-Themes/theme/light/*.png', 'TK-Themes/theme/light'), ('TK-Themes/water.tcl', 'TK-Themes'), ('TK-Themes/LICENSE', 'TK-Themes'), ('TK-Themes/theme/dark.tcl', 'TK-Themes/theme'), ('TK-Themes/theme/light.tcl', 'TK-Themes/theme'), ('lx-icons/32/*.png', 'lx-icons/32'), ('lx-icons/48/*.png', 'lx-icons/48'), ('lx-icons/64/*.png', 'lx-icons/64'), ('lx-icons/128/*.png', 'lx-icons/128'), ('lx-icons/256/*.png', 'lx-icons/256'), ('certs/cacert.pem', 'certs')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='lxtools_installer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

487
lxtoolsinstaller.pot Normal file
View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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."

View File

@ -1,14 +1,38 @@
import locale import locale
import gettext import gettext
import signal
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from pathlib import Path from pathlib import Path
from typing import Optional, NoReturn, Any, Dict
import logging
import os import os
import sys import sys
import shutil import shutil
import subprocess import subprocess
import stat 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: class Detector:
@ -96,6 +120,131 @@ class Detector:
else: else:
return False return False
@staticmethod
def get_polkit() -> bool:
"""Check if network manager is installed"""
os_system = Detector.get_os()
deb = ["Debian", "Ubuntu", "Linux Mint", "Pop!_OS"]
arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"]
if os_system in deb:
result = subprocess.run(
["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:
result = subprocess.run(
["pacman", "-Qs", "polkit"],
capture_output=True,
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(
["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 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
def get_networkmanager() -> bool:
"""Check if network manager is installed"""
os_system = Detector.get_os()
deb = ["Debian", "Ubuntu", "Linux Mint", "Pop!_OS"]
arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"]
if os_system in deb:
result = subprocess.run(
["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:
result = subprocess.run(
["pacman", "-Qs", "networkmanager"],
capture_output=True,
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(
["which NetworkManager"],
capture_output=True,
shell=True,
text=True,
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"],
capture_output=True,
shell=True,
text=True,
check=True,
)
if result.returncode == 0:
return True
else:
print(f"STDERR: {result.stderr}")
return False
class Theme: class Theme:
@staticmethod @staticmethod
@ -180,7 +329,7 @@ class Image:
def __init__(self): def __init__(self):
self.images = {} self.images = {}
def load_image(self, image_key, fallback_paths=None) -> None | tk.PhotoImage: def load_image(self, image_key, fallback_paths=None) -> Optional[tk.PhotoImage]:
"""Load PNG image using tk.PhotoImage with fallback options""" """Load PNG image using tk.PhotoImage with fallback options"""
if image_key in self.images: if image_key in self.images:
return self.images[image_key] return self.images[image_key]
@ -211,6 +360,10 @@ class Image:
"./lx-icons/48/log.png", "./lx-icons/48/log.png",
"/usr/share/icons/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 # Get paths to try
@ -228,7 +381,7 @@ class Image:
self.images[image_key] = photo self.images[image_key] = photo
return photo return photo
except tk.TclError as e: except tk.TclError as e:
print(f"{LocaleStrings.MSGP["fail_load_image"]}{path}: {e}") print(f"{LocaleStrings.MSGP['fail_load_image']}{path}: {e}")
continue continue
# Return None if no image found # Return None if no image found
@ -239,7 +392,7 @@ class AppManager:
def __init__(self): def __init__(self):
self.projects = LXToolsAppConfig.PROJECTS self.projects = LXToolsAppConfig.PROJECTS
def get_project_info(self, project_key) -> dict | None: def get_project_info(self, project_key) -> Optional[dict]:
"""Get project information by key""" """Get project information by key"""
return self.projects.get(project_key) return self.projects.get(project_key)
@ -281,10 +434,10 @@ class AppManager:
# Debug logging # Debug logging
print(LocaleStrings.MSGP["logviewer_check"]) print(LocaleStrings.MSGP["logviewer_check"])
print(f"{LocaleStrings.MSGP["symlink_exist"]}{symlink_exists}") print(f"{LocaleStrings.MSGP['symlink_exist']}{symlink_exists}")
print(f"{LocaleStrings.MSGP["executable_exist"]}{executable_exists}") print(f"{LocaleStrings.MSGP['executable_exist']}{executable_exists}")
print(f"{LocaleStrings.MSGP["is_executable"]}{executable_is_executable}") print(f"{LocaleStrings.MSGP['is_executable']}{executable_is_executable}")
print(f"{LocaleStrings.MSGP["final_result"]}{is_installed}") print(f"{LocaleStrings.MSGP['final_result']}{is_installed}")
return is_installed return is_installed
@ -310,7 +463,7 @@ class AppManager:
return version return version
return "Unknown" return "Unknown"
except Exception as e: except Exception as e:
print(f"{LocaleStrings.MSGP["get_version_error"]}{project_key}: {e}") print(f"{LocaleStrings.MSGP['get_version_error']}{project_key}: {e}")
return "Unknown" return "Unknown"
def get_latest_version(self, project_key) -> str: def get_latest_version(self, project_key) -> str:
@ -319,7 +472,7 @@ class AppManager:
if not project_info: if not project_info:
return "Unknown" 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: def check_other_apps_installed(self, exclude_key) -> bool:
"""Check if other apps are still installed""" """Check if other apps are still installed"""
@ -417,12 +570,98 @@ class LxTools:
y = (screen_height - height) // 2 y = (screen_height - height) // 2
window.geometry(f"{width}x{height}+{x}+{y}") window.geometry(f"{width}x{height}+{x}+{y}")
@staticmethod
def clean_files(tmp_dir: Path = None, file: Path = None) -> None:
"""
Deletes temporary files and directories for cleanup when exiting the application.
This method safely removes an optional directory defined by `AppConfig.TEMP_DIR`
and a single file to free up resources at the end of the program's execution.
All operations are performed securely, and errors such as `FileNotFoundError`
are ignored if the target files or directories do not exist.
:param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted.
If `None`, the value of `AppConfig.TEMP_DIR` is used.
:param file: (Path, optional): Path to the file that should be deleted.
If `None`, no additional file will be deleted.
Returns:
None: The method does not return any value.
"""
if tmp_dir is not None:
shutil.rmtree(tmp_dir, ignore_errors=True)
try:
if file is not None:
Path.unlink(file)
except FileNotFoundError:
pass
@staticmethod
def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None:
"""
Function for cleanup after a program interruption
:param file: Optional - File to be deleted
:param file_path: Optional - Directory to be deleted
"""
def signal_handler(signum: int, frame: Any) -> NoReturn:
"""
Determines clear text names for signal numbers and handles signals
Args:
signum: The signal number
frame: The current stack frame
Returns:
NoReturn since the function either exits the program or continues execution
"""
signals_to_names_dict: Dict[int, str] = dict(
(getattr(signal, n), n)
for n in dir(signal)
if n.startswith("SIG") and "_" not in n
)
signal_name: str = signals_to_names_dict.get(
signum, f"Unnamed signal: {signum}"
)
# End program for certain signals, report to others only reception
if signum in (signal.SIGINT, signal.SIGTERM):
exit_code: int = 1
logging.error(
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.",
exc_info=True,
)
LxTools.clean_files(file_path, file)
logging.info("Breakdown by user...")
sys.exit(exit_code)
else:
logging.info(f"Signal {signum} received and ignored.")
LxTools.clean_files(file_path, file)
logging.error("Process unexpectedly ended...")
# Register signal handlers for various signals
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
@staticmethod
def remove_lxtools_files() -> None:
if getattr(sys, "_MEIPASS", None) is not None:
shutil.rmtree("./locale")
shutil.rmtree("./TK-Themes")
shutil.rmtree("./lx-icons")
class LXToolsAppConfig: class LXToolsAppConfig:
@staticmethod @staticmethod
def extract_data_files() -> None: def extract_data_files() -> None:
if getattr(sys, "_MEIPASS", None) is not None: if getattr(sys, "_MEIPASS", None) is not None:
os.makedirs("/tmp/lxtools", exist_ok=True)
# Liste der Quellordner (entspricht dem "datas"-Eintrag in lxtools_installer.spec) # Liste der Quellordner (entspricht dem "datas"-Eintrag in lxtools_installer.spec)
source_dirs = [ source_dirs = [
os.path.join(sys._MEIPASS, "locale"), # für locale/... os.path.join(sys._MEIPASS, "locale"), # für locale/...
@ -437,7 +676,7 @@ class LXToolsAppConfig:
for source_dir in source_dirs: for source_dir in source_dirs:
group_name = os.path.basename( group_name = os.path.basename(
source_dir source_dir
) # Erhält den Gruppen-Name (z.B. 'locale', 'TK-Themes') ) # Erhält den Gruppen-Name (z. B. 'locale', 'TK-Themes')
for root, dirs, files in os.walk(source_dir): for root, dirs, files in os.walk(source_dir):
for file in files: for file in files:
@ -461,33 +700,14 @@ class LXToolsAppConfig:
os.path.dirname(os.path.abspath(__file__)), "certs", "cacert.pem" os.path.dirname(os.path.abspath(__file__)), "certs", "cacert.pem"
) )
@staticmethod VERSION = "1.1.8"
def setup_translations() -> gettext.gettext: WINDOW_WIDTH = 460
"""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.6"
APP_NAME = "lxtoolsinstaller"
WINDOW_WIDTH = 450
WINDOW_HEIGHT = 580 WINDOW_HEIGHT = 580
# Working directory # Working directory
WORK_DIR = os.getcwd() WORK_DIR = os.getcwd()
ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") ICONS_DIR = os.path.join(WORK_DIR, "lx-icons")
THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes")
TEMP_DIR = "/tmp/lxtools"
# Locale settings
LOCALE_DIR = "./locale/"
# Download URLs # Download URLs
WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip" WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip"
@ -499,11 +719,20 @@ class LXToolsAppConfig:
"https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" "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 # Project configurations
PROJECTS = { PROJECTS = {
"wirepy": { "wirepy": {
"name": "Wire-Py", "name": "Wire-Py",
"description": "WireGuard VPN Manager with GUI", "description": _("WireGuard VPN Manager with GUI"),
"download_url": WIREPY_URL, "download_url": WIREPY_URL,
"api_url": WIREPY_API_URL, "api_url": WIREPY_API_URL,
"icon_key": "icon_vpn", "icon_key": "icon_vpn",
@ -516,7 +745,7 @@ class LXToolsAppConfig:
}, },
"logviewer": { "logviewer": {
"name": "LogViewer", "name": "LogViewer",
"description": "System Log Viewer with GUI", "description": _("System Log Viewer with GUI"),
"download_url": SHARED_LIBS_URL, "download_url": SHARED_LIBS_URL,
"api_url": SHARED_LIBS_API_URL, "api_url": SHARED_LIBS_API_URL,
"icon_key": "icon_log", "icon_key": "icon_log",
@ -575,8 +804,6 @@ class LXToolsAppConfig:
LXToolsAppConfig.extract_data_files() LXToolsAppConfig.extract_data_files()
# Initialize translations
_ = LXToolsAppConfig.setup_translations()
class LocaleStrings: class LocaleStrings:
@ -618,14 +845,18 @@ class LocaleStrings:
"progress": _("Progress"), "progress": _("Progress"),
"refresh2": _("Status refresh completed"), "refresh2": _("Status refresh completed"),
"python_check": _("Python not installed"), "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 = Strings on Cards
MSGC = { MSGC = {
"checking": _("Checking..."), "checking": _("Checking..."),
"version_check": _("Version: Checking..."), "version_check": _("Version: Checking..."),
"latest": _("Latest: "), "update_on": _("Update on "),
"update_available": _("Update available "), "available_lower": _("available"),
"up_to_date": _("Up to date"), "up_to_date": _("Up to date"),
"latest_unknown": _("Latest unknown"), "latest_unknown": _("Latest unknown"),
"could_not_check": _("Could not check latest version"), "could_not_check": _("Could not check latest version"),
@ -674,7 +905,7 @@ class LocaleStrings:
# MSGP = Others print strings # MSGP = Others print strings
MSGP = { MSGP = {
"tk_install": _("Installing tkinter for )"), "tk_install": _("Installing tkinter for "),
"command_string": _("Command: "), "command_string": _("Command: "),
"tk_success": _("TKinter installation completed successfully!"), "tk_success": _("TKinter installation completed successfully!"),
"tk_failed": _("TKinter installation failed: "), "tk_failed": _("TKinter installation failed: "),
@ -689,3 +920,52 @@ class LocaleStrings:
"final_result": _(" Final result: "), "final_result": _(" Final result: "),
"get_version_error": _("Error getting version for "), "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."),
}

View File

@ -129,7 +129,7 @@ class MessageDialog:
self.window = tk.Toplevel(master) self.window = tk.Toplevel(master)
self.window.grab_set() self.window.grab_set()
self.window.resizable(False, False) self.window.resizable(False, False)
ttk.Style().configure("TButton", font=("Helvetica", 11), padding=5) ttk.Style().configure("TButton")
self.buttons_widgets = [] self.buttons_widgets = []
self.current_button_index = 0 self.current_button_index = 0
self._load_icons() self._load_icons()

View File

@ -1,28 +1,428 @@
import socket import socket
import os
import urllib.request import urllib.request
import json import json
import hashlib
import subprocess
from typing import Union, List
import re
import tempfile
class GiteaUpdate: class GPGManager:
@staticmethod @staticmethod
def api_down(url, current_version=""): def get_gpg() -> bool:
"""Get latest version from Gitea API""" """
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: try:
with urllib.request.urlopen(url, timeout=10) as response: with urllib.request.urlopen(url, timeout=10) as response:
data = json.loads(response.read().decode()) data = json.loads(response.read().decode())
if data and len(data) > 0: if data and len(data) > 0:
latest_version = data[0].get("tag_name", "Unknown") 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" return "Unknown"
except Exception as e: except Exception as e:
print(f"API Error: {e}") print(f"API Error: {e}")
return "Unknown" 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: class NetworkChecker:
@staticmethod @staticmethod
def check_internet_connection(host="8.8.8.8", port=53, timeout=3): def check_internet_connection(
"""Check if internet connection is available""" 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: try:
socket.setdefaulttimeout(timeout) socket.setdefaulttimeout(timeout)
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
@ -31,10 +431,22 @@ class NetworkChecker:
return False return False
@staticmethod @staticmethod
def check_repository_access(url="https://git.ilunix.de", timeout=5): def check_repository_access(
"""Check if repository is accessible""" 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: try:
urllib.request.urlopen(url, timeout=timeout) urllib.request.urlopen(url, timeout=timeout)
return True return True
except: except Exception as e:
print(f"Error accessing Gitea repository: {e}")
return False return False

64
public_key.asc Normal file
View File

@ -0,0 +1,64 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
40WrGrMw8RQJRui1bNFIz4BT0w==
=G2Bo
-----END PGP PUBLIC KEY BLOCK-----