972 lines
35 KiB
Python
972 lines
35 KiB
Python
import locale
|
|
import gettext
|
|
import signal
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from pathlib import Path
|
|
from typing import Optional, NoReturn, Any, Dict
|
|
import logging
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import subprocess
|
|
import stat
|
|
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:
|
|
@staticmethod
|
|
def get_os() -> str:
|
|
"""Detect operating system using ordered list"""
|
|
try:
|
|
with open("/etc/os-release", "r") as f:
|
|
content = f.read().lower()
|
|
|
|
# Check each OS in order (specific first)
|
|
for keyword, os_name in LXToolsAppConfig.OS_DETECTION:
|
|
if keyword in content:
|
|
return os_name
|
|
|
|
return "Unknown System"
|
|
except FileNotFoundError:
|
|
return "File not found"
|
|
|
|
@staticmethod
|
|
def get_host_python_version() -> str:
|
|
try:
|
|
result = subprocess.run(
|
|
["python3", "--version"], capture_output=True, text=True, check=True
|
|
)
|
|
version_str = result.stdout.strip().replace("Python ", "")
|
|
return version_str[:4] # example "3.13"
|
|
except Exception:
|
|
print("Python not found")
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_user_gt_1() -> bool:
|
|
"""This method may be required for the future if the number of users"""
|
|
path = Path("/home")
|
|
|
|
user_directories = [
|
|
entry
|
|
for entry in path.iterdir()
|
|
if entry.is_dir() and entry.name != "root" and entry.name != "lost+found"
|
|
]
|
|
|
|
# Count the number of user directories
|
|
numbers = len(user_directories)
|
|
if not numbers > 1:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@staticmethod
|
|
def get_wget() -> bool:
|
|
"""Check if wget is installed"""
|
|
|
|
result = subprocess.run(
|
|
["which", "wget"], capture_output=True, text=True, check=False
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@staticmethod
|
|
def get_unzip() -> bool:
|
|
"""Check if wget is installed"""
|
|
|
|
result = subprocess.run(
|
|
["which", "unzip"], capture_output=True, text=True, check=False
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@staticmethod
|
|
def get_requests() -> bool:
|
|
"""Check if requests is installed"""
|
|
result = subprocess.run(
|
|
["pacman", "-Qs", "python-requests"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@staticmethod
|
|
def get_polkit() -> bool:
|
|
"""Check if network manager is installed"""
|
|
os_system = Detector.get_os()
|
|
deb = ["Debian", "Ubuntu", "Linux Mint", "Pop!_OS"]
|
|
arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"]
|
|
if os_system in deb:
|
|
result = subprocess.run(
|
|
["apt list --installed | grep polkit"],
|
|
capture_output=True,
|
|
shell=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
elif os_system in arch:
|
|
result = subprocess.run(
|
|
["pacman", "-Qs", "polkit"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
elif os_system == "Fedora":
|
|
result = subprocess.run(
|
|
["systemctl --type=service | grep polkit"],
|
|
capture_output=True,
|
|
shell=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
elif os_system == "SUSE Tumbleweed" or os_system == "SUSE Leap":
|
|
result = subprocess.run(
|
|
["zypper search --installed-only | grep pkexec"],
|
|
capture_output=True,
|
|
shell=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
@staticmethod
|
|
def get_networkmanager() -> bool:
|
|
"""Check if network manager is installed"""
|
|
os_system = Detector.get_os()
|
|
deb = ["Debian", "Ubuntu", "Linux Mint", "Pop!_OS"]
|
|
arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"]
|
|
if os_system in deb:
|
|
result = subprocess.run(
|
|
["apt list --installed | grep network-manager"],
|
|
capture_output=True,
|
|
shell=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
elif os_system in arch:
|
|
result = subprocess.run(
|
|
["pacman", "-Qs", "networkmanager"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
elif os_system == "Fedora":
|
|
|
|
result = subprocess.run(
|
|
["which NetworkManager"],
|
|
capture_output=True,
|
|
shell=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
elif os_system == "SUSE Tumbleweed" or os_system == "SUSE Leap":
|
|
result = subprocess.run(
|
|
["zypper search --installed-only | grep NetworkManager"],
|
|
capture_output=True,
|
|
shell=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
if result.returncode == 0:
|
|
return True
|
|
else:
|
|
print(f"STDERR: {result.stderr}")
|
|
return False
|
|
|
|
|
|
class Theme:
|
|
@staticmethod
|
|
def apply_light_theme(root) -> bool:
|
|
"""Apply light theme"""
|
|
try:
|
|
theme_dir = LXToolsAppConfig.THEMES_DIR
|
|
water_theme_path = os.path.join(theme_dir, "water.tcl")
|
|
|
|
if os.path.exists(water_theme_path):
|
|
try:
|
|
root.tk.call("source", water_theme_path)
|
|
root.tk.call("set_theme", "light")
|
|
return True
|
|
except tk.TclError:
|
|
pass
|
|
|
|
# System theme fallback
|
|
try:
|
|
style = ttk.Style()
|
|
if "clam" in style.theme_names():
|
|
style.theme_use("clam")
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
class System:
|
|
@staticmethod
|
|
def create_directories(directories) -> None:
|
|
"""Create system directories using pkexec"""
|
|
for directory in directories:
|
|
subprocess.run(["pkexec", "mkdir", "-p", directory], check=True)
|
|
|
|
@staticmethod
|
|
def copy_file(src, dest, make_executable=False) -> None:
|
|
"""Copy file using pkexec"""
|
|
subprocess.run(["pkexec", "cp", src, dest], check=True)
|
|
if make_executable:
|
|
subprocess.run(["pkexec", "chmod", "755", dest], check=True)
|
|
|
|
@staticmethod
|
|
def copy_directory(src, dest) -> None:
|
|
"""Copy directory using pkexec"""
|
|
subprocess.run(["pkexec", "cp", "-r", src, dest], check=True)
|
|
|
|
@staticmethod
|
|
def remove_file(path) -> None:
|
|
"""Remove file using pkexec"""
|
|
subprocess.run(["pkexec", "rm", "-f", path], check=False)
|
|
|
|
@staticmethod
|
|
def remove_directory(path) -> None:
|
|
"""Remove directory using pkexec"""
|
|
subprocess.run(["pkexec", "rm", "-rf", path], check=False)
|
|
|
|
@staticmethod
|
|
def create_symlink(target, link_name) -> None:
|
|
"""Create symbolic link using pkexec"""
|
|
subprocess.run(["pkexec", "rm", "-f", link_name], check=False)
|
|
subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True)
|
|
|
|
@staticmethod
|
|
def create_ssl_key(pem_file) -> bool:
|
|
"""Create SSL key using pkexec"""
|
|
try:
|
|
subprocess.run(
|
|
["pkexec", "openssl", "genrsa", "-out", pem_file, "4096"], check=True
|
|
)
|
|
subprocess.run(["pkexec", "chmod", "600", pem_file], check=True)
|
|
return True
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
|
|
class Image:
|
|
def __init__(self):
|
|
self.images = {}
|
|
|
|
def load_image(self, image_key, fallback_paths=None) -> Optional[tk.PhotoImage]:
|
|
"""Load PNG image using tk.PhotoImage with fallback options"""
|
|
if image_key in self.images:
|
|
return self.images[image_key]
|
|
|
|
# Define image paths based on key
|
|
image_paths = {
|
|
"app_icon": [
|
|
"./lx-icons/48/wg_vpn.png",
|
|
"/usr/share/icons/lx-icons/48/wg_vpn.png",
|
|
],
|
|
"download_icon": [
|
|
"./lx-icons/32/download.png",
|
|
"/usr/share/icons/lx-icons/32/download.png",
|
|
],
|
|
"download_error_icon": [
|
|
"./lx-icons/32/download_error.png",
|
|
"/usr/share/icons/lx-icons/32/download_error.png",
|
|
],
|
|
"success_icon": [
|
|
"./lx-icons/32/download.png",
|
|
"/usr/share/icons/lx-icons/32/download.png",
|
|
],
|
|
"icon_vpn": [
|
|
"./lx-icons/48/wg_vpn.png",
|
|
"/usr/share/icons/lx-icons/48/wg_vpn.png",
|
|
],
|
|
"icon_log": [
|
|
"./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
|
|
paths_to_try = image_paths.get(image_key, [])
|
|
|
|
# Add fallback paths if provided
|
|
if fallback_paths:
|
|
paths_to_try.extend(fallback_paths)
|
|
|
|
# Try to load image from paths
|
|
for path in paths_to_try:
|
|
try:
|
|
if os.path.exists(path):
|
|
photo = tk.PhotoImage(file=path)
|
|
self.images[image_key] = photo
|
|
return photo
|
|
except tk.TclError as e:
|
|
print(f"{LocaleStrings.MSGP['fail_load_image']}{path}: {e}")
|
|
continue
|
|
|
|
# Return None if no image found
|
|
return None
|
|
|
|
|
|
class AppManager:
|
|
def __init__(self):
|
|
self.projects = LXToolsAppConfig.PROJECTS
|
|
|
|
def get_project_info(self, project_key) -> Optional[dict]:
|
|
"""Get project information by key"""
|
|
return self.projects.get(project_key)
|
|
|
|
def get_all_projects(self) -> dict:
|
|
"""Get all project configurations"""
|
|
return self.projects
|
|
|
|
def is_installed(self, project_key) -> bool:
|
|
detected_os = Detector.get_os()
|
|
"""Check if project is installed with better detection"""
|
|
if project_key == "wirepy":
|
|
# Check for wirepy symlink
|
|
return os.path.exists("/usr/local/bin/wirepy") and os.path.islink(
|
|
"/usr/local/bin/wirepy"
|
|
)
|
|
|
|
elif project_key == "logviewer":
|
|
# Check for logviewer symlink AND executable file
|
|
symlink_exists = os.path.exists("/usr/local/bin/logviewer")
|
|
executable_exists = os.path.exists(
|
|
f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py"
|
|
)
|
|
executable_is_executable = False
|
|
|
|
if executable_exists:
|
|
try:
|
|
# Check if file is executable
|
|
file_stat = os.stat(
|
|
f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py"
|
|
)
|
|
executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC)
|
|
except:
|
|
executable_is_executable = False
|
|
|
|
# LogViewer is installed if symlink exists AND executable file exists AND is executable
|
|
is_installed = (
|
|
symlink_exists and executable_exists and executable_is_executable
|
|
)
|
|
|
|
# Debug logging
|
|
print(LocaleStrings.MSGP["logviewer_check"])
|
|
print(f"{LocaleStrings.MSGP['symlink_exist']}{symlink_exists}")
|
|
print(f"{LocaleStrings.MSGP['executable_exist']}{executable_exists}")
|
|
print(f"{LocaleStrings.MSGP['is_executable']}{executable_is_executable}")
|
|
print(f"{LocaleStrings.MSGP['final_result']}{is_installed}")
|
|
|
|
return is_installed
|
|
|
|
return False
|
|
|
|
def get_installed_version(self, project_key) -> str:
|
|
detected_os = Detector.get_os()
|
|
"""Get installed version from config file"""
|
|
try:
|
|
if project_key == "wirepy":
|
|
config_file = f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/wp_app_config.py"
|
|
elif project_key == "logviewer":
|
|
config_file = f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logview_app_config.py"
|
|
else:
|
|
return "Unknown"
|
|
|
|
if os.path.exists(config_file):
|
|
with open(config_file, "r") as f:
|
|
content = f.read()
|
|
for line in content.split("\n"):
|
|
if "VERSION" in line and "=" in line:
|
|
version = line.split("=")[1].strip().strip("\"'")
|
|
return version
|
|
return "Unknown"
|
|
except Exception as e:
|
|
print(f"{LocaleStrings.MSGP['get_version_error']}{project_key}: {e}")
|
|
return "Unknown"
|
|
|
|
def get_latest_version(self, project_key) -> str:
|
|
"""Get latest version from API"""
|
|
project_info = self.get_project_info(project_key)
|
|
if not project_info:
|
|
return "Unknown"
|
|
|
|
return GiteaUpdater.get_latest_version_from_api(project_info["api_url"])
|
|
|
|
def check_other_apps_installed(self, exclude_key) -> bool:
|
|
"""Check if other apps are still installed"""
|
|
return any(
|
|
self.is_installed(key) for key in self.projects.keys() if key != exclude_key
|
|
)
|
|
|
|
|
|
class LxTools:
|
|
@staticmethod
|
|
def center_window_cross_platform(window, width, height) -> None:
|
|
"""
|
|
Centers a window on the primary monitor in a way that works on both X11 and Wayland
|
|
|
|
Args:
|
|
window: The tkinter window to center
|
|
width: Window width
|
|
height: Window height
|
|
"""
|
|
# Calculate the position before showing the window
|
|
|
|
# First attempt: Try to use GDK if available (works on both X11 and Wayland)
|
|
try:
|
|
import gi
|
|
|
|
gi.require_version("Gdk", "3.0")
|
|
from gi.repository import Gdk
|
|
|
|
display = Gdk.Display.get_default()
|
|
monitor = display.get_primary_monitor() or display.get_monitor(0)
|
|
geometry = monitor.get_geometry()
|
|
scale_factor = monitor.get_scale_factor()
|
|
|
|
# Calculate center position on the primary monitor
|
|
x = geometry.x + (geometry.width - width // scale_factor) // 2
|
|
y = geometry.y + (geometry.height - height // scale_factor) // 2
|
|
|
|
# Set window geometry
|
|
window.geometry(f"{width}x{height}+{x}+{y}")
|
|
return
|
|
except (ImportError, AttributeError):
|
|
pass
|
|
|
|
# Second attempt: Try xrandr for X11
|
|
try:
|
|
import subprocess
|
|
|
|
output = subprocess.check_output(
|
|
["xrandr", "--query"], universal_newlines=True
|
|
)
|
|
|
|
# Parse the output to find the primary monitor
|
|
primary_info = None
|
|
for line in output.splitlines():
|
|
if "primary" in line:
|
|
parts = line.split()
|
|
for part in parts:
|
|
if "x" in part and "+" in part:
|
|
primary_info = part
|
|
break
|
|
break
|
|
|
|
if primary_info:
|
|
# Parse the geometry: WIDTH x HEIGHT+X+Y
|
|
geometry = primary_info.split("+")
|
|
dimensions = geometry[0].split("x")
|
|
primary_width = int(dimensions[0])
|
|
primary_height = int(dimensions[1])
|
|
primary_x = int(geometry[1])
|
|
primary_y = int(geometry[2])
|
|
|
|
# Calculate center position on the primary monitor
|
|
x = primary_x + (primary_width - width) // 2
|
|
y = primary_y + (primary_height - height) // 2
|
|
|
|
# Set window geometry
|
|
window.geometry(f"{width}x{height}+{x}+{y}")
|
|
return
|
|
except (ImportError, IndexError, ValueError):
|
|
pass
|
|
|
|
# Final fallback: Use standard Tkinter method
|
|
screen_width = window.winfo_screenwidth()
|
|
screen_height = window.winfo_screenheight()
|
|
|
|
# Try to make an educated guess for multi-monitor setups
|
|
# If screen width is much larger than height, assume multiple monitors side by side
|
|
if (
|
|
screen_width > screen_height * 1.8
|
|
): # Heuristic for detecting multiple monitors
|
|
# Assume the primary monitor is on the left half
|
|
screen_width = screen_width // 2
|
|
|
|
x = (screen_width - width) // 2
|
|
y = (screen_height - height) // 2
|
|
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:
|
|
|
|
@staticmethod
|
|
def extract_data_files() -> 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)
|
|
source_dirs = [
|
|
os.path.join(sys._MEIPASS, "locale"), # für locale/...
|
|
os.path.join(sys._MEIPASS, "TK-Themes"), # für TK-Themes/...
|
|
os.path.join(sys._MEIPASS, "lx-icons"), # für lx-icons/...
|
|
]
|
|
|
|
target_dir = os.path.abspath(
|
|
os.getcwd()
|
|
) # Zielverzeichnis: aktueller Ordner
|
|
|
|
for source_dir in source_dirs:
|
|
group_name = os.path.basename(
|
|
source_dir
|
|
) # Erhält den Gruppen-Name (z. B. 'locale', 'TK-Themes')
|
|
|
|
for root, dirs, files in os.walk(source_dir):
|
|
for file in files:
|
|
src_path = os.path.join(root, file)
|
|
|
|
# Relativer Pfad innerhalb des Quellordners
|
|
rel_path_from_source_root = os.path.relpath(
|
|
src_path, source_dir
|
|
)
|
|
|
|
# Ziel-Pfad unter dem Gruppen-Ordner im aktuellen Verzeichnis
|
|
dst_path = os.path.join(
|
|
target_dir, group_name, rel_path_from_source_root
|
|
)
|
|
|
|
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
|
shutil.copy2(src_path, dst_path)
|
|
|
|
# Set the SSL certificate file path by start as appimage
|
|
os.environ["SSL_CERT_FILE"] = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "certs", "cacert.pem"
|
|
)
|
|
|
|
VERSION = "1.1.8"
|
|
WINDOW_WIDTH = 460
|
|
WINDOW_HEIGHT = 580
|
|
# Working directory
|
|
WORK_DIR = os.getcwd()
|
|
ICONS_DIR = os.path.join(WORK_DIR, "lx-icons")
|
|
THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes")
|
|
TEMP_DIR = "/tmp/lxtools"
|
|
|
|
# Download URLs
|
|
WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip"
|
|
SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip"
|
|
|
|
# API URLs for version checking
|
|
WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases"
|
|
SHARED_LIBS_API_URL = (
|
|
"https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases"
|
|
)
|
|
|
|
# GPG required
|
|
EXPECTED_FINGERPRINT = "743745087C6414E00F1EF84D4CCF06B6CE2A4C7F"
|
|
KEY_URL_OPENPGP = (
|
|
f"https://keys.openpgp.org/vks/v1/by-fingerprint/{EXPECTED_FINGERPRINT}"
|
|
)
|
|
KEY_URL_GITILUNIX = (
|
|
"https://git.ilunix.de/punix/lxtools_installer/raw/branch/main/public_key.asc"
|
|
)
|
|
|
|
# Project configurations
|
|
PROJECTS = {
|
|
"wirepy": {
|
|
"name": "Wire-Py",
|
|
"description": _("WireGuard VPN Manager with GUI"),
|
|
"download_url": WIREPY_URL,
|
|
"api_url": WIREPY_API_URL,
|
|
"icon_key": "icon_vpn",
|
|
"main_executable": "wirepy.py",
|
|
"symlink_name": "wirepy",
|
|
"config_file": "wp_app_config.py",
|
|
"desktop_file": "Wire-Py.desktop",
|
|
"policy_file": "org.sslcrypt.policy",
|
|
"requires_ssl": True,
|
|
},
|
|
"logviewer": {
|
|
"name": "LogViewer",
|
|
"description": _("System Log Viewer with GUI"),
|
|
"download_url": SHARED_LIBS_URL,
|
|
"api_url": SHARED_LIBS_API_URL,
|
|
"icon_key": "icon_log",
|
|
"main_executable": "logviewer.py",
|
|
"symlink_name": "logviewer",
|
|
"config_file": "logview_app_config.py",
|
|
"desktop_file": "LogViewer.desktop",
|
|
"policy_file": None,
|
|
"requires_ssl": False,
|
|
},
|
|
}
|
|
|
|
# OS Detection List (order matters - specific first, generic last)
|
|
OS_DETECTION = [
|
|
("mint", "Linux Mint"),
|
|
("pop", "Pop!_OS"),
|
|
("manjaro", "Manjaro"),
|
|
("garuda", "Garuda Linux"),
|
|
("endeavouros", "EndeavourOS"),
|
|
("fedora", "Fedora"),
|
|
("tumbleweed", "SUSE Tumbleweed"),
|
|
("leap", "SUSE Leap"),
|
|
("arch", "Arch Linux"),
|
|
("ubuntu", "Ubuntu"),
|
|
("debian", "Debian"),
|
|
]
|
|
|
|
# Package manager commands for TKinter installation
|
|
TKINTER_INSTALL_COMMANDS = {
|
|
"Ubuntu": "apt install -y python3-tk",
|
|
"Debian": "apt install -y python3-tk",
|
|
"Linux Mint": "apt install -y python3-tk",
|
|
"Pop!_OS": "apt install -y python3-tk",
|
|
"Fedora": "dnf install -y python3-tkinter",
|
|
"Arch Linux": "pacman -S --noconfirm tk",
|
|
"Manjaro": "pacman -S --noconfirm tk",
|
|
"Garuda Linux": "pacman -S --noconfirm tk",
|
|
"EndeavourOS": "pacman -S --noconfirm tk",
|
|
"SUSE Tumbleweed": "zypper install -y python3-tk",
|
|
"SUSE Leap": "zypper install -y python3-tk",
|
|
}
|
|
|
|
SHARED_LIBS_DESTINATION = {
|
|
"Ubuntu": "/usr/lib/python3/dist-packages/shared_libs",
|
|
"Debian": "/usr/lib/python3/dist-packages/shared_libs",
|
|
"Linux Mint": "/usr/lib/python3/dist-packages/shared_libs",
|
|
"Pop!_OS": "/usr/lib/python3/dist-packages/shared_libs",
|
|
"Fedora": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs",
|
|
"Arch Linux": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs",
|
|
"Manjaro": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs",
|
|
"Garuda Linux": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs",
|
|
"EndeavourOS": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs",
|
|
"SUSE Tumbleweed": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs",
|
|
"SUSE Leap": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs",
|
|
}
|
|
|
|
|
|
LXToolsAppConfig.extract_data_files()
|
|
|
|
|
|
class LocaleStrings:
|
|
|
|
MSGI = {
|
|
"refresh_and_check": _("Refreshing status and checking versions..."),
|
|
"start_install": _("Starting installation of "),
|
|
"install": _("Installing "),
|
|
"install_success": _(" installation successfully!"),
|
|
"install_failed": _("Installation failed: "),
|
|
"install_create": _("Created install script: "),
|
|
"install_script_failed": _("Installation script failed: "),
|
|
"install_timeout": _("Installation timed out"),
|
|
"installed": _("Installed "),
|
|
}
|
|
|
|
MSGU = {
|
|
"uninstall": _("Uninstalling "),
|
|
"uninstall_success": _(" uninstalled successfully!"),
|
|
"uninstall_failed": _("Uninstallation failed: "),
|
|
"uninstall_create": _("Created uninstall script: "),
|
|
"uninstall_script_failed": _("Uninstallation script failed: "),
|
|
"uninstall_timeout": _("Uninstallation timed out"),
|
|
}
|
|
# MSGO = Other messages
|
|
MSGO = {
|
|
"unknown_project": _("Unknown project: "),
|
|
"not_install": _(" is not installed."),
|
|
"download_from": _("Downloading from "),
|
|
"extract_files": _("Extracting files..."),
|
|
"download_failed": _("Download failed: "),
|
|
"head_string2": _("System: "),
|
|
"head_string3": _("Linux App Installer"),
|
|
"ready": _("Ready for installation"),
|
|
"no_internet": _("No internet connection"),
|
|
"repo_unavailable": _("Repository unavailable"),
|
|
"system_check": _("System checking..."),
|
|
"applications": _("Applications"),
|
|
"progress": _("Progress"),
|
|
"refresh2": _("Status refresh completed"),
|
|
"python_check": _("Python not installed"),
|
|
"polkit_check": _("Please install Polkit!"),
|
|
"networkmanager_check": _("Please install Networkmanager!"),
|
|
"polkit_check_log": _("Polkit check: "),
|
|
"networkmanager_check_log": _("Networkmanager check: "),
|
|
}
|
|
|
|
# MSGC = Strings on Cards
|
|
MSGC = {
|
|
"checking": _("Checking..."),
|
|
"version_check": _("Version: Checking..."),
|
|
"update_on": _("Update on "),
|
|
"available_lower": _("available"),
|
|
"up_to_date": _("Up to date"),
|
|
"latest_unknown": _("Latest unknown"),
|
|
"could_not_check": _("Could not check latest version"),
|
|
"check_last_failed": _("Latest: Check failed"),
|
|
"version_check_failed": _("Version check failed"),
|
|
"not_installed": _("Not installed"),
|
|
"available": _("Available "),
|
|
"available_unknown": _("Available unknown"),
|
|
"available_ckeck_failed": _("Available: Check failed"),
|
|
}
|
|
|
|
# MSGL = Strings on Logmessages
|
|
MSGL = {
|
|
"selected_app": _("Selected project: "),
|
|
"log_name": _("Installation Log"),
|
|
"work_dir": _("Working directory: "),
|
|
"icons_dir": _("Icons directory: "),
|
|
"detected_os": _("Detected OS: "),
|
|
"log_cleared": _("Log cleared"),
|
|
"working_dir": _("Working directory: "),
|
|
"user_interuppt": _("\nApplication interrupted by user."),
|
|
"fatal_error": _("Fatal error: "),
|
|
"fatal_app_error": _("Fatal Error Application failed to start: "),
|
|
}
|
|
|
|
# MSGB = Strings on Buttons
|
|
MSGB = {
|
|
"clear_log": _("Clear Log"),
|
|
"install": _("Install/Update"),
|
|
"uninstall": _("Uninstall"),
|
|
"refresh": _("Refresh Status"),
|
|
}
|
|
|
|
# MSGM = String on MessagDialogs
|
|
MSGM = {
|
|
"please_select": _("Please select a project to install."),
|
|
"network_error": _(
|
|
"No internet connection available.\nPlease check your network connection.",
|
|
),
|
|
"repo_error": _(
|
|
"Cannot access repository.\nPlease try again later.",
|
|
),
|
|
"has_success_update": _("has been successfully installed/updated."),
|
|
"please_select_uninstall": _("Please select a project to uninstall."),
|
|
}
|
|
|
|
# MSGP = Others print strings
|
|
MSGP = {
|
|
"tk_install": _("Installing tkinter for "),
|
|
"command_string": _("Command: "),
|
|
"tk_success": _("TKinter installation completed successfully!"),
|
|
"tk_failed": _("TKinter installation failed: "),
|
|
"tk_timeout": _("TKinter installation timed out"),
|
|
"tk_install_error": _("Error installing tkinter: "),
|
|
"tk_command_error": _("No tkinter installation command defined for "),
|
|
"fail_load_image": _("Failed to load image from "),
|
|
"logviewer_check": _("LogViewer installation check:"),
|
|
"symlink_exist": _(" Symlink exists: "),
|
|
"executable_exist": _(" Executable exists: "),
|
|
"is_executable": _(" Is executable: "),
|
|
"final_result": _(" Final result: "),
|
|
"get_version_error": _("Error getting version for "),
|
|
}
|
|
|
|
# MSGG = String on AppImageMessagDialogs and Strings
|
|
MSGA = {
|
|
"gitea_gpg_error": _("Error verifying signature: "),
|
|
"not_gpg_found": _("Could not find GPG signature: "),
|
|
"SHA256_File_not_found": _("SHA256-File not found: "),
|
|
"SHA256_File_not_found1": _("Would you like to continue?"),
|
|
"SHA256_hash mismatch": _("SHA256 hash mismatch. File might be corrupted!"),
|
|
"Failed_retrieving": _("Failed to retrieve version from Gitea API"),
|
|
"Error_retrieving": _("Error retrieving latest version: "),
|
|
"gpg_verify_success": _("GPG verification successful. Signature is valid."),
|
|
"error_gpg_check": _("Error GPG check: "),
|
|
"error_gpg_download": _("Error downloading or verifying AppImage: "),
|
|
"error_repo": _("Error accessing Gitea repository: "),
|
|
"error": _("Error: "),
|
|
"appimage_renamed": _("The AppImage renamed..."),
|
|
"appimage_rename_error": _("Error renaming the AppImage: "),
|
|
"appimage_executable_error": _("Error making the AppImage executable: "),
|
|
"appimage_executable_success": _("The AppImage has been made executable"),
|
|
"appimage_not_exist": _("Error: The AppImage file does not exist."),
|
|
}
|
|
|
|
MSGGPG = {
|
|
"gpg_missing": _(
|
|
"Warning: 'gpg' is not installed. Please install it to verify the AppImage signature."
|
|
),
|
|
"url_not_reachable": _("URL not reachable: "),
|
|
"corrupted_file": _(
|
|
"No fingerprint found in the key. File might be corrupted or empty."
|
|
),
|
|
"mismatch": _("Fingerprint mismatch: Expected "),
|
|
"but_got": _("but got: "),
|
|
"failed_import": _("Failed to import public key: "),
|
|
"fingerprint_extract": _("GPG fingerprint extraction failed: "),
|
|
"error_import_key": _("Error importing GPG key from "),
|
|
"error_updating_trust_level": _("Error updating trust level: "),
|
|
"error_executing_script": _("Error executing script to update trust level: "),
|
|
"keyserver_reachable": _(
|
|
"OpenPGP keyserver is reachable. Proceeding with download."
|
|
),
|
|
"keyserver_unreachable": _(
|
|
"OpenPGP keyserver unreachable. Skipping this source."
|
|
),
|
|
"all_keys_valid": _(
|
|
"All keys have valid fingerprints matching the expected value."
|
|
),
|
|
"set_trust_level": _("Trust level 5 successfully applied to key "),
|
|
"not_all_keys_are_valid": _("Not all keys are valid."),
|
|
}
|