Installer divided into several modules and added new MessageDialog module
13
Changelog
@ -4,6 +4,19 @@ Changelog for LXTools installer
|
|||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
|
### Added
|
||||||
|
14-06-2025
|
||||||
|
|
||||||
|
- Installer divided into several modules and added new MessageDialog module
|
||||||
|
|
||||||
|
### Added
|
||||||
|
4-06-2025
|
||||||
|
|
||||||
|
- replace modul path /usr/lib/python3/dist-packages/shared_libs
|
||||||
|
with /usr/local/share/shared_libs for better ensure that the shared libs are found
|
||||||
|
|
||||||
|
- add ensure_shared_libs_pth_exists Script to install
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
4-06-2025
|
4-06-2025
|
||||||
|
|
||||||
|
68
ensure_modules.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# ✅ Path to be added in the .pth file
|
||||||
|
SHARED_LIBS_PATH = "/usr/local/share/shared_libs"
|
||||||
|
PTH_FILE_NAME = "shared_libs.pth"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_shared_libs_pth_exists():
|
||||||
|
"""
|
||||||
|
Checks if all site-packages directories have a `.pth` file with the correct path.
|
||||||
|
Creates or updates it if missing or incorrect.
|
||||||
|
"""
|
||||||
|
# Search for all site-packages directories (e.g., /usr/lib/python3.x/site-packages/)
|
||||||
|
for root, dirs, files in os.walk("/usr"):
|
||||||
|
if "site-packages" in dirs:
|
||||||
|
site_packages_dir = os.path.join(root, "site-packages")
|
||||||
|
|
||||||
|
pth_file_path = os.path.join(site_packages_dir, PTH_FILE_NAME)
|
||||||
|
|
||||||
|
# Check if the file exists and is correct
|
||||||
|
if not os.path.exists(pth_file_path):
|
||||||
|
print(f"⚠️ .pth file not found: {pth_file_path}. Creating...")
|
||||||
|
with open(pth_file_path, "w") as f:
|
||||||
|
f.write(SHARED_LIBS_PATH + "\n")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Check if the correct path is in the file
|
||||||
|
with open(pth_file_path, "r") as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
|
||||||
|
if not content == SHARED_LIBS_PATH:
|
||||||
|
print(f"⚠️ .pth file exists but has incorrect content. Fixing...")
|
||||||
|
with open(pth_file_path, "w") as f:
|
||||||
|
f.write(SHARED_LIBS_PATH + "\n")
|
||||||
|
|
||||||
|
print("✅ All .pth files checked and corrected.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
# Try to import the module
|
||||||
|
from shared_libs.wp_app_config import AppConfig
|
||||||
|
|
||||||
|
print("✅ 'shared_libs' is correctly loaded. Starting the application...")
|
||||||
|
|
||||||
|
# Your main program logic here...
|
||||||
|
except ModuleNotFoundError as e:
|
||||||
|
# Only handle errors related to missing .pth file
|
||||||
|
if "No module named 'shared_libs'" in str(e):
|
||||||
|
print("⚠️ Error: 'shared_libs' module not found. Checking .pth file...")
|
||||||
|
ensure_shared_libs_pth_exists()
|
||||||
|
|
||||||
|
# Try again after fixing the .pth file
|
||||||
|
try:
|
||||||
|
from shared_libs.wp_app_config import AppConfig
|
||||||
|
|
||||||
|
print("✅ After correcting the .pth file: Module loaded.")
|
||||||
|
# Your main program logic here...
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"❌ Error after correcting the .pth file: {e2}")
|
||||||
|
else:
|
||||||
|
# For other errors, re-raise them
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
lx-icons/128/question_mark.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
lx-icons/128/warning.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
lx-icons/256/question_mark.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
lx-icons/256/warning.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
lx-icons/32/question_mark.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
lx-icons/32/warning.png
Normal file
After Width: | Height: | Size: 838 B |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
lx-icons/48/question_mark.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
lx-icons/48/warning.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.5 KiB |
BIN
lx-icons/64/question_mark.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
lx-icons/64/warning.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 4.6 KiB |
493
manager.py
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
import locale
|
||||||
|
import gettext
|
||||||
|
import tkinter as tk
|
||||||
|
from pathlib import Path
|
||||||
|
from tkinter import ttk
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import stat
|
||||||
|
from network import GiteaUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class LXToolsAppConfig:
|
||||||
|
VERSION = "1.1.4"
|
||||||
|
APP_NAME = "Lunix Tools Installer"
|
||||||
|
WINDOW_WIDTH = 450
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Locale settings
|
||||||
|
LOCALE_DIR = Path("/usr/share/locale/")
|
||||||
|
|
||||||
|
# Download URLs
|
||||||
|
WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip"
|
||||||
|
SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip"
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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", "update", "&&", "apt", "install", "-y", "python3-tk"],
|
||||||
|
"Debian": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"],
|
||||||
|
"Linux Mint": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"],
|
||||||
|
"Pop!_OS": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"],
|
||||||
|
"Fedora": ["dnf", "install", "-y", "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", "python314-tk"],
|
||||||
|
"SUSE Leap": ["zypper", "install", "-y", "python312-tk"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def setup_translations():
|
||||||
|
"""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
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize translations
|
||||||
|
_ = LXToolsAppConfig.setup_translations()
|
||||||
|
|
||||||
|
|
||||||
|
class OSDetector:
|
||||||
|
@staticmethod
|
||||||
|
def detect_os():
|
||||||
|
"""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 check_tkinter_available():
|
||||||
|
"""Check if tkinter is available"""
|
||||||
|
try:
|
||||||
|
import tkinter
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def install_tkinter():
|
||||||
|
"""Install tkinter based on detected OS"""
|
||||||
|
detected_os = OSDetector.detect_os()
|
||||||
|
|
||||||
|
if detected_os in LXToolsAppConfig.TKINTER_INSTALL_COMMANDS:
|
||||||
|
commands = LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]
|
||||||
|
|
||||||
|
print(f"Installing tkinter for {detected_os}...")
|
||||||
|
print(_(f"Command: {' '.join(commands)}"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use pkexec for privilege escalation
|
||||||
|
full_command = ["pkexec", "bash", "-c", " ".join(commands)]
|
||||||
|
result = subprocess.run(
|
||||||
|
full_command, capture_output=True, text=True, timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(_("TKinter installation completed successfully!"))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(_(f"TKinter installation failed: {result.stderr}"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print(_("TKinter installation timed out"))
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(_(f"Error installing tkinter: {e}"))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(_(f"No tkinter installation command defined for {detected_os}"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Theme:
|
||||||
|
@staticmethod
|
||||||
|
def apply_light_theme(root):
|
||||||
|
"""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):
|
||||||
|
"""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):
|
||||||
|
"""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):
|
||||||
|
"""Copy directory using pkexec"""
|
||||||
|
subprocess.run(["pkexec", "cp", "-r", src, dest], check=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_file(path):
|
||||||
|
"""Remove file using pkexec"""
|
||||||
|
subprocess.run(["pkexec", "rm", "-f", path], check=False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_directory(path):
|
||||||
|
"""Remove directory using pkexec"""
|
||||||
|
subprocess.run(["pkexec", "rm", "-rf", path], check=False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_symlink(target, link_name):
|
||||||
|
"""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):
|
||||||
|
"""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):
|
||||||
|
"""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",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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"Failed to load image from {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):
|
||||||
|
"""Get project information by key"""
|
||||||
|
return self.projects.get(project_key)
|
||||||
|
|
||||||
|
def get_all_projects(self):
|
||||||
|
"""Get all project configurations"""
|
||||||
|
return self.projects
|
||||||
|
|
||||||
|
def is_installed(self, project_key):
|
||||||
|
"""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(
|
||||||
|
"/usr/local/share/shared_libs/logviewer.py"
|
||||||
|
)
|
||||||
|
executable_is_executable = False
|
||||||
|
|
||||||
|
if executable_exists:
|
||||||
|
try:
|
||||||
|
# Check if file is executable
|
||||||
|
file_stat = os.stat("/usr/local/share/shared_libs/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(_("LogViewer installation check:"))
|
||||||
|
print(_(f" Symlink exists: {symlink_exists}"))
|
||||||
|
print(_(f" Executable exists: {executable_exists}"))
|
||||||
|
print(_(f" Is executable: {executable_is_executable}"))
|
||||||
|
print(_(f" Final result: {is_installed}"))
|
||||||
|
|
||||||
|
return is_installed
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_installed_version(self, project_key):
|
||||||
|
"""Get installed version from config file"""
|
||||||
|
try:
|
||||||
|
if project_key == "wirepy":
|
||||||
|
config_file = "/usr/local/share/shared_libs/wp_app_config.py"
|
||||||
|
elif project_key == "logviewer":
|
||||||
|
config_file = "/usr/local/share/shared_libs/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"Error getting version for {project_key}: {e}"))
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
def get_latest_version(self, project_key):
|
||||||
|
"""Get latest version from API"""
|
||||||
|
project_info = self.get_project_info(project_key)
|
||||||
|
if not project_info:
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
return GiteaUpdate.api_down(project_info["api_url"])
|
||||||
|
|
||||||
|
def check_other_apps_installed(self, exclude_key):
|
||||||
|
"""Check if other apps are still installed"""
|
||||||
|
return any(
|
||||||
|
self.is_installed(key) for key in self.projects.keys() if key != exclude_key
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Center:
|
||||||
|
@staticmethod
|
||||||
|
def center_window_cross_platform(window, width, height):
|
||||||
|
"""
|
||||||
|
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}")
|
384
message.py
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
import os
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
from manager import Center
|
||||||
|
|
||||||
|
"""
|
||||||
|
####################################################
|
||||||
|
Attention! MessageDialog returns different values.
|
||||||
|
From 3 buttons with Cancel, Cancel and the Close (x)
|
||||||
|
None returns. otherwise always False.
|
||||||
|
####################################################
|
||||||
|
Usage Examples
|
||||||
|
1. Basic Info Dialog
|
||||||
|
from tkinter import Tk
|
||||||
|
|
||||||
|
root = Tk()
|
||||||
|
dialog = MessageDialog(
|
||||||
|
message_type="info",
|
||||||
|
text="This is an information message.",
|
||||||
|
buttons=["OK"],
|
||||||
|
master=root,
|
||||||
|
)
|
||||||
|
result = dialog.show()
|
||||||
|
print("User clicked OK:", result)
|
||||||
|
|
||||||
|
-----------------------------------------------------
|
||||||
|
My Favorite Example,
|
||||||
|
for simply information message:
|
||||||
|
|
||||||
|
MessageDialog(text="This is an information message.")
|
||||||
|
result = MessageDialog(text="This is an information message.").show()
|
||||||
|
-----------------------------------------------------
|
||||||
|
Explanation: if you need the return value e.g. in the vaiable result,
|
||||||
|
you need to add .show(). otherwise only if no root.mainloop z.b is used to test the window.
|
||||||
|
#####################################################
|
||||||
|
|
||||||
|
2. Error Dialog with Custom Icon and Command
|
||||||
|
def on_cancel():
|
||||||
|
print("User canceled the operation.")
|
||||||
|
|
||||||
|
root = Tk()
|
||||||
|
result = MessageDialog(
|
||||||
|
message_type="error",
|
||||||
|
text="An error occurred during processing.",
|
||||||
|
buttons=["Retry", "Cancel"],
|
||||||
|
commands=[None, on_cancel],
|
||||||
|
icon="/path/to/custom/error_icon.png",
|
||||||
|
title="Critical Error"
|
||||||
|
).show()
|
||||||
|
|
||||||
|
print("User clicked Retry:", result)
|
||||||
|
|
||||||
|
-----------------------------------------------------
|
||||||
|
My Favorite Example,
|
||||||
|
for simply Error message:
|
||||||
|
|
||||||
|
MessageDialog(
|
||||||
|
"error",
|
||||||
|
text="An error occurred during processing.",
|
||||||
|
).show()
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
|
||||||
|
3. Confirmation Dialog with Yes/No Buttons
|
||||||
|
def on_confirm():
|
||||||
|
print("User confirmed the action.")
|
||||||
|
|
||||||
|
root = Tk()
|
||||||
|
dialog = MessageDialog(
|
||||||
|
message_type="ask",
|
||||||
|
text="Are you sure you want to proceed?",
|
||||||
|
buttons=["Yes", "No"],
|
||||||
|
commands=[on_confirm, None], # Either use comando or work with the values True and False
|
||||||
|
)
|
||||||
|
result = dialog.show()
|
||||||
|
print("User confirmed:", result)
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
My Favorite Example,
|
||||||
|
for simply Question message:
|
||||||
|
|
||||||
|
dialog = MessageDialog(
|
||||||
|
"ask",
|
||||||
|
text="Are you sure you want to proceed?",
|
||||||
|
buttons=["Yes", "No"]
|
||||||
|
).show()
|
||||||
|
#####################################################
|
||||||
|
|
||||||
|
4. Warning Dialog with Custom Title
|
||||||
|
|
||||||
|
root = Tk()
|
||||||
|
dialog = MessageDialog(
|
||||||
|
message_type="warning",
|
||||||
|
text="This action cannot be undone.",
|
||||||
|
buttons=["Proceed", "Cancel"],
|
||||||
|
title="Warning: Irreversible Action"
|
||||||
|
)
|
||||||
|
result = dialog.show()
|
||||||
|
print("User proceeded:", result)
|
||||||
|
-----------------------------------------------------
|
||||||
|
And a special example for a "open link" button:
|
||||||
|
Be careful not to forget to import it into the script in which this dialog is used!!!
|
||||||
|
import webbrowser
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
dialog = MessageDialog(
|
||||||
|
"ask",
|
||||||
|
text="Are you sure you want to proceed?",
|
||||||
|
buttons=["Yes", "Go to Exapmle"],
|
||||||
|
commands=[
|
||||||
|
None, # Default on "OK"
|
||||||
|
partial(webbrowser.open, "https://exapmle.com"),
|
||||||
|
],
|
||||||
|
icon="/pathh/to/custom/icon.png",
|
||||||
|
title="Example",
|
||||||
|
).show()
|
||||||
|
|
||||||
|
|
||||||
|
In all dialogues, a font can also be specified as a tuple. With font=("ubuntu", 11)
|
||||||
|
and wraplength=300, the text is automatically wrapped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MessageDialog:
|
||||||
|
"""
|
||||||
|
A customizable message dialog window using tkinter.
|
||||||
|
|
||||||
|
This class creates modal dialogs for displaying information, warnings, errors,
|
||||||
|
or questions to the user. It supports multiple button configurations and custom
|
||||||
|
icons. The dialog is centered on the screen and handles user interactions.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
message_type (str): Type of message ("info", "error", "warning", "ask").
|
||||||
|
text (str): Main message content.
|
||||||
|
buttons (List[str]): List of button labels (e.g., ["OK", "Cancel"]).
|
||||||
|
result (bool): True if the user clicked a positive button (like "Yes" or "OK"), else False.
|
||||||
|
icons: Dictionary mapping message types to tkinter.PhotoImage objects.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
message_type: Type of message dialog (default: "info").
|
||||||
|
text: Message content to display.
|
||||||
|
buttons: List of button labels (default: ["OK"]).
|
||||||
|
master: Parent tkinter window (optional).
|
||||||
|
commands: List of callables for each button (default: [None]).
|
||||||
|
icon: Custom icon path (overrides default icons if provided).
|
||||||
|
title: Window title (default: derived from message_type).
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
_get_title(): Returns the default window title based on message type.
|
||||||
|
_load_icons(): Loads icons from system paths or fallback locations.
|
||||||
|
_on_button_click(button_text): Sets result and closes the dialog.
|
||||||
|
show(): Displays the dialog and waits for user response (returns self.result).
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_ICON_PATH = "/usr/share/icons/lx-icons"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message_type: str = "info",
|
||||||
|
text: str = "",
|
||||||
|
buttons: List[str] = ["OK"],
|
||||||
|
master: Optional[tk.Tk] = None,
|
||||||
|
commands: List[Optional[callable]] = [None],
|
||||||
|
icon: str = None,
|
||||||
|
title: str = None,
|
||||||
|
font: tuple = None,
|
||||||
|
wraplength: int = None,
|
||||||
|
):
|
||||||
|
self.message_type = message_type or "info" # Default is "info"
|
||||||
|
self.text = text
|
||||||
|
self.buttons = buttons
|
||||||
|
self.master = master
|
||||||
|
self.result: bool = False # Default is False
|
||||||
|
|
||||||
|
self.icon_path = self._get_icon_path()
|
||||||
|
self.icon = icon
|
||||||
|
self.title = title
|
||||||
|
# Window creation
|
||||||
|
self.window = tk.Toplevel(master)
|
||||||
|
self.window.grab_set()
|
||||||
|
self.window.resizable(False, False)
|
||||||
|
ttk.Style().configure("TButton", font=("Helvetica", 11), padding=5)
|
||||||
|
self.buttons_widgets = []
|
||||||
|
self.current_button_index = 0
|
||||||
|
self._load_icons()
|
||||||
|
|
||||||
|
# Window title and icon
|
||||||
|
self.window.title(self._get_title() if not self.title else self.title)
|
||||||
|
self.window.iconphoto(False, self.icons[self.message_type])
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
frame = ttk.Frame(self.window)
|
||||||
|
frame.pack(expand=True, fill="both")
|
||||||
|
|
||||||
|
# Grid-Configuration
|
||||||
|
frame.grid_rowconfigure(0, weight=1)
|
||||||
|
frame.grid_columnconfigure(0, weight=1)
|
||||||
|
frame.grid_columnconfigure(1, weight=3)
|
||||||
|
|
||||||
|
# Icon and Text
|
||||||
|
icon_label = ttk.Label(frame, image=self.icons[self.message_type])
|
||||||
|
pady_value = 5 if self.icon is not None else 15
|
||||||
|
icon_label.grid(
|
||||||
|
row=0, column=0, padx=(20, 10), pady=(pady_value, 15), sticky="nsew"
|
||||||
|
)
|
||||||
|
|
||||||
|
text_label = tk.Label(
|
||||||
|
frame,
|
||||||
|
text=text,
|
||||||
|
wraplength=wraplength if wraplength else 300,
|
||||||
|
justify="left",
|
||||||
|
anchor="center",
|
||||||
|
font=font if font else ("Helvetica", 12),
|
||||||
|
pady=20,
|
||||||
|
)
|
||||||
|
text_label.grid(
|
||||||
|
row=0,
|
||||||
|
column=1,
|
||||||
|
padx=(10, 20),
|
||||||
|
pady=(8, 20),
|
||||||
|
sticky="nsew",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create button frame
|
||||||
|
self.button_frame = ttk.Frame(frame)
|
||||||
|
self.button_frame.grid(row=1, columnspan=2, pady=(15, 10))
|
||||||
|
|
||||||
|
for i, btn_text in enumerate(buttons):
|
||||||
|
if commands and len(commands) > i and commands[i] is not None:
|
||||||
|
# Button with individual command
|
||||||
|
btn = ttk.Button(
|
||||||
|
self.button_frame,
|
||||||
|
text=btn_text,
|
||||||
|
command=commands[i],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Default button set self.result and close window
|
||||||
|
btn = ttk.Button(
|
||||||
|
self.button_frame,
|
||||||
|
text=btn_text,
|
||||||
|
command=lambda t=btn_text: self._on_button_click(t),
|
||||||
|
)
|
||||||
|
|
||||||
|
padx_value = 50 if self.icon is not None and len(buttons) == 2 else 10
|
||||||
|
btn.pack(side="left" if i == 0 else "right", padx=padx_value, pady=15)
|
||||||
|
btn.focus_set() if i == 0 else None # Set focus on first button
|
||||||
|
self.buttons_widgets.append(btn)
|
||||||
|
|
||||||
|
self.window.bind("<Return>", lambda event: self._on_enter_pressed())
|
||||||
|
self.window.bind("<Left>", lambda event: self._navigate_left())
|
||||||
|
self.window.bind("<Right>", lambda event: self._navigate_right())
|
||||||
|
self.window.update_idletasks()
|
||||||
|
self.window.attributes("-alpha", 0.0) # 100% Transparencence
|
||||||
|
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
|
||||||
|
self.window.update() # Window update before centering!
|
||||||
|
Center.center_window_cross_platform(
|
||||||
|
self.window, self.window.winfo_width(), self.window.winfo_height()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Close Window on Cancel
|
||||||
|
self.window.protocol(
|
||||||
|
"WM_DELETE_WINDOW", lambda: self._on_button_click("Cancel")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_title(self) -> str:
|
||||||
|
return {
|
||||||
|
"error": "Error",
|
||||||
|
"info": "Info",
|
||||||
|
"ask": "Question",
|
||||||
|
"warning": "Warning",
|
||||||
|
}[self.message_type]
|
||||||
|
|
||||||
|
def _load_icons(self):
|
||||||
|
# Try to load the icon from the provided path
|
||||||
|
self.icons = {}
|
||||||
|
icon_paths: Dict[str, str] = {
|
||||||
|
"error": os.path.join(self.icon_path, "64/error.png"),
|
||||||
|
"info": os.path.join(self.icon_path, "64/info.png"),
|
||||||
|
"warning": os.path.join(self.icon_path, "64/warning.png"),
|
||||||
|
"ask": os.path.join(self.icon_path, "64/question_mark.png"),
|
||||||
|
}
|
||||||
|
|
||||||
|
fallback_paths: Dict[str, str] = {
|
||||||
|
"error": "./lx-icons/64/error.png",
|
||||||
|
"info": "./lx-icons/64/info.png",
|
||||||
|
"warning": "./lx-icons/64/warning.png",
|
||||||
|
"ask": "./lx-icons/64/question_mark.png",
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in icon_paths:
|
||||||
|
try:
|
||||||
|
# Check if an individual icon is provided
|
||||||
|
if (
|
||||||
|
self.message_type == key
|
||||||
|
and self.icon is not None
|
||||||
|
and os.path.exists(self.icon)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self.icons[key] = tk.PhotoImage(file=self.icon)
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Erro on loading individual icon '{key}': {e}\n",
|
||||||
|
"Try to use the default icon",
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Check for standard path
|
||||||
|
if os.path.exists(icon_paths[key]):
|
||||||
|
self.icons[key] = tk.PhotoImage(file=icon_paths[key])
|
||||||
|
else:
|
||||||
|
self.icons[key] = tk.PhotoImage(file=fallback_paths[key])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error on load Icon '{[key]}': {e}")
|
||||||
|
self.icons[key] = tk.PhotoImage()
|
||||||
|
print(f"⚠️ No Icon found for '{key}'. Use standard Tkinter icon.")
|
||||||
|
|
||||||
|
return self.icons
|
||||||
|
|
||||||
|
def _get_icon_path(self) -> str:
|
||||||
|
"""Get the path to the default icon."""
|
||||||
|
if os.path.exists(self.DEFAULT_ICON_PATH):
|
||||||
|
return self.DEFAULT_ICON_PATH
|
||||||
|
else:
|
||||||
|
# Fallback to the directory of the script
|
||||||
|
return os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
def _navigate_left(self):
|
||||||
|
if not self.buttons_widgets:
|
||||||
|
return
|
||||||
|
self.current_button_index = (self.current_button_index - 1) % len(
|
||||||
|
self.buttons_widgets
|
||||||
|
)
|
||||||
|
self.buttons_widgets[self.current_button_index].focus_set()
|
||||||
|
|
||||||
|
def _navigate_right(self):
|
||||||
|
if not self.buttons_widgets:
|
||||||
|
return
|
||||||
|
self.current_button_index = (self.current_button_index + 1) % len(
|
||||||
|
self.buttons_widgets
|
||||||
|
)
|
||||||
|
self.buttons_widgets[self.current_button_index].focus_set()
|
||||||
|
|
||||||
|
def _on_enter_pressed(self):
|
||||||
|
focused = self.window.focus_get()
|
||||||
|
if isinstance(focused, ttk.Button):
|
||||||
|
focused.invoke()
|
||||||
|
|
||||||
|
def _on_button_click(self, button_text: str) -> None:
|
||||||
|
"""
|
||||||
|
Sets `self.result` based on the clicked button.
|
||||||
|
- Returns `None` if the button is "Cancel", "Abort", or "Exit" **and** there are 3 or more buttons.
|
||||||
|
- Returns `True` if the button is "Yes", "Ok", "Continue", "Next", or "Start".
|
||||||
|
- Returns `False` in all other cases (e.g., "No", closing with X, or fewer than 3 buttons).
|
||||||
|
"""
|
||||||
|
# Check: If there are 3+ buttons and the button text matches "Cancel", "Abort", or "Exit"
|
||||||
|
if len(self.buttons) >= 3 and button_text.lower() in [
|
||||||
|
"cancel",
|
||||||
|
"abort",
|
||||||
|
"exit",
|
||||||
|
]:
|
||||||
|
self.result = None
|
||||||
|
# Check: Button text is "Yes", "Ok", "Continue", "Next", or "Start"
|
||||||
|
elif button_text.lower() in ["yes", "ok", "continue", "next", "start"]:
|
||||||
|
self.result = True
|
||||||
|
else:
|
||||||
|
# Fallback for all other cases (e.g., "No", closing with X, or fewer than 3 buttons)
|
||||||
|
self.result = False
|
||||||
|
|
||||||
|
self.window.destroy()
|
||||||
|
|
||||||
|
def show(self) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Displays the dialog window and waits for user interaction.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool or None:
|
||||||
|
- `True` if "Yes", "Ok", etc. was clicked.
|
||||||
|
- `False` if "No" was clicked, or the window was closed with X (when there are 2 buttons).
|
||||||
|
- `None` if "Cancel", "Abort", or "Exit" was clicked **and** there are 3+ buttons,
|
||||||
|
or the window was closed with X (when there are 3+ buttons).
|
||||||
|
"""
|
||||||
|
self.window.wait_window()
|
||||||
|
return self.result
|
40
network.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import socket
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaUpdate:
|
||||||
|
@staticmethod
|
||||||
|
def api_down(url, current_version=""):
|
||||||
|
"""Get latest version from Gitea API"""
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=10) as response:
|
||||||
|
data = json.loads(response.read().decode())
|
||||||
|
if data and len(data) > 0:
|
||||||
|
latest_version = data[0].get("tag_name", "Unknown")
|
||||||
|
return latest_version.lstrip("v") # Remove 'v' prefix if present
|
||||||
|
return "Unknown"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"API Error: {e}")
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkChecker:
|
||||||
|
@staticmethod
|
||||||
|
def check_internet_connection(host="8.8.8.8", port=53, timeout=3):
|
||||||
|
"""Check if internet connection is available"""
|
||||||
|
try:
|
||||||
|
socket.setdefaulttimeout(timeout)
|
||||||
|
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
|
||||||
|
return True
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_repository_access(url="https://git.ilunix.de", timeout=5):
|
||||||
|
"""Check if repository is accessible"""
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(url, timeout=timeout)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|