Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
ff97ed5b20 | |||
fbba0028ab | |||
9873a293f7 | |||
29f237ab42 | |||
5986ce3b48 | |||
bc1ec45ecd | |||
4006b917f9 | |||
9db3aff611 | |||
aa8923ca47 | |||
ac3a375357 | |||
c500b4f1ea | |||
80a7018a72 | |||
cfebeafd9b | |||
299404eaac | |||
be43e50065 | |||
44e75fa1b0 | |||
f416f66ee1 | |||
12904e843c |
32
.gitignore
vendored
32
.gitignore
vendored
@ -4,3 +4,35 @@ 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
|
||||||
|
gpg_setup.sh
|
||||||
|
gpg_simple_setup.sh
|
||||||
|
lxtools_installer.spec
|
||||||
|
lxtoolsinstaller.pot
|
||||||
|
|
||||||
|
# Docker-Build
|
||||||
|
docker_build/
|
||||||
|
debug_docker.sh
|
||||||
|
Dockerfile.nuitka
|
||||||
|
DOCKER_BUILD_ANLEITUNG.md
|
||||||
|
nuitka_builder.py
|
||||||
|
Dockerfile.test
|
||||||
|
Dockerfile.simple
|
||||||
|
52
Changelog
52
Changelog
@ -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
|
||||||
|
|
||||||
|
20
README.md
20
README.md
@ -2,6 +2,26 @@
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
# Fingerprint
|
||||||
|
|
||||||
|
743745087C6414E00F1EF84D4CCF06B6CE2A4C7F
|
||||||
|
|
||||||
|
add to your gpg keyring:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://git.ilunix.de/punix/lxtools_installer/raw/branch/main/public_key.asc -O - | gpg --import
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```bash
|
||||||
|
wget https://keys.openpgp.org/vks/v1/by-fingerprint/743745087C6414E00F1EF84D4CCF06B6CE2A4C7F -O - | gpg --import
|
||||||
|
```
|
||||||
|
The Appimage automatically checks whether the public_key has already been imported,
|
||||||
|
and if not it is downloaded from both sources and only imported when all the keys match.
|
||||||
|
|
||||||
|
This is to ensure that no manipulated software is used.
|
||||||
|
|
||||||
|
# Not currently supported
|
||||||
|
- Open Suse Tumbleweed and Leap (Let's get back)
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
[](https://fb.ilunix.de/share/ZnfG9gxv)
|
[](https://fb.ilunix.de/share/ZnfG9gxv)
|
Binary file not shown.
BIN
lx-icons/16/settings.png
Normal file
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
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
BIN
lx-icons/32/lxtools_key.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@ -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,8 +267,9 @@ 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
|
||||||
@ -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
|
||||||
@ -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,6 +666,7 @@ 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
|
||||||
@ -522,6 +714,7 @@ 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
|
||||||
@ -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)
|
||||||
|
360
manager.py
360
manager.py
@ -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(
|
||||||
|
["systemctl status 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."),
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
|
430
network.py
430
network.py
@ -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
64
public_key.asc
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
40WrGrMw8RQJRui1bNFIz4BT0w==
|
||||||
|
=G2Bo
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
Reference in New Issue
Block a user