From 09f9f69f52d28d6b93c0240f2cd65731d7f93b24 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sun, 28 Apr 2024 07:28:27 +0100 Subject: [PATCH 001/253] if running GUI use pkexec if installed prefer pkexec over sudo only use pkexec if running GUI Modify condition Add an error message based on UI. --- installer.py | 1 - utils.py | 24 +++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/installer.py b/installer.py index 52fe2eba..07f8222e 100644 --- a/installer.py +++ b/installer.py @@ -314,7 +314,6 @@ def ensure_sys_deps(app=None): update_install_feedback("Ensuring system dependencies are met…", app=app) if not config.SKIP_DEPENDENCIES: - utils.get_package_manager() utils.check_dependencies() logging.debug("> Done.") else: diff --git a/utils.py b/utils.py index a3caa6d4..278a05b1 100644 --- a/utils.py +++ b/utils.py @@ -133,6 +133,7 @@ def get_md5(self): # Set "global" variables. def set_default_config(): get_os() + get_superuser_command() get_package_manager() if config.CONFIG_FILE is None: config.CONFIG_FILE = config.DEFAULT_CONFIG_PATH @@ -355,13 +356,23 @@ def get_os(): return config.OS_NAME, config.OS_RELEASE -def get_package_manager(): - # Check for superuser command - if shutil.which('sudo') is not None: - config.SUPERUSER_COMMAND = "sudo" - elif shutil.which('doas') is not None: - config.SUPERUSER_COMMAND = "doas" +def get_superuser_command(): + if config.DIALOG == 'tk': + if shutil.which('pkexec'): + config.SUPERUSER_COMMAND = "pkexec" + else: + logging.critical("No superuser command found. Please install pkexec.") + elif config.DIALOG != 'tk': + if shutil.which('sudo'): + config.SUPERUSER_COMMAND = "sudo" + elif shutil.which('doas'): + config.SUPERUSER_COMMAND = "doas") + else: + logging.critical("No superuser command found. Please install sudo or doas.") + logging.debug(f"{config.SUPERUSER_COMMAND=}") + +def get_package_manager(): # Check for package manager and associated packages if shutil.which('apt') is not None: # debian, ubuntu config.PACKAGE_MANAGER_COMMAND_INSTALL = "apt install -y" @@ -396,7 +407,6 @@ def get_package_manager(): # Add more conditions for other package managers as needed # Add logging output. - logging.debug(f"{config.SUPERUSER_COMMAND=}") logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_INSTALL=}") logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_QUERY=}") logging.debug(f"{config.PACKAGES=}") From 9e8dad23c51aa3fa4f6d730372f4ab1684bbfcfd Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sun, 28 Apr 2024 07:33:54 +0100 Subject: [PATCH 002/253] fix TUI install_dependencies --- tui_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tui_app.py b/tui_app.py index 80e487da..5bd03377 100644 --- a/tui_app.py +++ b/tui_app.py @@ -96,6 +96,8 @@ def control_panel_app(): if choice is None or choice == "Exit": sys.exit(0) + elif choice == "Install Dependencies": + utils.check_dependencies() elif choice.startswith("Install"): installer.ensure_launcher_shortcuts() elif choice.startswith("Update Logos Linux Installer"): @@ -110,8 +112,6 @@ def control_panel_app(): control.remove_all_index_files() elif choice == "Edit Config": control.edit_config() - elif choice == "Install Dependencies": - utils.check_dependencies() elif choice == "Back up Data": control.backup() elif choice == "Restore Data": From 4ea4721fa757f2a721742f952979dedd546646c9 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 29 Apr 2024 06:38:44 +0100 Subject: [PATCH 003/253] in GUI install dependencies in a thread --- gui_app.py | 4 +++- utils.py | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gui_app.py b/gui_app.py index 80fb75af..6c518604 100644 --- a/gui_app.py +++ b/gui_app.py @@ -701,7 +701,9 @@ def run_restore(self, evt=None): t.start() def install_deps(self, evt=None): - utils.check_dependencies() + t = Thread(target=utils.check_dependencies, daemon=True) + self.start_indeterminate_progress() + t.start() def open_file_dialog(self, filetype_name, filetype_extension): file_path = fd.askopenfilename( diff --git a/utils.py b/utils.py index 278a05b1..509748d3 100644 --- a/utils.py +++ b/utils.py @@ -758,12 +758,16 @@ def check_libs(libraries): f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the library: {library}. Please install the package associated with {library} for {config.OS_NAME}.") # noqa: E501 -def check_dependencies(): +def check_dependencies(app=None): if config.TARGETVERSION: targetversion = int(config.TARGETVERSION) else: targetversion = 10 logging.info(f"Checking Logos {str(targetversion)} dependencies…") + if app: + app.status_q.put(f"Checking Logos {str(targetversion)} dependencies…") + app.root.event_generate('<>') + if targetversion == 10: install_dependencies(config.PACKAGES, config.BADPACKAGES) elif targetversion == 9: @@ -775,6 +779,9 @@ def check_dependencies(): else: logging.error(f"TARGETVERSION not found: {config.TARGETVERSION}.") + if app: + app.root.event_generate('<>') + def file_exists(file_path): if file_path is not None: From 70ffcc090f5d4025733e4753cd47253862563cff Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 29 Apr 2024 14:40:50 +0100 Subject: [PATCH 004/253] error exit if no superuser cmd found --- utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils.py b/utils.py index 509748d3..095116b2 100644 --- a/utils.py +++ b/utils.py @@ -361,14 +361,14 @@ def get_superuser_command(): if shutil.which('pkexec'): config.SUPERUSER_COMMAND = "pkexec" else: - logging.critical("No superuser command found. Please install pkexec.") - elif config.DIALOG != 'tk': + msg.logos_error("No superuser command found. Please install pkexec.") # noqa: E501 + else: if shutil.which('sudo'): config.SUPERUSER_COMMAND = "sudo" elif shutil.which('doas'): - config.SUPERUSER_COMMAND = "doas") + config.SUPERUSER_COMMAND = "doas" else: - logging.critical("No superuser command found. Please install sudo or doas.") + msg.logos_error("No superuser command found. Please install sudo or doas.") # noqa: E501 logging.debug(f"{config.SUPERUSER_COMMAND=}") From 6b3811b3521ee78a321a539ef2057367ecc9347b Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 20 Apr 2024 10:49:59 -0400 Subject: [PATCH 005/253] Update Manjaro deps --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 095116b2..cccdf9e5 100644 --- a/utils.py +++ b/utils.py @@ -394,7 +394,7 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_INSTALL = "pamac install --no-upgrade --no-confirm" # noqa: E501 config.PACKAGE_MANAGER_COMMAND_REMOVE = "pamac remove --no-confirm" config.PACKAGE_MANAGER_COMMAND_QUERY = "pamac list -i | grep -E ^" - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 + config.PACKAGES = "wget sed grep gawk cabextract samba" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('pacman') is not None: # arch, steamOS From 312599c7fcd36b06671e9029111c9abe12b5f9ee Mon Sep 17 00:00:00 2001 From: Thomas Bleher Date: Sat, 13 Apr 2024 15:01:28 +0200 Subject: [PATCH 006/253] Correct Debian/Ubuntu package list - mktemp and tr are part of package coreutils - find is part of package findutils Note: both findutils and coreutils are marked as essential, so should always be installed. So listing these packages is not strictly necessary, but it also doesn't really hurt. Sync the package lists between the README.md and utils.py. Switch to fuse3 in utils.py, to match the description in the README. --- README.md | 2 +- utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70c7335d..84b7d8c0 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ NOTE: The following section is WIP. ### Install Dependencies ``` -sudo apt install mktemp patch lsof wget find sed grep gawk tr winbind cabextract x11-apps bc +sudo apt install coreutils patch lsof wget findutils sed grep gawk winbind cabextract x11-apps bc binutils ``` If using wine from a repo, you must install wine staging. Run: diff --git a/utils.py b/utils.py index cccdf9e5..21b07bf2 100644 --- a/utils.py +++ b/utils.py @@ -380,7 +380,7 @@ def get_package_manager(): # IDEA: Switch to Python APT library? # See https://github.com/FaithLife-Community/LogosLinuxInstaller/pull/33#discussion_r1443623996 # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l | grep -E '^.i '" - config.PACKAGES = "binutils cabextract fuse wget winbind" + config.PACKAGES = "coreutils patch lsof wget findutils sed grep gawk winbind cabextract x11-apps bc binutils fuse3" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora From ed09916ce867b9d24d34aa7a890e5059d0cf2139 Mon Sep 17 00:00:00 2001 From: Connor Reeder Date: Thu, 13 Jun 2024 20:40:06 -0400 Subject: [PATCH 007/253] Update Arch and SteamOS Deps (#82) * Separate Arch from SteamOS --------- Co-authored-by: T. H. Wright --- utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 21b07bf2..0e39e0b2 100644 --- a/utils.py +++ b/utils.py @@ -401,7 +401,10 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_INSTALL = r"pacman -Syu --overwrite * --noconfirm --needed" # noqa: E501 config.PACKAGE_MANAGER_COMMAND_REMOVE = r"pacman -R --no-confirm" config.PACKAGE_MANAGER_COMMAND_QUERY = "pacman -Q | grep -E ^" - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks cabextract appmenu-gtk-module patch bc lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 + if config.OS_NAME == "steamos": # steamOS + config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" + else: # arch + config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" # Add more conditions for other package managers as needed From 225b015253601c542dd3c83f9b7643103fb9a7f2 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 13 Jun 2024 20:43:06 -0400 Subject: [PATCH 008/253] Fix #1. --- utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils.py b/utils.py index 0e39e0b2..20eb05a5 100644 --- a/utils.py +++ b/utils.py @@ -380,14 +380,14 @@ def get_package_manager(): # IDEA: Switch to Python APT library? # See https://github.com/FaithLife-Community/LogosLinuxInstaller/pull/33#discussion_r1443623996 # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l | grep -E '^.i '" - config.PACKAGES = "coreutils patch lsof wget findutils sed grep gawk winbind cabextract x11-apps bc binutils fuse3" + config.PACKAGES = "coreutils patch lsof wget findutils sed grep gawk winbind cabextract x11-apps binutils fuse3" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora config.PACKAGE_MANAGER_COMMAND_INSTALL = "dnf install -y" config.PACKAGE_MANAGER_COMMAND_REMOVE = "dnf remove -y" config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed | grep -E ^" - config.PACKAGES = "patch mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 + config.PACKAGES = "patch mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appiamgelauncher" elif shutil.which('pamac') is not None: # manjaro From 0106998cd32b0ed3593ccfa7f16f599ea8771ed0 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 30 May 2024 09:06:59 -0400 Subject: [PATCH 009/253] Fix #102. --- tui_app.py | 4 +++ utils.py | 103 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 63 insertions(+), 44 deletions(-) diff --git a/tui_app.py b/tui_app.py index 5bd03377..a2e20119 100644 --- a/tui_app.py +++ b/tui_app.py @@ -31,6 +31,10 @@ def set_appimage(): config.SELECTED_APPIMAGE_FILENAME = appimage_filename utils.set_appimage_symlink() +# TODO: Fix hitting cancel in Dialog Screens; currently crashes program. +# TODO: Add a checklist page for deps check. Python Dialog Mixed Gauge? +# TODO: Add a waiting page for long functions? + def control_panel_app(): # Run TUI. diff --git a/utils.py b/utils.py index 20eb05a5..df118819 100644 --- a/utils.py +++ b/utils.py @@ -227,6 +227,11 @@ def die(message): sys.exit(1) +def run_command(command): + result = subprocess.run(command, check=True, text=True, capture_output=True) + return result.stdout + + def reboot(): logging.info("Rebooting system.") command = f"{config.SUPERUSER_COMMAND} reboot now" @@ -466,7 +471,7 @@ def install_packages(packages): if packages: command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {' '.join(packages)}" # noqa: E501 logging.debug(f"install_packages cmd: {command}") - subprocess.run(command, shell=True, check=True) + result = run_command(command) def remove_packages(packages): @@ -476,7 +481,7 @@ def remove_packages(packages): if packages: command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {' '.join(packages)}" # noqa: E501 logging.debug(f"remove_packages cmd: {command}") - subprocess.run(command, shell=True, check=True) + result = run_command(command) def have_dep(cmd): @@ -623,53 +628,65 @@ def delete_symlink(symlink_path): logging.error(f"Error removing symlink: {e}") -def steam_preinstall_dependencies(): - subprocess.run( - [config.SUPERUSER_COMMAND, "steamos-readonly", "disable"], - check=True - ) - subprocess.run( - [config.SUPERUSER_COMMAND, "pacman-key", "--init"], - check=True - ) - subprocess.run( - [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"], - check=True - ) - - -def steam_postinstall_dependencies(): - subprocess.run( - [ +def preinstall_dependencies_ubuntu(): + try: + run_command(["sudo", "dpkg", "--add-architecture", "i386"]) + run_command(["sudo", "mkdir", "-pm755", "/etc/apt/keyrings"]) + run_command( + ["sudo", "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", "https://dl.winehq.org/wine-builds/winehq.key"]) + lsboutput = run_command(["lsb_release", "-a"]) + codename = [line for line in lsboutput.split('\n') if "Description" in line][0].split()[1].strip() + run_command(["sudo", "wget", "-NP", "/etc/apt/sources.list.d/", + f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources"]) + run_command(["sudo", "apt", "update"]) + run_command(["sudo", "apt", "install", "--install-recommends", "winehq-staging"]) + except subprocess.CalledProcessError as e: + print(f"An error occurred: {e}") + print(f"Command output: {e.output}") + +def preinstall_dependencies_steamos(): + command = [config.SUPERUSER_COMMAND, "steamos-readonly", "disable"] + result = run_command(command) + command = [config.SUPERUSER_COMMAND, "pacman-key", "--init"] + result = run_command(command) + command = [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"] + result = run_command(command) + + +def postinstall_dependencies_steamos(): + command =[ config.SUPERUSER_COMMAND, "sed", '-i', 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 '/etc/nsswitch.conf' - ], - check=True - ) - subprocess.run( - [config.SUPERUSER_COMMAND, "locale-gen"], - check=True - ) - subprocess.run( - [ + ] + result = run_command(command) + command =[config.SUPERUSER_COMMAND, "locale-gen"] + result = run_command(command) + command =[ config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "avahi-daemon" - ], - check=True - ) - subprocess.run( - [config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"], - check=True - ) - subprocess.run( - [config.SUPERUSER_COMMAND, "steamos-readonly", "enable"], - check=True - ) + ] + result = run_command(command) + command =[config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"] + result = run_command(command) + command = [config.SUPERUSER_COMMAND, "steamos-readonly", "enable"] + result = run_command(command) + + +def preinstall_dependencies(): + if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mintn": + preinstall_dependencies_ubuntu() + elif config.OS_NAME == "Steam": + preinstall_dependencies_steamos() + + +def postinstall_dependencies(): + if config.OS_NAME == "Steam": + postinstall_dependencies_steamos() def install_dependencies(packages, badpackages, logos9_packages=None): @@ -706,8 +723,7 @@ def install_dependencies(packages, badpackages, logos9_packages=None): # Do we need a TK continue question? I see we have a CLI and curses one # in msg.py - if config.OS_NAME == "Steam": - steam_preinstall_dependencies() + preinstall_dependencies() # libfuse: for AppImage use. This is the only known needed library. check_libs(["libfuse"]) @@ -720,8 +736,7 @@ def install_dependencies(packages, badpackages, logos9_packages=None): remove_packages(conflicting_packages) config.REBOOT_REQUIRED = True - if config.OS_NAME == "Steam": - steam_postinstall_dependencies() + postinstall_dependencies() if config.REBOOT_REQUIRED: # TODO: Add resumable install functionality to speed up running the From 5682465906c8a93b005e5d3f288cc9fb2dba7cec Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 15 Jun 2024 08:07:01 +0100 Subject: [PATCH 010/253] return raw bytes with net_get; decode later --- utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils.py b/utils.py index df118819..d61fd694 100644 --- a/utils.py +++ b/utils.py @@ -836,16 +836,16 @@ def get_logos_releases(app=None): # NOTE: This assumes that Verbum release numbers continue to mirror Logos. url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 - response_xml = net_get(url) + response_xml_bytes = net_get(url) # if response_xml is None and None not in [q, app]: - if response_xml is None: + if response_xml_bytes is None: if app: app.releases_q.put(None) app.root.event_generate(app.release_evt) return None # Parse XML - root = ET.fromstring(response_xml) + root = ET.fromstring(response_xml_bytes.decode('utf-8-sig')) # Define namespaces namespaces = { @@ -1094,7 +1094,7 @@ def net_get(url, target=None, app=None, evt=None, q=None): logging.error(f"HTTP error occurred: {e.response.status_code}") # noqa: E501 return None - return r.text + return r._content # raw bytes else: # download url to target.path with requests.get(url.path, stream=True, headers=headers) as r: with target.path.open(mode=file_mode) as f: @@ -1254,7 +1254,7 @@ def get_latest_release_data(releases_url): data = net_get(releases_url) if data: try: - json_data = json.loads(data) + json_data = json.loads(data.decode()) logging.debug(f"{json_data=}") except json.JSONDecodeError as e: logging.error(f"Error decoding JSON response: {e}") From dc2ce026401af5ffaa1517c73923f06c8071d5a4 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 15 Jun 2024 20:08:48 -0400 Subject: [PATCH 011/253] Update version --- CHANGELOG.md | 6 ++++++ config.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d1ed8e..71cfe075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +- 4.0.0-alpha.8 + - Fix #1 [T. H. Wright, N. Marti, T. Bleher, C. Reeder] + - Fix #102 [T. H. Wright] + - Fix #110 [N. Marti] +- 4.0.0-alpha.7 + - Various fixes [N. Marti] - 4.0.0-alpha.6 - Hotfix to get correct LOGOS_EXE value after installation [N. Marti] - 4.0.0-alpha.5 diff --git a/config.py b/config.py index 95b12657..baacb479 100644 --- a/config.py +++ b/config.py @@ -55,7 +55,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.6" +LLI_CURRENT_VERSION = "4.0.0-alpha.8" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING From 3e2d85b89de2793a0995d103040135187a5ab2b2 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 26 Apr 2024 13:10:17 -0400 Subject: [PATCH 012/253] Convert TUI to a full UI. - Refactor for TUI as class. Fix #42. - Add a Curses console and waiting screen. - Add PythonDialog as alternative to Curses - Check if user has an up-to-date `dialog` package. - Replicates Curses functionality, except Menu + Console - Adds a file picker - Adds a package install screen - Implement threading for TUI - Add download_packages(). Rework code for individual package testing. - Add pythondialog.buildlist - Add pythondialog.checklist - Fix #104 - Various code fixes --- LogosLinuxInstaller.spec | 2 +- README.md | 2 +- config.py | 12 +- control.py | 21 +- gui_app.py | 3 + installer.py | 246 ++++------- LogosLinuxInstaller.py => main.py | 21 +- msg.py | 99 ++++- requirements.txt | 1 + tui.py | 179 -------- tui_app.py | 671 +++++++++++++++++++++++++----- tui_curses.py | 258 ++++++++++++ tui_dialog.py | 127 ++++++ tui_screen.py | 376 +++++++++++++++++ utils.py | 396 ++++++++++++++---- wine.py | 12 +- 16 files changed, 1853 insertions(+), 573 deletions(-) rename LogosLinuxInstaller.py => main.py (93%) delete mode 100644 tui.py create mode 100644 tui_curses.py create mode 100644 tui_dialog.py create mode 100644 tui_screen.py diff --git a/LogosLinuxInstaller.spec b/LogosLinuxInstaller.spec index e26942cd..f1d889b1 100644 --- a/LogosLinuxInstaller.spec +++ b/LogosLinuxInstaller.spec @@ -2,7 +2,7 @@ a = Analysis( - ['LogosLinuxInstaller.py'], + ['main.py'], pathex=[], #binaries=[('/usr/bin/tclsh8.6', '.')], binaries=[], diff --git a/README.md b/README.md index 84b7d8c0..e463054f 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ LogosLinuxInstaller$ source env/bin/activate # activate the env Python 3.12.1 (env) LogosLinuxInstaller$ python -m tkinter # verify that tkinter test window opens (env) LogosLinuxInstaller$ pip install -r requirements.txt # install python packages -(env) LogosLinuxInstaller$ ./LogosLinuxInstaller.py --help # run the script +(env) LogosLinuxInstaller$ ./main.py --help # run the script ``` ## Install Guide diff --git a/config.py b/config.py index baacb479..a99edac1 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,7 @@ # Define and set variables that are required in the config file. core_config_keys = [ - "FLPRODUCT", "TARGETVERSION", "LOGOS_RELEASE_VERSION", + "FLPRODUCT", "FLPRODUCTi", "TARGETVERSION", "LOGOS_RELEASE_VERSION", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION" @@ -17,6 +17,7 @@ # Define and set additional variables that can be set in the env. extended_config = { 'APPIMAGE_LINK_SELECTION_NAME': 'selected_wine.AppImage', + 'APPDIR_BINDIR': None, 'CHECK_UPDATES': False, 'CONFIG_FILE': None, 'CUSTOMBINPATH': None, @@ -35,9 +36,11 @@ 'SKIP_FONTS': False, 'VERBOSE': False, 'WINE_EXE': None, + 'WINEBIN_CODE': None, 'WINEDEBUG': "fixme-all,err-all", 'WINEDLLOVERRIDES': '', 'WINEPREFIX': None, + 'WINE_EXE': None, 'WINESERVER_EXE': None, 'WINETRICKS_UNATTENDED': None, } @@ -82,6 +85,7 @@ PACKAGES = None PASSIVE = None PRESENT_WORKING_DIRECTORY = os.getcwd() +QUERY_PREFIX = None REBOOT_REQUIRED = None RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME = None RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION = None @@ -93,6 +97,12 @@ WINETRICKS_URL = "https://raw.githubusercontent.com/Winetricks/winetricks/5904ee355e37dff4a3ab37e1573c56cffe6ce223/src/winetricks" # noqa: E501 WINETRICKS_VERSION = '20220411' WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") +install_finished = False +current_option = 0 +current_page = 0 +total_pages = 0 +options_per_page = 8 +use_python_dialog = True def get_config_file_dict(config_file_path): diff --git a/control.py b/control.py index e98a53f0..555034e8 100644 --- a/control.py +++ b/control.py @@ -17,7 +17,8 @@ import config # import installer import msg -import tui +import tui_curses +import tui_app import utils # import wine @@ -70,7 +71,7 @@ def backup_and_restore(mode='backup', app=None): answer = msg.cli_ask_filepath("Give backups folder path:") answer = Path(answer).expanduser().resolve() if not answer.is_dir(): - msg.cli_msg(f"Not a valid folder path: {answer}") + msg.logos_msg(f"Not a valid folder path: {answer}") config.BACKUPDIR = answer # Set source folders. @@ -109,7 +110,7 @@ def backup_and_restore(mode='backup', app=None): app.status_q.put(m) app.root.event_generate('<>') app.root.event_generate('<>') - msg.cli_msg(m, end='') + msg.logos_msg(m, end='') t.start() try: while t.is_alive(): @@ -184,7 +185,7 @@ def backup_and_restore(mode='backup', app=None): else: m = f"Backing up to {str(dst_dir)}" logging.info(m) - msg.cli_msg(m) + msg.logos_msg(m) if app is not None: app.status_q.put(m) app.root.event_generate('<>') @@ -248,7 +249,7 @@ def remove_all_index_files(app=None): except OSError as e: logging.error(f"Error removing {file_to_remove}: {e}") - msg.cli_msg("======= Removing all LogosBible index files done! =======") + msg.logos_msg("======= Removing all LogosBible index files done! =======") if app is not None: app.root.event_generate(app.message_event) sys.exit(0) @@ -266,7 +267,7 @@ def remove_library_catalog(): def set_winetricks(): - msg.cli_msg("Preparing winetricks…") + msg.logos_msg("Preparing winetricks…") if not config.APPDIR_BINDIR: config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" # Check if local winetricks version available; else, download it @@ -304,10 +305,10 @@ def set_winetricks(): ) return 0 else: - msg.cli_msg("Installation canceled!") + msg.logos_msg("Installation canceled!") sys.exit(0) else: - msg.cli_msg("The system's winetricks is too old. Downloading an up-to-date winetricks from the Internet...") # noqa: E501 + msg.logos_msg("The system's winetricks is too old. Downloading an up-to-date winetricks from the Internet...") # noqa: E501 # download_winetricks() utils.install_winetricks(config.APPDIR_BINDIR) config.WINETRICKSBIN = os.path.join( @@ -316,7 +317,7 @@ def set_winetricks(): ) return 0 else: - msg.cli_msg("Local winetricks not found. Downloading winetricks from the Internet…") # noqa: E501 + msg.logos_msg("Local winetricks not found. Downloading winetricks from the Internet…") # noqa: E501 # download_winetricks() utils.install_winetricks(config.APPDIR_BINDIR) config.WINETRICKSBIN = os.path.join( @@ -328,7 +329,7 @@ def set_winetricks(): def download_winetricks(): - msg.cli_msg("Downloading winetricks…") + msg.logos_msg("Downloading winetricks…") appdir_bindir = f"{config.INSTALLDIR}/data/bin" utils.logos_reuse_download( config.WINETRICKS_URL, diff --git a/gui_app.py b/gui_app.py index 6c518604..e1efaed0 100644 --- a/gui_app.py +++ b/gui_app.py @@ -287,6 +287,9 @@ def todo(self, evt=None, task=None): self.set_input_widgets_state('disabled') elif task == 'DONE': self.update_install_progress() + elif task == 'CONFIG': + logging.info("Updating config file.") + utils.write_config(config.CONFIG_FILE) def set_product(self, evt=None): if self.gui.productvar.get()[0] == 'C': # ignore default text diff --git a/installer.py b/installer.py index 07f8222e..b03cdd12 100644 --- a/installer.py +++ b/installer.py @@ -1,13 +1,11 @@ import logging import os -import re import shutil import sys from pathlib import Path import config import msg -import tui import utils import wine @@ -20,26 +18,11 @@ def ensure_product_choice(app=None): logging.debug('- config.VERBUM_PATH') if not config.FLPRODUCT: - logging.debug('FLPRODUCT not set.') - if config.DIALOG == 'tk' and app: - send_gui_task(app, 'FLPRODUCT') + if app: + utils.send_task(app, 'FLPRODUCT') + if config.DIALOG == 'curses': + app.product_e.wait() config.FLPRODUCT = app.product_q.get() - else: - TITLE = "Choose Product" - QUESTION_TEXT = "Choose which FaithLife product the script should install:" # noqa: E501 - options = ["Logos", "Verbum", "Exit"] - product_choice = tui.menu(options, TITLE, QUESTION_TEXT) - logging.info(f"Product: {str(product_choice)}") - if str(product_choice).startswith("Logos"): - logging.info("Installing Logos Bible Software") - config.FLPRODUCT = "Logos" - elif str(product_choice).startswith("Verbum"): - logging.info("Installing Verbum Bible Software") - config.FLPRODUCT = "Verbum" - elif str(product_choice).startswith("Exit"): - msg.logos_error("Exiting installation.", "") - else: - msg.logos_error("Unknown product. Installation canceled!", "") if config.FLPRODUCT == 'Logos': config.FLPRODUCTi = 'logos4' @@ -59,25 +42,12 @@ def ensure_version_choice(app=None): config.INSTALL_STEP += 1 update_install_feedback("Choose version…", app=app) logging.debug('- config.TARGETVERSION') - if not config.TARGETVERSION: - if config.DIALOG == 'tk' and app: - send_gui_task(app, 'TARGETVERSION') + if app: + utils.send_task(app, 'TARGETVERSION') + if config.DIALOG == 'curses': + app.version_e.wait() config.TARGETVERSION = app.version_q.get() - else: - TITLE = "Choose Product Version" - QUESTION_TEXT = f"Which version of {config.FLPRODUCT} should the script install?" # noqa: E501 - options = ["10", "9", "Exit"] - version_choice = tui.menu(options, TITLE, QUESTION_TEXT) - logging.info(f"Target version: {version_choice}") - if "10" in version_choice: - config.TARGETVERSION = "10" - elif "9" in version_choice: - config.TARGETVERSION = "9" - elif version_choice == "Exit.": - msg.logos_error("Exiting installation.", "") - else: - msg.logos_error("Unknown version. Installation canceled!", "") logging.debug(f"> {config.TARGETVERSION=}") @@ -89,25 +59,12 @@ def ensure_release_choice(app=None): logging.debug('- config.LOGOS_RELEASE_VERSION') if not config.LOGOS_RELEASE_VERSION: - if config.DIALOG == 'tk' and app: - send_gui_task(app, 'LOGOS_RELEASE_VERSION') + if app: + utils.send_task(app, 'LOGOS_RELEASE_VERSION') + if config.DIALOG == 'curses': + app.release_e.wait() config.LOGOS_RELEASE_VERSION = app.release_q.get() logging.debug(f"{config.LOGOS_RELEASE_VERSION=}") - else: - TITLE = f"Choose {config.FLPRODUCT} {config.TARGETVERSION} Release" - QUESTION_TEXT = f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?" # noqa: E501 - releases = utils.get_logos_releases() - if releases is None: - msg.logos_error("Failed to fetch LOGOS_RELEASE_VERSION.") - releases.append("Exit") - logos_release_version = tui.menu(releases, TITLE, QUESTION_TEXT) - logging.info(f"Release version: {logos_release_version}") - if logos_release_version == "Exit": - msg.logos_error("Exiting installation.", "") - elif logos_release_version: - config.LOGOS_RELEASE_VERSION = logos_release_version - else: - msg.logos_error("Failed to fetch LOGOS_RELEASE_VERSION.") logging.debug(f"> {config.LOGOS_RELEASE_VERSION=}") @@ -121,22 +78,20 @@ def ensure_install_dir_choice(app=None): ) logging.debug('- config.INSTALLDIR') + default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 if not config.INSTALLDIR: - default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - if config.DIALOG == 'tk' and app: - config.INSTALLDIR = default - else: - # TITLE = "Choose Installation Folder" - QUESTION_TEXT = f"Where should {config.FLPRODUCT} files be instaled to? [{default}]: " # noqa: E501 - installdir = input(f"{QUESTION_TEXT} ") - if not installdir: - msg.cli_msg("Using default location.") - installdir = default - config.INSTALLDIR = installdir - # Ensure APPDIR_BINDIR is set. - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + if app: + if config.DIALOG == 'tk': + config.INSTALLDIR = default + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + elif config.DIALOG == 'curses': + utils.send_task(app, 'INSTALLDIR') + app.installdir_e.wait() + config.INSTALLDIR = app.installdir_q.get() + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" logging.debug(f"> {config.INSTALLDIR=}") + logging.debug(f"> {config.APPDIR_BINDIR=}") def ensure_wine_choice(app=None): @@ -152,27 +107,15 @@ def ensure_wine_choice(app=None): logging.debug('- config.WINEBIN_CODE') if config.WINE_EXE is None: - # Set relevant config based on up-to-date details from URL. utils.set_recommended_appimage_config() - if config.DIALOG == 'tk' and app: - send_gui_task(app, 'WINE_EXE') + if app: + utils.send_task(app, 'WINE_EXE') + if config.DIALOG == 'curses': + app.wine_e.wait() config.WINE_EXE = app.wine_q.get() - else: - logging.info("Creating binary list.") - TITLE = "Choose Wine Binary" - QUESTION_TEXT = f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.LOGOS_RELEASE_VERSION} in {config.INSTALLDIR}?" # noqa: E501 - WINEBIN_OPTIONS = utils.get_wine_options( - utils.find_appimage_files(), - utils.find_wine_binary_files() - ) - - installation_choice = tui.menu(WINEBIN_OPTIONS, TITLE, QUESTION_TEXT) # noqa: E501 - config.WINEBIN_CODE = installation_choice[0] - config.WINE_EXE = installation_choice[1] - if config.WINEBIN_CODE == "Exit": - msg.logos_error("Exiting installation.", "") # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. + logging.debug(f"Preparing to process WINE_EXE. Currently set to: {config.WINE_EXE}.") if config.WINE_EXE.lower().endswith('.appimage'): config.SELECTED_APPIMAGE_FILENAME = config.WINE_EXE if not config.WINEBIN_CODE: @@ -193,34 +136,17 @@ def ensure_winetricks_choice(app=None): update_install_feedback("Choose winetricks binary…", app=app) logging.debug('- config.WINETRICKSBIN') - # Check if local winetricks version available; else, download it. if config.WINETRICKSBIN is None: + # Check if local winetricks version available; else, download it. config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" - if config.DIALOG == 'tk': - send_gui_task(app, 'WINETRICKSBIN') - winetricksbin = app.tricksbin_q.get() - if not winetricksbin.startswith('Download'): - config.WINETRICKSBIN = winetricksbin - else: - winetricks_options = utils.get_winetricks_options() - if len(winetricks_options) > 1: - title = "Choose Winetricks" - question_text = "Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that FLPRODUCT requires on Linux." # noqa: E501 - - options = [ - "1: Use local winetricks.", - "2: Download winetricks from the Internet" - ] - winetricks_choice = tui.menu(options, title, question_text) - - logging.debug(f"winetricks_choice: {winetricks_choice}") - if winetricks_choice.startswith("1"): - logging.info("Setting winetricks to the local binary…") - config.WINETRICKSBIN = winetricks_options[0] - elif not winetricks_choice.startswith("2"): - msg.logos_error("Installation canceled!") - else: - msg.cli_msg("Winetricks will be downloaded from the Internet.") + + utils.send_task(app, 'WINETRICKSBIN') + if config.DIALOG == 'curses': + app.tricksbin_e.wait() + winetricksbin = app.tricksbin_q.get() + if not winetricksbin.startswith('Download'): + config.WINETRICKSBIN = winetricksbin + logging.debug(f"> {config.WINETRICKSBIN=}") @@ -274,8 +200,8 @@ def ensure_installation_config(app=None): logging.debug(f"> {config.LOGOS64_MSI=}") logging.debug(f"> {config.LOGOS64_URL=}") - if config.DIALOG == 'tk' and app: - send_gui_task(app, 'INSTALL') + if app: + utils.send_task(app, 'INSTALL') def ensure_install_dirs(app=None): @@ -287,24 +213,28 @@ def ensure_install_dirs(app=None): logging.debug('- config.WINEPREFIX') logging.debug('- data/bin') logging.debug('- data/wine64_bottle') + wine_dir = Path("") if config.INSTALLDIR is None: config.INSTALLDIR = f"{os.getenv('HOME')}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - logging.debug(f"> {config.INSTALLDIR=}") - if config.WINEPREFIX is None: - config.WINEPREFIX = f"{config.INSTALLDIR}/data/wine64_bottle" - logging.debug(f"> {config.WINEPREFIX=}") + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" #noqa: E501 bin_dir = Path(config.APPDIR_BINDIR) bin_dir.mkdir(parents=True, exist_ok=True) logging.debug(f"> {bin_dir} exists: {bin_dir.is_dir()}") + logging.debug(f"> {config.INSTALLDIR=}") + logging.debug(f"> {config.APPDIR_BINDIR=}") + + config.WINEPREFIX = f"{config.INSTALLDIR}/data/wine64_bottle" wine_dir = Path(f"{config.WINEPREFIX}") wine_dir.mkdir(parents=True, exist_ok=True) + logging.debug(f"> {wine_dir} exists: {wine_dir.is_dir()}") + logging.debug(f"> {config.WINEPREFIX=}") if config.DIALOG == 'tk' and app: - send_gui_task(app, 'INSTALLING') + utils.send_task(app, 'INSTALLING') def ensure_sys_deps(app=None): @@ -314,7 +244,7 @@ def ensure_sys_deps(app=None): update_install_feedback("Ensuring system dependencies are met…", app=app) if not config.SKIP_DEPENDENCIES: - utils.check_dependencies() + utils.check_dependencies(app) logging.debug("> Done.") else: logging.debug("> Skipped.") @@ -331,6 +261,7 @@ def ensure_appimage_download(app=None): app=app ) + downloaded_file = None filename = Path(config.SELECTED_APPIMAGE_FILENAME).name downloaded_file = utils.get_downloaded_file_path(filename) if not downloaded_file: @@ -341,7 +272,8 @@ def ensure_appimage_download(app=None): config.MYDOWNLOADS, app=app, ) - logging.debug(f"> File exists?: {downloaded_file}: {Path(downloaded_file).is_file()}") # noqa: E501 + if downloaded_file: + logging.debug(f"> File exists?: {downloaded_file}: {Path(downloaded_file).is_file()}") # noqa: E501 def ensure_wine_executables(app=None): @@ -358,10 +290,9 @@ def ensure_wine_executables(app=None): logging.debug('- wineserver') # Add APPDIR_BINDIR to PATH. - appdir_bindir = Path(config.APPDIR_BINDIR) - os.environ['PATH'] = f"{config.APPDIR_BINDIR}:{os.getenv('PATH')}" - if not os.access(config.WINE_EXE, os.X_OK): + appdir_bindir = Path(config.APPDIR_BINDIR) + os.environ['PATH'] = f"{config.APPDIR_BINDIR}:{os.getenv('PATH')}" # Ensure AppImage symlink. appimage_link = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME appimage_file = Path(config.SELECTED_APPIMAGE_FILENAME) @@ -370,14 +301,14 @@ def ensure_wine_executables(app=None): # Ensure appimage is copied to appdir_bindir. downloaded_file = utils.get_downloaded_file_path(appimage_filename) # noqa: E501 if not appimage_file.is_file(): - msg.cli_msg(f"Copying: {downloaded_file} into: {str(appdir_bindir)}") # noqa: E501 + msg.logos_msg(f"Copying: {downloaded_file} into: {str(appdir_bindir)}") # noqa: E501 shutil.copy(downloaded_file, str(appdir_bindir)) os.chmod(appimage_file, 0o755) appimage_filename = appimage_file.name elif config.WINEBIN_CODE in ["System", "Proton", "PlayOnLinux", "Custom"]: # noqa: E501 appimage_filename = "none.AppImage" else: - msg.logos_error("WINEBIN_CODE error. Installation canceled!") + msg.logos_error(f"WINEBIN_CODE error. WINEBIN_CODE is {config.WINEBIN_CODE}. Installation canceled!") appimage_link.unlink(missing_ok=True) # remove & replace appimage_link.symlink_to(f"./{appimage_filename}") @@ -410,13 +341,14 @@ def ensure_winetricks_executable(app=None): tricksbin = Path(config.WINETRICKSBIN) tricksbin.unlink(missing_ok=True) # The choice of System winetricks was made previously. Here we are only - # concerned about whether or not the downloaded winetricks is usable. - msg.cli_msg("Downloading winetricks from the Internet…") + # concerned about whether the downloaded winetricks is usable. + msg.logos_msg("Downloading winetricks from the Internet…") utils.install_winetricks( tricksbin.parent, app=app ) - logging.debug(f"> {config.WINETRICKSBIN} is executable?: {os.access(config.WINETRICKSBIN, os.X_OK)}") # noqa: E501 + if config.WINETRICKSBIN: + logging.debug(f"> {config.WINETRICKSBIN} is executable?: {os.access(config.WINETRICKSBIN, os.X_OK)}") # noqa: E501 return 0 @@ -492,7 +424,8 @@ def ensure_wineprefix_init(app=None): f"{config.INSTALLDIR}/data", ) else: - wine.initializeWineBottle() + if config.WINE_EXE: + wine.initializeWineBottle() logging.debug(f"> {init_file} exists?: {init_file.is_file()}") @@ -509,9 +442,13 @@ def ensure_winetricks_applied(app=None): logging.debug('- settings fontsmooth=rgb') logging.debug('- d3dcompiler_47') + usr_reg = None + sys_reg = None + workdir = Path(f"{config.WORKDIR}") + workdir.mkdir(parents=True, exist_ok=True) usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") - if not grep(r'"winemenubuilder.exe"=""', usr_reg): + if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') with open(reg_file, 'w') as f: f.write(r'''REGEDIT4 @@ -521,23 +458,23 @@ def ensure_winetricks_applied(app=None): ''') wine.wine_reg_install(reg_file) - if not grep(r'"renderer"="gdi"', usr_reg): + if not utils.grep(r'"renderer"="gdi"', usr_reg): wine.winetricks_install("-q", "settings", "renderer=gdi") - if not config.SKIP_FONTS and not grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 + if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 wine.installFonts() - if not grep(r'"\*d3dcompiler_47"="native"', usr_reg): + if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): wine.installD3DCompiler() - if not grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): + if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): args = ["settings", "win10"] if not config.WINETRICKS_UNATTENDED: args.insert(0, "-q") wine.winetricks_install(*args) if config.TARGETVERSION == '9': - msg.cli_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") + msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") exe_args = [ 'add', f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 @@ -558,8 +495,9 @@ def ensure_product_installed(app=None): if not utils.find_installed_product(): wine.install_msi() config.LOGOS_EXE = utils.find_installed_product() - if config.DIALOG == 'tk' and app: - send_gui_task(app, 'DONE') + utils.send_task(app, 'DONE') + if config.DIALOG == 'curses': + app.finished_e.wait() # Clean up temp files, etc. utils.clean_all() @@ -593,15 +531,23 @@ def ensure_config_file(app=None): different = True break if different: - if config.DIALOG == 'tk' and app: - logging.info("Updating config file.") - utils.write_config(config.CONFIG_FILE) + if app: + utils.send_task(app, 'CONFIG') + if config.DIALOG == 'curses': + app.config_e.wait() + choice = app.config_q.get() + if choice == "Yes": + logging.info("Updating config file.") + utils.write_config(config.CONFIG_FILE) + else: + logging.info("Config file left unchanged.") elif msg.logos_acknowledge_question( f"Update config file at {config.CONFIG_FILE}?", "The existing config file was not overwritten." ): logging.info("Updating config file.") utils.write_config(config.CONFIG_FILE) + utils.send_task(app, "TUI-UPDATE-MENU") logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 @@ -683,26 +629,6 @@ def update_install_feedback(text, app=None): msg.progress(percent, app=app) -def send_gui_task(app, task): - logging.debug(f"{task=}") - app.todo_q.put(task) - app.root.event_generate('<>') - - -def grep(regexp, filepath): - fp = Path(filepath) - found = False - ct = 0 - with fp.open() as f: - for line in f: - ct += 1 - text = line.rstrip() - if re.search(regexp, text): - logging.debug(f"{filepath}:{ct}:{text}") - found = True - return found - - def get_progress_pct(current, total): return round(current * 100 / total) diff --git a/LogosLinuxInstaller.py b/main.py similarity index 93% rename from LogosLinuxInstaller.py rename to main.py index 211cd299..565953a2 100755 --- a/LogosLinuxInstaller.py +++ b/main.py @@ -2,6 +2,7 @@ import logging import os import argparse +import curses import config import control @@ -219,7 +220,6 @@ def parse_args(args, parser): 'update_self': utils.update_to_latest_lli_release, 'update_latest_appimage': utils.update_to_latest_recommended_appimage, 'set_appimage': utils.set_appimage_symlink, - # 'get_winetricks': control.get_winetricks, 'get_winetricks': control.set_winetricks, 'run_winetricks': wine.run_winetricks, 'toggle_app_logging': wine.switch_logging, @@ -256,7 +256,12 @@ def run_control_panel(): if config.DIALOG is None or config.DIALOG == 'tk': gui_app.control_panel_app() else: - tui_app.control_panel_app() + try: + curses.wrapper(tui_app.control_panel_app) + except curses.error as e: + logging.error(f"Curses error in run_control_panel(): {e}") + except Exception as e: + logging.error(f"An error occurred in run_control_panel(): {e}") def main(): @@ -305,6 +310,18 @@ def main(): if config.DIALOG == 'tk': config.GUI = True + if config.DIALOG == 'curses': + config.use_python_dialog = utils.test_dialog_version() + + if config.use_python_dialog is None: + logging.debug("The 'dialog' package was not found.") + elif config.use_python_dialog: + logging.debug("Dialog version is up-to-date.") + config.use_python_dialog = True + else: + logging.error("Dialog version is outdated. Please use the GUI.") + config.use_python_dialog = False + # Log persistent config. utils.log_current_persistent_config() diff --git a/msg.py b/msg.py index f6decd40..68d064dc 100644 --- a/msg.py +++ b/msg.py @@ -2,11 +2,26 @@ import os import signal import sys +import time from pathlib import Path import config +logging.console_log = [] + +class CursesHandler(logging.Handler): + def __init__(self, screen): + logging.Handler.__init__(self) + self.screen = screen + def emit(self, record): + try: + msg = self.format(record) + screen = self.screen + status(msg) + screen.refresh() + except: + raise def get_log_level_name(level): name = None @@ -63,6 +78,40 @@ def initialize_logging(stderr_log_level): ) +def initialize_curses_logging(stdscr): + ''' + Log levels: + Level Value Description + CRITICAL 50 the program can't continue + ERROR 40 the program has not been able to do something + WARNING 30 something unexpected happened (maybe neg. effect) + INFO 20 confirmation that things are working as expected + DEBUG 10 detailed, dev-level information + NOTSET 0 all events are handled + ''' + + # Ensure log file parent folders exist. + log_parent = Path(config.LOGOS_LOG).parent + if not log_parent.is_dir(): + log_parent.mkdir(parents=True) + + # Define logging handlers. + file_h = logging.FileHandler(config.LOGOS_LOG, encoding='UTF8') + file_h.setLevel(logging.DEBUG) + curses_h = CursesHandler(stdscr) + handlers = [ + curses_h, + ] + + # Set initial config. + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + handlers=handlers, + ) + + def update_log_level(new_level): # Update logging level from config. for h in logging.getLogger().handlers: @@ -76,9 +125,19 @@ def cli_msg(message, end='\n'): print(message, end=end) +def logos_msg(message, end='\n'): + if config.DIALOG == 'curses': + logging.debug(message) + else: + cli_msg(message, end) + + def logos_progress(): - sys.stdout.write('.') - sys.stdout.flush() + if config.DIALOG == 'curses': + pass + else: + sys.stdout.write('.') + sys.stdout.flush() # i = 0 # spinner = "|/-\\" # sys.stdout.write(f"\r{text} {spinner[i]}") @@ -89,7 +148,9 @@ def logos_progress(): def logos_warn(message): if config.DIALOG == 'curses': - cli_msg(message) + logging.warning(message) + else: + logos_msg(message) def logos_error(message, secondary=None): @@ -97,11 +158,15 @@ def logos_error(message, secondary=None): TELEGRAM_LINK = "https://t.me/linux_logos" MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 - if secondary != "info": + if config.DIALOG == 'curses' and secondary != "info": + logging.critical(message) + status(message) + status(help_message) + elif secondary != "info": logging.critical(message) - cli_msg(help_message) + logos_msg(message) else: - cli_msg(message) + logos_msg(message) if secondary is None or secondary == "": try: @@ -125,7 +190,7 @@ def cli_question(QUESTION_TEXT): elif yn.lower() == 'n': return False else: - cli_msg("Type Y[es] or N[o].") + logos_msg("Type Y[es] or N[o].") def cli_continue_question(QUESTION_TEXT, NO_TEXT, SECONDARY): @@ -135,7 +200,7 @@ def cli_continue_question(QUESTION_TEXT, NO_TEXT, SECONDARY): def cli_acknowledge_question(QUESTION_TEXT, NO_TEXT): if not cli_question(QUESTION_TEXT): - cli_msg(NO_TEXT) + logos_msg(NO_TEXT) return False else: return True @@ -152,11 +217,15 @@ def cli_ask_filepath(question_text): def logos_continue_question(QUESTION_TEXT, NO_TEXT, SECONDARY): if config.DIALOG == 'curses': + pass + else: cli_continue_question(QUESTION_TEXT, NO_TEXT, SECONDARY) def logos_acknowledge_question(QUESTION_TEXT, NO_TEXT): if config.DIALOG == 'curses': + pass + else: return cli_acknowledge_question(QUESTION_TEXT, NO_TEXT) @@ -173,15 +242,23 @@ def progress(percent, app=None): if config.DIALOG == 'tk' and app: app.progress_q.put(percent) app.root.event_generate('<>') + elif config.DIALOG == 'curses': + status(f"Progress: {get_progress_str(percent)}", app) else: - cli_msg(get_progress_str(percent)) # provisional + logos_msg(get_progress_str(percent)) # provisional def status(text, app=None): + timestamp = time.strftime("%Y-%m-%dT%H:%M:%S") """Handles status messages for both TUI and GUI.""" logging.debug(f"Status: {text}") - if config.DIALOG == 'tk' and app: + if config.DIALOG == 'tk': app.status_q.put(text) app.root.event_generate('<>') + elif config.DIALOG == 'curses': + if app is not None: + app.status_q.put(f"{timestamp} {text}") + app.report_waiting(f"{app.status_q.get()}", dialog=True) else: - cli_msg(text) # provisional + '''Prints message to stdout regardless of log level.''' + logos_msg(text) diff --git a/requirements.txt b/requirements.txt index e60d46c3..ba3bd95e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,6 @@ distro==1.9.0 idna==3.6 packaging==23.2 psutil==5.9.7 +pythondialog==3.5.3 requests==2.31.0 urllib3==2.1.0 diff --git a/tui.py b/tui.py deleted file mode 100644 index 2fbcc651..00000000 --- a/tui.py +++ /dev/null @@ -1,179 +0,0 @@ -import textwrap -import curses -import logging - - -def get_user_input(question_text): - def _get_user_input(stdscr): - curses.echo() - stdscr.addstr(0, 0, question_text) - stdscr.refresh() - user_input = stdscr.getstr(1, 0, 50).decode('utf-8') - curses.noecho() - return user_input - - curses.wrapper(_get_user_input) - - -def confirm(title, question_text): - def _confirm(stdscr): - curses.curs_set(0) # Hide the cursor - - stdscr.clear() - window_height, window_width = stdscr.getmaxyx() - - # Wrap the title and question text - wrapped_title = textwrap.fill(title, window_width - 4) - wrapped_question = textwrap.fill(question_text + " [Y/n]: ", window_width - 4) - - # Display the wrapped title text, line by line, centered - title_lines = wrapped_title.split('\n') - title_start_y = max(0, window_height // 2 - len(title_lines) // 2) - for i, line in enumerate(title_lines): - if i < window_height: - stdscr.addstr(i, 2, line, curses.A_BOLD) - - # Display the wrapped question text, line by line, centered - question_lines = wrapped_question.split('\n') - question_start_y = title_start_y + len(title_lines) - 4 - question_width = max(len(line) for line in question_lines) - for i, line in enumerate(question_lines): - if question_start_y + i < window_height: - x = window_width // 2 - question_width // 2 - stdscr.addstr(question_start_y + i, x, line) - - y = question_start_y + len(question_lines) + 2 - - while True: - key = stdscr.getch() - key = chr(key) - - if key.lower() == 'y' or key == '\n': # '\n' for Enter key, defaults to "Yes" - return True - elif key.lower() == 'n': - return False - - stdscr.addstr(y, 0, "Type Y[es] or N[o]. ") - - curses.wrapper(_confirm) - - -def menu(options, title, question_text): - def _menu(stdscr): - # Set up the screen - curses.curs_set(0) - - current_option = 0 - current_page = 0 - options_per_page = 8 - total_pages = (len(options) - 1) // options_per_page + 1 - - while True: - stdscr.clear() - - window_height, window_width = stdscr.getmaxyx() - # window_y = window_height // 2 - options_per_page // 2 - # window_x = window_width // 2 - max(len(option) for option in options) // 2 - - # Wrap the title and question text - wrapped_title = textwrap.fill(title, window_width - 4) - wrapped_question = textwrap.fill(question_text, window_width - 4) - - # Display the wrapped title text, line by line, centered - title_lines = wrapped_title.split('\n') - title_start_y = max(0, window_height // 2 - len(title_lines) // 2) - for i, line in enumerate(title_lines): - if i < window_height: - stdscr.addstr(i, 2, line, curses.A_BOLD) - - # Display the wrapped question text, line by line, centered - question_lines = wrapped_question.split('\n') - question_start_y = title_start_y + len(title_lines) - 4 - question_width = max(len(line) for line in question_lines) - for i, line in enumerate(question_lines): - if question_start_y + i < window_height: - x = window_width // 2 - question_width // 2 - stdscr.addstr(question_start_y + i, x, line) - - # Display the options, centered - options_start_y = question_start_y + len(question_lines) + 2 - for i in range(options_per_page): - index = current_page * options_per_page + i - if index < len(options): - option = options[index] - if type(option) is list: - option_lines = [] - wine_binary_code = option[0] - if wine_binary_code != "Exit": - wine_binary_path = option[1] - wine_binary_description = option[2] - wine_binary_path_wrapped = textwrap.wrap( - f"Binary Path: {wine_binary_path}", window_width - 4) - option_lines.extend(wine_binary_path_wrapped) - wine_binary_desc_wrapped = textwrap.wrap( - f"Description: {wine_binary_description}", window_width - 4) - option_lines.extend(wine_binary_desc_wrapped) - else: - wine_binary_path = option[1] - wine_binary_description = option[2] - wine_binary_path_wrapped = textwrap.wrap( - f"{wine_binary_path}", window_width - 4) - option_lines.extend(wine_binary_path_wrapped) - wine_binary_desc_wrapped = textwrap.wrap( - f"{wine_binary_description}", window_width - 4) - option_lines.extend(wine_binary_desc_wrapped) - else: - option_lines = textwrap.wrap(option, window_width - 4) - - for j, line in enumerate(option_lines): - y = options_start_y + i + j - x = max(0, window_width // 2 - len(line) // 2) - if y < window_height: - if index == current_option: - stdscr.addstr(y, x, line, curses.A_REVERSE) - else: - stdscr.addstr(y, x, line) - - if type(option) is list: - options_start_y += (len(option_lines)) - - # Display pagination information - page_info = f"Page {current_page + 1}/{total_pages} | Selected Option: {current_option + 1}/{len(options)}" - stdscr.addstr(window_height - 1, 2, page_info, curses.A_BOLD) - - # Refresh the windows - stdscr.refresh() - - # Get user input - key = stdscr.getch() - - if key == 65 or key == 259: # Up arrow - if current_option == current_page * options_per_page and current_page > 0: - # Move to the previous page - current_page -= 1 - current_option = min(len(options) - 1, (current_page + 1) * options_per_page - 1) - elif current_option == 0: - if total_pages == 1: - current_option = len(options) - 1 - else: - current_page = total_pages - 1 - current_option = len(options) - 1 - else: - current_option = max(0, current_option - 1) - elif key == 66 or key == 258: # Down arrow - if current_option == (current_page + 1) * options_per_page - 1 and current_page < total_pages - 1: - # Move to the next page - current_page += 1 - current_option = min(len(options) - 1, current_page * options_per_page) - elif current_option == len(options) - 1: - current_page = 0 - current_option = 0 - else: - current_option = min(len(options) - 1, current_option + 1) - elif key == ord('\n'): # Enter key - choice = options[current_option] - break - - return choice - - return curses.wrapper(_menu) diff --git a/tui_app.py b/tui_app.py index a2e20119..14eb50c5 100644 --- a/tui_app.py +++ b/tui_app.py @@ -1,134 +1,585 @@ import logging +import os import sys +import signal +import threading +import time +import curses +from pathlib import Path +from queue import Queue import config import control -import tui +import tui_curses +import tui_dialog +import tui_screen import installer import msg import utils import wine - -def set_appimage(): - # TODO: Allow specifying the AppImage File - appimages = utils.find_appimage_files() - appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 - appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) - sai_choice = tui.menu( - appimage_choices, - "AppImage Updater", - "Which AppImage should be used?" - ) - if sai_choice == "Return to Main Menu": - pass # Do nothing. - elif sai_choice == "Input Custom AppImage": - appimage_filename = tui.get_user_input("Enter AppImage filename: ") - config.SELECTED_APPIMAGE_FILENAME = appimage_filename - utils.set_appimage_symlink() - else: - appimage_filename = sai_choice - config.SELECTED_APPIMAGE_FILENAME = appimage_filename - utils.set_appimage_symlink() +console_message = "" # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. -# TODO: Add a checklist page for deps check. Python Dialog Mixed Gauge? -# TODO: Add a waiting page for long functions? +class TUI(): + def __init__(self, stdscr): + self.stdscr = stdscr + self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" + self.console_message = "Starting TUI…" + self.running = True + self.choice = "Processing" + self.active_progress = False + + # Queues + self.main_thread = threading.Thread() + self.get_q = Queue() + self.get_e = threading.Event() + self.input_q = Queue() + self.input_e = threading.Event() + self.status_q = Queue() + self.status_e = threading.Event() + self.progress_q = Queue() + self.progress_e = threading.Event() + self.todo_q = Queue() + self.todo_e = threading.Event() + + # Install and Options + self.product_q = Queue() + self.product_e = threading.Event() + self.version_q = Queue() + self.version_e = threading.Event() + self.releases_q = Queue() + self.releases_e = threading.Event() + self.release_q = Queue() + self.release_e = threading.Event() + self.installdir_q = Queue() + self.installdir_e = threading.Event() + self.wine_q = Queue() + self.wine_e = threading.Event() + self.tricksbin_q = Queue() + self.tricksbin_e = threading.Event() + self.deps_q = Queue() + self.deps_e = threading.Event() + self.finished_q = Queue() + self.finished_e = threading.Event() + self.config_q = Queue() + self.config_e = threading.Event() + self.appimage_q = Queue() + self.appimage_e = threading.Event() + + # Window and Screen Management + self.window_height = "" + self.window_width = "" + self.update_tty_dimensions() + self.main_window_height = 9 + self.menu_window_height = 14 + self.tui_screens = [] + self.menu_options = [] + self.threads = [] + self.threads_started = [] + self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) + self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) + self.console = None + self.menu_screen = None + self.active_screen = None + + def init_curses(self): + try: + if curses.has_colors(): + curses.start_color() + curses.use_default_colors() + curses.init_color(curses.COLOR_BLUE, 0, 510, 1000) # Logos Blue + curses.init_color(curses.COLOR_CYAN, 906, 906, 906) # Logos Gray + curses.init_color(curses.COLOR_WHITE, 988, 988, 988) # Logos White + curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_CYAN) + curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE) + curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLUE) + curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) + curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_BLUE) + self.stdscr.bkgd(' ', curses.color_pair(3)) + self.main_window.bkgd(' ', curses.color_pair(3)) + self.menu_window.bkgd(' ', curses.color_pair(3)) + + curses.curs_set(0) + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + + self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title) + self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, + "Main Menu", self.set_tui_menu_options(curses=True)) + #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", + # self.set_tui_menu_options(curses=False)) + + self.main_window.noutrefresh() + self.menu_window.noutrefresh() + curses.doupdate() + except curses.error as e: + logging.error(f"Curses error in init_curses: {e}") + except Exception as e: + self.end_curses() + logging.error(f"An error occurred in init_curses(): {e}") + raise + + def end_curses(self): + try: + self.stdscr.keypad(False) + self.stdscr.clear() + self.main_window.clear() + self.menu_window.clear() + curses.nocbreak() + curses.echo() + except curses.error as e: + logging.error(f"Curses error in end_curses: {e}") + except Exception as e: + logging.error(f"An error occurred in end_curses(): {e}") + + def end(self, signal, frame): + logging.debug("Exiting…") + self.stdscr.clear() + curses.endwin() + for thread in self.threads_started: + thread.join() + + sys.exit(0) + + def resize_curses(self): + curses.endwin() + self.stdscr = curses.initscr() + curses.curs_set(0) + self.stdscr.clear() + self.stdscr.noutrefresh() + self.update_tty_dimensions() + self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) + self.console = tui_screen.ConsoleScreen(self.main_window, 0, self.status_q, self.status_e, self.title) + self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 7, 0) + self.main_window.noutrefresh() + self.menu_window.noutrefresh() + curses.doupdate() + msg.status("Resizing window.", self) + + def display(self): + signal.signal(signal.SIGINT, self.end) + msg.initialize_curses_logging(self.stdscr) + msg.status(self.console_message, self) + + while self.running: + self.active_screen = self.tui_screens[-1] if self.tui_screens else self.menu_screen + + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.main_window.erase() + self.menu_window.erase() + self.stdscr.timeout(100) + self.console.display() + + self.active_screen.display() + + if (not isinstance(self.active_screen, tui_screen.TextScreen) + and not isinstance(self.active_screen, tui_screen.TextDialog)): + self.choice_processor( + self.menu_window, + self.active_screen.get_screen_id(), + self.active_screen.get_choice()) + self.choice = "Processing" # Reset for next round + + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.main_window.noutrefresh() + self.menu_window.noutrefresh() + + def run(self): + try: + self.init_curses() + self.display() + finally: + self.end_curses() + signal.signal(signal.SIGINT, self.end) + + def task_processor(self, evt=None, task=None): + if task == 'FLPRODUCT': + utils.start_thread(self.get_product(config.use_python_dialog)) + elif task == 'TARGETVERSION': + utils.start_thread(self.get_version(config.use_python_dialog)) + elif task == 'LOGOS_RELEASE_VERSION': + utils.start_thread(self.get_release(config.use_python_dialog)) + elif task == 'INSTALLDIR': + utils.start_thread(self.get_installdir(config.use_python_dialog)) + elif task == 'WINE_EXE': + utils.start_thread(self.get_wine(config.use_python_dialog)) + elif task == 'WINETRICKSBIN': + utils.start_thread(self.get_winetricksbin(config.use_python_dialog)) + elif task == 'INSTALLING': + utils.start_thread(self.get_waiting(config.use_python_dialog)) + elif task == 'CONFIG': + utils.start_thread(self.get_config(config.use_python_dialog)) + elif task == 'DONE': + self.finish_install() + elif task == 'TUI-RESIZE': + self.resize_curses() + elif task == 'TUI-UPDATE-MENU': + self.menu_screen.set_options(self.set_tui_menu_options(curses=True)) + #self.menu_screen.set_options(self.set_tui_menu_options(curses=False)) + + def choice_processor(self, stdscr, screen_id, choice): + if choice == "Processing": + pass + elif screen_id == 0 and choice == "Exit": + msg.logos_warn("Exiting installation.") + self.tui_screens = [] + self.running = False + elif screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): + self.tui_screens.pop(0) + self.stdscr.clear() + elif screen_id == 0: + if choice is None or choice == "Exit": + sys.exit(0) + elif choice.startswith("Install"): + config.INSTALL_STEPS_COUNT = 0 + config.INSTALL_STEP = 0 + utils.start_thread(installer.ensure_launcher_shortcuts, True, self) + elif choice.startswith("Update Logos Linux Installer"): + utils.update_to_latest_lli_release() + elif choice == f"Run {config.FLPRODUCT}": + wine.run_logos() + elif choice == "Run Indexing": + wine.run_indexing() + elif choice == "Remove Library Catalog": + control.remove_library_catalog() + elif choice == "Remove All Index Files": + control.remove_all_index_files() + elif choice == "Edit Config": + control.edit_config() + elif choice == "Install Dependencies": + utils.check_dependencies() + elif choice == "Back up Data": + control.backup() + elif choice == "Restore Data": + control.restore() + elif choice == "Update to Latest AppImage": + utils.update_to_latest_recommended_appimage() + elif choice == "Set AppImage": + # TODO: Allow specifying the AppImage File + appimages = utils.find_appimage_files() + appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in + appimages] # noqa: E501 + appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) + self.menu_options = appimage_choices + question = "Which AppImage should be used?" + self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices) + elif choice == "Download or Update Winetricks": + control.set_winetricks() + elif choice == "Run Winetricks": + wine.run_winetricks() + elif choice.endswith("Logging"): + wine.switch_logging() + else: + msg.logos_error("Unknown menu choice.") + elif screen_id == 1: + if choice == "Input Custom AppImage": + appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") + else: + appimage_filename = choice + config.SELECTED_APPIMAGE_FILENAME = appimage_filename + utils.set_appimage_symlink() + self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) + self.appimage_e.set() + elif screen_id == 2: + if str(choice).startswith("Logos"): + config.FLPRODUCT = "Logos" + self.product_q.put(config.FLPRODUCT) + self.product_e.set() + elif str(choice).startswith("Verbum"): + config.FLPRODUCT = "Verbum" + self.product_q.put(config.FLPRODUCT) + self.product_e.set() + elif screen_id == 3: + if "10" in choice: + config.TARGETVERSION = "10" + self.version_q.put(config.TARGETVERSION) + self.version_e.set() + elif "9" in choice: + config.TARGETVERSION = "9" + self.version_q.put(config.TARGETVERSION) + self.version_e.set() + elif screen_id == 4: + if choice: + config.LOGOS_RELEASE_VERSION = choice + self.release_q.put(config.LOGOS_RELEASE_VERSION) + self.release_e.set() + elif screen_id == 5: + if choice: + config.INSTALLDIR = choice + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + self.installdir_q.put(config.INSTALLDIR) + self.installdir_e.set() + elif screen_id == 6: + config.WINE_EXE = choice + if choice: + self.wine_q.put(config.WINE_EXE) + self.wine_e.set() + elif screen_id == 7: + winetricks_options = utils.get_winetricks_options() + if choice.startswith("Use"): + config.WINETRICKSBIN = winetricks_options[0] + self.tricksbin_q.put(config.WINETRICKSBIN) + self.tricksbin_e.set() + elif choice.startswith("Download"): + self.tricksbin_q.put("Download") + self.tricksbin_e.set() + elif screen_id == 8: + if config.install_finished: + self.finished_q.put(True) + self.finished_e.set() + elif screen_id == 9: + if choice: + self.config_q.put(choice) + self.config_e.set() + self.tui_screens = [] + elif screen_id == 10: + pass + elif screen_id == 11: + pass + + def switch_screen(self, dialog): + if self.active_screen != self.menu_screen: + self.tui_screens.pop(0) + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.stdscr.clear() + + def get_product(self, dialog): + question = "Choose which FaithLife product the script should install:" # noqa: E501 + options = [("0", "Logos"), ("1", "Verbum"), ("2", "Exit")] + self.menu_options = options + self.stack_menu(2, self.product_q, self.product_e, question, options, dialog=dialog) + self.switch_screen(dialog) + + def get_version(self, dialog): + self.product_e.wait() + question = f"Which version of {config.FLPRODUCT} should the script install?" # noqa: E501 + options = [("0", "10"), ("1", "9"), ("2", "Exit")] + self.menu_options = options + self.stack_menu(3, self.version_q, self.version_e, question, options, dialog=dialog) + self.switch_screen(dialog) + + def get_release(self, dialog): + self.stack_text(10, self.version_q, self.version_e, "Waiting to acquire Logos versions…", wait=True, dialog=dialog) + self.version_e.wait() + self.switch_screen(dialog) + question = f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?" # noqa: E501 + utils.start_thread(utils.get_logos_releases, True, self) + self.releases_e.wait() + + if config.TARGETVERSION == '10': + options = self.releases_q.get() + elif config.TARGETVERSION == '9': + options = self.releases_q.get() + + if options is None: + msg.logos_error("Failed to fetch LOGOS_RELEASE_VERSION.") + options.append("Exit") + enumerated_options = [(str(i), option) for i, option in enumerate(options, start=1)] + self.menu_options = enumerated_options + self.stack_menu(4, self.release_q, self.release_e, question, enumerated_options, dialog=dialog) + self.switch_screen(dialog) + + def get_installdir(self, dialog): + self.release_e.wait() + default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 + question = f"Where should {config.FLPRODUCT} files be installed to? [{default}]: " # noqa: E501 + self.stack_input(5, self.installdir_q, self.installdir_e, question, default, dialog=dialog) + self.switch_screen(dialog) + + def get_wine(self, dialog): + self.installdir_e.wait() + self.stack_text(10, self.wine_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog) + question = f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.LOGOS_RELEASE_VERSION} in {config.INSTALLDIR}?" # noqa: E501 + options = utils.get_wine_options( + utils.find_appimage_files(), + utils.find_wine_binary_files() + ) + max_length = max(len(option) for option in options) + max_length += len(str(len(options))) + 10 + self.switch_screen(dialog) + if dialog: + enumerated_options = [(str(i), option) for i, option in enumerate(options, start=1)] + self.menu_options = enumerated_options + self.stack_menu(6, self.wine_q, self.wine_e, question, enumerated_options, width=max_length, + dialog=dialog) + else: + self.menu_options = options + self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, + dialog=dialog) + self.switch_screen(dialog) + + def get_winetricksbin(self, dialog): + self.wine_e.wait() + winetricks_options = utils.get_winetricks_options() + if len(winetricks_options) > 1: + question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux." # noqa: E501 + options = [ + ("1", "Use local winetricks."), + ("2", "Download winetricks from the Internet.") + ] + self.menu_options = options + self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog) + self.switch_screen(dialog) + + def get_waiting(self, dialog): + self.tricksbin_e.wait() + text = ["Install is running…\n"] + logging.console_log[-2:] + processed_text = utils.str_array_to_string(text) + percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + self.stack_text(8, self.status_q, self.status_e, processed_text, wait=True, percent=percent, + dialog=dialog) + self.switch_screen(dialog) -def control_panel_app(): - # Run TUI. - while True: - options_first = [] - options_default = ["Install Logos Bible Software"] - options_main = [ + def get_config(self, dialog): + question = f"Update config file at {config.CONFIG_FILE}?" + options = ["Yes", "No"] + self.menu_options = options + self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog) + self.switch_screen(dialog) + + def finish_install(self): + utils.send_task(self, 'TUI-UPDATE-MENU') + + def report_waiting(self, text, dialog): + if dialog: + self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog) + self.switch_screen(dialog) + else: + self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog) + logging.console_log.append(text) + + def report_dependencies(self, text, percent, elements, dialog): + if elements is not None: + if dialog: + self.stack_tasklist(11, self.deps_q, self.deps_e, text, elements, percent, dialog=dialog) + self.switch_screen(dialog) + else: + #TODO + pass + + def set_tui_menu_options(self, curses=False): + labels = [] + options = [] + option_number = 1 + if config.LLI_LATEST_VERSION and utils.get_runmode() == 'binary': + logging.debug("Checking if Logos Linux Installers needs updated.") # noqa: E501 + status, error_message = utils.compare_logos_linux_installer_version() # noqa: E501 + if status == 0: + labels.append("Update Logos Linux Installer") + elif status == 1: + logging.warning("Logos Linux Installer is up-to-date.") + elif status == 2: + logging.warning("Logos Linux Installer is newer than the latest release.") # noqa: E501 + else: + logging.error(f"{error_message}") + + if utils.file_exists(config.LOGOS_EXE): + labels_default = [ + f"Run {config.FLPRODUCT}", + "Run Indexing", + "Remove Library Catalog", + "Remove All Index Files", + "Edit Config", + "Back up Data", + "Restore Data", + ] + else: + labels_default = ["Install Logos Bible Software"] + labels.extend(labels_default) + + labels_support = [ "Install Dependencies", "Download or Update Winetricks", "Run Winetricks" ] - options_installed = [ - f"Run {config.FLPRODUCT}", - "Run Indexing", - "Remove Library Catalog", - "Remove All Index Files", - "Edit Config", - "Back up Data", - "Restore Data", - ] - options_exit = ["Exit"] - if utils.file_exists(config.LOGOS_EXE): - if config.LLI_LATEST_VERSION and utils.get_runmode() == 'binary': - logging.debug("Checking if Logos Linux Installers needs updated.") # noqa: E501 - status, error_message = utils.compare_logos_linux_installer_version() # noqa: E501 - if status == 0: - options_first.append("Update Logos Linux Installer") - elif status == 1: - logging.warning("Logos Linux Installer is up-to-date.") - elif status == 2: - logging.warning("Logos Linux Installer is newer than the latest release.") # noqa: E501 - else: - logging.error(f"{error_message}") - - if config.WINEBIN_CODE == "AppImage" or config.WINEBIN_CODE == "Recommended": # noqa: E501 - logging.debug("Checking if the AppImage needs updated.") - status, error_message = utils.compare_recommended_appimage_version() # noqa: E501 - if status == 0: - options_main.insert(0, "Update to Latest AppImage") - elif status == 1: - logging.warning("The AppImage is already set to the latest recommended.") # noqa: E501 - elif status == 2: - logging.warning("The AppImage version is newer than the latest recommended.") # noqa: E501 - else: - logging.error(f"{error_message}") - - options_main.insert(1, "Set AppImage") - - if config.LOGS == "DISABLED": - options_installed.append("Enable Logging") + labels.extend(labels_support) + + label = "Enable Logging" if config.LOGS == "DISABLED" else "Disable Logging" + labels.append(label) + + labels.append("Exit") + + for label in labels: + if curses: + options.append(label) else: - options_installed.append("Disable Logging") + options.append((str(option_number), label)) + option_number += 1 + + return options - options = options_first + options_installed + options_main + options_default + options_exit # noqa: E501 + def stack_menu(self, screen_id, queue, event, question, options, height=None, width=None, menu_height=8, dialog=False): + if dialog: + utils.append_unique(self.tui_screens, + tui_screen.MenuDialog(self, screen_id, queue, event, question, options, height, width, menu_height)) else: - options = options_first + options_default + options_main + options_exit # noqa: E501 + utils.append_unique(self.tui_screens, + tui_screen.MenuScreen(self, screen_id, queue, event, question, options, height, width, menu_height)) - choice = tui.menu( - options, - f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})", - "What would you like to do?" - ) + def stack_input(self, screen_id, queue, event, question, default, dialog=False): + if dialog: + utils.append_unique(self.tui_screens, + tui_screen.InputDialog(self, screen_id, queue, event, question, default)) + else: + utils.append_unique(self.tui_screens, + tui_screen.InputScreen(self, screen_id, queue, event, question, default)) - if choice is None or choice == "Exit": - sys.exit(0) - elif choice == "Install Dependencies": - utils.check_dependencies() - elif choice.startswith("Install"): - installer.ensure_launcher_shortcuts() - elif choice.startswith("Update Logos Linux Installer"): - utils.update_to_latest_lli_release() - elif choice == f"Run {config.FLPRODUCT}": - wine.run_logos() - elif choice == "Run Indexing": - wine.run_indexing() - elif choice == "Remove Library Catalog": - control.remove_library_catalog() - elif choice == "Remove All Index Files": - control.remove_all_index_files() - elif choice == "Edit Config": - control.edit_config() - elif choice == "Back up Data": - control.backup() - elif choice == "Restore Data": - control.restore() - elif choice == "Update to Latest AppImage": - utils.update_to_latest_recommended_appimage() - elif choice == "Set AppImage": - set_appimage() - elif choice == "Download or Update Winetricks": - control.set_winetricks() - elif choice == "Run Winetricks": - wine.run_winetricks() - elif choice.endswith("Logging"): - wine.switch_logging() + def stack_confirm(self, screen_id, queue, event, question, options, dialog=False): + if dialog: + utils.append_unique(self.tui_screens, + tui_screen.ConfirmDialog(self, screen_id, queue, event, question, options)) else: - msg.logos_error("Unknown menu choice.") + #TODO: curses version + pass + + def stack_text(self, screen_id, queue, event, text, wait=False, percent=None, dialog=False): + if dialog: + utils.append_unique(self.tui_screens, + tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) + else: + utils.append_unique(self.tui_screens, + tui_screen.TextScreen(self, screen_id, queue, event, text, wait)) + + def stack_tasklist(self, screen_id, queue, event, text, elements, percent, dialog=False): + logging.debug(f"Elements stacked: {elements}") + if dialog: + utils.append_unique(self.tui_screens, tui_screen.TaskListDialog(self, screen_id, queue, event, text, + elements, percent)) + else: + #TODO: curses version + pass + + def stack_buildlist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): + if dialog: + utils.append_unique(self.tui_screens, + tui_screen.BuildListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) + else: + # TODO + pass + + def stack_checklist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): + if dialog: + utils.append_unique(self.tui_screens, + tui_screen.CheckListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) + else: + # TODO + pass + + + def update_tty_dimensions(self): + self.window_height, self.window_width = self.stdscr.getmaxyx() + + def get_main_window(self): + return self.main_window + + def get_menu_window(self): + return self.menu_window + + +def control_panel_app(stdscr): + os.environ.setdefault('ESCDELAY', '100') + TUI(stdscr).run() + diff --git a/tui_curses.py b/tui_curses.py new file mode 100644 index 00000000..9edd16ff --- /dev/null +++ b/tui_curses.py @@ -0,0 +1,258 @@ +import curses +import logging +import signal +import textwrap + +import config +import msg +import utils + + +def wrap_text(app, text): + # Turn text into wrapped text, line by line, centered + wrapped_text = textwrap.fill(text, app.window_width - 4) + lines = wrapped_text.split('\n') + return lines + + +def title(app, title_text): + stdscr = app.get_main_window() + title_lines = wrap_text(app, title_text) + title_start_y = max(0, app.window_height // 2 - len(title_lines) // 2) + for i, line in enumerate(title_lines): + if i < app.window_height: + stdscr.addstr(i, 2, line, curses.A_BOLD) + + +def text_centered(app, text, start_y=0): + stdscr = app.get_menu_window() + text_lines = wrap_text(app, text) + text_start_y = start_y + text_width = max(len(line) for line in text_lines) + for i, line in enumerate(text_lines): + if text_start_y + i < app.window_height: + x = app.window_width // 2 - text_width // 2 + stdscr.addstr(text_start_y + i, x, line) + + return text_start_y, text_lines + + +def confirm(app, question_text, height=None, width=None): + stdscr = app.get_menu_window() + question_text = question_text + " [Y/n]: " + question_start_y, question_lines = text_centered(app, question_text) + + y = question_start_y + len(question_lines) + 2 + + while True: + key = stdscr.getch() + key = chr(key) + + if key.lower() == 'y' or key == '\n': # '\n' for Enter key, defaults to "Yes" + return True + elif key.lower() == 'n': + return False + + stdscr.addstr(y, 0, "Type Y[es] or N[o]. ") + + +def spinner(app, index, start_y=0): + spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"] + i = index + text_centered(app, spinner_chars[i], start_y) + i = (i + 1) % len(spinner_chars) + + return i + + +def get_user_input(app, question_text, default_text): + stdscr = app.get_menu_window() + curses.echo() + curses.curs_set(1) + user_input = input_keyboard(app, question_text, default_text) + curses.curs_set(0) + curses.noecho() + + return user_input + + +def input_keyboard(app, question_text, default): + stdscr = app.get_menu_window() + done = False + choice = "" + + stdscr.clear() + question_start_y, question_lines = text_centered(app, question_text) + + try: + while done is False: + curses.echo() + key = stdscr.getch(question_start_y + len(question_lines) + 2, 10 + len(choice)) + if key == -1: # If key not found, keep processing. + pass + elif key == ord('\n'): # Enter key + if choice is None or not choice: + choice = default + logging.debug(f"Selected Path: {choice}") + done = True + elif key == curses.KEY_RESIZE: + utils.send_task(app, 'RESIZE') + elif key == curses.KEY_BACKSPACE or key == 127: + if len(choice) > 0: + choice = choice[:-1] + stdscr.addstr(question_start_y + len(question_lines) + 2, 10, ' ' * (len(choice) + 1)) + stdscr.addstr(question_start_y + len(question_lines) + 2, 10, choice) + else: + choice += chr(key) + stdscr.addch(question_start_y + len(question_lines) + 2, 10 + len(choice) - 1, chr(key)) + stdscr.refresh() + curses.noecho() + + stdscr.refresh() + if done: + return choice + except KeyboardInterrupt: + signal.signal(signal.SIGINT, app.end) + + +def do_menu_up(app): + if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: + # Move to the previous page + config.current_page -= 1 + config.current_option = min(len(app.menu_options) - 1, (config.current_page + 1) * config.options_per_page - 1) + elif config.current_option == 0: + if config.total_pages == 1: + config.current_option = len(app.menu_options) - 1 + else: + config.current_page = config.total_pages - 1 + config.current_option = len(app.menu_options) - 1 + else: + config.current_option = max(0, config.current_option - 1) + + +def do_menu_down(app): + if config.current_option == (config.current_page + 1) * config.options_per_page - 1 and config.current_page < config.total_pages - 1: + # Move to the next page + config.current_page += 1 + config.current_option = min(len(app.menu_options) - 1, config.current_page * config.options_per_page) + elif config.current_option == len(app.menu_options) - 1: + config.current_page = 0 + config.current_option = 0 + else: + config.current_option = min(len(app.menu_options) - 1, config.current_option + 1) + +def menu(app, question_text, options): + stdscr = app.get_menu_window() + current_option = config.current_option + current_page = config.current_page + options_per_page = config.options_per_page + config.total_pages = (len(options) - 1) // options_per_page + 1 + + app.menu_options = options + + while True: + stdscr.erase() + question_start_y, question_lines = text_centered(app, question_text) + + # Display the options, centered + options_start_y = question_start_y + len(question_lines) + 2 + for i in range(options_per_page): + index = current_page * options_per_page + i + if index < len(options): + option = options[index] + if type(option) is list: + option_lines = [] + wine_binary_code = option[0] + if wine_binary_code != "Exit": + wine_binary_path = option[1] + wine_binary_description = option[2] + wine_binary_path_wrapped = textwrap.wrap( + f"Binary Path: {wine_binary_path}", app.window_width - 4) + option_lines.extend(wine_binary_path_wrapped) + wine_binary_desc_wrapped = textwrap.wrap( + f"Description: {wine_binary_description}", app.window_width - 4) + option_lines.extend(wine_binary_desc_wrapped) + else: + wine_binary_path = option[1] + wine_binary_description = option[2] + wine_binary_path_wrapped = textwrap.wrap( + f"{wine_binary_path}", app.window_width - 4) + option_lines.extend(wine_binary_path_wrapped) + wine_binary_desc_wrapped = textwrap.wrap( + f"{wine_binary_description}", app.window_width - 4) + option_lines.extend(wine_binary_desc_wrapped) + else: + option_lines = textwrap.wrap(option, app.window_width - 4) + + for j, line in enumerate(option_lines): + y = options_start_y + i + j + x = max(0, app.window_width // 2 - len(line) // 2) + if y < app.menu_window_height: + if index == current_option: + stdscr.addstr(y, x, line, curses.A_REVERSE) + else: + stdscr.addstr(y, x, line) + + if type(option) is list: + options_start_y += (len(option_lines)) + + # Display pagination information + page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(options)}" + stdscr.addstr(app.menu_window_height - 1, 2, page_info, curses.A_BOLD) + + # Refresh the windows + stdscr.noutrefresh() + + # Get user input + thread = utils.start_thread(menu_keyboard, True, app) + thread.join() + + stdscr.noutrefresh() + + return app.choice + + +def menu_keyboard(app): + if len(app.tui_screens) > 0: + stdscr = app.tui_screens[-1].get_stdscr() + else: + stdscr = app.menu_screen.get_stdscr() + options = app.menu_options + key = stdscr.getch() + choice = "" + + try: + if key == -1: # If key not found, keep processing. + pass + elif key == curses.KEY_RESIZE: + utils.send_task(app, 'RESIZE') + elif key == curses.KEY_UP or key == 259: # Up arrow + do_menu_up(app) + elif key == curses.KEY_DOWN or key == 258: # Down arrow + do_menu_down(app) + elif key == 27: # Sometimes the up/down arrow key is represented by a series of three keys. + next_key = stdscr.getch() + if next_key == 91: + final_key = stdscr.getch() + if final_key == 65: + do_menu_up(app) + elif final_key == 66: + do_menu_down(app) + elif key == ord('\n'): # Enter key + choice = options[config.current_option] + # Reset for next menu + config.current_option = 0 + config.current_page = 0 + elif key == ord('\x1b'): + signal.signal(signal.SIGINT, app.end) + else: + msg.status("Input unknown.", app) + pass + except KeyboardInterrupt: + signal.signal(signal.SIGINT, app.end) + + stdscr.refresh() + if choice: + app.choice = choice + else: + return "Processing" diff --git a/tui_dialog.py b/tui_dialog.py new file mode 100644 index 00000000..4e28f8f1 --- /dev/null +++ b/tui_dialog.py @@ -0,0 +1,127 @@ +import curses +from dialog import Dialog +import logging + +import installer + + +def text(app, text, height=None, width=None, title=None, backtitle=None, colors=True): + d = Dialog() + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle + d.infobox(text, **options) + + +def progress_bar(app, text, percent, height=None, width=None, title=None, backtitle=None, colors=True): + d = Dialog() + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle + + d.gauge_start(text=text, percent=percent, **options) + + +#FIXME: Not working. See tui_screen.py#262. +def update_progress_bar(app, percent, text='', update_text=False): + d = Dialog() + d.gauge_update(percent, text, update_text) + + +def stop_progress_bar(app): + d = Dialog() + d.gauge_stop() + + +def tasklist_progress_bar(app, text, percent, elements, height=None, width=None, title=None, backtitle=None, colors=None): + d = Dialog() + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle + + if elements is None: + elements = {} + + elements_list = [(k, v) for k, v in elements.items()] + try: + d.mixedgauge(text=text, percent=percent, elements=elements_list, **options) + except Exception as e: + logging.debug(f"Error in mixedgauge: {e}") + raise + + +def confirm(app, question_text, height=None, width=None): + dialog = Dialog() + check = dialog.yesno(question_text, height, width) + return check + + +def directory_picker(app, path_dir): + str_dir = str(path_dir) + + try: + dialog = Dialog() + curses.curs_set(1) + _, path = dialog.dselect(str_dir) + curses.curs_set(0) + except Exception as e: + logging.error("An error occurred:", e) + curses.endwin() + + return path + + +def menu(app, question_text, options, height=None, width=None, menu_height=8): + tag_to_description = {tag: description for tag, description in options} + dialog = Dialog(dialog="dialog") + + menu_options = [(tag, description) for i, (tag, description) in enumerate(options)] + code, tag = dialog.menu(question_text, height, width, menu_height, choices=menu_options) + selected_description = tag_to_description.get(tag) + + if code == dialog.OK: + return code, tag, selected_description + elif code == dialog.CANCEL: + return None + + +def buildlist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=None): + # items is an interable of (tag, item, status) + dialog = Dialog(dialog="dialog") + + code, tags = dialog.buildlist(text, height, width, list_height, items, title, backtitle, colors) + + if code == dialog.OK: + return code, tags + elif code == dialog.CANCEL: + return None + + +def checklist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=None): + # items is an iterable of (tag, item, status) + dialog = Dialog(dialog="dialog") + + code, tags = dialog.checklist(text, items, height, width, list_height, title, backtitle, colors) + + if code == dialog.OK: + return code, tags + elif code == dialog.Cancel: + return None diff --git a/tui_screen.py b/tui_screen.py new file mode 100644 index 00000000..730693ca --- /dev/null +++ b/tui_screen.py @@ -0,0 +1,376 @@ +import logging +import queue + +import time +from pathlib import Path +import curses + +import msg +import config +import installer +import tui_curses +import tui_dialog +import utils + + +class Screen: + def __init__(self, app, screen_id, queue, event): + self.app = app + self.screen_id = screen_id + self.choice = "" + self.queue = queue + self.event = event + # running: + # This var is used for DialogScreens. The var indicates whether a Dialog has already started. + # If the dialog has already started, then the program will not display the dialog again + # in order to prevent phantom key presses. + # 0 = not started + # 1 = started + # 2 = finished + self.running = 0 + + def __str__(self): + return f"Curses Screen" + + def display(self): + pass + + def get_stdscr(self): + return self.app.stdscr + + def get_screen_id(self): + return self.screen_id + + def get_choice(self): + return self.choice + + def submit_choice_to_queue(self): + self.queue.put(self.choice) + + def wait_event(self): + self.event.wait() + + def is_set(self): + return self.event.is_set() + + +class CursesScreen(Screen): + pass + + +class DialogScreen(Screen): + pass + + +class ConsoleScreen(CursesScreen): + def __init__(self, app, screen_id, queue, event, title): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_main_window() + self.title = title + + def display(self): + self.stdscr.erase() + tui_curses.title(self.app, self.title) + + self.stdscr.addstr(2, 2, f"---Console---") + recent_messages = logging.console_log[-6:] + for i, message in enumerate(recent_messages, 1): + message_lines = tui_curses.wrap_text(self.app, message) + for j, line in enumerate(message_lines): + if 2 + j < self.app.window_height: + self.stdscr.addstr(2 + i, 2, f"{message}") + + self.stdscr.noutrefresh() + curses.doupdate() + + +class MenuScreen(CursesScreen): + def __init__(self, app, screen_id, queue, event, question, options, height=None, width=None, menu_height=8): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.question = question + self.options = options + self.height = height + self.width = width + self.menu_height = menu_height + + def display(self): + self.stdscr.erase() + self.choice = tui_curses.menu( + self.app, + self.question, + self.options + ) + self.stdscr.noutrefresh() + curses.doupdate() + + def get_question(self): + return self.question + + def set_options(self, new_options): + self.options = new_options + + +class InputScreen(CursesScreen): + def __init__(self, app, screen_id, queue, event, question, default): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.question = question + self.default = default + + def display(self): + self.stdscr.erase() + self.choice = tui_curses.get_user_input( + self.app, + self.question, + self.default + ) + self.stdscr.noutrefresh() + curses.doupdate() + + def get_question(self): + return self.question + + def get_default(self): + return self.default + + +class TextScreen(CursesScreen): + def __init__(self, app, screen_id, queue, event, text, wait): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.text = text + self.wait = wait + self.spinner_index = 0 + + def display(self): + self.stdscr.erase() + text_start_y, text_lines = tui_curses.text_centered(self.app, self.text) + if self.wait: + self.spinner_index = tui_curses.spinner(self.app, self.spinner_index, text_start_y + len(text_lines) + 1) + time.sleep(0.1) + self.stdscr.noutrefresh() + curses.doupdate() + + def get_text(self): + return self.text + + +class MenuDialog(DialogScreen): + def __init__(self, app, screen_id, queue, event, question, options, height=None, width=None, menu_height=8): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.question = question + self.options = options + self.height = height + self.width = width + self.menu_height = menu_height + + def __str__(self): + return f"PyDialog Screen" + + def display(self): + if self.running == 0: + self.running = 1 + _, _, self.choice = tui_dialog.menu(self.app, self.question, self.options, self.height, self.width, + self.menu_height) + self.running = 2 + + def get_question(self): + return self.question + + def set_options(self, new_options): + self.options = new_options + + +class InputDialog(DialogScreen): + def __init__(self, app, screen_id, queue, event, question, default): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.question = question + self.default = default + + def __str__(self): + return f"PyDialog Screen" + + def display(self): + if self.running == 0: + self.running = 1 + self.choice = tui_dialog.directory_picker(self.app, self.default) + if self.choice: + self.choice = Path(self.choice) + self.running = 2 + + def get_question(self): + return self.question + + def get_default(self): + return self.default + + +class ConfirmDialog(DialogScreen): + def __init__(self, app, screen_id, queue, event, question, yes_label="Yes", no_label="No"): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.question = question + self.yes_label = yes_label + self.no_label = no_label + + def __str__(self): + return f"PyDialog Screen" + + def display(self): + if self.running == 0: + self.running = 1 + _, _, self.choice = tui_dialog.confirm(self.app, self.question, self.yes_label, self.no_label) + self.running = 2 + + def get_question(self): + return self.question + + +class TextDialog(DialogScreen): + def __init__(self, app, screen_id, queue, event, text, wait=False, percent=None, height=None, width=None, + title=None, backtitle=None, colors=True): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.text = text + self.percent = percent + self.wait = wait + self.height = height + self.width = width + self.title = title + self.backtitle = backtitle + self.colors = colors + self.lastpercent = 0 + + def __str__(self): + return f"PyDialog Screen" + + def display(self): + if self.running == 0: + if self.wait: + self.running = 1 + self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + tui_dialog.progress_bar(self, self.text, self.percent) + self.lastpercent = self.percent + else: + tui_dialog.text(self, self.text) + elif self.running == 1: + if self.wait: + self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + # tui_dialog.update_progress_bar(self, self.percent, self.text, True) + if self.lastpercent != self.percent: + tui_dialog.progress_bar(self, self.text, self.percent) + + if self.percent == 100: + tui_dialog.stop_progress_bar(self) + self.running = 2 + self.wait = False + + time.sleep(0.1) + + def get_text(self): + return self.text + + +class TaskListDialog(DialogScreen): + def __init__(self, app, screen_id, queue, event, text, elements, percent, + height=None, width=None, title=None, backtitle=None, colors=True): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.text = text + self.elements = elements if elements is not None else {} + self.percent = percent + self.height = height + self.width = width + self.title = title + self.backtitle = backtitle + self.colors = colors + self.updated = False + + def __str__(self): + return f"PyDialog Screen" + + def display(self): + if self.running == 0: + tui_dialog.tasklist_progress_bar(self, self.text, self.percent, self.elements, + self.height, self.width, self.title, self.backtitle, self.colors) + self.running = 1 + elif self.running == 1: + if self.updated: + tui_dialog.tasklist_progress_bar(self, self.text, self.percent, self.elements, + self.height, self.width, self.title, self.backtitle, self.colors) + else: + pass + + time.sleep(0.1) + + def set_text(self, text): + self.text = text + self.updated = True + + def set_percent(self, percent): + self.percent = percent + self.updated = True + + def set_elements(self, elements): + self.elements = elements + self.updated = True + + def get_text(self): + return self.text + + +class BuildListDialog(DialogScreen): + def __init__(self, app, screen_id, queue, event, question, options, list_height=None, height=None, width=None): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.question = question + self.options = options + self.height = height + self.width = width + self.list_height = list_height + + def __str__(self): + return f"PyDialog Screen" + + def display(self): + if self.running == 0: + self.running = 1 + code, self.choice = tui_dialog.buildlist(self.app, self.question, self.options, self.height, self.width, + self.list_height) + self.running = 2 + + def get_question(self): + return self.question + + def set_options(self, new_options): + self.options = new_options + + +class CheckListDialog(DialogScreen): + def __init__(self, app, screen_id, queue, event, question, options, list_height=None, height=None, width=None): + super().__init__(app, screen_id, queue, event) + self.stdscr = self.app.get_menu_window() + self.question = question + self.options = options + self.height = height + self.width = width + self.list_height = list_height + + def __str__(self): + return f"PyDialog Screen" + + def display(self): + if self.running == 0: + self.running = 1 + code, self.choice = tui_dialog.checklist(self.app, self.question, self.options, self.height, self.width, + self.list_height) + self.running = 2 + + def get_question(self): + return self.question + + def set_options(self, new_options): + self.options = new_options diff --git a/utils.py b/utils.py index d61fd694..a01e3428 100644 --- a/utils.py +++ b/utils.py @@ -9,6 +9,7 @@ import re import requests import shutil +import shlex import signal import stat import subprocess @@ -27,7 +28,7 @@ import config import msg import wine -import tui +import tui_dialog class Props(): @@ -130,6 +131,13 @@ def get_md5(self): return self.md5 +def append_unique(list, item): + if item not in list: + list.append(item) + else: + msg.logos_warn(f"{item} already in {list}.") + + # Set "global" variables. def set_default_config(): get_os() @@ -205,7 +213,7 @@ def remove_pid_file(): confirm = tk.messagebox.askquestion("Confirmation", message) tk_root.destroy() elif config.DIALOG == "curses": - confirm = tui.confirm("Confirmation", message) + confirm = tui_dialog.confirm("Confirmation", message) else: confirm = msg.cli_question(message) @@ -227,9 +235,24 @@ def die(message): sys.exit(1) -def run_command(command): - result = subprocess.run(command, check=True, text=True, capture_output=True) - return result.stdout +def run_command(command, stdin=None, shell=False): + try: + logging.debug(f"Attempting to execute {command}") + result = subprocess.run( + command, + stdin=stdin, + check=True, + text=True, + shell=shell, + capture_output=True + ) + return result.stdout + except subprocess.CalledProcessError as e: + logging.error(f"Error occurred while executing {command}: {e}") + return None + except Exception as e: + logging.error(f"An unexpected error occurred when running {command}: {e}") + return None def reboot(): @@ -381,35 +404,43 @@ def get_package_manager(): # Check for package manager and associated packages if shutil.which('apt') is not None: # debian, ubuntu config.PACKAGE_MANAGER_COMMAND_INSTALL = "apt install -y" + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "apt install --download-only -y" config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" # IDEA: Switch to Python APT library? # See https://github.com/FaithLife-Community/LogosLinuxInstaller/pull/33#discussion_r1443623996 # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l | grep -E '^.i '" - config.PACKAGES = "coreutils patch lsof wget findutils sed grep gawk winbind cabextract x11-apps binutils fuse3" + config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" + config.QUERY_PREFIX = '.i ' + config.PACKAGES = "binutils cabextract fuse wget winbind" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora config.PACKAGE_MANAGER_COMMAND_INSTALL = "dnf install -y" + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "dnf install --downloadonly -y" config.PACKAGE_MANAGER_COMMAND_REMOVE = "dnf remove -y" - config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed | grep -E ^" - config.PACKAGES = "patch mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract" # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed" + config.QUERY_PREFIX = '' + config.PACKAGES = "patch mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appiamgelauncher" elif shutil.which('pamac') is not None: # manjaro config.PACKAGE_MANAGER_COMMAND_INSTALL = "pamac install --no-upgrade --no-confirm" # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pamac install --download-only --no-confirm" config.PACKAGE_MANAGER_COMMAND_REMOVE = "pamac remove --no-confirm" - config.PACKAGE_MANAGER_COMMAND_QUERY = "pamac list -i | grep -E ^" - config.PACKAGES = "wget sed grep gawk cabextract samba" # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_QUERY = "pamac list -i" + config.QUERY_PREFIX = '' + config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('pacman') is not None: # arch, steamOS config.PACKAGE_MANAGER_COMMAND_INSTALL = r"pacman -Syu --overwrite * --noconfirm --needed" # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pacman -Sw -y" config.PACKAGE_MANAGER_COMMAND_REMOVE = r"pacman -R --no-confirm" - config.PACKAGE_MANAGER_COMMAND_QUERY = "pacman -Q | grep -E ^" + config.PACKAGE_MANAGER_COMMAND_QUERY = "pacman -Q" + config.QUERY_PREFIX = '' if config.OS_NAME == "steamos": # steamOS - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" + config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" #noqa: #E501 else: # arch - config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" + config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" # Add more conditions for other package managers as needed @@ -428,60 +459,139 @@ def get_runmode(): return 'script' -def query_packages(packages, mode="install"): +def query_packages(packages, elements=None, mode="install", app=None): if config.SKIP_DEPENDENCIES: return missing_packages = [] conflicting_packages = [] - for p in packages: - command = f"{config.PACKAGE_MANAGER_COMMAND_QUERY}{p}" - logging.debug(f"pkg query command: \"{command}\"") - result = subprocess.run( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - text=True - ) - logging.debug(f"pkg query result: {result.returncode}") - if result.returncode != 0 and mode == "install": - missing_packages.append(p) - elif result.returncode == 0 and mode == "remove": - conflicting_packages.append(p) + command = config.PACKAGE_MANAGER_COMMAND_QUERY + + try: + package_list = run_command(command, shell=True) + except Exception as e: + logging.error(f"Error occurred while executing command: {e}") + + logging.debug(f"Checking packages: {packages} in package list.") + if app is not None: + if elements is None: + elements = {} # Initialize elements if not provided + elif isinstance(elements, list): + elements = {element[0]: element[1] for element in elements} + + for p in packages: + logging.debug(f"Current elements: {elements}") + logging.debug(f"Checking: package: {p}") + status = "Unchecked" + for line in package_list.split('\n'): + if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": + status = "Installed" + break + elif line.strip().startswith(p) and mode == "remove": + conflicting_packages.append(p) + status = "Conflicting" + break + + if status == "Unchecked": + if mode == "install": + missing_packages.append(p) + status = "Missing" + elif mode == "remove": + status = "Not Installed" + + logging.debug(f"Setting {p}: {status}") + elements[p] = status + + logging.debug(f"DEV: {elements}") + + if app is not None and config.DIALOG == "curses": + app.report_dependencies( + f"Checking Packages (packages.index(p) + 1)/{len(packages)})", + 100 * (packages.index(p) + 1) // len(packages), + elements, + dialog=True) msg = 'None' if mode == "install": if missing_packages: msg = f"Missing packages: {' '.join(missing_packages)}" logging.info(f"Missing packages: {msg}") - return missing_packages - if mode == "remove": + return missing_packages, elements + elif mode == "remove": if conflicting_packages: msg = f"Conflicting packages: {' '.join(conflicting_packages)}" logging.info(f"Conflicting packages: {msg}") - return conflicting_packages + return conflicting_packages, elements -def install_packages(packages): +def download_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return if packages: - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {' '.join(packages)}" # noqa: E501 - logging.debug(f"install_packages cmd: {command}") - result = run_command(command) + total_packages = len(packages) + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_DOWNLOAD} {' '.join(packages)}" + logging.debug(f"download_packages cmd: {command}") + command_args = shlex.split(command) + result = run_command(command_args) + + for index, package in enumerate(packages): + status = "Downloaded" if result.returncode == 0 else "Failed" + if elements is not None: + elements[index] = (package, status) + if app is not None and config.DIALOG == "curses" and elements is not None: + app.report_dependencies(f"Downloading Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, elements, dialog=True) -def remove_packages(packages): + +def install_packages(packages, elements, app=None): + if config.SKIP_DEPENDENCIES: + return + + if packages: + total_packages = len(packages) + for index, package in enumerate(packages): + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" + logging.debug(f"install_packages cmd: {command}") + result = run_command(command) + + if elements is not None: + elements[index] = ( + package, + "Installed" if result.returncode == 0 else "Failed") + + if app is not None and config.DIALOG == "curses" and elements is not None: + app.report_dependencies( + f"Installing Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, + elements, + dialog=True) + + +def remove_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return if packages: - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {' '.join(packages)}" # noqa: E501 - logging.debug(f"remove_packages cmd: {command}") - result = run_command(command) + total_packages = len(packages) + for index, package in enumerate(packages): + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {package}" + logging.debug(f"remove_packages cmd: {command}") + result = run_command(command) + + if elements is not None: + elements[index] = ( + package, + "Removed" if result.returncode == 0 else "Failed") + + if app is not None and config.DIALOG == "curses" and elements is not None: + app.report_dependencies( + f"Removing Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, + elements, + dialog=True) def have_dep(cmd): @@ -491,6 +601,42 @@ def have_dep(cmd): return False +def check_dialog_version(): + if have_dep("dialog"): + try: + result = run_command(["dialog", "--version"]) + version_info = result.strip() + if version_info.startswith("Version: "): + version_info = version_info[len("Version: "):] + return version_info + except subprocess.CalledProcessError as e: + print(f"Error running command: {e.stderr}") + except FileNotFoundError: + print("The 'dialog' command is not found. Please ensure it is installed and in your PATH.") + return None + + +def test_dialog_version(): + version = check_dialog_version() + + def parse_date(version): + try: + return version.split('-')[1] + except IndexError: + return '' + + minimum_version = "1.3-20201126-1" + + logging.debug(f"Current dialog version: {version}") + if version is not None: + minimum_version = parse_date(minimum_version) + current_version = parse_date(version) + logging.debug(f"Minimum dialog version: {minimum_version}. Installed version: {current_version}.") + return current_version > minimum_version + else: + return None + + def clean_all(): logging.info("Cleaning all temp files…") os.system("rm -fr /tmp/LBS.*") @@ -526,7 +672,7 @@ def get_user_downloads_dir(): def cli_download(uri, destination): message = f"Downloading '{uri}' to '{destination}'" logging.info(message) - msg.cli_msg(message) + msg.logos_msg(message) # Set target. if destination != destination.rstrip('/'): @@ -582,7 +728,7 @@ def logos_reuse_download( app=app, ): logging.info(f"{FILE} properties match. Using it…") - msg.cli_msg(f"Copying {FILE} into {TARGETDIR}") + msg.logos_msg(f"Copying {FILE} into {TARGETDIR}") try: shutil.copy(os.path.join(i, FILE), TARGETDIR) except shutil.SameFileError: @@ -609,7 +755,7 @@ def logos_reuse_download( file_path, app=app, ): - msg.cli_msg(f"Copying: {FILE} into: {TARGETDIR}") + msg.logos_msg(f"Copying: {FILE} into: {TARGETDIR}") try: shutil.copy(os.path.join(config.MYDOWNLOADS, FILE), TARGETDIR) except shutil.SameFileError: @@ -678,7 +824,7 @@ def postinstall_dependencies_steamos(): def preinstall_dependencies(): - if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mintn": + if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mint": preinstall_dependencies_ubuntu() elif config.OS_NAME == "Steam": preinstall_dependencies_steamos() @@ -689,21 +835,36 @@ def postinstall_dependencies(): postinstall_dependencies_steamos() -def install_dependencies(packages, badpackages, logos9_packages=None): - missing_packages = [] - conflicting_packages = [] +def install_dependencies(packages, badpackages, logos9_packages=None, app=None): + missing_packages = {} + conflicting_packages = {} package_list = [] + elements = {} + bad_elements = {} + if packages: package_list = packages.split() + bad_package_list = [] if badpackages: bad_package_list = badpackages.split() + if logos9_packages: package_list.extend(logos9_packages.split()) + if config.DIALOG == "curses" and app is not None and elements is not None: + for p in package_list: + elements[p] = "Unchecked" + if config.DIALOG == "curses" and app is not None and bad_elements is not None: + for p in bad_package_list: + bad_elements[p] = "Unchecked" + + if config.DIALOG == "curses" and app is not None: + app.report_dependencies("Checking Packages", 0, elements, dialog=True) + if config.PACKAGE_MANAGER_COMMAND_QUERY: - missing_packages = query_packages(package_list) - conflicting_packages = query_packages(bad_package_list, "remove") + missing_packages, elements = query_packages(package_list, elements, app=app) + conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", app=app) if config.PACKAGE_MANAGER_COMMAND_INSTALL: if missing_packages and conflicting_packages: @@ -729,18 +890,18 @@ def install_dependencies(packages, badpackages, logos9_packages=None): check_libs(["libfuse"]) if missing_packages: - install_packages(missing_packages) + download_packages(missing_packages, elements, app) + install_packages(missing_packages, elements, app) if conflicting_packages: # AppImage Launcher is the only known conflicting package. - remove_packages(conflicting_packages) - config.REBOOT_REQUIRED = True + remove_packages(conflicting_packages, bad_elements, app) + #config.REBOOT_REQUIRED = True + #TODO: Verify with user before executing postinstall_dependencies() if config.REBOOT_REQUIRED: - # TODO: Add resumable install functionality to speed up running the - # program after reboot. See #19. reboot() else: @@ -784,21 +945,24 @@ def check_dependencies(app=None): logging.info(f"Checking Logos {str(targetversion)} dependencies…") if app: app.status_q.put(f"Checking Logos {str(targetversion)} dependencies…") - app.root.event_generate('<>') + if config.DIALOG == "tk": + app.root.event_generate('<>') if targetversion == 10: - install_dependencies(config.PACKAGES, config.BADPACKAGES) + install_dependencies(config.PACKAGES, config.BADPACKAGES, app=app) elif targetversion == 9: install_dependencies( config.PACKAGES, config.BADPACKAGES, - config.L9PACKAGES + config.L9PACKAGES, + app=app ) else: logging.error(f"TARGETVERSION not found: {config.TARGETVERSION}.") if app: - app.root.event_generate('<>') + if config.DIALOG == "tk": + app.root.event_generate('<>') def file_exists(file_path): @@ -832,7 +996,7 @@ def get_logos_releases(app=None): app.root.event_generate(app.release_evt) return downloaded_releases - msg.cli_msg(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 + msg.logos_msg(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 @@ -841,7 +1005,8 @@ def get_logos_releases(app=None): if response_xml_bytes is None: if app: app.releases_q.put(None) - app.root.event_generate(app.release_evt) + if config.DIALOG == 'tk': + app.root.event_generate(app.release_evt) return None # Parse XML @@ -868,7 +1033,10 @@ def get_logos_releases(app=None): if app: app.releases_q.put(filtered_releases) - app.root.event_generate(app.release_evt) + if config.DIALOG == 'tk': + app.root.event_generate(app.release_evt) + elif config.DIALOG == 'curses': + app.releases_e.set() return filtered_releases @@ -914,17 +1082,17 @@ def get_wine_options(appimages, binaries, app=None) -> Union[List[List[str]], Li wine_binary_options = [] # Add AppImages to list - if config.DIALOG == 'tk': - wine_binary_options.append(f"{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}") # noqa: E501 - wine_binary_options.extend(appimages) - else: - appimage_entries = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 - wine_binary_options.append([ - "Recommended", # Code - f'{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}', # noqa: E501 - f"AppImage of Wine64 {config.RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION}" # noqa: E501 - ]) - wine_binary_options.extend(appimage_entries) + # if config.DIALOG == 'tk': + wine_binary_options.append(f"{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}") # noqa: E501 + wine_binary_options.extend(appimages) + # else: + # appimage_entries = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 + # wine_binary_options.append([ + # "Recommended", # Code + # f'{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}', # noqa: E501 + # f"AppImage of Wine64 {config.RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION}" # noqa: E501 + # ]) + # wine_binary_options.extend(appimage_entries) sorted_binaries = sorted(list(set(binaries))) logging.debug(f"{sorted_binaries=}") @@ -933,15 +1101,15 @@ def get_wine_options(appimages, binaries, app=None) -> Union[List[List[str]], Li WINEBIN_CODE, WINEBIN_DESCRIPTION = get_winebin_code_and_desc(WINEBIN_PATH) # noqa: E501 # Create wine binary option array - if config.DIALOG == 'tk': - wine_binary_options.append(WINEBIN_PATH) - else: - wine_binary_options.append( - [WINEBIN_CODE, WINEBIN_PATH, WINEBIN_DESCRIPTION] - ) - - if config.DIALOG != 'tk': - wine_binary_options.append(["Exit", "Exit", "Cancel installation."]) + # if config.DIALOG == 'tk': + wine_binary_options.append(WINEBIN_PATH) + # else: + # wine_binary_options.append( + # [WINEBIN_CODE, WINEBIN_PATH, WINEBIN_DESCRIPTION] + # ) + # + # if config.DIALOG != 'tk': + # wine_binary_options.append(["Exit", "Exit", "Cancel installation."]) logging.debug(f"{wine_binary_options=}") if app: @@ -971,7 +1139,7 @@ def install_winetricks( app=None, version=config.WINETRICKS_VERSION, ): - msg.cli_msg(f"Installing winetricks v{version}…") + msg.logos_msg(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 zip_name = f"{version}.zip" logos_reuse_download( @@ -1130,9 +1298,12 @@ def net_get(url, target=None, app=None, evt=None, q=None): def verify_downloaded_file(url, file_path, app=None, evt=None): if app: - app.root.event_generate('<>') - app.status_q.put(f"Verifying {file_path}…") - app.root.event_generate('<>') + if config.DIALOG == "tk": + app.root.event_generate('<>') + app.status_q.put(f"Verifying {file_path}…") + app.root.event_generate('<>') + else: + app.status_q.put(f"Verifying {file_path}…") res = False msg = f"{file_path} is the wrong size." right_size = same_size(url, file_path) @@ -1144,9 +1315,10 @@ def verify_downloaded_file(url, file_path, app=None, evt=None): res = True logging.info(msg) if app: - if not evt: - evt = app.check_evt - app.root.event_generate(evt) + if config.DIALOG == "tk": + if not evt: + evt = app.check_evt + app.root.event_generate(evt) return res @@ -1180,7 +1352,10 @@ def write_progress_bar(percent, screen_width=80): l_f = int(screen_width * 0.75) # progress bar length l_y = int(l_f * percent / 100) # num. of chars. complete l_n = l_f - l_y # num. of chars. incomplete - print(f" [{y * l_y}{n * l_n}] {percent:>3}%", end='\r') + if config.DIALOG == 'curses': + msg.status(f" [{y * l_y}{n * l_n}] {percent:>3}%") + else: + print(f" [{y * l_y}{n * l_n}] {percent:>3}%", end='\r') def app_is_installed(): @@ -1366,7 +1541,7 @@ def get_recommended_appimage(): def install_premade_wine_bottle(srcdir, appdir): - msg.cli_msg(f"Extracting: '{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 + msg.logos_msg(f"Extracting: '{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 shutil.unpack_archive( f"{srcdir}/{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}", appdir @@ -1642,7 +1817,7 @@ def set_appimage_symlink(app=None): confirm = tk.messagebox.askquestion("Confirmation", copy_message) tk_root.destroy() else: - confirm = tui.confirm("Confirmation", copy_message) + confirm = tui_dialog.confirm("Confirmation", copy_message) # FIXME: What if user cancels the confirmation dialog? appimage_symlink_path = Path(f"{config.APPDIR_BINDIR}/{config.APPIMAGE_LINK_SELECTION_NAME}") # noqa: E501 @@ -1708,3 +1883,40 @@ def get_downloaded_file_path(filename): logging.info(f"'{filename}' exists in {str(d)}.") return str(file_path) logging.debug(f"File not found: {filename}") + + +def send_task(app, task): + logging.debug(f"{task=}") + app.todo_q.put(task) + if config.DIALOG == 'tk': + app.root.event_generate('<>') + elif config.DIALOG == 'curses': + app.task_processor(app, task=task) + + +def grep(regexp, filepath): + fp = Path(filepath) + found = False + ct = 0 + with fp.open() as f: + for line in f: + ct += 1 + text = line.rstrip() + if re.search(regexp, text): + logging.debug(f"{filepath}:{ct}:{text}") + found = True + return found + + +def start_thread(task, daemon_bool=True, *args): + thread = threading.Thread(name=f"{task}", target=task, daemon=daemon_bool, args=args) + thread.start() + return thread + + +def str_array_to_string(text, delimeter="\n"): + try: + processed_text = delimeter.join(text) + return processed_text + except TypeError: + return text diff --git a/wine.py b/wine.py index b1dde802..15ffbb22 100644 --- a/wine.py +++ b/wine.py @@ -37,7 +37,7 @@ def wait_on(command): stderr=subprocess.PIPE, text=True ) - msg.cli_msg(f"Waiting on \"{' '.join(command)}\" to finish.", end='') + msg.logos_msg(f"Waiting on \"{' '.join(command)}\" to finish.", end='') time.sleep(1.0) while process.poll() is None: msg.logos_progress() @@ -160,7 +160,7 @@ def check_wine_version_and_branch(TESTBINARY): def initializeWineBottle(app=None): - msg.cli_msg("Initializing wine bottle...") + msg.logos_msg("Initializing wine bottle...") # Avoid wine-mono window orig_overrides = config.WINEDLLOVERRIDES @@ -171,7 +171,7 @@ def initializeWineBottle(app=None): def wine_reg_install(REG_FILE): - msg.cli_msg(f"Installing registry file: {REG_FILE}") + msg.logos_msg(f"Installing registry file: {REG_FILE}") env = get_wine_env() p = subprocess.run( [config.WINE_EXE, "regedit.exe", REG_FILE], @@ -189,7 +189,7 @@ def wine_reg_install(REG_FILE): def install_msi(): - msg.cli_msg(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.") + msg.logos_msg(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.") # Execute the .MSI exe_args = ["/i", f"{config.INSTALLDIR}/data/{config.LOGOS_EXECUTABLE}"] if config.PASSIVE is True: @@ -247,7 +247,7 @@ def run_winetricks(cmd=None): def winetricks_install(*args): cmd = [*args] - msg.cli_msg(f"Running winetricks \"{args[-1]}\"") + msg.logos_msg(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") run_wine_proc(config.WINETRICKSBIN, exe_args=cmd) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") @@ -255,7 +255,7 @@ def winetricks_install(*args): def installFonts(): - msg.cli_msg("Configuring fonts...") + msg.logos_msg("Configuring fonts…") fonts = ['corefonts', 'tahoma'] if not config.SKIP_FONTS: for f in fonts: From 98667539320684c6841707df4185da57911581ad Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 19 Jun 2024 21:06:29 -0400 Subject: [PATCH 013/253] Fix #115. --- main.py | 13 +++++++++++++ wine.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/main.py b/main.py index 565953a2..c19d9054 100755 --- a/main.py +++ b/main.py @@ -13,6 +13,8 @@ import utils import wine +processes = {} + def get_parser(): desc = "Installs FaithLife Bible Software with Wine on Linux." @@ -371,5 +373,16 @@ def main(): run_control_panel() +def close(): + logging.debug("Closing Logos on Linux.") + if len(processes) > 0: + wine.end_wine_processes() + else: + logging.debug("No processes found.") + logging.debug("Closing Logos on Linux finished.") + + if __name__ == '__main__': main() + + close() diff --git a/wine.py b/wine.py index 15ffbb22..0c36588f 100644 --- a/wine.py +++ b/wine.py @@ -11,6 +11,8 @@ import msg import utils +from main import processes + def get_pids_using_file(file_path, mode=None): # Make list (set) of pids using 'directory'. @@ -222,6 +224,8 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): stderr=subprocess.STDOUT, env=env ) + if exe is not None and isinstance(process, subprocess.Popen): + processes[exe] = process with process.stdout: for line in iter(process.stdout.readline, b''): if winecmd.endswith('winetricks'): @@ -427,3 +431,15 @@ def run_indexing(): run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) run_wine_proc(config.WINE_EXE, exe=logos_indexer_exe) run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) + + +def end_wine_processes(): + for process_name, process in processes.items(): + if isinstance(process, subprocess.Popen): + logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") + try: + process.terminate() + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() From 66b2f6ebc25b0dccfeb051fb8f38873d157b3ed8 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 20 Jun 2024 22:16:56 -0400 Subject: [PATCH 014/253] Fix #76. --- utils.py | 53 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/utils.py b/utils.py index a01e3428..dce201d9 100644 --- a/utils.py +++ b/utils.py @@ -15,6 +15,7 @@ import subprocess import sys import threading +import time import tkinter as tk import zipfile from base64 import b64encode @@ -235,24 +236,34 @@ def die(message): sys.exit(1) -def run_command(command, stdin=None, shell=False): - try: - logging.debug(f"Attempting to execute {command}") - result = subprocess.run( - command, - stdin=stdin, - check=True, - text=True, - shell=shell, - capture_output=True - ) - return result.stdout - except subprocess.CalledProcessError as e: - logging.error(f"Error occurred while executing {command}: {e}") - return None - except Exception as e: - logging.error(f"An unexpected error occurred when running {command}: {e}") - return None +def run_command(command, retries=1, delay=0, stdin=None, shell=False): + if retries < 1: + retries = 1 + + for attempt in range(retries): + try: + logging.debug(f"Attempting to execute {command}") + result = subprocess.run( + command, + stdin=stdin, + check=True, + text=True, + shell=shell, + capture_output=True + ) + return result.stdout + except subprocess.CalledProcessError as e: + logging.error(f"Error occurred while executing {command}: {e}") + if "lock" in str(e): + logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") + time.sleep(delay) + else: + raise e + except Exception as e: + logging.error(f"An unexpected error occurred when running {command}: {e}") + return None + + logging.error(f"Failed to execute after {retries} attempts: '{command}'") def reboot(): @@ -534,7 +545,7 @@ def download_packages(packages, elements, app=None): command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_DOWNLOAD} {' '.join(packages)}" logging.debug(f"download_packages cmd: {command}") command_args = shlex.split(command) - result = run_command(command_args) + result = run_command(command_args, retries=5, delay=15) for index, package in enumerate(packages): status = "Downloaded" if result.returncode == 0 else "Failed" @@ -555,7 +566,7 @@ def install_packages(packages, elements, app=None): for index, package in enumerate(packages): command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" logging.debug(f"install_packages cmd: {command}") - result = run_command(command) + result = run_command(command, retries=5, delay=15) if elements is not None: elements[index] = ( @@ -579,7 +590,7 @@ def remove_packages(packages, elements, app=None): for index, package in enumerate(packages): command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {package}" logging.debug(f"remove_packages cmd: {command}") - result = run_command(command) + result = run_command(command, retries=5, delay=15) if elements is not None: elements[index] = ( From 22940edb023b9368f483997d057d530bfddd63c8 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 21 Jun 2024 16:54:25 -0400 Subject: [PATCH 015/253] Fix #111. --- utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index dce201d9..62594465 100644 --- a/utils.py +++ b/utils.py @@ -421,7 +421,11 @@ def get_package_manager(): # See https://github.com/FaithLife-Community/LogosLinuxInstaller/pull/33#discussion_r1443623996 # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" config.QUERY_PREFIX = '.i ' - config.PACKAGES = "binutils cabextract fuse wget winbind" + if distro.id() == "ubuntu" and distro.version() < "24.04": + fuse = "fuse" + else: + fuse = "fuse3" + config.PACKAGES = f"binutils cabextract {fuse} wget winbind" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora From 8a661beb8b24e45dca366d0e16f089156820ff70 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 6 Jul 2024 20:31:24 -0400 Subject: [PATCH 016/253] Anticipate #118 --- tui_app.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tui_app.py b/tui_app.py index 14eb50c5..e7f65611 100644 --- a/tui_app.py +++ b/tui_app.py @@ -270,6 +270,12 @@ def choice_processor(self, stdscr, screen_id, choice): control.set_winetricks() elif choice == "Run Winetricks": wine.run_winetricks() + elif choice == "Install d3dcompiler": + wine.installD3DCompiler() + elif choice == "Install Fonts": + wine.installFonts() + elif choice == "Install ICU": + wine.installICUDataFiles() elif choice.endswith("Logging"): wine.switch_logging() else: @@ -493,7 +499,10 @@ def set_tui_menu_options(self, curses=False): labels_support = [ "Install Dependencies", "Download or Update Winetricks", - "Run Winetricks" + "Run Winetricks", + "Install d3dcompiler", + "Install Fonts", + "Install ICU" ] labels.extend(labels_support) From e80f984a466eab0d3580def8cf187ab952af5ec7 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 13 Jun 2024 21:04:49 -0400 Subject: [PATCH 017/253] Update Version and Changelog --- CHANGELOG.md | 3 +++ config.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71cfe075..8657ac97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +- 4.0.0-alpha.9 + - Fix #42 [T. H. Wright] + - Fix #76, #104 [T. H. Wright] - 4.0.0-alpha.8 - Fix #1 [T. H. Wright, N. Marti, T. Bleher, C. Reeder] - Fix #102 [T. H. Wright] diff --git a/config.py b/config.py index a99edac1..668f1a15 100644 --- a/config.py +++ b/config.py @@ -58,7 +58,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.8" +LLI_CURRENT_VERSION = "4.0.0-alpha.9" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING From 2387f2a6746647c605bc80a3a46a49d7fc68bbd0 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 6 Jul 2024 13:06:01 -0400 Subject: [PATCH 018/253] Fix #121. --- utils.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/utils.py b/utils.py index 62594465..92af3d4a 100644 --- a/utils.py +++ b/utils.py @@ -417,8 +417,6 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_INSTALL = "apt install -y" config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "apt install --download-only -y" config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" - # IDEA: Switch to Python APT library? - # See https://github.com/FaithLife-Community/LogosLinuxInstaller/pull/33#discussion_r1443623996 # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" config.QUERY_PREFIX = '.i ' if distro.id() == "ubuntu" and distro.version() < "24.04": @@ -791,16 +789,16 @@ def delete_symlink(symlink_path): def preinstall_dependencies_ubuntu(): try: - run_command(["sudo", "dpkg", "--add-architecture", "i386"]) - run_command(["sudo", "mkdir", "-pm755", "/etc/apt/keyrings"]) + run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) + run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) run_command( - ["sudo", "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", "https://dl.winehq.org/wine-builds/winehq.key"]) + [config.SUPERUSER_COMMAND, "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", "https://dl.winehq.org/wine-builds/winehq.key"]) lsboutput = run_command(["lsb_release", "-a"]) codename = [line for line in lsboutput.split('\n') if "Description" in line][0].split()[1].strip() - run_command(["sudo", "wget", "-NP", "/etc/apt/sources.list.d/", + run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources"]) - run_command(["sudo", "apt", "update"]) - run_command(["sudo", "apt", "install", "--install-recommends", "winehq-staging"]) + run_command([config.SUPERUSER_COMMAND, "apt", "update"]) + run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) except subprocess.CalledProcessError as e: print(f"An error occurred: {e}") print(f"Command output: {e.output}") From 172f67ecc0f77fb2e74105df305a749334a5d8c5 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 6 Jul 2024 13:48:16 -0400 Subject: [PATCH 019/253] Add utils.get_logos_version() --- config.py | 6 +++++- utils.py | 26 ++++++++++++++++++++++++++ wine.py | 5 ++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index 668f1a15..2863b533 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,11 @@ # Define and set variables that are required in the config file. core_config_keys = [ +<<<<<<< HEAD "FLPRODUCT", "FLPRODUCTi", "TARGETVERSION", "LOGOS_RELEASE_VERSION", +======= + "current_logos_version", "FLPRODUCT", "TARGETVERSION", "LOGOS_RELEASE_VERSION", +>>>>>>> 1a0c240 (Add utils.get_logos_version()) "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION" @@ -58,7 +62,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.9" +LLI_CURRENT_VERSION = "4.0.0-alpha.10" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING diff --git a/utils.py b/utils.py index 92af3d4a..3f910333 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,6 @@ import atexit import distro +import glob import hashlib import json import logging @@ -986,6 +987,26 @@ def file_exists(file_path): return False +def get_logos_version(): + path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" + file_paths = glob.glob(path_regex) + if file_paths: + logos_version_file = file_paths[0] + with open(logos_version_file, 'r') as json_file: + json_data = json.load(json_file) + + dependencies = json_data["targets"]['.NETCoreApp,Version=v6.0/win10-x64']['Logos/1.0.0']['dependencies'] + logos_version_number = dependencies.get("LogosUpdater.Reference") + + if logos_version_number is not None: + return logos_version_number + else: + logging.debug("Couldn't determine installed Logos version.") + return None + else: + logging.debug(f"Logos.deps.json not found.") + + def check_logos_release_version(version, threshold, check_version_part): version_parts = list(map(int, version.split('.'))) return version_parts[check_version_part - 1] < threshold @@ -1516,6 +1537,11 @@ def check_for_updates(): # order to avoid GitHub API limits. This sets the check to once every 12 # hours. + config.current_logos_version = get_logos_version() + write_config(config.CONFIG_FILE) + + # TODO: Check for New Logos Versions. See #116. + now = datetime.now().replace(microsecond=0) if config.CHECK_UPDATES: check_again = now diff --git a/wine.py b/wine.py index 0c36588f..7857173f 100644 --- a/wine.py +++ b/wine.py @@ -112,7 +112,10 @@ def check_wine_version_and_branch(TESTBINARY): # Does not check for Staging. Will not implement: expecting merging of # commits in time. if config.TARGETVERSION == "10": - WINE_MINIMUM = [7, 18] + if utils.check_logos_release_version(config.current_logos_version, 30, 1): + WINE_MINIMUM = [7, 18] + else: + WINE_MINIMUM = [9, 10] elif config.TARGETVERSION == "9": WINE_MINIMUM = [7, 0] else: From aec00139f91c9adb63537a47e9ebff8932fd530a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 6 Jul 2024 19:00:25 -0400 Subject: [PATCH 020/253] Permit Wine 9.10+ --- wine.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wine.py b/wine.py index 7857173f..74327483 100644 --- a/wine.py +++ b/wine.py @@ -140,7 +140,7 @@ def check_wine_version_and_branch(TESTBINARY): return False, "Can't use Stable release" elif wine_release[0] < 7: return False, "Version is < 7.0" - elif wine_release[0] < 8: + elif wine_release[0] == 7: if ( "Proton" in TESTBINARY or ("Proton" in os.path.realpath(TESTBINARY) if os.path.islink(TESTBINARY) else False) # noqa: E501 @@ -152,12 +152,17 @@ def check_wine_version_and_branch(TESTBINARY): elif wine_release[1] < WINE_MINIMUM[1]: reason = f"{'.'.join(wine_release)} is below minimum required, {'.'.join(WINE_MINIMUM)}" # noqa: E501 return False, reason - elif wine_release[0] < 9: + elif wine_release[0] == 8: if wine_release[1] < 1: return False, "Version is 8.0" elif wine_release[1] < 16: if wine_release[2] != 'staging': return False, "Version < 8.16 needs to be Staging release" + elif wine_release[0] == 9: + if wine_release[1] < 10: + return False, "Version < 9.10" + elif wine_release[0] > 9: + pass else: return False, error_message From f9f985593d0f4810534c3d6039208e5c172541a8 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 6 Jul 2024 20:46:50 -0400 Subject: [PATCH 021/253] Detect if Wine too low for Logos. --- utils.py | 20 ++++++++++++++++++-- wine.py | 18 +++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/utils.py b/utils.py index 3f910333..934f2836 100644 --- a/utils.py +++ b/utils.py @@ -987,7 +987,7 @@ def file_exists(file_path): return False -def get_logos_version(): +def get_current_logos_version(): path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" file_paths = glob.glob(path_regex) if file_paths: @@ -1007,6 +1007,22 @@ def get_logos_version(): logging.debug(f"Logos.deps.json not found.") +def convert_logos_release(logos_release): + if logos_release is not None: + ver_major = logos_release.split('.')[0] + ver_minor = logos_release.split('.')[1] + release = logos_release.split('.')[2] + point = logos_release.split('.')[3] + else: + ver_major = 0 + ver_minor = 0 + release = 0 + point = 0 + + logos_release_arr = [int(ver_major), int(ver_minor), int(release), int(point)] + return logos_release_arr + + def check_logos_release_version(version, threshold, check_version_part): version_parts = list(map(int, version.split('.'))) return version_parts[check_version_part - 1] < threshold @@ -1537,7 +1553,7 @@ def check_for_updates(): # order to avoid GitHub API limits. This sets the check to once every 12 # hours. - config.current_logos_version = get_logos_version() + config.current_logos_version = get_current_logos_version() write_config(config.CONFIG_FILE) # TODO: Check for New Logos Versions. See #116. diff --git a/wine.py b/wine.py index 74327483..b2dae2fa 100644 --- a/wine.py +++ b/wine.py @@ -67,7 +67,6 @@ def heavy_wineserver_wait(): utils.wait_process_using_dir(config.WINEPREFIX) wait_on([f"{config.WINESERVER_EXE}", "-w"]) - def get_wine_release(binary): cmd = [binary, "--version"] try: @@ -425,8 +424,21 @@ def get_wine_env(): def run_logos(): - run_wine_proc(config.WINE_EXE, exe=config.LOGOS_EXE) - run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) + logos_release = utils.convert_logos_release(config.current_logos_version) + wine_release = get_wine_release(config.WINE_EXE) + + #TODO: Find a way to incorporate check_wine_version_and_branch() + if logos_release[0] < 30 and logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): + txt = "Can't run Logos 10+ with Wine below 7.18." + logging.critical(txt) + msg.status(txt) + if logos_release[0] > 29 and wine_release[0] < 9 and wine_release[1] < 10: + txt = "Can't run Logos 30+ with Wine below 9.10." + logging.critical(txt) + msg.status(txt) + else: + run_wine_proc(config.WINE_EXE, exe=config.LOGOS_EXE) + run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) def run_indexing(): From a13cc67dab3ce501bb1b5c142ff6058ea01f1d9e Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 8 Jul 2024 13:41:05 -0400 Subject: [PATCH 022/253] Fix config checking w/ current Logos release. - Fix tui_app.TUI.report_dependencies(). --- config.py | 7 ++----- gui.py | 4 ++-- gui_app.py | 25 ++++++++++++++----------- installer.py | 18 ++++++++++-------- tui_app.py | 24 +++++++++++++++--------- tui_curses.py | 8 ++++++-- tui_screen.py | 9 ++++++--- utils.py | 27 ++++++++++++++++++++------- wine.py | 14 +++++++------- 9 files changed, 82 insertions(+), 54 deletions(-) diff --git a/config.py b/config.py index 2863b533..0de21fa5 100644 --- a/config.py +++ b/config.py @@ -6,11 +6,8 @@ # Define and set variables that are required in the config file. core_config_keys = [ -<<<<<<< HEAD - "FLPRODUCT", "FLPRODUCTi", "TARGETVERSION", "LOGOS_RELEASE_VERSION", -======= - "current_logos_version", "FLPRODUCT", "TARGETVERSION", "LOGOS_RELEASE_VERSION", ->>>>>>> 1a0c240 (Add utils.get_logos_version()) + "FLPRODUCT", "FLPRODUCTi", "TARGETVERSION", "TARGET_RELEASE_VERSION", + "current_logos_version", "FLPRODUCT", "TARGETVERSION", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION" diff --git a/gui.py b/gui.py index b39fbf9c..aa66a48f 100644 --- a/gui.py +++ b/gui.py @@ -26,7 +26,7 @@ def __init__(self, root, **kwargs): # Initialize vars from ENV. self.flproduct = config.FLPRODUCT self.targetversion = config.TARGETVERSION - self.logos_release_version = config.LOGOS_RELEASE_VERSION + self.logos_release_version = config.TARGET_RELEASE_VERSION self.default_config_path = config.DEFAULT_CONFIG_PATH self.wine_exe = config.WINE_EXE self.winetricksbin = config.WINETRICKSBIN @@ -153,7 +153,7 @@ def __init__(self, root, *args, **kwargs): self.installdir = config.INSTALLDIR self.flproduct = config.FLPRODUCT self.targetversion = config.TARGETVERSION - self.logos_release_version = config.LOGOS_RELEASE_VERSION + self.logos_release_version = config.TARGET_RELEASE_VERSION self.logs = config.LOGS self.config_file = config.CONFIG_FILE diff --git a/gui_app.py b/gui_app.py index e1efaed0..3160597b 100644 --- a/gui_app.py +++ b/gui_app.py @@ -248,7 +248,7 @@ def todo(self, evt=None, task=None): if not self.gui.versionvar.get(): self.gui.versionvar.set(self.gui.version_dropdown['values'][1]) self.set_version() - elif task == 'LOGOS_RELEASE_VERSION': + elif task == 'TARGET_RELEASE_VERSION': # Disable all input widgets after Release. widgets = [ self.gui.wine_dropdown, @@ -272,7 +272,7 @@ def todo(self, evt=None, task=None): self.gui.okay_button, ] self.set_input_widgets_state('disabled', widgets=widgets) - self.start_wine_versions_check() + self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) elif task == 'WINETRICKSBIN': # Disable all input widgets after Winetricks. widgets = [ @@ -312,7 +312,7 @@ def set_version(self, evt=None): logging.debug(f"Change TARGETVERSION to {self.gui.targetversion}") config.TARGETVERSION = None self.gui.releasevar.set('') - config.LOGOS_RELEASE_VERSION = None + config.TARGET_RELEASE_VERSION = None config.INSTALLDIR = None config.WINE_EXE = None config.WINETRICKSBIN = None @@ -349,12 +349,12 @@ def set_release(self, evt=None): self.gui.logos_release_version = self.gui.releasevar.get() self.gui.release_dropdown.selection_clear() if evt: # manual override - config.LOGOS_RELEASE_VERSION = None + config.TARGET_RELEASE_VERSION = None self.start_ensure_config() else: self.release_q.put(self.gui.logos_release_version) - def start_find_appimage_files(self): + def start_find_appimage_files(self, release_version): # Setup queue, signal, thread. self.appimage_q = Queue() self.appimage_evt = "<>" @@ -364,7 +364,10 @@ def start_find_appimage_files(self): ) self.appimage_thread = Thread( target=utils.find_appimage_files, - kwargs={'app': self}, + kwargs={ + 'release_version': release_version, + 'app': self + }, daemon=True ) # Start progress. @@ -374,9 +377,9 @@ def start_find_appimage_files(self): # Start thread. self.appimage_thread.start() - def start_wine_versions_check(self): + def start_wine_versions_check(self, release_version): if not self.appimages: - self.start_find_appimage_files() + self.start_find_appimage_files(release_version) return # Setup queue, signal, thread. self.wines_q = Queue() @@ -389,7 +392,7 @@ def start_wine_versions_check(self): target=utils.get_wine_options, args=[ self.appimages, - utils.find_wine_binary_files() + utils.find_wine_binary_files(release_version) ], kwargs={'app': self}, daemon=True @@ -424,7 +427,7 @@ def on_release_check_released(self, evt=None): def on_wine_check_released(self, evt=None): self.gui.wine_check_button.state(['disabled']) - self.start_wine_versions_check() + self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) def set_skip_fonts(self, evt=None): self.gui.skip_fonts = 1 - self.gui.fontsvar.get() # invert True/False @@ -481,7 +484,7 @@ def update_find_appimage_progress(self, evt=None): self.stop_indeterminate_progress() if not self.appimage_q.empty(): self.appimages = self.appimage_q.get() - self.start_wine_versions_check() + self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) def update_wine_check_progress(self, evt=None): if evt and self.wines_q.empty(): diff --git a/installer.py b/installer.py index b03cdd12..9ff2bd17 100644 --- a/installer.py +++ b/installer.py @@ -48,6 +48,7 @@ def ensure_version_choice(app=None): if config.DIALOG == 'curses': app.version_e.wait() config.TARGETVERSION = app.version_q.get() + logging.debug(f"> {config.TARGETVERSION=}") @@ -56,16 +57,17 @@ def ensure_release_choice(app=None): ensure_version_choice(app=app) config.INSTALL_STEP += 1 update_install_feedback("Choose product release…", app=app) - logging.debug('- config.LOGOS_RELEASE_VERSION') + logging.debug('- config.TARGET_RELEASE_VERSION') - if not config.LOGOS_RELEASE_VERSION: + if not config.TARGET_RELEASE_VERSION: if app: - utils.send_task(app, 'LOGOS_RELEASE_VERSION') + utils.send_task(app, 'TARGET_RELEASE_VERSION') if config.DIALOG == 'curses': app.release_e.wait() - config.LOGOS_RELEASE_VERSION = app.release_q.get() - logging.debug(f"{config.LOGOS_RELEASE_VERSION=}") - logging.debug(f"> {config.LOGOS_RELEASE_VERSION=}") + config.TARGET_RELEASE_VERSION = app.release_q.get() + logging.debug(f"{config.TARGET_RELEASE_VERSION=}") + + logging.debug(f"> {config.TARGET_RELEASE_VERSION=}") def ensure_install_dir_choice(app=None): @@ -189,9 +191,9 @@ def ensure_installation_config(app=None): logos_icon_url = app_dir / 'img' / f"{config.FLPRODUCTi}-128-icon.png" config.LOGOS_ICON_URL = str(logos_icon_url) config.LOGOS_ICON_FILENAME = logos_icon_url.name - config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{config.TARGETVERSION}{config.VERBUM_PATH}Installer/{config.LOGOS_RELEASE_VERSION}/{config.FLPRODUCT}-x64.msi" # noqa: E501 + config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{config.TARGETVERSION}{config.VERBUM_PATH}Installer/{config.TARGET_RELEASE_VERSION}/{config.FLPRODUCT}-x64.msi" # noqa: E501 - config.LOGOS_VERSION = config.LOGOS_RELEASE_VERSION + config.LOGOS_VERSION = config.TARGET_RELEASE_VERSION config.LOGOS64_MSI = Path(config.LOGOS64_URL).name logging.debug(f"> {config.LOGOS_ICON_URL=}") diff --git a/tui_app.py b/tui_app.py index e7f65611..1283d7f6 100644 --- a/tui_app.py +++ b/tui_app.py @@ -25,7 +25,11 @@ class TUI(): def __init__(self, stdscr): self.stdscr = stdscr + #if config.current_logos_version is not None: self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" + self.subtitle = f"Logos Version: {config.current_logos_version}" + #else: + # self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" self.console_message = "Starting TUI…" self.running = True self.choice = "Processing" @@ -106,7 +110,7 @@ def init_curses(self): curses.cbreak() self.stdscr.keypad(True) - self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title) + self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, "Main Menu", self.set_tui_menu_options(curses=True)) #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", @@ -200,7 +204,7 @@ def task_processor(self, evt=None, task=None): utils.start_thread(self.get_product(config.use_python_dialog)) elif task == 'TARGETVERSION': utils.start_thread(self.get_version(config.use_python_dialog)) - elif task == 'LOGOS_RELEASE_VERSION': + elif task == 'TARGET_RELEASE_VERSION': utils.start_thread(self.get_release(config.use_python_dialog)) elif task == 'INSTALLDIR': utils.start_thread(self.get_installdir(config.use_python_dialog)) @@ -259,7 +263,7 @@ def choice_processor(self, stdscr, screen_id, choice): utils.update_to_latest_recommended_appimage() elif choice == "Set AppImage": # TODO: Allow specifying the AppImage File - appimages = utils.find_appimage_files() + appimages = utils.find_appimage_files(utils.which_release()) appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) @@ -309,8 +313,8 @@ def choice_processor(self, stdscr, screen_id, choice): self.version_e.set() elif screen_id == 4: if choice: - config.LOGOS_RELEASE_VERSION = choice - self.release_q.put(config.LOGOS_RELEASE_VERSION) + config.TARGET_RELEASE_VERSION = choice + self.release_q.put(config.TARGET_RELEASE_VERSION) self.release_e.set() elif screen_id == 5: if choice: @@ -381,7 +385,7 @@ def get_release(self, dialog): options = self.releases_q.get() if options is None: - msg.logos_error("Failed to fetch LOGOS_RELEASE_VERSION.") + msg.logos_error("Failed to fetch TARGET_RELEASE_VERSION.") options.append("Exit") enumerated_options = [(str(i), option) for i, option in enumerate(options, start=1)] self.menu_options = enumerated_options @@ -398,10 +402,10 @@ def get_installdir(self, dialog): def get_wine(self, dialog): self.installdir_e.wait() self.stack_text(10, self.wine_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog) - question = f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.LOGOS_RELEASE_VERSION} in {config.INSTALLDIR}?" # noqa: E501 + question = f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?" # noqa: E501 options = utils.get_wine_options( - utils.find_appimage_files(), - utils.find_wine_binary_files() + utils.find_appimage_files(config.TARGET_RELEASE_VERSION), + utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) ) max_length = max(len(option) for option in options) max_length += len(str(len(options))) + 10 @@ -462,6 +466,8 @@ def report_dependencies(self, text, percent, elements, dialog): if dialog: self.stack_tasklist(11, self.deps_q, self.deps_e, text, elements, percent, dialog=dialog) self.switch_screen(dialog) + # Without this delay, the reporting works too quickly and instead appears all at once. + time.sleep(0.1) else: #TODO pass diff --git a/tui_curses.py b/tui_curses.py index 9edd16ff..28ced7a5 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -15,13 +15,17 @@ def wrap_text(app, text): return lines -def title(app, title_text): +def title(app, title_text, title_start_y_adj): stdscr = app.get_main_window() title_lines = wrap_text(app, title_text) title_start_y = max(0, app.window_height // 2 - len(title_lines) // 2) + last_index = 0 for i, line in enumerate(title_lines): if i < app.window_height: - stdscr.addstr(i, 2, line, curses.A_BOLD) + stdscr.addstr(i + title_start_y_adj, 2, line, curses.A_BOLD) + last_index = i + + return last_index def text_centered(app, text, start_y=0): diff --git a/tui_screen.py b/tui_screen.py index 730693ca..6e2569eb 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -63,16 +63,19 @@ class DialogScreen(Screen): class ConsoleScreen(CursesScreen): - def __init__(self, app, screen_id, queue, event, title): + def __init__(self, app, screen_id, queue, event, title, subtitle, title_start_y): super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_main_window() self.title = title + self.subtitle = subtitle + self.title_start_y = title_start_y def display(self): self.stdscr.erase() - tui_curses.title(self.app, self.title) + subtitle_start = tui_curses.title(self.app, self.title, self.title_start_y) + tui_curses.title(self.app, self.subtitle, subtitle_start + 1) - self.stdscr.addstr(2, 2, f"---Console---") + self.stdscr.addstr(3, 2, f"---Console---") recent_messages = logging.console_log[-6:] for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) diff --git a/utils.py b/utils.py index 934f2836..1360a295 100644 --- a/utils.py +++ b/utils.py @@ -170,6 +170,9 @@ def write_config(config_file_path): config_data = {key: config.__dict__.get(key) for key in config.core_config_keys} # noqa: E501 try: + for key, value in config_data.items(): + if isinstance(value, Path): + config_data[key] = str(value) with open(config_file_path, 'w') as config_file: json.dump(config_data, config_file, indent=4, sort_keys=True) config_file.write('\n') @@ -521,7 +524,7 @@ def query_packages(packages, elements=None, mode="install", app=None): if app is not None and config.DIALOG == "curses": app.report_dependencies( - f"Checking Packages (packages.index(p) + 1)/{len(packages)})", + f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", 100 * (packages.index(p) + 1) // len(packages), elements, dialog=True) @@ -1023,9 +1026,19 @@ def convert_logos_release(logos_release): return logos_release_arr +def which_release(): + if config.current_logos_release: + return config.current_logos_release + else: + return config.TARGET_RELEASE_VERSION + + def check_logos_release_version(version, threshold, check_version_part): - version_parts = list(map(int, version.split('.'))) - return version_parts[check_version_part - 1] < threshold + if version is not None: + version_parts = list(map(int, version.split('.'))) + return version_parts[check_version_part - 1] < threshold + else: + return False def filter_versions(versions, threshold, check_version_part): @@ -1770,7 +1783,7 @@ def check_appimage(filestr): return False -def find_appimage_files(app=None): +def find_appimage_files(release_version, app=None): appimages = [] directories = [ os.path.expanduser("~") + "/bin", @@ -1787,7 +1800,7 @@ def find_appimage_files(app=None): appimage_paths = Path(d).rglob('wine*.appimage', case_sensitive=False) for p in appimage_paths: if p is not None and check_appimage(p): - output1, output2 = wine.check_wine_version_and_branch(p) + output1, output2 = wine.check_wine_version_and_branch(release_version, p) if output1 is not None and output1: appimages.append(str(p)) else: @@ -1800,7 +1813,7 @@ def find_appimage_files(app=None): return appimages -def find_wine_binary_files(): +def find_wine_binary_files(release_version): wine_binary_path_list = [ "/usr/local/bin", os.path.expanduser("~") + "/bin", @@ -1827,7 +1840,7 @@ def find_wine_binary_files(): binaries.append(binary_path) for binary in binaries[:]: - output1, output2 = wine.check_wine_version_and_branch(binary) + output1, output2 = wine.check_wine_version_and_branch(release_version, binary) if output1 is not None and output1: continue else: diff --git a/wine.py b/wine.py index b2dae2fa..b16a8b93 100644 --- a/wine.py +++ b/wine.py @@ -107,11 +107,11 @@ def get_wine_release(binary): return False, f"Error: {e}" -def check_wine_version_and_branch(TESTBINARY): +def check_wine_version_and_branch(release_version, test_binary): # Does not check for Staging. Will not implement: expecting merging of # commits in time. if config.TARGETVERSION == "10": - if utils.check_logos_release_version(config.current_logos_version, 30, 1): + if utils.check_logos_release_version(release_version, 30, 1): WINE_MINIMUM = [7, 18] else: WINE_MINIMUM = [9, 10] @@ -123,16 +123,16 @@ def check_wine_version_and_branch(TESTBINARY): # Check if the binary is executable. If so, check if TESTBINARY's version # is ≥ WINE_MINIMUM, or if it is Proton or a link to a Proton binary, else # remove. - if not os.path.exists(TESTBINARY): + if not os.path.exists(test_binary): reason = "Binary does not exist." return False, reason - if not os.access(TESTBINARY, os.X_OK): + if not os.access(test_binary, os.X_OK): reason = "Binary is not executable." return False, reason wine_release = [] - wine_release, error_message = get_wine_release(TESTBINARY) + wine_release, error_message = get_wine_release(test_binary) if wine_release is not False and error_message is not None: if wine_release[2] == 'stable': @@ -141,8 +141,8 @@ def check_wine_version_and_branch(TESTBINARY): return False, "Version is < 7.0" elif wine_release[0] == 7: if ( - "Proton" in TESTBINARY - or ("Proton" in os.path.realpath(TESTBINARY) if os.path.islink(TESTBINARY) else False) # noqa: E501 + "Proton" in test_binary + or ("Proton" in os.path.realpath(test_binary) if os.path.islink(test_binary) else False) # noqa: E501 ): if wine_release[1] == 0: return True, "None" From fd5e4bbdf672ca3ffb6ab513ef9ae5f5047d75db Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 2 Jul 2024 18:01:42 +0100 Subject: [PATCH 023/253] use appimage .desktop file to get wine branch --- wine.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/wine.py b/wine.py index b16a8b93..9dc4e8aa 100644 --- a/wine.py +++ b/wine.py @@ -373,13 +373,22 @@ def get_wine_branch(binary): stdout=subprocess.PIPE, encoding='UTF8' ) + branch = None while p.returncode is None: for line in p.stdout: if line.startswith('/tmp'): tmp_dir = Path(line.rstrip()) - for f in tmp_dir.glob('**/lib64/**/mscoree.dll'): - branch = get_mscoree_winebranch(f) - break + for f in tmp_dir.glob('org.winehq.wine.desktop'): + if not branch: + for dline in f.read_text().splitlines(): + try: + k, v = dline.split('=') + except ValueError: # not a key=value line + continue + if k == 'X-AppImage-Version': + branch = v.split('_')[0] + logging.debug(f"{branch=}") + break p.send_signal(signal.SIGINT) p.poll() return branch From 9d0b92ef3e533b7785ff3aec83dd7a7884450120 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 9 Jul 2024 08:40:25 -0400 Subject: [PATCH 024/253] Fix #124. Regression. --- main.py | 5 +++-- utils.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index c19d9054..fba08dc5 100755 --- a/main.py +++ b/main.py @@ -316,12 +316,13 @@ def main(): config.use_python_dialog = utils.test_dialog_version() if config.use_python_dialog is None: - logging.debug("The 'dialog' package was not found.") + logging.debug("The 'dialog' package was not found. Please install it or use the GUI.") + config.use_python_dialog = False # In order to prevent errors, make sure now to set dialog to False. elif config.use_python_dialog: logging.debug("Dialog version is up-to-date.") config.use_python_dialog = True else: - logging.error("Dialog version is outdated. Please use the GUI.") + logging.error("Dialog version is outdated. The program will fall back to Curses.") config.use_python_dialog = False # Log persistent config. diff --git a/utils.py b/utils.py index 1360a295..aceb1e1d 100644 --- a/utils.py +++ b/utils.py @@ -527,7 +527,7 @@ def query_packages(packages, elements=None, mode="install", app=None): f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", 100 * (packages.index(p) + 1) // len(packages), elements, - dialog=True) + dialog=config.use_python_dialog) msg = 'None' if mode == "install": @@ -560,7 +560,7 @@ def download_packages(packages, elements, app=None): if app is not None and config.DIALOG == "curses" and elements is not None: app.report_dependencies(f"Downloading Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, elements, dialog=True) + 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog) def install_packages(packages, elements, app=None): @@ -584,7 +584,7 @@ def install_packages(packages, elements, app=None): f"Installing Packages ({index + 1}/{total_packages})", 100 * (index + 1) // total_packages, elements, - dialog=True) + dialog=config.use_python_dialog) def remove_packages(packages, elements, app=None): @@ -608,7 +608,7 @@ def remove_packages(packages, elements, app=None): f"Removing Packages ({index + 1}/{total_packages})", 100 * (index + 1) // total_packages, elements, - dialog=True) + dialog=config.use_python_dialog) def have_dep(cmd): @@ -877,7 +877,7 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): bad_elements[p] = "Unchecked" if config.DIALOG == "curses" and app is not None: - app.report_dependencies("Checking Packages", 0, elements, dialog=True) + app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) if config.PACKAGE_MANAGER_COMMAND_QUERY: missing_packages, elements = query_packages(package_list, elements, app=app) From 8d06d7e6a54256c13a40cd908f5d2388691c592d Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 6 Jul 2024 20:13:05 -0400 Subject: [PATCH 025/253] Update Version and Changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8657ac97..72e203e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ # Changelog +- 4.0.0-alpha.10 + - Fix #121 [T. H. Wright] + - Prep for Logos 30+ support [N. Marti, T. H. Wright] - 4.0.0-alpha.9 - Fix #42 [T. H. Wright] - - Fix #76, #104 [T. H. Wright] + - Fix #76, #104, #111, #115 [T. H. Wright] - 4.0.0-alpha.8 - Fix #1 [T. H. Wright, N. Marti, T. Bleher, C. Reeder] - Fix #102 [T. H. Wright] From cd81d24df76f43f6b047d2776ca808c9fe72262a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 11 Jul 2024 08:34:35 -0400 Subject: [PATCH 026/253] Fix #124 - Update tui_screen print strings - Fix divide by 0 issue - Switch to use of queues for choice processing - Switch to use of queues for screen stacking/switching - Disable curses try/except errors for fuller debugging - Switch to which_dialog_options - Fix display issues and menu switching - Fix PyDialog Menu cancel button --- config.py | 2 +- installer.py | 4 ++ main.py | 14 ++-- msg.py | 2 +- tui_app.py | 178 ++++++++++++++++++++++++-------------------------- tui_curses.py | 6 +- tui_dialog.py | 4 +- tui_screen.py | 76 ++++++++++++++------- 8 files changed, 156 insertions(+), 130 deletions(-) diff --git a/config.py b/config.py index 0de21fa5..392d6fb6 100644 --- a/config.py +++ b/config.py @@ -103,7 +103,7 @@ current_page = 0 total_pages = 0 options_per_page = 8 -use_python_dialog = True +use_python_dialog = False def get_config_file_dict(config_file_path): diff --git a/installer.py b/installer.py index 9ff2bd17..ac2b6956 100644 --- a/installer.py +++ b/installer.py @@ -9,6 +9,10 @@ import utils import wine +#TODO: Fix install progress if user returns to main menu? +# To replicate, start a TUI install, return/cancel on second step +# Then launch a new install + def ensure_product_choice(app=None): config.INSTALL_STEPS_COUNT += 1 diff --git a/main.py b/main.py index fba08dc5..31f3fc95 100755 --- a/main.py +++ b/main.py @@ -258,12 +258,12 @@ def run_control_panel(): if config.DIALOG is None or config.DIALOG == 'tk': gui_app.control_panel_app() else: - try: + #try: curses.wrapper(tui_app.control_panel_app) - except curses.error as e: - logging.error(f"Curses error in run_control_panel(): {e}") - except Exception as e: - logging.error(f"An error occurred in run_control_panel(): {e}") + #except curses.error as e: + # logging.error(f"Curses error in run_control_panel(): {e}") + #except Exception as e: + # logging.error(f"An error occurred in run_control_panel(): {e}") def main(): @@ -316,8 +316,8 @@ def main(): config.use_python_dialog = utils.test_dialog_version() if config.use_python_dialog is None: - logging.debug("The 'dialog' package was not found. Please install it or use the GUI.") - config.use_python_dialog = False # In order to prevent errors, make sure now to set dialog to False. + logging.debug("The 'dialog' package was not found. Falling back to Python Curses.") + config.use_python_dialog = False elif config.use_python_dialog: logging.debug("Dialog version is up-to-date.") config.use_python_dialog = True diff --git a/msg.py b/msg.py index 68d064dc..33a0c6b8 100644 --- a/msg.py +++ b/msg.py @@ -258,7 +258,7 @@ def status(text, app=None): elif config.DIALOG == 'curses': if app is not None: app.status_q.put(f"{timestamp} {text}") - app.report_waiting(f"{app.status_q.get()}", dialog=True) + app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) else: '''Prints message to stdout regardless of log level.''' logos_msg(text) diff --git a/tui_app.py b/tui_app.py index 1283d7f6..1a4490b1 100644 --- a/tui_app.py +++ b/tui_app.py @@ -31,8 +31,7 @@ def __init__(self, stdscr): #else: # self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" self.console_message = "Starting TUI…" - self.running = True - self.choice = "Processing" + self.llirunning = True self.active_progress = False # Queues @@ -47,6 +46,9 @@ def __init__(self, stdscr): self.progress_e = threading.Event() self.todo_q = Queue() self.todo_e = threading.Event() + self.screen_q = Queue() + self.choice_q = Queue() + self.switch_q = Queue() # Install and Options self.product_q = Queue() @@ -112,9 +114,9 @@ def init_curses(self): self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, - "Main Menu", self.set_tui_menu_options(curses=True)) + "Main Menu", self.set_tui_menu_options(dialog=False)) #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", - # self.set_tui_menu_options(curses=False)) + # self.set_tui_menu_options(dialog=True)) self.main_window.noutrefresh() self.menu_window.noutrefresh() @@ -167,10 +169,9 @@ def display(self): signal.signal(signal.SIGINT, self.end) msg.initialize_curses_logging(self.stdscr) msg.status(self.console_message, self) + self.active_screen = self.menu_screen - while self.running: - self.active_screen = self.tui_screens[-1] if self.tui_screens else self.menu_screen - + while self.llirunning: if isinstance(self.active_screen, tui_screen.CursesScreen): self.main_window.erase() self.menu_window.erase() @@ -179,17 +180,31 @@ def display(self): self.active_screen.display() - if (not isinstance(self.active_screen, tui_screen.TextScreen) - and not isinstance(self.active_screen, tui_screen.TextDialog)): + #if (not isinstance(self.active_screen, tui_screen.TextScreen) + # and not isinstance(self.active_screen, tui_screen.TextDialog)): + if self.choice_q.qsize() > 0: self.choice_processor( self.menu_window, self.active_screen.get_screen_id(), - self.active_screen.get_choice()) - self.choice = "Processing" # Reset for next round + self.choice_q.get()) + + if self.screen_q.qsize() > 0: + self.screen_q.get() + self.switch_q.put(1) + + if self.switch_q.qsize() > 0: + self.switch_q.get() + self.switch_screen(config.use_python_dialog) + + if len(self.tui_screens) == 0: + self.active_screen = self.menu_screen + else: + self.active_screen = self.tui_screens[-1] if isinstance(self.active_screen, tui_screen.CursesScreen): self.main_window.noutrefresh() self.menu_window.noutrefresh() + curses.doupdate() def run(self): try: @@ -221,21 +236,17 @@ def task_processor(self, evt=None, task=None): elif task == 'TUI-RESIZE': self.resize_curses() elif task == 'TUI-UPDATE-MENU': - self.menu_screen.set_options(self.set_tui_menu_options(curses=True)) - #self.menu_screen.set_options(self.set_tui_menu_options(curses=False)) + self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) def choice_processor(self, stdscr, screen_id, choice): - if choice == "Processing": - pass - elif screen_id == 0 and choice == "Exit": - msg.logos_warn("Exiting installation.") - self.tui_screens = [] - self.running = False - elif screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): - self.tui_screens.pop(0) - self.stdscr.clear() + if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): + self.switch_q.put(1) elif screen_id == 0: if choice is None or choice == "Exit": + msg.logos_warn("Exiting installation.") + self.tui_screens = [] + self.llirunning = False sys.exit(0) elif choice.startswith("Install"): config.INSTALL_STEPS_COUNT = 0 @@ -269,7 +280,7 @@ def choice_processor(self, stdscr, screen_id, choice): appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) self.menu_options = appimage_choices question = "Which AppImage should be used?" - self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices) + self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) elif choice == "Download or Update Winetricks": control.set_winetricks() elif choice == "Run Winetricks": @@ -282,8 +293,6 @@ def choice_processor(self, stdscr, screen_id, choice): wine.installICUDataFiles() elif choice.endswith("Logging"): wine.switch_logging() - else: - msg.logos_error("Unknown menu choice.") elif screen_id == 1: if choice == "Input Custom AppImage": appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") @@ -351,131 +360,125 @@ def choice_processor(self, stdscr, screen_id, choice): pass def switch_screen(self, dialog): - if self.active_screen != self.menu_screen: + if self.active_screen is not None and self.active_screen != self.menu_screen: self.tui_screens.pop(0) + if self.active_screen == self.menu_screen: + self.menu_screen.choice = "Processing" + self.menu_screen.running = 0 if isinstance(self.active_screen, tui_screen.CursesScreen): self.stdscr.clear() def get_product(self, dialog): question = "Choose which FaithLife product the script should install:" # noqa: E501 - options = [("0", "Logos"), ("1", "Verbum"), ("2", "Exit")] + labels = ["Logos", "Verbum", "Return to Main Menu"] + options = self.which_dialog_options(labels, dialog) self.menu_options = options - self.stack_menu(2, self.product_q, self.product_e, question, options, dialog=dialog) - self.switch_screen(dialog) + self.screen_q.put(self.stack_menu(2, self.product_q, self.product_e, question, options, dialog=dialog)) def get_version(self, dialog): self.product_e.wait() question = f"Which version of {config.FLPRODUCT} should the script install?" # noqa: E501 - options = [("0", "10"), ("1", "9"), ("2", "Exit")] + labels = ["10", "9", "Return to Main Menu"] + options = self.which_dialog_options(labels, dialog) self.menu_options = options - self.stack_menu(3, self.version_q, self.version_e, question, options, dialog=dialog) - self.switch_screen(dialog) + self.screen_q.put(self.stack_menu(3, self.version_q, self.version_e, question, options, dialog=dialog)) def get_release(self, dialog): - self.stack_text(10, self.version_q, self.version_e, "Waiting to acquire Logos versions…", wait=True, dialog=dialog) + labels = [] + self.screen_q.put(self.stack_text(10, self.version_q, self.version_e, "Waiting to acquire Logos versions…", wait=True, dialog=dialog)) self.version_e.wait() - self.switch_screen(dialog) question = f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?" # noqa: E501 utils.start_thread(utils.get_logos_releases, True, self) self.releases_e.wait() if config.TARGETVERSION == '10': - options = self.releases_q.get() + labels = self.releases_q.get() elif config.TARGETVERSION == '9': - options = self.releases_q.get() + labels = self.releases_q.get() - if options is None: + if labels is None: msg.logos_error("Failed to fetch TARGET_RELEASE_VERSION.") - options.append("Exit") - enumerated_options = [(str(i), option) for i, option in enumerate(options, start=1)] - self.menu_options = enumerated_options - self.stack_menu(4, self.release_q, self.release_e, question, enumerated_options, dialog=dialog) - self.switch_screen(dialog) + labels.append("Return to Main Menu") + options = self.which_dialog_options(labels, dialog) + self.menu_options = options + self.screen_q.put(self.stack_menu(4, self.release_q, self.release_e, question, options, dialog=dialog)) def get_installdir(self, dialog): self.release_e.wait() default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 question = f"Where should {config.FLPRODUCT} files be installed to? [{default}]: " # noqa: E501 - self.stack_input(5, self.installdir_q, self.installdir_e, question, default, dialog=dialog) - self.switch_screen(dialog) + self.screen_q.put(self.stack_input(5, self.installdir_q, self.installdir_e, question, default, dialog=dialog)) def get_wine(self, dialog): self.installdir_e.wait() - self.stack_text(10, self.wine_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog) + self.screen_q.put(self.stack_text(10, self.wine_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog)) question = f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?" # noqa: E501 - options = utils.get_wine_options( + labels = utils.get_wine_options( utils.find_appimage_files(config.TARGET_RELEASE_VERSION), utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) ) - max_length = max(len(option) for option in options) - max_length += len(str(len(options))) + 10 - self.switch_screen(dialog) - if dialog: - enumerated_options = [(str(i), option) for i, option in enumerate(options, start=1)] - self.menu_options = enumerated_options - self.stack_menu(6, self.wine_q, self.wine_e, question, enumerated_options, width=max_length, - dialog=dialog) - else: - self.menu_options = options - self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, - dialog=dialog) - self.switch_screen(dialog) + labels.append("Return to Main Menu") + max_length = max(len(label) for label in labels) + max_length += len(str(len(labels))) + 10 + options = self.which_dialog_options(labels, dialog) + self.menu_options = options + self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, dialog=dialog) def get_winetricksbin(self, dialog): self.wine_e.wait() winetricks_options = utils.get_winetricks_options() if len(winetricks_options) > 1: question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux." # noqa: E501 - options = [ - ("1", "Use local winetricks."), - ("2", "Download winetricks from the Internet.") - ] + labels = ["Use local winetricks.", "Download winetricks from the Internet.", "Return to Main Menu"] + options = self.which_dialog_options(labels, dialog) self.menu_options = options - self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog) - self.switch_screen(dialog) + self.screen_q.put(self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog)) def get_waiting(self, dialog): self.tricksbin_e.wait() text = ["Install is running…\n"] + logging.console_log[-2:] processed_text = utils.str_array_to_string(text) percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) - self.stack_text(8, self.status_q, self.status_e, processed_text, wait=True, percent=percent, - dialog=dialog) - self.switch_screen(dialog) + self.screen_q.put(self.stack_text(8, self.status_q, self.status_e, processed_text, wait=True, percent=percent, + dialog=dialog)) def get_config(self, dialog): question = f"Update config file at {config.CONFIG_FILE}?" - options = ["Yes", "No"] + labels = ["Yes", "No"] + options = self.which_dialog_options(labels, dialog) self.menu_options = options - self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog) - self.switch_screen(dialog) + self.screen_q.put(self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog)) def finish_install(self): utils.send_task(self, 'TUI-UPDATE-MENU') def report_waiting(self, text, dialog): - if dialog: - self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog) - self.switch_screen(dialog) - else: - self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog) - logging.console_log.append(text) + #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) + logging.console_log.append(text) def report_dependencies(self, text, percent, elements, dialog): if elements is not None: if dialog: - self.stack_tasklist(11, self.deps_q, self.deps_e, text, elements, percent, dialog=dialog) - self.switch_screen(dialog) + self.screen_q.put(self.stack_tasklist(11, self.deps_q, self.deps_e, text, elements, percent, dialog=dialog)) # Without this delay, the reporting works too quickly and instead appears all at once. time.sleep(0.1) else: #TODO pass - def set_tui_menu_options(self, curses=False): - labels = [] + def which_dialog_options(self, labels, dialog=False): options = [] option_number = 1 + for label in labels: + if dialog: + options.append((str(option_number), label)) + option_number += 1 + else: + options.append(label) + return options + + def set_tui_menu_options(self, dialog=False): + labels = [] if config.LLI_LATEST_VERSION and utils.get_runmode() == 'binary': logging.debug("Checking if Logos Linux Installers needs updated.") # noqa: E501 status, error_message = utils.compare_logos_linux_installer_version() # noqa: E501 @@ -517,12 +520,7 @@ def set_tui_menu_options(self, curses=False): labels.append("Exit") - for label in labels: - if curses: - options.append(label) - else: - options.append((str(option_number), label)) - option_number += 1 + options = self.which_dialog_options(labels, dialog=False) return options @@ -552,11 +550,9 @@ def stack_confirm(self, screen_id, queue, event, question, options, dialog=False def stack_text(self, screen_id, queue, event, text, wait=False, percent=None, dialog=False): if dialog: - utils.append_unique(self.tui_screens, - tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) + utils.append_unique(self.tui_screens, tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) else: - utils.append_unique(self.tui_screens, - tui_screen.TextScreen(self, screen_id, queue, event, text, wait)) + utils.append_unique(self.tui_screens, tui_screen.TextScreen(self, screen_id, queue, event, text, wait)) def stack_tasklist(self, screen_id, queue, event, text, elements, percent, dialog=False): logging.debug(f"Elements stacked: {elements}") diff --git a/tui_curses.py b/tui_curses.py index 28ced7a5..63163343 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -145,6 +145,7 @@ def do_menu_down(app): else: config.current_option = min(len(app.menu_options) - 1, config.current_option + 1) + def menu(app, question_text, options): stdscr = app.get_menu_window() current_option = config.current_option @@ -209,11 +210,12 @@ def menu(app, question_text, options): # Get user input thread = utils.start_thread(menu_keyboard, True, app) + thread.join() stdscr.noutrefresh() - return app.choice + return def menu_keyboard(app): @@ -257,6 +259,6 @@ def menu_keyboard(app): stdscr.refresh() if choice: - app.choice = choice + app.active_screen.choice = choice else: return "Processing" diff --git a/tui_dialog.py b/tui_dialog.py index 4e28f8f1..0978bb8a 100644 --- a/tui_dialog.py +++ b/tui_dialog.py @@ -2,8 +2,6 @@ from dialog import Dialog import logging -import installer - def text(app, text, height=None, width=None, title=None, backtitle=None, colors=True): d = Dialog() @@ -100,7 +98,7 @@ def menu(app, question_text, options, height=None, width=None, menu_height=8): if code == dialog.OK: return code, tag, selected_description elif code == dialog.CANCEL: - return None + return None, None, "Exit" def buildlist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=None): diff --git a/tui_screen.py b/tui_screen.py index 6e2569eb..7497751e 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -10,6 +10,7 @@ import installer import tui_curses import tui_dialog +import tui_screen import utils @@ -17,16 +18,17 @@ class Screen: def __init__(self, app, screen_id, queue, event): self.app = app self.screen_id = screen_id - self.choice = "" + self.choice = "Processing" self.queue = queue self.event = event # running: - # This var is used for DialogScreens. The var indicates whether a Dialog has already started. - # If the dialog has already started, then the program will not display the dialog again - # in order to prevent phantom key presses. - # 0 = not started - # 1 = started - # 2 = finished + # This var indicates either whether: + # A CursesScreen has already submitted its choice to the choice_q, or + # The var indicates whether a Dialog has already started. If the dialog has already started, + # then the program will not display the dialog again in order to prevent phantom key presses. + # 0 = not submitted or not started + # 1 = submitted or started + # 2 = none or finished self.running = 0 def __str__(self): @@ -44,9 +46,6 @@ def get_screen_id(self): def get_choice(self): return self.choice - def submit_choice_to_queue(self): - self.queue.put(self.choice) - def wait_event(self): self.event.wait() @@ -55,11 +54,17 @@ def is_set(self): class CursesScreen(Screen): - pass + def submit_choice_to_queue(self): + if self.running == 0 and self.choice != "Processing": + self.app.choice_q.put(self.choice) + self.running = 1 class DialogScreen(Screen): - pass + def submit_choice_to_queue(self): + if self.running == 1 and self.choice != "Processing": + self.app.choice_q.put(self.choice) + self.running = 2 class ConsoleScreen(CursesScreen): @@ -70,6 +75,9 @@ def __init__(self, app, screen_id, queue, event, title, subtitle, title_start_y) self.subtitle = subtitle self.title_start_y = title_start_y + def __str__(self): + return f"Curses Console Screen" + def display(self): self.stdscr.erase() subtitle_start = tui_curses.title(self.app, self.title, self.title_start_y) @@ -97,13 +105,17 @@ def __init__(self, app, screen_id, queue, event, question, options, height=None, self.width = width self.menu_height = menu_height + def __str__(self): + return f"Curses Menu Screen" + def display(self): self.stdscr.erase() - self.choice = tui_curses.menu( + tui_curses.menu( self.app, self.question, self.options ) + self.submit_choice_to_queue() self.stdscr.noutrefresh() curses.doupdate() @@ -121,6 +133,9 @@ def __init__(self, app, screen_id, queue, event, question, default): self.question = question self.default = default + def __str__(self): + return f"Curses Input Screen" + def display(self): self.stdscr.erase() self.choice = tui_curses.get_user_input( @@ -128,6 +143,7 @@ def display(self): self.question, self.default ) + self.submit_choice_to_queue() self.stdscr.noutrefresh() curses.doupdate() @@ -146,6 +162,9 @@ def __init__(self, app, screen_id, queue, event, text, wait): self.wait = wait self.spinner_index = 0 + def __str__(self): + return f"Curses Text Screen" + def display(self): self.stdscr.erase() text_start_y, text_lines = tui_curses.text_centered(self.app, self.text) @@ -170,14 +189,14 @@ def __init__(self, app, screen_id, queue, event, question, options, height=None, self.menu_height = menu_height def __str__(self): - return f"PyDialog Screen" + return f"PyDialog Menu Screen" def display(self): if self.running == 0: self.running = 1 _, _, self.choice = tui_dialog.menu(self.app, self.question, self.options, self.height, self.width, self.menu_height) - self.running = 2 + self.submit_choice_to_queue() def get_question(self): return self.question @@ -194,7 +213,7 @@ def __init__(self, app, screen_id, queue, event, question, default): self.default = default def __str__(self): - return f"PyDialog Screen" + return f"PyDialog Input Screen" def display(self): if self.running == 0: @@ -202,7 +221,7 @@ def display(self): self.choice = tui_dialog.directory_picker(self.app, self.default) if self.choice: self.choice = Path(self.choice) - self.running = 2 + self.submit_choice_to_queue() def get_question(self): return self.question @@ -220,13 +239,13 @@ def __init__(self, app, screen_id, queue, event, question, yes_label="Yes", no_l self.no_label = no_label def __str__(self): - return f"PyDialog Screen" + return f"PyDialog Confirm Screen" def display(self): if self.running == 0: self.running = 1 _, _, self.choice = tui_dialog.confirm(self.app, self.question, self.yes_label, self.no_label) - self.running = 2 + self.submit_choice_to_queue() def get_question(self): return self.question @@ -248,20 +267,27 @@ def __init__(self, app, screen_id, queue, event, text, wait=False, percent=None, self.lastpercent = 0 def __str__(self): - return f"PyDialog Screen" + return f"PyDialog Text Screen" def display(self): if self.running == 0: if self.wait: self.running = 1 - self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + if config.INSTALL_STEPS_COUNT > 0: + self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + else: + self.percent = 0 + tui_dialog.progress_bar(self, self.text, self.percent) self.lastpercent = self.percent else: tui_dialog.text(self, self.text) elif self.running == 1: if self.wait: - self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + if config.INSTALL_STEPS_COUNT > 0: + self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + else: + self.percent = 0 # tui_dialog.update_progress_bar(self, self.percent, self.text, True) if self.lastpercent != self.percent: tui_dialog.progress_bar(self, self.text, self.percent) @@ -293,7 +319,7 @@ def __init__(self, app, screen_id, queue, event, text, elements, percent, self.updated = False def __str__(self): - return f"PyDialog Screen" + return f"PyDialog Task List Screen" def display(self): if self.running == 0: @@ -336,7 +362,7 @@ def __init__(self, app, screen_id, queue, event, question, options, list_height= self.list_height = list_height def __str__(self): - return f"PyDialog Screen" + return f"PyDialog Build List Screen" def display(self): if self.running == 0: @@ -363,7 +389,7 @@ def __init__(self, app, screen_id, queue, event, question, options, list_height= self.list_height = list_height def __str__(self): - return f"PyDialog Screen" + return f"PyDialog Check List Screen" def display(self): if self.running == 0: From 0ffe0df15d90c22d61b5d8aede1d1d981554b727 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 11 Jul 2024 09:47:39 -0400 Subject: [PATCH 027/253] Update config version --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 392d6fb6..e9e1e252 100644 --- a/config.py +++ b/config.py @@ -59,7 +59,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.10" +LLI_CURRENT_VERSION = "4.0.0-alpha.11" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING From 4b1c80505ebc1bb0dd37b2688a688ef1b2a671a1 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 11 Jul 2024 09:48:55 -0400 Subject: [PATCH 028/253] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e203e6..9ae3efd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +- 4.0.0-alpha.11 + - Fix #124 [T. H. Wright] - 4.0.0-alpha.10 - Fix #121 [T. H. Wright] - Prep for Logos 30+ support [N. Marti, T. H. Wright] From f22e643cfd079099e9e4218257bd7b0d1c2b94b2 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 11 Jul 2024 23:05:00 -0400 Subject: [PATCH 029/253] Add missing queue.put() --- tui_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tui_app.py b/tui_app.py index 1a4490b1..4ca41807 100644 --- a/tui_app.py +++ b/tui_app.py @@ -422,7 +422,7 @@ def get_wine(self, dialog): max_length += len(str(len(labels))) + 10 options = self.which_dialog_options(labels, dialog) self.menu_options = options - self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, dialog=dialog) + self.screen_q.put(self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, dialog=dialog)) def get_winetricksbin(self, dialog): self.wine_e.wait() From f3adbfb07adda1ffce872f38c68d17feb8ca6ed0 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 11 Jul 2024 23:24:02 -0400 Subject: [PATCH 030/253] Fix tui_app.get_winetricksbin() --- tui_app.py | 14 ++++++++------ tui_dialog.py | 2 +- utils.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tui_app.py b/tui_app.py index 4ca41807..669636f6 100644 --- a/tui_app.py +++ b/tui_app.py @@ -242,6 +242,9 @@ def task_processor(self, evt=None, task=None): def choice_processor(self, stdscr, screen_id, choice): if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): self.switch_q.put(1) + #FIXME: There is some kind of graphical glitch that activates on returning to Main Menu, + # but not from all submenus. + # Further, there appear to be issues with how the program exits on Ctrl+C as part of this. elif screen_id == 0: if choice is None or choice == "Exit": msg.logos_warn("Exiting installation.") @@ -294,6 +297,7 @@ def choice_processor(self, stdscr, screen_id, choice): elif choice.endswith("Logging"): wine.switch_logging() elif screen_id == 1: + #FIXME if choice == "Input Custom AppImage": appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") else: @@ -427,12 +431,10 @@ def get_wine(self, dialog): def get_winetricksbin(self, dialog): self.wine_e.wait() winetricks_options = utils.get_winetricks_options() - if len(winetricks_options) > 1: - question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux." # noqa: E501 - labels = ["Use local winetricks.", "Download winetricks from the Internet.", "Return to Main Menu"] - options = self.which_dialog_options(labels, dialog) - self.menu_options = options - self.screen_q.put(self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog)) + question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux." # noqa: E501 + options = self.which_dialog_options(winetricks_options, dialog) + self.menu_options = options + self.screen_q.put(self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog)) def get_waiting(self, dialog): self.tricksbin_e.wait() diff --git a/tui_dialog.py b/tui_dialog.py index 0978bb8a..d3237bd6 100644 --- a/tui_dialog.py +++ b/tui_dialog.py @@ -98,7 +98,7 @@ def menu(app, question_text, options, height=None, width=None, menu_height=8): if code == dialog.OK: return code, tag, selected_description elif code == dialog.CANCEL: - return None, None, "Exit" + return None, None, "Return to Main Menu" def buildlist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=None): diff --git a/utils.py b/utils.py index aceb1e1d..1777cc21 100644 --- a/utils.py +++ b/utils.py @@ -1183,7 +1183,7 @@ def get_wine_options(appimages, binaries, app=None) -> Union[List[List[str]], Li def get_winetricks_options(): local_winetricks_path = shutil.which('winetricks') - winetricks_options = ['Download'] + winetricks_options = ['Download', 'Return to Main Menu'] if local_winetricks_path is not None: # Check if local winetricks version is up-to-date. cmd = ["winetricks", "--version"] From fd59df0838997ad535920cf6863dad2e3bff27b6 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 11 Jul 2024 23:25:44 -0400 Subject: [PATCH 031/253] Update Version and Changelog --- CHANGELOG.md | 2 ++ config.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae3efd1..b9ca0eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +- 4.0.0-alpha.12 + - Fix TUI app's installer [T. H. Wright] - 4.0.0-alpha.11 - Fix #124 [T. H. Wright] - 4.0.0-alpha.10 diff --git a/config.py b/config.py index e9e1e252..11b541a4 100644 --- a/config.py +++ b/config.py @@ -59,7 +59,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.11" +LLI_CURRENT_VERSION = "4.0.0-alpha.12" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING From 4c50505393c64bd34218c3bedb8ec8c8649fa256 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 12 Jul 2024 10:03:03 -0400 Subject: [PATCH 032/253] Fix TUI winetricksbin --- installer.py | 2 +- tui_app.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/installer.py b/installer.py index ac2b6956..e6b39b8f 100644 --- a/installer.py +++ b/installer.py @@ -239,7 +239,7 @@ def ensure_install_dirs(app=None): logging.debug(f"> {wine_dir} exists: {wine_dir.is_dir()}") logging.debug(f"> {config.WINEPREFIX=}") - if config.DIALOG == 'tk' and app: + if app: utils.send_task(app, 'INSTALLING') diff --git a/tui_app.py b/tui_app.py index 669636f6..b5b78193 100644 --- a/tui_app.py +++ b/tui_app.py @@ -342,13 +342,13 @@ def choice_processor(self, stdscr, screen_id, choice): self.wine_e.set() elif screen_id == 7: winetricks_options = utils.get_winetricks_options() - if choice.startswith("Use"): + if choice.startswith("Download"): + self.tricksbin_q.put("Download") + self.tricksbin_e.set() + else: config.WINETRICKSBIN = winetricks_options[0] self.tricksbin_q.put(config.WINETRICKSBIN) self.tricksbin_e.set() - elif choice.startswith("Download"): - self.tricksbin_q.put("Download") - self.tricksbin_e.set() elif screen_id == 8: if config.install_finished: self.finished_q.put(True) @@ -438,7 +438,7 @@ def get_winetricksbin(self, dialog): def get_waiting(self, dialog): self.tricksbin_e.wait() - text = ["Install is running…\n"] + logging.console_log[-2:] + text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) self.screen_q.put(self.stack_text(8, self.status_q, self.status_e, processed_text, wait=True, percent=percent, From 048722db11ff5593e97acda1daab4f9807429581 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 13 Jul 2024 14:26:53 -0400 Subject: [PATCH 033/253] Fix utils.run_command() --- utils.py | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/utils.py b/utils.py index 1777cc21..ac0c0f78 100644 --- a/utils.py +++ b/utils.py @@ -255,7 +255,7 @@ def run_command(command, retries=1, delay=0, stdin=None, shell=False): shell=shell, capture_output=True ) - return result.stdout + return result except subprocess.CalledProcessError as e: logging.error(f"Error occurred while executing {command}: {e}") if "lock" in str(e): @@ -477,6 +477,7 @@ def get_runmode(): def query_packages(packages, elements=None, mode="install", app=None): + result = "" if config.SKIP_DEPENDENCIES: return @@ -486,10 +487,12 @@ def query_packages(packages, elements=None, mode="install", app=None): command = config.PACKAGE_MANAGER_COMMAND_QUERY try: - package_list = run_command(command, shell=True) + result = run_command(command, shell=True) except Exception as e: logging.error(f"Error occurred while executing command: {e}") + package_list = result.stdout + logging.debug(f"Checking packages: {packages} in package list.") if app is not None: if elements is None: @@ -529,16 +532,16 @@ def query_packages(packages, elements=None, mode="install", app=None): elements, dialog=config.use_python_dialog) - msg = 'None' + txt = 'None' if mode == "install": if missing_packages: - msg = f"Missing packages: {' '.join(missing_packages)}" - logging.info(f"Missing packages: {msg}") + txt = f"Missing packages: {' '.join(missing_packages)}" + logging.info(f"Missing packages: {txt}") return missing_packages, elements elif mode == "remove": if conflicting_packages: - msg = f"Conflicting packages: {' '.join(conflicting_packages)}" - logging.info(f"Conflicting packages: {msg}") + txt = f"Conflicting packages: {' '.join(conflicting_packages)}" + logging.info(f"Conflicting packages: {txt}") return conflicting_packages, elements @@ -622,7 +625,7 @@ def check_dialog_version(): if have_dep("dialog"): try: result = run_command(["dialog", "--version"]) - version_info = result.strip() + version_info = result.stdout.strip() if version_info.startswith("Version: "): version_info = version_info[len("Version: "):] return version_info @@ -793,27 +796,27 @@ def delete_symlink(symlink_path): def preinstall_dependencies_ubuntu(): try: - run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) - run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) - run_command( + dpkg_output = run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) + mkdir_output = run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) + wget_key_output = run_command( [config.SUPERUSER_COMMAND, "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", "https://dl.winehq.org/wine-builds/winehq.key"]) - lsboutput = run_command(["lsb_release", "-a"]) - codename = [line for line in lsboutput.split('\n') if "Description" in line][0].split()[1].strip() - run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", + lsb_release_output = run_command(["lsb_release", "-a"]) + codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[1].strip() + wget_sources_output = run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources"]) - run_command([config.SUPERUSER_COMMAND, "apt", "update"]) - run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) + apt_update_output = run_command([config.SUPERUSER_COMMAND, "apt", "update"]) + apt_install_output = run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) except subprocess.CalledProcessError as e: print(f"An error occurred: {e}") print(f"Command output: {e.output}") def preinstall_dependencies_steamos(): command = [config.SUPERUSER_COMMAND, "steamos-readonly", "disable"] - result = run_command(command) + steamsos_readonly_output = run_command(command) command = [config.SUPERUSER_COMMAND, "pacman-key", "--init"] - result = run_command(command) + pacman_key_init_output = run_command(command) command = [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"] - result = run_command(command) + pacman_key_populate_output = run_command(command) def postinstall_dependencies_steamos(): @@ -823,9 +826,9 @@ def postinstall_dependencies_steamos(): 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 '/etc/nsswitch.conf' ] - result = run_command(command) + sed_output = run_command(command) command =[config.SUPERUSER_COMMAND, "locale-gen"] - result = run_command(command) + locale_gen_output = run_command(command) command =[ config.SUPERUSER_COMMAND, "systemctl", @@ -833,11 +836,11 @@ def postinstall_dependencies_steamos(): "--now", "avahi-daemon" ] - result = run_command(command) + systemctl_avahi_daemon_output = run_command(command) command =[config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"] - result = run_command(command) + systemctl_cups = run_command(command) command = [config.SUPERUSER_COMMAND, "steamos-readonly", "enable"] - result = run_command(command) + steamos_readonly_output = run_command(command) def preinstall_dependencies(): From cc482c5ad753fa2e391d1b7f6d032a01455c569d Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 13 Jul 2024 14:34:30 -0400 Subject: [PATCH 034/253] Add Fedora pkgs: fuse3 --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index ac0c0f78..bca81595 100644 --- a/utils.py +++ b/utils.py @@ -436,7 +436,7 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_REMOVE = "dnf remove -y" config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed" config.QUERY_PREFIX = '' - config.PACKAGES = "patch mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 + config.PACKAGES = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appiamgelauncher" elif shutil.which('pamac') is not None: # manjaro From 61f1c8db2f7fdf1f1d57f319a1abe9426d574f56 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 13 Jul 2024 14:44:27 -0400 Subject: [PATCH 035/253] Reduce logging output --- utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/utils.py b/utils.py index bca81595..5472f15c 100644 --- a/utils.py +++ b/utils.py @@ -501,8 +501,6 @@ def query_packages(packages, elements=None, mode="install", app=None): elements = {element[0]: element[1] for element in elements} for p in packages: - logging.debug(f"Current elements: {elements}") - logging.debug(f"Checking: package: {p}") status = "Unchecked" for line in package_list.split('\n'): if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": @@ -523,8 +521,6 @@ def query_packages(packages, elements=None, mode="install", app=None): logging.debug(f"Setting {p}: {status}") elements[p] = status - logging.debug(f"DEV: {elements}") - if app is not None and config.DIALOG == "curses": app.report_dependencies( f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", From e30d8dcd27834086ad28cd0bfe7317fffca5dfae Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sun, 14 Jul 2024 22:48:33 -0400 Subject: [PATCH 036/253] Create networking.py - Add logging warning for #128 --- control.py | 3 +- gui_app.py | 3 +- installer.py | 35 +++- main.py | 5 +- network.py | 581 ++++++++++++++++++++++++++++++++++++++++++++++++++ tui_app.py | 3 +- utils.py | 582 ++------------------------------------------------- 7 files changed, 629 insertions(+), 583 deletions(-) create mode 100644 network.py diff --git a/control.py b/control.py index 555034e8..6b03d47f 100644 --- a/control.py +++ b/control.py @@ -17,6 +17,7 @@ import config # import installer import msg +import network import tui_curses import tui_app import utils @@ -331,7 +332,7 @@ def set_winetricks(): def download_winetricks(): msg.logos_msg("Downloading winetricks…") appdir_bindir = f"{config.INSTALLDIR}/data/bin" - utils.logos_reuse_download( + network.logos_reuse_download( config.WINETRICKS_URL, "winetricks", appdir_bindir diff --git a/gui_app.py b/gui_app.py index 3160597b..979cab67 100644 --- a/gui_app.py +++ b/gui_app.py @@ -18,6 +18,7 @@ import control import gui import installer +import network import utils import wine @@ -332,7 +333,7 @@ def start_releases_check(self): self.update_release_check_progress ) self.release_thread = Thread( - target=utils.get_logos_releases, + target=network.get_logos_releases, kwargs={'app': self}, daemon=True, ) diff --git a/installer.py b/installer.py index e6b39b8f..05a81965 100644 --- a/installer.py +++ b/installer.py @@ -6,6 +6,7 @@ import config import msg +import network import utils import wine @@ -13,6 +14,8 @@ # To replicate, start a TUI install, return/cancel on second step # Then launch a new install +#TODO: Reimplement `--install-app`? + def ensure_product_choice(app=None): config.INSTALL_STEPS_COUNT += 1 @@ -27,6 +30,8 @@ def ensure_product_choice(app=None): if config.DIALOG == 'curses': app.product_e.wait() config.FLPRODUCT = app.product_q.get() + else: + logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") if config.FLPRODUCT == 'Logos': config.FLPRODUCTi = 'logos4' @@ -52,6 +57,8 @@ def ensure_version_choice(app=None): if config.DIALOG == 'curses': app.version_e.wait() config.TARGETVERSION = app.version_q.get() + else: + logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") logging.debug(f"> {config.TARGETVERSION=}") @@ -70,6 +77,8 @@ def ensure_release_choice(app=None): app.release_e.wait() config.TARGET_RELEASE_VERSION = app.release_q.get() logging.debug(f"{config.TARGET_RELEASE_VERSION=}") + else: + logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") logging.debug(f"> {config.TARGET_RELEASE_VERSION=}") @@ -95,6 +104,8 @@ def ensure_install_dir_choice(app=None): app.installdir_e.wait() config.INSTALLDIR = app.installdir_q.get() config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + else: + logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") logging.debug(f"> {config.INSTALLDIR=}") logging.debug(f"> {config.APPDIR_BINDIR=}") @@ -113,12 +124,14 @@ def ensure_wine_choice(app=None): logging.debug('- config.WINEBIN_CODE') if config.WINE_EXE is None: - utils.set_recommended_appimage_config() + network.set_recommended_appimage_config() if app: utils.send_task(app, 'WINE_EXE') if config.DIALOG == 'curses': app.wine_e.wait() config.WINE_EXE = app.wine_q.get() + else: + logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. logging.debug(f"Preparing to process WINE_EXE. Currently set to: {config.WINE_EXE}.") @@ -146,10 +159,14 @@ def ensure_winetricks_choice(app=None): # Check if local winetricks version available; else, download it. config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" - utils.send_task(app, 'WINETRICKSBIN') - if config.DIALOG == 'curses': - app.tricksbin_e.wait() - winetricksbin = app.tricksbin_q.get() + if app: + utils.send_task(app, 'WINETRICKSBIN') + if config.DIALOG == 'curses': + app.tricksbin_e.wait() + winetricksbin = app.tricksbin_q.get() + else: + logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + if not winetricksbin.startswith('Download'): config.WINETRICKSBIN = winetricksbin @@ -241,6 +258,8 @@ def ensure_install_dirs(app=None): if app: utils.send_task(app, 'INSTALLING') + else: + logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") def ensure_sys_deps(app=None): @@ -272,7 +291,7 @@ def ensure_appimage_download(app=None): downloaded_file = utils.get_downloaded_file_path(filename) if not downloaded_file: downloaded_file = Path(f"{config.MYDOWNLOADS}/{filename}") - utils.logos_reuse_download( + network.logos_reuse_download( config.RECOMMENDED_WINE64_APPIMAGE_URL, filename, config.MYDOWNLOADS, @@ -372,7 +391,7 @@ def ensure_premade_winebottle_download(app=None): downloaded_file = utils.get_downloaded_file_path(config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 if not downloaded_file: downloaded_file = Path(config.MYDOWNLOADS) / config.LOGOS_EXECUTABLE - utils.logos_reuse_download( + network.logos_reuse_download( config.LOGOS9_WINE64_BOTTLE_TARGZ_URL, config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME, config.MYDOWNLOADS, @@ -402,7 +421,7 @@ def ensure_product_installer_download(app=None): downloaded_file = utils.get_downloaded_file_path(config.LOGOS_EXECUTABLE) if not downloaded_file: downloaded_file = Path(config.MYDOWNLOADS) / config.LOGOS_EXECUTABLE - utils.logos_reuse_download( + network.logos_reuse_download( config.LOGOS64_URL, config.LOGOS_EXECUTABLE, config.MYDOWNLOADS, diff --git a/main.py b/main.py index 31f3fc95..47851783 100755 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ import gui_app import installer import msg +import network import tui_app import utils import wine @@ -186,7 +187,7 @@ def parse_args(args, parser): if args.skip_fonts: config.SKIP_FONTS = True - if args.check_for_updates: + if network.check_for_updates: config.CHECK_UPDATES = True if args.skip_dependencies: @@ -347,7 +348,7 @@ def main(): logging.info(f"{config.LLI_TITLE}, {config.LLI_CURRENT_VERSION} by {config.LLI_AUTHOR}.") # noqa: E501 logging.debug(f"Installer log file: {config.LOGOS_LOG}") - utils.check_for_updates() + network.check_for_updates() # Check if app is installed. install_required = [ diff --git a/network.py b/network.py new file mode 100644 index 00000000..9165361a --- /dev/null +++ b/network.py @@ -0,0 +1,581 @@ +import hashlib +import json +import logging +import os +import queue +import shutil +import sys +import threading +from base64 import b64encode +from datetime import datetime, timedelta +from pathlib import Path +from urllib.parse import urlparse +from xml.etree import ElementTree as ET + +import requests + +import config +import msg +import utils + + +class Props(): + def __init__(self, uri=None): + self.path = None + self.size = None + self.md5 = None + if uri is not None: + self.path = uri + + +class FileProps(Props): + def __init__(self, f=None): + super().__init__(f) + if f is not None: + self.path = Path(self.path) + if self.path.is_file(): + self.get_size() + # self.get_md5() + + def get_size(self): + if self.path is None: + return + self.size = self.path.stat().st_size + return self.size + + def get_md5(self): + if self.path is None: + return + md5 = hashlib.md5() + with self.path.open('rb') as f: + for chunk in iter(lambda: f.read(4096), b''): + md5.update(chunk) + self.md5 = b64encode(md5.digest()).decode('utf-8') + logging.debug(f"{str(self.path)} MD5: {self.md5}") + return self.md5 + + +class UrlProps(Props): + def __init__(self, url=None): + super().__init__(url) + self.headers = None + if url is not None: + self.get_headers() + self.get_size() + self.get_md5() + + def get_headers(self): + if self.path is None: + self.headers = None + logging.debug(f"Getting headers from {self.path}.") + try: + h = {'Accept-Encoding': 'identity'} # force non-compressed txfr + r = requests.head(self.path, allow_redirects=True, headers=h) + except requests.exceptions.ConnectionError: + logging.critical("Failed to connect to the server.") + return None + except Exception as e: + logging.error(e) + return None + except KeyboardInterrupt: + print() + msg.logos_error("Interrupted by Ctrl+C") + return None + self.headers = r.headers + return self.headers + + def get_size(self): + if self.headers is None: + r = self.get_headers() + if r is None: + return + content_length = self.headers.get('Content-Length') + content_encoding = self.headers.get('Content-Encoding') + if content_encoding is not None: + logging.critical(f"The server requires receiving the file compressed as '{content_encoding}'.") # noqa: E501 + logging.debug(f"{content_length=}") + if content_length is not None: + self.size = int(content_length) + return self.size + + def get_md5(self): + if self.headers is None: + r = self.get_headers() + if r is None: + return + if self.headers.get('server') == 'AmazonS3': + content_md5 = self.headers.get('etag') + if content_md5 is not None: + # Convert from hex to base64 + content_md5_hex = content_md5.strip('"').strip("'") + content_md5 = b64encode(bytes.fromhex(content_md5_hex)).decode() # noqa: E501 + else: + content_md5 = self.headers.get('Content-MD5') + if content_md5 is not None: + content_md5 = content_md5.strip('"').strip("'") + logging.debug(f"{content_md5=}") + if content_md5 is not None: + self.md5 = content_md5 + return self.md5 + + +def cli_download(uri, destination): + message = f"Downloading '{uri}' to '{destination}'" + logging.info(message) + msg.logos_msg(message) + + # Set target. + if destination != destination.rstrip('/'): + target = os.path.join(destination, os.path.basename(uri)) + if not os.path.isdir(destination): + os.makedirs(destination) + elif os.path.isdir(destination): + target = os.path.join(destination, os.path.basename(uri)) + else: + target = destination + dirname = os.path.dirname(destination) + if not os.path.isdir(dirname): + os.makedirs(dirname) + + # Download from uri in thread while showing progress bar. + cli_queue = queue.Queue() + args = [uri] + kwargs = {'q': cli_queue, 'target': target} + t = threading.Thread(target=net_get, args=args, kwargs=kwargs, daemon=True) + t.start() + try: + while t.is_alive(): + if cli_queue.empty(): + continue + utils.write_progress_bar(cli_queue.get()) + print() + except KeyboardInterrupt: + print() + msg.logos_error('Interrupted with Ctrl+C') + + +def logos_reuse_download( + SOURCEURL, + FILE, + TARGETDIR, + app=None, +): + DIRS = [ + config.INSTALLDIR, + os.getcwd(), + config.MYDOWNLOADS, + ] + FOUND = 1 + for i in DIRS: + if i is not None: + logging.debug(f"Checking {i} for {FILE}.") + file_path = Path(i) / FILE + if os.path.isfile(file_path): + logging.info(f"{FILE} exists in {i}. Verifying properties.") + if verify_downloaded_file( + SOURCEURL, + file_path, + app=app, + ): + logging.info(f"{FILE} properties match. Using it…") + msg.logos_msg(f"Copying {FILE} into {TARGETDIR}") + try: + shutil.copy(os.path.join(i, FILE), TARGETDIR) + except shutil.SameFileError: + pass + FOUND = 0 + break + else: + logging.info(f"Incomplete file: {file_path}.") + if FOUND == 1: + file_path = os.path.join(config.MYDOWNLOADS, FILE) + if config.DIALOG == 'tk' and app: + # Ensure progress bar. + app.stop_indeterminate_progress() + # Start download. + net_get( + SOURCEURL, + target=file_path, + app=app, + ) + else: + cli_download(SOURCEURL, file_path) + if verify_downloaded_file( + SOURCEURL, + file_path, + app=app, + ): + msg.logos_msg(f"Copying: {FILE} into: {TARGETDIR}") + try: + shutil.copy(os.path.join(config.MYDOWNLOADS, FILE), TARGETDIR) + except shutil.SameFileError: + pass + else: + msg.logos_error(f"Bad file size or checksum: {file_path}") + + +def net_get(url, target=None, app=None, evt=None, q=None): + + # TODO: + # - Check available disk space before starting download + logging.debug(f"Download source: {url}") + logging.debug(f"Download destination: {target}") + target = FileProps(target) # sets path and size attribs + if app and target.path: + app.status_q.put(f"Downloading {target.path.name}…") # noqa: E501 + app.root.event_generate('<>') + parsed_url = urlparse(url) + domain = parsed_url.netloc # Gets the requested domain + url = UrlProps(url) # uses requests to set headers, size, md5 attribs + if url.headers is None: + logging.critical("Could not get headers.") + return None + + # Initialize variables. + local_size = 0 + total_size = url.size # None or int + logging.debug(f"File size on server: {total_size}") + percent = None + chunk_size = 100 * 1024 # 100 KB default + if type(total_size) is int: + # Use smaller of 2% of filesize or 2 MB for chunk_size. + chunk_size = min([int(total_size / 50), 2 * 1024 * 1024]) + # Force non-compressed file transfer for accurate progress tracking. + headers = {'Accept-Encoding': 'identity'} + file_mode = 'wb' + + # If file exists and URL is resumable, set download Range. + if target.path is not None and target.path.is_file(): + logging.debug(f"File exists: {str(target.path)}") + local_size = target.get_size() + logging.info(f"Current downloaded size in bytes: {local_size}") + if url.headers.get('Accept-Ranges') == 'bytes': + logging.debug("Server accepts byte range; attempting to resume download.") # noqa: E501 + file_mode = 'ab' + if type(url.size) is int: + headers['Range'] = f'bytes={local_size}-{total_size}' + else: + headers['Range'] = f'bytes={local_size}-' + + logging.debug(f"{chunk_size=}; {file_mode=}; {headers=}") + + # Log download type. + if 'Range' in headers.keys(): + message = f"Continuing download for {url.path}." + else: + message = f"Starting new download for {url.path}." + logging.info(message) + + # Initiate download request. + try: + if target.path is None: # return url content as text + with requests.get(url.path, headers=headers) as r: + if callable(r): + logging.error("Failed to retrieve data from the URL.") + return None + + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + if domain == "github.com": + if ( + e.response.status_code == 403 + or e.response.status_code == 429 + ): + logging.error("GitHub API rate limit exceeded. Please wait before trying again.") # noqa: E501 + else: + logging.error(f"HTTP error occurred: {e.response.status_code}") # noqa: E501 + return None + + return r._content # raw bytes + else: # download url to target.path + with requests.get(url.path, stream=True, headers=headers) as r: + with target.path.open(mode=file_mode) as f: + if file_mode == 'wb': + mode_text = 'Writing' + else: + mode_text = 'Appending' + logging.debug(f"{mode_text} data to file {target.path}.") + for chunk in r.iter_content(chunk_size=chunk_size): + f.write(chunk) + local_size = target.get_size() + if type(total_size) is int: + percent = round(local_size / total_size * 100) + # if None not in [app, evt]: + if app: + # Send progress value to tk window. + app.get_q.put(percent) + if not evt: + evt = app.get_evt + app.root.event_generate(evt) + elif q is not None: + # Send progress value to queue param. + q.put(percent) + except requests.exceptions.RequestException as e: + logging.error(f"Error occurred during HTTP request: {e}") + return None # Return None values to indicate an error condition + except Exception as e: + msg.logos_error(e) + except KeyboardInterrupt: + print() + msg.logos_error("Killed with Ctrl+C") + + +def verify_downloaded_file(url, file_path, app=None, evt=None): + if app: + if config.DIALOG == "tk": + app.root.event_generate('<>') + app.status_q.put(f"Verifying {file_path}…") + app.root.event_generate('<>') + else: + app.status_q.put(f"Verifying {file_path}…") + res = False + msg = f"{file_path} is the wrong size." + right_size = same_size(url, file_path) + if right_size: + msg = f"{file_path} has the wrong MD5 sum." + right_md5 = same_md5(url, file_path) + if right_md5: + msg = f"{file_path} is verified." + res = True + logging.info(msg) + if app: + if config.DIALOG == "tk": + if not evt: + evt = app.check_evt + app.root.event_generate(evt) + return res + + +def same_md5(url, file_path): + logging.debug(f"Comparing MD5 of {url} and {file_path}.") + url_md5 = UrlProps(url).get_md5() + logging.debug(f"{url_md5=}") + if url_md5 is None: # skip MD5 check if not provided with URL + res = True + else: + file_md5 = FileProps(file_path).get_md5() + logging.debug(f"{file_md5=}") + res = url_md5 == file_md5 + return res + + +def same_size(url, file_path): + logging.debug(f"Comparing size of {url} and {file_path}.") + url_size = UrlProps(url).size + if not url_size: + return True + file_size = FileProps(file_path).size + logging.debug(f"{url_size=} B; {file_size=} B") + res = url_size == file_size + return res + + +def get_latest_release_data(releases_url): + data = net_get(releases_url) + if data: + try: + json_data = json.loads(data.decode()) + logging.debug(f"{json_data=}") + except json.JSONDecodeError as e: + logging.error(f"Error decoding JSON response: {e}") + return None + + if not isinstance(json_data, list) or len(json_data) == 0: + logging.error("Invalid or empty JSON response.") + return None + else: + return json_data + else: + logging.critical("Could not get latest release URL.") + return None + + +def get_latest_release_url(json_data): + release_url = None + if json_data: + release_url = json_data[0].get('assets')[0].get('browser_download_url') # noqa: E501 + logging.info(f"Release URL: {release_url}") + return release_url + + +def get_latest_release_version_tag_name(json_data): + release_tag_name = None + if json_data: + release_tag_name = json_data[0].get('tag_name') # noqa: E501 + logging.info(f"Release URL Tag Name: {release_tag_name}") + return release_tag_name + + +def set_logoslinuxinstaller_latest_release_config(): + releases_url = "https://api.github.com/repos/FaithLife-Community/LogosLinuxInstaller/releases" # noqa: E501 + json_data = get_latest_release_data(releases_url) + logoslinuxinstaller_url = get_latest_release_url(json_data) + logoslinuxinstaller_tag_name = get_latest_release_version_tag_name(json_data) # noqa: E501 + if logoslinuxinstaller_url is None: + logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 + return + config.LOGOS_LATEST_VERSION_URL = logoslinuxinstaller_url + config.LOGOS_LATEST_VERSION_FILENAME = os.path.basename(logoslinuxinstaller_url) # noqa: #501 + # Getting version relies on the the tag_name field in the JSON data. This + # is already parsed down to vX.X.X. Therefore we must strip the v. + config.LLI_LATEST_VERSION = logoslinuxinstaller_tag_name.lstrip('v') + logging.info(f"{config.LLI_LATEST_VERSION}") + + +def set_recommended_appimage_config(): + releases_url = "https://api.github.com/repos/FaithLife-Community/wine-appimages/releases" # noqa: E501 + if not config.RECOMMENDED_WINE64_APPIMAGE_URL: + json_data = get_latest_release_data(releases_url) + appimage_url = get_latest_release_url(json_data) + if appimage_url is None: + logging.critical("Unable to set recommended appimage config without URL.") # noqa: E501 + return + config.RECOMMENDED_WINE64_APPIMAGE_URL = appimage_url + config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME = os.path.basename(config.RECOMMENDED_WINE64_APPIMAGE_URL) # noqa: E501 + config.RECOMMENDED_WINE64_APPIMAGE_FILENAME = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME.split(".AppImage")[0] # noqa: E501 + # Getting version and branch rely on the filename having this format: + # wine-[branch]_[version]-[arch] + parts = config.RECOMMENDED_WINE64_APPIMAGE_FILENAME.split('-') + branch_version = parts[1] + branch, version = branch_version.split('_') + config.RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION = f"v{version}-{branch}" + config.RECOMMENDED_WINE64_APPIMAGE_VERSION = f"{version}" + config.RECOMMENDED_WINE64_APPIMAGE_BRANCH = f"{branch}" + + +def check_for_updates(): + # We limit the number of times set_recommended_appimage_config is run in + # order to avoid GitHub API limits. This sets the check to once every 12 + # hours. + + config.current_logos_version = utils.get_current_logos_version() + utils.write_config(config.CONFIG_FILE) + + # TODO: Check for New Logos Versions. See #116. + + now = datetime.now().replace(microsecond=0) + if config.CHECK_UPDATES: + check_again = now + elif config.LAST_UPDATED is not None: + check_again = datetime.strptime( + config.LAST_UPDATED.strip(), + '%Y-%m-%dT%H:%M:%S' + ) + check_again += timedelta(hours=12) + else: + check_again = now + + if now >= check_again: + logging.debug("Running self-update.") + + set_logoslinuxinstaller_latest_release_config() + set_recommended_appimage_config() + + config.LAST_UPDATED = now.isoformat() + utils.write_config(config.CONFIG_FILE) + else: + logging.debug("Skipping self-update.") + + +def get_recommended_appimage(): + wine64_appimage_full_filename = Path(config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME) # noqa: E501 + dest_path = Path(config.APPDIR_BINDIR) / wine64_appimage_full_filename + if dest_path.is_file(): + return + else: + logos_reuse_download( + config.RECOMMENDED_WINE64_APPIMAGE_URL, + config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME, + config.APPDIR_BINDIR) + + +def get_logos_releases(app=None): + # Use already-downloaded list if requested again. + downloaded_releases = None + if config.TARGETVERSION == '9' and config.LOGOS9_RELEASES: + downloaded_releases = config.LOGOS9_RELEASES + elif config.TARGETVERSION == '10' and config.LOGOS10_RELEASES: + downloaded_releases = config.LOGOS10_RELEASES + if downloaded_releases: + logging.debug(f"Using already-downloaded list of v{config.TARGETVERSION} releases") # noqa: E501 + if app: + app.releases_q.put(downloaded_releases) + app.root.event_generate(app.release_evt) + return downloaded_releases + + msg.logos_msg(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 + # NOTE: This assumes that Verbum release numbers continue to mirror Logos. + url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 + + response_xml_bytes = net_get(url) + # if response_xml is None and None not in [q, app]: + if response_xml_bytes is None: + if app: + app.releases_q.put(None) + if config.DIALOG == 'tk': + app.root.event_generate(app.release_evt) + return None + + # Parse XML + root = ET.fromstring(response_xml_bytes.decode('utf-8-sig')) + + # Define namespaces + namespaces = { + 'ns0': 'http://www.w3.org/2005/Atom', + 'ns1': 'http://services.logos.com/update/v1/' + } + + # Extract versions + releases = [] + # Obtain all listed releases. + for entry in root.findall('.//ns1:version', namespaces): + release = entry.text + releases.append(release) + # if len(releases) == 5: + # break + + filtered_releases = utils.filter_versions(releases, 30, 1) + logging.debug(f"Available releases: {', '.join(releases)}") + logging.debug(f"Filtered releases: {', '.join(filtered_releases)}") + + if app: + app.releases_q.put(filtered_releases) + if config.DIALOG == 'tk': + app.root.event_generate(app.release_evt) + elif config.DIALOG == 'curses': + app.releases_e.set() + return filtered_releases + +def update_lli_binary(app=None): + lli_file_path = os.path.realpath(sys.argv[0]) + lli_download_path = Path(config.MYDOWNLOADS) / "LogosLinuxInstaller" + temp_path = Path(config.MYDOWNLOADS) / "LogosLinuxInstaller.tmp" + logging.debug( + f"Updating Logos Linux Installer to latest version by overwriting: {lli_file_path}") # noqa: E501 + + # Remove existing downloaded file if different version. + if lli_download_path.is_file(): + logging.info("Checking if existing LLI binary is latest version.") + lli_download_ver = utils.get_lli_release_version(lli_download_path) + if not lli_download_ver or lli_download_ver != config.LLI_LATEST_VERSION: # noqa: E501 + logging.info(f"Removing \"{lli_download_path}\", version: {lli_download_ver}") # noqa: E501 + # Remove incompatible file. + lli_download_path.unlink() + + logos_reuse_download( + config.LOGOS_LATEST_VERSION_URL, + "LogosLinuxInstaller", + config.MYDOWNLOADS, + app=app, + ) + shutil.copy(lli_download_path, temp_path) + try: + shutil.move(temp_path, lli_file_path) + except Exception as e: + logging.error(f"Failed to replace the binary: {e}") + return + + os.chmod(sys.argv[0], os.stat(sys.argv[0]).st_mode | 0o111) + logging.debug("Successfully updated Logos Linux Installer.") + utils.restart_lli() diff --git a/tui_app.py b/tui_app.py index b5b78193..37fa4347 100644 --- a/tui_app.py +++ b/tui_app.py @@ -10,6 +10,7 @@ import config import control +import network import tui_curses import tui_dialog import tui_screen @@ -392,7 +393,7 @@ def get_release(self, dialog): self.screen_q.put(self.stack_text(10, self.version_q, self.version_e, "Waiting to acquire Logos versions…", wait=True, dialog=dialog)) self.version_e.wait() question = f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?" # noqa: E501 - utils.start_thread(utils.get_logos_releases, True, self) + utils.start_thread(network.get_logos_releases, True, self) self.releases_e.wait() if config.TARGETVERSION == '10': diff --git a/utils.py b/utils.py index 5472f15c..c1aba80d 100644 --- a/utils.py +++ b/utils.py @@ -1,14 +1,12 @@ import atexit import distro import glob -import hashlib +import inspect import json import logging import os import psutil -import queue import re -import requests import shutil import shlex import signal @@ -19,118 +17,25 @@ import time import tkinter as tk import zipfile -from base64 import b64encode -from datetime import datetime, timedelta from packaging import version from pathlib import Path from typing import List, Union -from urllib.parse import urlparse -from xml.etree import ElementTree as ET import config import msg +import network import wine import tui_dialog -class Props(): - def __init__(self, uri=None): - self.path = None - self.size = None - self.md5 = None - if uri is not None: - self.path = uri - - -class FileProps(Props): - def __init__(self, f=None): - super().__init__(f) - if f is not None: - self.path = Path(self.path) - if self.path.is_file(): - self.get_size() - # self.get_md5() - - def get_size(self): - if self.path is None: - return - self.size = self.path.stat().st_size - return self.size - - def get_md5(self): - if self.path is None: - return - md5 = hashlib.md5() - with self.path.open('rb') as f: - for chunk in iter(lambda: f.read(4096), b''): - md5.update(chunk) - self.md5 = b64encode(md5.digest()).decode('utf-8') - logging.debug(f"{str(self.path)} MD5: {self.md5}") - return self.md5 - - -class UrlProps(Props): - def __init__(self, url=None): - super().__init__(url) - self.headers = None - if url is not None: - self.get_headers() - self.get_size() - self.get_md5() - - def get_headers(self): - if self.path is None: - self.headers = None - logging.debug(f"Getting headers from {self.path}.") - try: - h = {'Accept-Encoding': 'identity'} # force non-compressed txfr - r = requests.head(self.path, allow_redirects=True, headers=h) - except requests.exceptions.ConnectionError: - logging.critical("Failed to connect to the server.") - return None - except Exception as e: - logging.error(e) - return None - except KeyboardInterrupt: - print() - msg.logos_error("Interrupted by Ctrl+C") - return None - self.headers = r.headers - return self.headers - - def get_size(self): - if self.headers is None: - r = self.get_headers() - if r is None: - return - content_length = self.headers.get('Content-Length') - content_encoding = self.headers.get('Content-Encoding') - if content_encoding is not None: - logging.critical(f"The server requires receiving the file compressed as '{content_encoding}'.") # noqa: E501 - logging.debug(f"{content_length=}") - if content_length is not None: - self.size = int(content_length) - return self.size - - def get_md5(self): - if self.headers is None: - r = self.get_headers() - if r is None: - return - if self.headers.get('server') == 'AmazonS3': - content_md5 = self.headers.get('etag') - if content_md5 is not None: - # Convert from hex to base64 - content_md5_hex = content_md5.strip('"').strip("'") - content_md5 = b64encode(bytes.fromhex(content_md5_hex)).decode() # noqa: E501 - else: - content_md5 = self.headers.get('Content-MD5') - if content_md5 is not None: - content_md5 = content_md5.strip('"').strip("'") - logging.debug(f"{content_md5=}") - if content_md5 is not None: - self.md5 = content_md5 - return self.md5 +def get_calling_function_name(): + if 'inspect' in sys.modules: + stack = inspect.stack() + caller_frame = stack[1] + caller_name = caller_frame.function + return caller_name + else: + return "Inspect Not Enabled" def append_unique(list, item): @@ -685,101 +590,6 @@ def get_user_downloads_dir(): return downloads_path -def cli_download(uri, destination): - message = f"Downloading '{uri}' to '{destination}'" - logging.info(message) - msg.logos_msg(message) - - # Set target. - if destination != destination.rstrip('/'): - target = os.path.join(destination, os.path.basename(uri)) - if not os.path.isdir(destination): - os.makedirs(destination) - elif os.path.isdir(destination): - target = os.path.join(destination, os.path.basename(uri)) - else: - target = destination - dirname = os.path.dirname(destination) - if not os.path.isdir(dirname): - os.makedirs(dirname) - - # Download from uri in thread while showing progress bar. - cli_queue = queue.Queue() - args = [uri] - kwargs = {'q': cli_queue, 'target': target} - t = threading.Thread(target=net_get, args=args, kwargs=kwargs, daemon=True) - t.start() - try: - while t.is_alive(): - if cli_queue.empty(): - continue - write_progress_bar(cli_queue.get()) - print() - except KeyboardInterrupt: - print() - msg.logos_error('Interrupted with Ctrl+C') - - -def logos_reuse_download( - SOURCEURL, - FILE, - TARGETDIR, - app=None, -): - DIRS = [ - config.INSTALLDIR, - os.getcwd(), - config.MYDOWNLOADS, - ] - FOUND = 1 - for i in DIRS: - if i is not None: - logging.debug(f"Checking {i} for {FILE}.") - file_path = Path(i) / FILE - if os.path.isfile(file_path): - logging.info(f"{FILE} exists in {i}. Verifying properties.") - if verify_downloaded_file( - SOURCEURL, - file_path, - app=app, - ): - logging.info(f"{FILE} properties match. Using it…") - msg.logos_msg(f"Copying {FILE} into {TARGETDIR}") - try: - shutil.copy(os.path.join(i, FILE), TARGETDIR) - except shutil.SameFileError: - pass - FOUND = 0 - break - else: - logging.info(f"Incomplete file: {file_path}.") - if FOUND == 1: - file_path = os.path.join(config.MYDOWNLOADS, FILE) - if config.DIALOG == 'tk' and app: - # Ensure progress bar. - app.stop_indeterminate_progress() - # Start download. - net_get( - SOURCEURL, - target=file_path, - app=app, - ) - else: - cli_download(SOURCEURL, file_path) - if verify_downloaded_file( - SOURCEURL, - file_path, - app=app, - ): - msg.logos_msg(f"Copying: {FILE} into: {TARGETDIR}") - try: - shutil.copy(os.path.join(config.MYDOWNLOADS, FILE), TARGETDIR) - except shutil.SameFileError: - pass - else: - msg.logos_error(f"Bad file size or checksum: {file_path}") - - def delete_symlink(symlink_path): symlink_path = Path(symlink_path) if symlink_path.is_symlink(): @@ -1044,64 +854,6 @@ def filter_versions(versions, threshold, check_version_part): return [version for version in versions if check_logos_release_version(version, threshold, check_version_part)] # noqa: E501 -def get_logos_releases(app=None): - # Use already-downloaded list if requested again. - downloaded_releases = None - if config.TARGETVERSION == '9' and config.LOGOS9_RELEASES: - downloaded_releases = config.LOGOS9_RELEASES - elif config.TARGETVERSION == '10' and config.LOGOS10_RELEASES: - downloaded_releases = config.LOGOS10_RELEASES - if downloaded_releases: - logging.debug(f"Using already-downloaded list of v{config.TARGETVERSION} releases") # noqa: E501 - if app: - app.releases_q.put(downloaded_releases) - app.root.event_generate(app.release_evt) - return downloaded_releases - - msg.logos_msg(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 - # NOTE: This assumes that Verbum release numbers continue to mirror Logos. - url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 - - response_xml_bytes = net_get(url) - # if response_xml is None and None not in [q, app]: - if response_xml_bytes is None: - if app: - app.releases_q.put(None) - if config.DIALOG == 'tk': - app.root.event_generate(app.release_evt) - return None - - # Parse XML - root = ET.fromstring(response_xml_bytes.decode('utf-8-sig')) - - # Define namespaces - namespaces = { - 'ns0': 'http://www.w3.org/2005/Atom', - 'ns1': 'http://services.logos.com/update/v1/' - } - - # Extract versions - releases = [] - # Obtain all listed releases. - for entry in root.findall('.//ns1:version', namespaces): - release = entry.text - releases.append(release) - # if len(releases) == 5: - # break - - filtered_releases = filter_versions(releases, 30, 1) - logging.debug(f"Available releases: {', '.join(releases)}") - logging.debug(f"Filtered releases: {', '.join(filtered_releases)}") - - if app: - app.releases_q.put(filtered_releases) - if config.DIALOG == 'tk': - app.root.event_generate(app.release_evt) - elif config.DIALOG == 'curses': - app.releases_e.set() - return filtered_releases - - def get_winebin_code_and_desc(binary): # Set binary code, description, and path based on path codes = { @@ -1204,7 +956,7 @@ def install_winetricks( msg.logos_msg(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 zip_name = f"{version}.zip" - logos_reuse_download( + network.logos_reuse_download( f"{base_url}/{version}", zip_name, config.MYDOWNLOADS, @@ -1251,163 +1003,6 @@ def wait_process_using_dir(directory): logging.info("* End of wait_process_using_dir.") -def net_get(url, target=None, app=None, evt=None, q=None): - - # TODO: - # - Check available disk space before starting download - logging.debug(f"Download source: {url}") - logging.debug(f"Download destination: {target}") - target = FileProps(target) # sets path and size attribs - if app and target.path: - app.status_q.put(f"Downloading {target.path.name}…") # noqa: E501 - app.root.event_generate('<>') - parsed_url = urlparse(url) - domain = parsed_url.netloc # Gets the requested domain - url = UrlProps(url) # uses requests to set headers, size, md5 attribs - if url.headers is None: - logging.critical("Could not get headers.") - return None - - # Initialize variables. - local_size = 0 - total_size = url.size # None or int - logging.debug(f"File size on server: {total_size}") - percent = None - chunk_size = 100 * 1024 # 100 KB default - if type(total_size) is int: - # Use smaller of 2% of filesize or 2 MB for chunk_size. - chunk_size = min([int(total_size / 50), 2 * 1024 * 1024]) - # Force non-compressed file transfer for accurate progress tracking. - headers = {'Accept-Encoding': 'identity'} - file_mode = 'wb' - - # If file exists and URL is resumable, set download Range. - if target.path is not None and target.path.is_file(): - logging.debug(f"File exists: {str(target.path)}") - local_size = target.get_size() - logging.info(f"Current downloaded size in bytes: {local_size}") - if url.headers.get('Accept-Ranges') == 'bytes': - logging.debug("Server accepts byte range; attempting to resume download.") # noqa: E501 - file_mode = 'ab' - if type(url.size) is int: - headers['Range'] = f'bytes={local_size}-{total_size}' - else: - headers['Range'] = f'bytes={local_size}-' - - logging.debug(f"{chunk_size=}; {file_mode=}; {headers=}") - - # Log download type. - if 'Range' in headers.keys(): - message = f"Continuing download for {url.path}." - else: - message = f"Starting new download for {url.path}." - logging.info(message) - - # Initiate download request. - try: - if target.path is None: # return url content as text - with requests.get(url.path, headers=headers) as r: - if callable(r): - logging.error("Failed to retrieve data from the URL.") - return None - - try: - r.raise_for_status() - except requests.exceptions.HTTPError as e: - if domain == "github.com": - if ( - e.response.status_code == 403 - or e.response.status_code == 429 - ): - logging.error("GitHub API rate limit exceeded. Please wait before trying again.") # noqa: E501 - else: - logging.error(f"HTTP error occurred: {e.response.status_code}") # noqa: E501 - return None - - return r._content # raw bytes - else: # download url to target.path - with requests.get(url.path, stream=True, headers=headers) as r: - with target.path.open(mode=file_mode) as f: - if file_mode == 'wb': - mode_text = 'Writing' - else: - mode_text = 'Appending' - logging.debug(f"{mode_text} data to file {target.path}.") - for chunk in r.iter_content(chunk_size=chunk_size): - f.write(chunk) - local_size = target.get_size() - if type(total_size) is int: - percent = round(local_size / total_size * 100) - # if None not in [app, evt]: - if app: - # Send progress value to tk window. - app.get_q.put(percent) - if not evt: - evt = app.get_evt - app.root.event_generate(evt) - elif q is not None: - # Send progress value to queue param. - q.put(percent) - except requests.exceptions.RequestException as e: - logging.error(f"Error occurred during HTTP request: {e}") - return None # Return None values to indicate an error condition - except Exception as e: - msg.logos_error(e) - except KeyboardInterrupt: - print() - msg.logos_error("Killed with Ctrl+C") - - -def verify_downloaded_file(url, file_path, app=None, evt=None): - if app: - if config.DIALOG == "tk": - app.root.event_generate('<>') - app.status_q.put(f"Verifying {file_path}…") - app.root.event_generate('<>') - else: - app.status_q.put(f"Verifying {file_path}…") - res = False - msg = f"{file_path} is the wrong size." - right_size = same_size(url, file_path) - if right_size: - msg = f"{file_path} has the wrong MD5 sum." - right_md5 = same_md5(url, file_path) - if right_md5: - msg = f"{file_path} is verified." - res = True - logging.info(msg) - if app: - if config.DIALOG == "tk": - if not evt: - evt = app.check_evt - app.root.event_generate(evt) - return res - - -def same_md5(url, file_path): - logging.debug(f"Comparing MD5 of {url} and {file_path}.") - url_md5 = UrlProps(url).get_md5() - logging.debug(f"{url_md5=}") - if url_md5 is None: # skip MD5 check if not provided with URL - res = True - else: - file_md5 = FileProps(file_path).get_md5() - logging.debug(f"{file_md5=}") - res = url_md5 == file_md5 - return res - - -def same_size(url, file_path): - logging.debug(f"Comparing size of {url} and {file_path}.") - url_size = UrlProps(url).size - if not url_size: - return True - file_size = FileProps(file_path).size - logging.debug(f"{url_size=} B; {file_size=} B") - res = url_size == file_size - return res - - def write_progress_bar(percent, screen_width=80): y = '.' n = ' ' @@ -1487,126 +1082,6 @@ def get_latest_folder(folder_path): return latest -def get_latest_release_data(releases_url): - data = net_get(releases_url) - if data: - try: - json_data = json.loads(data.decode()) - logging.debug(f"{json_data=}") - except json.JSONDecodeError as e: - logging.error(f"Error decoding JSON response: {e}") - return None - - if not isinstance(json_data, list) or len(json_data) == 0: - logging.error("Invalid or empty JSON response.") - return None - else: - return json_data - else: - logging.critical("Could not get latest release URL.") - return None - - -def get_latest_release_url(json_data): - release_url = None - if json_data: - release_url = json_data[0].get('assets')[0].get('browser_download_url') # noqa: E501 - logging.info(f"Release URL: {release_url}") - return release_url - - -def get_latest_release_version_tag_name(json_data): - release_tag_name = None - if json_data: - release_tag_name = json_data[0].get('tag_name') # noqa: E501 - logging.info(f"Release URL Tag Name: {release_tag_name}") - return release_tag_name - - -def set_logoslinuxinstaller_latest_release_config(): - releases_url = "https://api.github.com/repos/FaithLife-Community/LogosLinuxInstaller/releases" # noqa: E501 - json_data = get_latest_release_data(releases_url) - logoslinuxinstaller_url = get_latest_release_url(json_data) - logoslinuxinstaller_tag_name = get_latest_release_version_tag_name(json_data) # noqa: E501 - if logoslinuxinstaller_url is None: - logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 - return - config.LOGOS_LATEST_VERSION_URL = logoslinuxinstaller_url - config.LOGOS_LATEST_VERSION_FILENAME = os.path.basename(logoslinuxinstaller_url) # noqa: #501 - # Getting version relies on the the tag_name field in the JSON data. This - # is already parsed down to vX.X.X. Therefore we must strip the v. - config.LLI_LATEST_VERSION = logoslinuxinstaller_tag_name.lstrip('v') - logging.info(f"{config.LLI_LATEST_VERSION}") - - -def set_recommended_appimage_config(): - releases_url = "https://api.github.com/repos/FaithLife-Community/wine-appimages/releases" # noqa: E501 - if not config.RECOMMENDED_WINE64_APPIMAGE_URL: - json_data = get_latest_release_data(releases_url) - appimage_url = get_latest_release_url(json_data) - if appimage_url is None: - logging.critical("Unable to set recommended appimage config without URL.") # noqa: E501 - return - config.RECOMMENDED_WINE64_APPIMAGE_URL = appimage_url - config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME = os.path.basename(config.RECOMMENDED_WINE64_APPIMAGE_URL) # noqa: E501 - config.RECOMMENDED_WINE64_APPIMAGE_FILENAME = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME.split(".AppImage")[0] # noqa: E501 - # Getting version and branch rely on the filename having this format: - # wine-[branch]_[version]-[arch] - parts = config.RECOMMENDED_WINE64_APPIMAGE_FILENAME.split('-') - branch_version = parts[1] - branch, version = branch_version.split('_') - config.RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION = f"v{version}-{branch}" - config.RECOMMENDED_WINE64_APPIMAGE_VERSION = f"{version}" - config.RECOMMENDED_WINE64_APPIMAGE_BRANCH = f"{branch}" - - -def check_for_updates(): - # We limit the number of times set_recommended_appimage_config is run in - # order to avoid GitHub API limits. This sets the check to once every 12 - # hours. - - config.current_logos_version = get_current_logos_version() - write_config(config.CONFIG_FILE) - - # TODO: Check for New Logos Versions. See #116. - - now = datetime.now().replace(microsecond=0) - if config.CHECK_UPDATES: - check_again = now - elif config.LAST_UPDATED is not None: - check_again = datetime.strptime( - config.LAST_UPDATED.strip(), - '%Y-%m-%dT%H:%M:%S' - ) - check_again += timedelta(hours=12) - else: - check_again = now - - if now >= check_again: - logging.debug("Running self-update.") - - set_logoslinuxinstaller_latest_release_config() - set_recommended_appimage_config() - - config.LAST_UPDATED = now.isoformat() - write_config(config.CONFIG_FILE) - else: - logging.debug("Skipping self-update.") - - -def get_recommended_appimage(): - wine64_appimage_full_filename = Path(config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME) # noqa: E501 - dest_path = Path(config.APPDIR_BINDIR) / wine64_appimage_full_filename - if dest_path.is_file(): - return - else: - logos_reuse_download( - config.RECOMMENDED_WINE64_APPIMAGE_URL, - config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME, - config.APPDIR_BINDIR - ) - - def install_premade_wine_bottle(srcdir, appdir): msg.logos_msg(f"Extracting: '{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 shutil.unpack_archive( @@ -1686,39 +1161,6 @@ def compare_recommended_appimage_version(): return status, message -def update_lli_binary(app=None): - lli_file_path = os.path.realpath(sys.argv[0]) - lli_download_path = Path(config.MYDOWNLOADS) / "LogosLinuxInstaller" - temp_path = Path(config.MYDOWNLOADS) / "LogosLinuxInstaller.tmp" - logging.debug(f"Updating Logos Linux Installer to latest version by overwriting: {lli_file_path}") # noqa: E501 - - # Remove existing downloaded file if different version. - if lli_download_path.is_file(): - logging.info("Checking if existing LLI binary is latest version.") - lli_download_ver = get_lli_release_version(lli_download_path) - if not lli_download_ver or lli_download_ver != config.LLI_LATEST_VERSION: # noqa: E501 - logging.info(f"Removing \"{lli_download_path}\", version: {lli_download_ver}") # noqa: E501 - # Remove incompatible file. - lli_download_path.unlink() - - logos_reuse_download( - config.LOGOS_LATEST_VERSION_URL, - "LogosLinuxInstaller", - config.MYDOWNLOADS, - app=app, - ) - shutil.copy(lli_download_path, temp_path) - try: - shutil.move(temp_path, lli_file_path) - except Exception as e: - logging.error(f"Failed to replace the binary: {e}") - return - - os.chmod(sys.argv[0], os.stat(sys.argv[0]).st_mode | 0o111) - logging.debug("Successfully updated Logos Linux Installer.") - restart_lli() - - def get_lli_release_version(lli_binary): lli_version = None # Ensure user-executable by adding 0o001. @@ -1920,7 +1362,7 @@ def update_to_latest_lli_release(app=None): if get_runmode() != 'binary': logging.error("Can't update LogosLinuxInstaller when run as a script.") elif status == 0: - update_lli_binary(app=app) + network.update_lli_binary(app=app) elif status == 1: logging.debug(f"{config.LLI_TITLE} is already at the latest version.") elif status == 2: From 23e56c8bc95d559dea773550ef2bebe3660e7dfa Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sun, 14 Jul 2024 23:02:58 -0400 Subject: [PATCH 037/253] Create system.py --- control.py | 9 +- gui_app.py | 5 +- installer.py | 7 +- main.py | 5 +- system.py | 600 +++++++++++++++++++++++++++++++++++++++++++++++++ tui_app.py | 8 +- utils.py | 618 ++------------------------------------------------- 7 files changed, 635 insertions(+), 617 deletions(-) create mode 100644 system.py diff --git a/control.py b/control.py index 6b03d47f..5855bfde 100644 --- a/control.py +++ b/control.py @@ -18,6 +18,7 @@ # import installer import msg import network +import system import tui_curses import tui_app import utils @@ -290,7 +291,7 @@ def set_winetricks(): "1: Use local winetricks.", "2: Download winetricks from the Internet" ] - winetricks_choice = tui.menu(options, title, question_text) + winetricks_choice = tui_curses.menu(options, title, question_text) logging.debug(f"winetricks_choice: {winetricks_choice}") if winetricks_choice.startswith("1"): @@ -299,7 +300,7 @@ def set_winetricks(): return 0 elif winetricks_choice.startswith("2"): # download_winetricks() - utils.install_winetricks(config.APPDIR_BINDIR) + system.install_winetricks(config.APPDIR_BINDIR) config.WINETRICKSBIN = os.path.join( config.APPDIR_BINDIR, "winetricks" @@ -311,7 +312,7 @@ def set_winetricks(): else: msg.logos_msg("The system's winetricks is too old. Downloading an up-to-date winetricks from the Internet...") # noqa: E501 # download_winetricks() - utils.install_winetricks(config.APPDIR_BINDIR) + system.install_winetricks(config.APPDIR_BINDIR) config.WINETRICKSBIN = os.path.join( config.APPDIR_BINDIR, "winetricks" @@ -320,7 +321,7 @@ def set_winetricks(): else: msg.logos_msg("Local winetricks not found. Downloading winetricks from the Internet…") # noqa: E501 # download_winetricks() - utils.install_winetricks(config.APPDIR_BINDIR) + system.install_winetricks(config.APPDIR_BINDIR) config.WINETRICKSBIN = os.path.join( config.APPDIR_BINDIR, "winetricks" diff --git a/gui_app.py b/gui_app.py index 979cab67..4b7a05df 100644 --- a/gui_app.py +++ b/gui_app.py @@ -19,6 +19,7 @@ import gui import installer import network +import system import utils import wine @@ -758,7 +759,7 @@ def set_appimage(self, evt=None): def get_winetricks(self, evt=None): self.gui.statusvar.set("Installing Winetricks…") t1 = Thread( - target=utils.install_winetricks, + target=system.install_winetricks, args=[config.APPDIR_BINDIR], kwargs={'app': self}, daemon=True, @@ -815,7 +816,7 @@ def update_app_button(self, evt=None): def update_latest_lli_release_button(self, evt=None): status, reason = utils.compare_logos_linux_installer_version() msg = None - if utils.get_runmode() != 'binary': + if system.get_runmode() != 'binary': state = 'disabled' msg = "This button is disabled. Can't run self-update from script." elif status == 0: diff --git a/installer.py b/installer.py index 05a81965..c4effc37 100644 --- a/installer.py +++ b/installer.py @@ -7,6 +7,7 @@ import config import msg import network +import system import utils import wine @@ -368,7 +369,7 @@ def ensure_winetricks_executable(app=None): # The choice of System winetricks was made previously. Here we are only # concerned about whether the downloaded winetricks is usable. msg.logos_msg("Downloading winetricks from the Internet…") - utils.install_winetricks( + system.install_winetricks( tricksbin.parent, app=app ) @@ -580,7 +581,7 @@ def ensure_launcher_executable(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_config_file(app=app) config.INSTALL_STEP += 1 - runmode = utils.get_runmode() + runmode = system.get_runmode() if runmode != 'binary': return update_install_feedback( @@ -602,7 +603,7 @@ def ensure_launcher_shortcuts(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_launcher_executable(app=app) config.INSTALL_STEP += 1 - runmode = utils.get_runmode() + runmode = system.get_runmode() if runmode != 'binary': return update_install_feedback("Creating launcher shortcuts…", app=app) diff --git a/main.py b/main.py index 47851783..ccac3940 100755 --- a/main.py +++ b/main.py @@ -10,6 +10,7 @@ import installer import msg import network +import system import tui_app import utils import wine @@ -307,14 +308,14 @@ def main(): # Set DIALOG and GUI variables. if config.DIALOG is None: - utils.get_dialog() + system.get_dialog() else: config.DIALOG = config.DIALOG.lower() if config.DIALOG == 'tk': config.GUI = True if config.DIALOG == 'curses': - config.use_python_dialog = utils.test_dialog_version() + config.use_python_dialog = system.test_dialog_version() if config.use_python_dialog is None: logging.debug("The 'dialog' package was not found. Falling back to Python Curses.") diff --git a/system.py b/system.py new file mode 100644 index 00000000..88356610 --- /dev/null +++ b/system.py @@ -0,0 +1,600 @@ +import logging +import distro +import os +import shlex +import shutil +import subprocess +import sys +import time +import zipfile +from pathlib import Path + +import config +import msg +import network + + +def run_command(command, retries=1, delay=0, stdin=None, shell=False): + if retries < 1: + retries = 1 + + for attempt in range(retries): + try: + logging.debug(f"Attempting to execute {command}") + result = subprocess.run( + command, + stdin=stdin, + check=True, + text=True, + shell=shell, + capture_output=True + ) + return result + except subprocess.CalledProcessError as e: + logging.error(f"Error occurred while executing {command}: {e}") + if "lock" in str(e): + logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") + time.sleep(delay) + else: + raise e + except Exception as e: + logging.error(f"An unexpected error occurred when running {command}: {e}") + return None + + logging.error(f"Failed to execute after {retries} attempts: '{command}'") + + +def reboot(): + logging.info("Rebooting system.") + command = f"{config.SUPERUSER_COMMAND} reboot now" + subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + text=True + ) + sys.exit(0) + + +def t(command): + if shutil.which(command) is not None: + return True + else: + return False + + +def tl(library): + try: + __import__(library) + return True + except ImportError: + return False + + +def get_dialog(): + if not os.environ.get('DISPLAY'): + msg.logos_error("The installer does not work unless you are running a display") # noqa: E501 + + DIALOG = os.getenv('DIALOG') + config.GUI = False + # Set config.DIALOG. + if DIALOG is not None: + DIALOG = DIALOG.lower() + if DIALOG not in ['curses', 'tk']: + msg.logos_error("Valid values for DIALOG are 'curses' or 'tk'.") + config.DIALOG = DIALOG + elif sys.__stdin__.isatty(): + config.DIALOG = 'curses' + else: + config.DIALOG = 'tk' + # Set config.GUI. + if config.DIALOG == 'tk': + config.GUI = True + + +def get_os(): + # TODO: Remove if we can verify these are no longer needed commented code. + + # Try reading /etc/os-release + # try: + # with open('/etc/os-release', 'r') as f: + # os_release_content = f.read() + # match = re.search( + # r'^ID=(\S+).*?VERSION_ID=(\S+)', + # os_release_content, re.MULTILINE + # ) + # if match: + # config.OS_NAME = match.group(1) + # config.OS_RELEASE = match.group(2) + # return config.OS_NAME, config.OS_RELEASE + # except FileNotFoundError: + # pass + + # Try using lsb_release command + # try: + # config.OS_NAME = platform.linux_distribution()[0] + # config.OS_RELEASE = platform.linux_distribution()[1] + # return config.OS_NAME, config.OS_RELEASE + # except AttributeError: + # pass + + # Try reading /etc/lsb-release + # try: + # with open('/etc/lsb-release', 'r') as f: + # lsb_release_content = f.read() + # match = re.search( + # r'^DISTRIB_ID=(\S+).*?DISTRIB_RELEASE=(\S+)', + # lsb_release_content, + # re.MULTILINE + # ) + # if match: + # config.OS_NAME = match.group(1) + # config.OS_RELEASE = match.group(2) + # return config.OS_NAME, config.OS_RELEASE + # except FileNotFoundError: + # pass + + # Try reading /etc/debian_version + # try: + # with open('/etc/debian_version', 'r') as f: + # config.OS_NAME = 'Debian' + # config.OS_RELEASE = f.read().strip() + # return config.OS_NAME, config.OS_RELEASE + # except FileNotFoundError: + # pass + + # Add more conditions for other distributions as needed + + # Fallback to platform module + config.OS_NAME = distro.id() # FIXME: Not working. Returns "Linux". + logging.info(f"OS name: {config.OS_NAME}") + config.OS_RELEASE = distro.version() + logging.info(f"OS release: {config.OS_RELEASE}") + return config.OS_NAME, config.OS_RELEASE + + +def get_superuser_command(): + if config.DIALOG == 'tk': + if shutil.which('pkexec'): + config.SUPERUSER_COMMAND = "pkexec" + else: + msg.logos_error("No superuser command found. Please install pkexec.") # noqa: E501 + else: + if shutil.which('sudo'): + config.SUPERUSER_COMMAND = "sudo" + elif shutil.which('doas'): + config.SUPERUSER_COMMAND = "doas" + else: + msg.logos_error("No superuser command found. Please install sudo or doas.") # noqa: E501 + logging.debug(f"{config.SUPERUSER_COMMAND=}") + + +def get_package_manager(): + # Check for package manager and associated packages + if shutil.which('apt') is not None: # debian, ubuntu + config.PACKAGE_MANAGER_COMMAND_INSTALL = "apt install -y" + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "apt install --download-only -y" + config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" + config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" + config.QUERY_PREFIX = '.i ' + if distro.id() == "ubuntu" and distro.version() < "24.04": + fuse = "fuse" + else: + fuse = "fuse3" + config.PACKAGES = f"binutils cabextract {fuse} wget winbind" + config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages + config.BADPACKAGES = "appimagelauncher" + elif shutil.which('dnf') is not None: # rhel, fedora + config.PACKAGE_MANAGER_COMMAND_INSTALL = "dnf install -y" + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "dnf install --downloadonly -y" + config.PACKAGE_MANAGER_COMMAND_REMOVE = "dnf remove -y" + config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed" + config.QUERY_PREFIX = '' + config.PACKAGES = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 + config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages + config.BADPACKAGES = "appiamgelauncher" + elif shutil.which('pamac') is not None: # manjaro + config.PACKAGE_MANAGER_COMMAND_INSTALL = "pamac install --no-upgrade --no-confirm" # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pamac install --download-only --no-confirm" + config.PACKAGE_MANAGER_COMMAND_REMOVE = "pamac remove --no-confirm" + config.PACKAGE_MANAGER_COMMAND_QUERY = "pamac list -i" + config.QUERY_PREFIX = '' + config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 + config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages + config.BADPACKAGES = "appimagelauncher" + elif shutil.which('pacman') is not None: # arch, steamOS + config.PACKAGE_MANAGER_COMMAND_INSTALL = r"pacman -Syu --overwrite * --noconfirm --needed" # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pacman -Sw -y" + config.PACKAGE_MANAGER_COMMAND_REMOVE = r"pacman -R --no-confirm" + config.PACKAGE_MANAGER_COMMAND_QUERY = "pacman -Q" + config.QUERY_PREFIX = '' + if config.OS_NAME == "steamos": # steamOS + config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" #noqa: #E501 + else: # arch + config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 + config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages + config.BADPACKAGES = "appimagelauncher" + # Add more conditions for other package managers as needed + + # Add logging output. + logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_INSTALL=}") + logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_QUERY=}") + logging.debug(f"{config.PACKAGES=}") + logging.debug(f"{config.L9PACKAGES=}") + + +def get_runmode(): + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + return 'binary' + else: + return 'script' + + +def query_packages(packages, elements=None, mode="install", app=None): + result = "" + if config.SKIP_DEPENDENCIES: + return + + missing_packages = [] + conflicting_packages = [] + + command = config.PACKAGE_MANAGER_COMMAND_QUERY + + try: + result = run_command(command, shell=True) + except Exception as e: + logging.error(f"Error occurred while executing command: {e}") + + package_list = result.stdout + + logging.debug(f"Checking packages: {packages} in package list.") + if app is not None: + if elements is None: + elements = {} # Initialize elements if not provided + elif isinstance(elements, list): + elements = {element[0]: element[1] for element in elements} + + for p in packages: + status = "Unchecked" + for line in package_list.split('\n'): + if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": + status = "Installed" + break + elif line.strip().startswith(p) and mode == "remove": + conflicting_packages.append(p) + status = "Conflicting" + break + + if status == "Unchecked": + if mode == "install": + missing_packages.append(p) + status = "Missing" + elif mode == "remove": + status = "Not Installed" + + logging.debug(f"Setting {p}: {status}") + elements[p] = status + + if app is not None and config.DIALOG == "curses": + app.report_dependencies( + f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", + 100 * (packages.index(p) + 1) // len(packages), + elements, + dialog=config.use_python_dialog) + + txt = 'None' + if mode == "install": + if missing_packages: + txt = f"Missing packages: {' '.join(missing_packages)}" + logging.info(f"Missing packages: {txt}") + return missing_packages, elements + elif mode == "remove": + if conflicting_packages: + txt = f"Conflicting packages: {' '.join(conflicting_packages)}" + logging.info(f"Conflicting packages: {txt}") + return conflicting_packages, elements + + +def download_packages(packages, elements, app=None): + if config.SKIP_DEPENDENCIES: + return + + if packages: + total_packages = len(packages) + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_DOWNLOAD} {' '.join(packages)}" + logging.debug(f"download_packages cmd: {command}") + command_args = shlex.split(command) + result = run_command(command_args, retries=5, delay=15) + + for index, package in enumerate(packages): + status = "Downloaded" if result.returncode == 0 else "Failed" + if elements is not None: + elements[index] = (package, status) + + if app is not None and config.DIALOG == "curses" and elements is not None: + app.report_dependencies(f"Downloading Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog) + + +def install_packages(packages, elements, app=None): + if config.SKIP_DEPENDENCIES: + return + + if packages: + total_packages = len(packages) + for index, package in enumerate(packages): + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" + logging.debug(f"install_packages cmd: {command}") + result = run_command(command, retries=5, delay=15) + + if elements is not None: + elements[index] = ( + package, + "Installed" if result.returncode == 0 else "Failed") + + if app is not None and config.DIALOG == "curses" and elements is not None: + app.report_dependencies( + f"Installing Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, + elements, + dialog=config.use_python_dialog) + + +def remove_packages(packages, elements, app=None): + if config.SKIP_DEPENDENCIES: + return + + if packages: + total_packages = len(packages) + for index, package in enumerate(packages): + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {package}" + logging.debug(f"remove_packages cmd: {command}") + result = run_command(command, retries=5, delay=15) + + if elements is not None: + elements[index] = ( + package, + "Removed" if result.returncode == 0 else "Failed") + + if app is not None and config.DIALOG == "curses" and elements is not None: + app.report_dependencies( + f"Removing Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, + elements, + dialog=config.use_python_dialog) + + +def have_dep(cmd): + if shutil.which(cmd) is not None: + return True + else: + return False + + +def check_dialog_version(): + if have_dep("dialog"): + try: + result = run_command(["dialog", "--version"]) + version_info = result.stdout.strip() + if version_info.startswith("Version: "): + version_info = version_info[len("Version: "):] + return version_info + except subprocess.CalledProcessError as e: + print(f"Error running command: {e.stderr}") + except FileNotFoundError: + print("The 'dialog' command is not found. Please ensure it is installed and in your PATH.") + return None + + +def test_dialog_version(): + version = check_dialog_version() + + def parse_date(version): + try: + return version.split('-')[1] + except IndexError: + return '' + + minimum_version = "1.3-20201126-1" + + logging.debug(f"Current dialog version: {version}") + if version is not None: + minimum_version = parse_date(minimum_version) + current_version = parse_date(version) + logging.debug(f"Minimum dialog version: {minimum_version}. Installed version: {current_version}.") + return current_version > minimum_version + else: + return None + + +def preinstall_dependencies_ubuntu(): + try: + dpkg_output = run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) + mkdir_output = run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) + wget_key_output = run_command( + [config.SUPERUSER_COMMAND, "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", "https://dl.winehq.org/wine-builds/winehq.key"]) + lsb_release_output = run_command(["lsb_release", "-a"]) + codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[1].strip() + wget_sources_output = run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", + f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources"]) + apt_update_output = run_command([config.SUPERUSER_COMMAND, "apt", "update"]) + apt_install_output = run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) + except subprocess.CalledProcessError as e: + print(f"An error occurred: {e}") + print(f"Command output: {e.output}") + + +def preinstall_dependencies_steamos(): + command = [config.SUPERUSER_COMMAND, "steamos-readonly", "disable"] + steamsos_readonly_output = run_command(command) + command = [config.SUPERUSER_COMMAND, "pacman-key", "--init"] + pacman_key_init_output = run_command(command) + command = [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"] + pacman_key_populate_output = run_command(command) + + +def postinstall_dependencies_steamos(): + command =[ + config.SUPERUSER_COMMAND, + "sed", '-i', + 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 + '/etc/nsswitch.conf' + ] + sed_output = run_command(command) + command =[config.SUPERUSER_COMMAND, "locale-gen"] + locale_gen_output = run_command(command) + command =[ + config.SUPERUSER_COMMAND, + "systemctl", + "enable", + "--now", + "avahi-daemon" + ] + systemctl_avahi_daemon_output = run_command(command) + command =[config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"] + systemctl_cups = run_command(command) + command = [config.SUPERUSER_COMMAND, "steamos-readonly", "enable"] + steamos_readonly_output = run_command(command) + + +def preinstall_dependencies(): + if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mint": + preinstall_dependencies_ubuntu() + elif config.OS_NAME == "Steam": + preinstall_dependencies_steamos() + + +def postinstall_dependencies(): + if config.OS_NAME == "Steam": + postinstall_dependencies_steamos() + + +def install_dependencies(packages, badpackages, logos9_packages=None, app=None): + missing_packages = {} + conflicting_packages = {} + package_list = [] + elements = {} + bad_elements = {} + + if packages: + package_list = packages.split() + + bad_package_list = [] + if badpackages: + bad_package_list = badpackages.split() + + if logos9_packages: + package_list.extend(logos9_packages.split()) + + if config.DIALOG == "curses" and app is not None and elements is not None: + for p in package_list: + elements[p] = "Unchecked" + if config.DIALOG == "curses" and app is not None and bad_elements is not None: + for p in bad_package_list: + bad_elements[p] = "Unchecked" + + if config.DIALOG == "curses" and app is not None: + app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) + + if config.PACKAGE_MANAGER_COMMAND_QUERY: + missing_packages, elements = query_packages(package_list, elements, app=app) + conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", app=app) + + if config.PACKAGE_MANAGER_COMMAND_INSTALL: + if missing_packages and conflicting_packages: + message = f"Your {config.OS_NAME} computer requires installing and removing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}) and will remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 + logging.critical(message) + elif missing_packages: + message = f"Your {config.OS_NAME} computer requires installing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}). Proceed?" # noqa: E501 + logging.critical(message) + elif conflicting_packages: + message = f"Your {config.OS_NAME} computer requires removing some software. To continue, the program will attempt to remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 + logging.critical(message) + else: + logging.debug("No missing or conflicting dependencies found.") + + # TODO: Need to send continue question to user based on DIALOG. + # All we do above is create a message that we never send. + # Do we need a TK continue question? I see we have a CLI and curses one + # in msg.py + + preinstall_dependencies() + + # libfuse: for AppImage use. This is the only known needed library. + check_libs(["libfuse"]) + + if missing_packages: + download_packages(missing_packages, elements, app) + install_packages(missing_packages, elements, app) + + if conflicting_packages: + # AppImage Launcher is the only known conflicting package. + remove_packages(conflicting_packages, bad_elements, app) + #config.REBOOT_REQUIRED = True + #TODO: Verify with user before executing + + postinstall_dependencies() + + if config.REBOOT_REQUIRED: + reboot() + + else: + msg.logos_error( + f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the command(s) {missing_packages}. Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 + + +def have_lib(library, ld_library_path): + roots = ['/usr/lib', '/lib'] + if ld_library_path is not None: + roots = [*ld_library_path.split(':'), *roots] + for root in roots: + libs = [lib for lib in Path(root).rglob(f"{library}*")] + if len(libs) > 0: + logging.debug(f"'{library}' found at '{libs[0]}'") + return True + return False + + +def check_libs(libraries): + ld_library_path = os.environ.get('LD_LIBRARY_PATH', '') + for library in libraries: + have_lib_result = have_lib(library, ld_library_path) + if have_lib_result: + logging.info(f"* {library} is installed!") + else: + if config.PACKAGE_MANAGER_COMMAND_INSTALL: + message = f"Your {config.OS_NAME} install is missing the library: {library}. To continue, the script will attempt to install the library by using {config.PACKAGE_MANAGER_COMMAND_INSTALL}. Proceed?" # noqa: E501 + if msg.cli_continue_question(message, "", ""): + install_packages(config.PACKAGES) + else: + msg.logos_error( + f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the library: {library}. Please install the package associated with {library} for {config.OS_NAME}.") # noqa: E501 + + +def install_winetricks( + installdir, + app=None, + version=config.WINETRICKS_VERSION, +): + msg.logos_msg(f"Installing winetricks v{version}…") + base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 + zip_name = f"{version}.zip" + network.logos_reuse_download( + f"{base_url}/{version}", + zip_name, + config.MYDOWNLOADS, + app=app, + ) + wtzip = f"{config.MYDOWNLOADS}/{zip_name}" + logging.debug(f"Extracting winetricks script into {installdir}…") + with zipfile.ZipFile(wtzip) as z: + for zi in z.infolist(): + if zi.is_dir(): + continue + zi.filename = Path(zi.filename).name + if zi.filename == 'winetricks': + z.extract(zi, path=installdir) + break + os.chmod(f"{installdir}/winetricks", 0o755) + logging.debug("Winetricks installed.") diff --git a/tui_app.py b/tui_app.py index 37fa4347..fb416612 100644 --- a/tui_app.py +++ b/tui_app.py @@ -10,12 +10,12 @@ import config import control +import installer +import msg import network +import system import tui_curses -import tui_dialog import tui_screen -import installer -import msg import utils import wine @@ -482,7 +482,7 @@ def which_dialog_options(self, labels, dialog=False): def set_tui_menu_options(self, dialog=False): labels = [] - if config.LLI_LATEST_VERSION and utils.get_runmode() == 'binary': + if config.LLI_LATEST_VERSION and system.get_runmode() == 'binary': logging.debug("Checking if Logos Linux Installers needs updated.") # noqa: E501 status, error_message = utils.compare_logos_linux_installer_version() # noqa: E501 if status == 0: diff --git a/utils.py b/utils.py index c1aba80d..d1ad0d3f 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,4 @@ import atexit -import distro import glob import inspect import json @@ -8,15 +7,12 @@ import psutil import re import shutil -import shlex import signal import stat import subprocess import sys import threading -import time import tkinter as tk -import zipfile from packaging import version from pathlib import Path from typing import List, Union @@ -24,8 +20,11 @@ import config import msg import network -import wine +import system import tui_dialog +import wine + +#TODO: Move config commands to config.py def get_calling_function_name(): @@ -47,9 +46,9 @@ def append_unique(list, item): # Set "global" variables. def set_default_config(): - get_os() - get_superuser_command() - get_package_manager() + system.get_os() + system.get_superuser_command() + system.get_package_manager() if config.CONFIG_FILE is None: config.CONFIG_FILE = config.DEFAULT_CONFIG_PATH config.PRESENT_WORKING_DIRECTORY = os.getcwd() @@ -68,6 +67,12 @@ def set_runtime_config(): config.LOGOS_EXE = find_installed_product() +def log_current_persistent_config(): + logging.debug("Current persistent config:") + for k in config.core_config_keys: + logging.debug(f"{k}: {config.__dict__.get(k)}") + + def write_config(config_file_path): logging.info(f"Writing config to {config_file_path}") os.makedirs(os.path.dirname(config_file_path), exist_ok=True) @@ -145,49 +150,6 @@ def die(message): sys.exit(1) -def run_command(command, retries=1, delay=0, stdin=None, shell=False): - if retries < 1: - retries = 1 - - for attempt in range(retries): - try: - logging.debug(f"Attempting to execute {command}") - result = subprocess.run( - command, - stdin=stdin, - check=True, - text=True, - shell=shell, - capture_output=True - ) - return result - except subprocess.CalledProcessError as e: - logging.error(f"Error occurred while executing {command}: {e}") - if "lock" in str(e): - logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") - time.sleep(delay) - else: - raise e - except Exception as e: - logging.error(f"An unexpected error occurred when running {command}: {e}") - return None - - logging.error(f"Failed to execute after {retries} attempts: '{command}'") - - -def reboot(): - logging.info("Rebooting system.") - command = f"{config.SUPERUSER_COMMAND} reboot now" - subprocess.run( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - text=True - ) - sys.exit(0) - - def restart_lli(): logging.debug("Restarting Logos Linux Installer.") pidfile = Path('/tmp/LogosLinuxInstaller.pid') @@ -207,357 +169,6 @@ def set_debug(): config.WINEDEBUG = "" -def t(command): - if shutil.which(command) is not None: - return True - else: - return False - - -def tl(library): - try: - __import__(library) - return True - except ImportError: - return False - - -def get_dialog(): - if not os.environ.get('DISPLAY'): - msg.logos_error("The installer does not work unless you are running a display") # noqa: E501 - - DIALOG = os.getenv('DIALOG') - config.GUI = False - # Set config.DIALOG. - if DIALOG is not None: - DIALOG = DIALOG.lower() - if DIALOG not in ['curses', 'tk']: - msg.logos_error("Valid values for DIALOG are 'curses' or 'tk'.") - config.DIALOG = DIALOG - elif sys.__stdin__.isatty(): - config.DIALOG = 'curses' - else: - config.DIALOG = 'tk' - # Set config.GUI. - if config.DIALOG == 'tk': - config.GUI = True - - -def get_os(): - # TODO: Remove if we can verify these are no longer needed commented code. - - # Try reading /etc/os-release - # try: - # with open('/etc/os-release', 'r') as f: - # os_release_content = f.read() - # match = re.search( - # r'^ID=(\S+).*?VERSION_ID=(\S+)', - # os_release_content, re.MULTILINE - # ) - # if match: - # config.OS_NAME = match.group(1) - # config.OS_RELEASE = match.group(2) - # return config.OS_NAME, config.OS_RELEASE - # except FileNotFoundError: - # pass - - # Try using lsb_release command - # try: - # config.OS_NAME = platform.linux_distribution()[0] - # config.OS_RELEASE = platform.linux_distribution()[1] - # return config.OS_NAME, config.OS_RELEASE - # except AttributeError: - # pass - - # Try reading /etc/lsb-release - # try: - # with open('/etc/lsb-release', 'r') as f: - # lsb_release_content = f.read() - # match = re.search( - # r'^DISTRIB_ID=(\S+).*?DISTRIB_RELEASE=(\S+)', - # lsb_release_content, - # re.MULTILINE - # ) - # if match: - # config.OS_NAME = match.group(1) - # config.OS_RELEASE = match.group(2) - # return config.OS_NAME, config.OS_RELEASE - # except FileNotFoundError: - # pass - - # Try reading /etc/debian_version - # try: - # with open('/etc/debian_version', 'r') as f: - # config.OS_NAME = 'Debian' - # config.OS_RELEASE = f.read().strip() - # return config.OS_NAME, config.OS_RELEASE - # except FileNotFoundError: - # pass - - # Add more conditions for other distributions as needed - - # Fallback to platform module - config.OS_NAME = distro.id() # FIXME: Not working. Returns "Linux". - logging.info(f"OS name: {config.OS_NAME}") - config.OS_RELEASE = distro.version() - logging.info(f"OS release: {config.OS_RELEASE}") - return config.OS_NAME, config.OS_RELEASE - - -def get_superuser_command(): - if config.DIALOG == 'tk': - if shutil.which('pkexec'): - config.SUPERUSER_COMMAND = "pkexec" - else: - msg.logos_error("No superuser command found. Please install pkexec.") # noqa: E501 - else: - if shutil.which('sudo'): - config.SUPERUSER_COMMAND = "sudo" - elif shutil.which('doas'): - config.SUPERUSER_COMMAND = "doas" - else: - msg.logos_error("No superuser command found. Please install sudo or doas.") # noqa: E501 - logging.debug(f"{config.SUPERUSER_COMMAND=}") - - -def get_package_manager(): - # Check for package manager and associated packages - if shutil.which('apt') is not None: # debian, ubuntu - config.PACKAGE_MANAGER_COMMAND_INSTALL = "apt install -y" - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "apt install --download-only -y" - config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" - config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" - config.QUERY_PREFIX = '.i ' - if distro.id() == "ubuntu" and distro.version() < "24.04": - fuse = "fuse" - else: - fuse = "fuse3" - config.PACKAGES = f"binutils cabextract {fuse} wget winbind" - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appimagelauncher" - elif shutil.which('dnf') is not None: # rhel, fedora - config.PACKAGE_MANAGER_COMMAND_INSTALL = "dnf install -y" - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "dnf install --downloadonly -y" - config.PACKAGE_MANAGER_COMMAND_REMOVE = "dnf remove -y" - config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed" - config.QUERY_PREFIX = '' - config.PACKAGES = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appiamgelauncher" - elif shutil.which('pamac') is not None: # manjaro - config.PACKAGE_MANAGER_COMMAND_INSTALL = "pamac install --no-upgrade --no-confirm" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pamac install --download-only --no-confirm" - config.PACKAGE_MANAGER_COMMAND_REMOVE = "pamac remove --no-confirm" - config.PACKAGE_MANAGER_COMMAND_QUERY = "pamac list -i" - config.QUERY_PREFIX = '' - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appimagelauncher" - elif shutil.which('pacman') is not None: # arch, steamOS - config.PACKAGE_MANAGER_COMMAND_INSTALL = r"pacman -Syu --overwrite * --noconfirm --needed" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pacman -Sw -y" - config.PACKAGE_MANAGER_COMMAND_REMOVE = r"pacman -R --no-confirm" - config.PACKAGE_MANAGER_COMMAND_QUERY = "pacman -Q" - config.QUERY_PREFIX = '' - if config.OS_NAME == "steamos": # steamOS - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" #noqa: #E501 - else: # arch - config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appimagelauncher" - # Add more conditions for other package managers as needed - - # Add logging output. - logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_INSTALL=}") - logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_QUERY=}") - logging.debug(f"{config.PACKAGES=}") - logging.debug(f"{config.L9PACKAGES=}") - - -def get_runmode(): - if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): - return 'binary' - else: - return 'script' - - -def query_packages(packages, elements=None, mode="install", app=None): - result = "" - if config.SKIP_DEPENDENCIES: - return - - missing_packages = [] - conflicting_packages = [] - - command = config.PACKAGE_MANAGER_COMMAND_QUERY - - try: - result = run_command(command, shell=True) - except Exception as e: - logging.error(f"Error occurred while executing command: {e}") - - package_list = result.stdout - - logging.debug(f"Checking packages: {packages} in package list.") - if app is not None: - if elements is None: - elements = {} # Initialize elements if not provided - elif isinstance(elements, list): - elements = {element[0]: element[1] for element in elements} - - for p in packages: - status = "Unchecked" - for line in package_list.split('\n'): - if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": - status = "Installed" - break - elif line.strip().startswith(p) and mode == "remove": - conflicting_packages.append(p) - status = "Conflicting" - break - - if status == "Unchecked": - if mode == "install": - missing_packages.append(p) - status = "Missing" - elif mode == "remove": - status = "Not Installed" - - logging.debug(f"Setting {p}: {status}") - elements[p] = status - - if app is not None and config.DIALOG == "curses": - app.report_dependencies( - f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", - 100 * (packages.index(p) + 1) // len(packages), - elements, - dialog=config.use_python_dialog) - - txt = 'None' - if mode == "install": - if missing_packages: - txt = f"Missing packages: {' '.join(missing_packages)}" - logging.info(f"Missing packages: {txt}") - return missing_packages, elements - elif mode == "remove": - if conflicting_packages: - txt = f"Conflicting packages: {' '.join(conflicting_packages)}" - logging.info(f"Conflicting packages: {txt}") - return conflicting_packages, elements - - -def download_packages(packages, elements, app=None): - if config.SKIP_DEPENDENCIES: - return - - if packages: - total_packages = len(packages) - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_DOWNLOAD} {' '.join(packages)}" - logging.debug(f"download_packages cmd: {command}") - command_args = shlex.split(command) - result = run_command(command_args, retries=5, delay=15) - - for index, package in enumerate(packages): - status = "Downloaded" if result.returncode == 0 else "Failed" - if elements is not None: - elements[index] = (package, status) - - if app is not None and config.DIALOG == "curses" and elements is not None: - app.report_dependencies(f"Downloading Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog) - - -def install_packages(packages, elements, app=None): - if config.SKIP_DEPENDENCIES: - return - - if packages: - total_packages = len(packages) - for index, package in enumerate(packages): - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" - logging.debug(f"install_packages cmd: {command}") - result = run_command(command, retries=5, delay=15) - - if elements is not None: - elements[index] = ( - package, - "Installed" if result.returncode == 0 else "Failed") - - if app is not None and config.DIALOG == "curses" and elements is not None: - app.report_dependencies( - f"Installing Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, - elements, - dialog=config.use_python_dialog) - - -def remove_packages(packages, elements, app=None): - if config.SKIP_DEPENDENCIES: - return - - if packages: - total_packages = len(packages) - for index, package in enumerate(packages): - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {package}" - logging.debug(f"remove_packages cmd: {command}") - result = run_command(command, retries=5, delay=15) - - if elements is not None: - elements[index] = ( - package, - "Removed" if result.returncode == 0 else "Failed") - - if app is not None and config.DIALOG == "curses" and elements is not None: - app.report_dependencies( - f"Removing Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, - elements, - dialog=config.use_python_dialog) - - -def have_dep(cmd): - if shutil.which(cmd) is not None: - return True - else: - return False - - -def check_dialog_version(): - if have_dep("dialog"): - try: - result = run_command(["dialog", "--version"]) - version_info = result.stdout.strip() - if version_info.startswith("Version: "): - version_info = version_info[len("Version: "):] - return version_info - except subprocess.CalledProcessError as e: - print(f"Error running command: {e.stderr}") - except FileNotFoundError: - print("The 'dialog' command is not found. Please ensure it is installed and in your PATH.") - return None - - -def test_dialog_version(): - version = check_dialog_version() - - def parse_date(version): - try: - return version.split('-')[1] - except IndexError: - return '' - - minimum_version = "1.3-20201126-1" - - logging.debug(f"Current dialog version: {version}") - if version is not None: - minimum_version = parse_date(minimum_version) - current_version = parse_date(version) - logging.debug(f"Minimum dialog version: {minimum_version}. Installed version: {current_version}.") - return current_version > minimum_version - else: - return None - - def clean_all(): logging.info("Cleaning all temp files…") os.system("rm -fr /tmp/LBS.*") @@ -600,169 +211,6 @@ def delete_symlink(symlink_path): logging.error(f"Error removing symlink: {e}") -def preinstall_dependencies_ubuntu(): - try: - dpkg_output = run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) - mkdir_output = run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) - wget_key_output = run_command( - [config.SUPERUSER_COMMAND, "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", "https://dl.winehq.org/wine-builds/winehq.key"]) - lsb_release_output = run_command(["lsb_release", "-a"]) - codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[1].strip() - wget_sources_output = run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", - f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources"]) - apt_update_output = run_command([config.SUPERUSER_COMMAND, "apt", "update"]) - apt_install_output = run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) - except subprocess.CalledProcessError as e: - print(f"An error occurred: {e}") - print(f"Command output: {e.output}") - -def preinstall_dependencies_steamos(): - command = [config.SUPERUSER_COMMAND, "steamos-readonly", "disable"] - steamsos_readonly_output = run_command(command) - command = [config.SUPERUSER_COMMAND, "pacman-key", "--init"] - pacman_key_init_output = run_command(command) - command = [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"] - pacman_key_populate_output = run_command(command) - - -def postinstall_dependencies_steamos(): - command =[ - config.SUPERUSER_COMMAND, - "sed", '-i', - 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 - '/etc/nsswitch.conf' - ] - sed_output = run_command(command) - command =[config.SUPERUSER_COMMAND, "locale-gen"] - locale_gen_output = run_command(command) - command =[ - config.SUPERUSER_COMMAND, - "systemctl", - "enable", - "--now", - "avahi-daemon" - ] - systemctl_avahi_daemon_output = run_command(command) - command =[config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"] - systemctl_cups = run_command(command) - command = [config.SUPERUSER_COMMAND, "steamos-readonly", "enable"] - steamos_readonly_output = run_command(command) - - -def preinstall_dependencies(): - if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mint": - preinstall_dependencies_ubuntu() - elif config.OS_NAME == "Steam": - preinstall_dependencies_steamos() - - -def postinstall_dependencies(): - if config.OS_NAME == "Steam": - postinstall_dependencies_steamos() - - -def install_dependencies(packages, badpackages, logos9_packages=None, app=None): - missing_packages = {} - conflicting_packages = {} - package_list = [] - elements = {} - bad_elements = {} - - if packages: - package_list = packages.split() - - bad_package_list = [] - if badpackages: - bad_package_list = badpackages.split() - - if logos9_packages: - package_list.extend(logos9_packages.split()) - - if config.DIALOG == "curses" and app is not None and elements is not None: - for p in package_list: - elements[p] = "Unchecked" - if config.DIALOG == "curses" and app is not None and bad_elements is not None: - for p in bad_package_list: - bad_elements[p] = "Unchecked" - - if config.DIALOG == "curses" and app is not None: - app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) - - if config.PACKAGE_MANAGER_COMMAND_QUERY: - missing_packages, elements = query_packages(package_list, elements, app=app) - conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", app=app) - - if config.PACKAGE_MANAGER_COMMAND_INSTALL: - if missing_packages and conflicting_packages: - message = f"Your {config.OS_NAME} computer requires installing and removing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}) and will remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 - logging.critical(message) - elif missing_packages: - message = f"Your {config.OS_NAME} computer requires installing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}). Proceed?" # noqa: E501 - logging.critical(message) - elif conflicting_packages: - message = f"Your {config.OS_NAME} computer requires removing some software. To continue, the program will attempt to remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 - logging.critical(message) - else: - logging.debug("No missing or conflicting dependencies found.") - - # TODO: Need to send continue question to user based on DIALOG. - # All we do above is create a message that we never send. - # Do we need a TK continue question? I see we have a CLI and curses one - # in msg.py - - preinstall_dependencies() - - # libfuse: for AppImage use. This is the only known needed library. - check_libs(["libfuse"]) - - if missing_packages: - download_packages(missing_packages, elements, app) - install_packages(missing_packages, elements, app) - - if conflicting_packages: - # AppImage Launcher is the only known conflicting package. - remove_packages(conflicting_packages, bad_elements, app) - #config.REBOOT_REQUIRED = True - #TODO: Verify with user before executing - - postinstall_dependencies() - - if config.REBOOT_REQUIRED: - reboot() - - else: - msg.logos_error( - f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the command(s) {missing_packages}. Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 - - -def have_lib(library, ld_library_path): - roots = ['/usr/lib', '/lib'] - if ld_library_path is not None: - roots = [*ld_library_path.split(':'), *roots] - for root in roots: - libs = [lib for lib in Path(root).rglob(f"{library}*")] - if len(libs) > 0: - logging.debug(f"'{library}' found at '{libs[0]}'") - return True - return False - - -def check_libs(libraries): - ld_library_path = os.environ.get('LD_LIBRARY_PATH', '') - for library in libraries: - have_lib_result = have_lib(library, ld_library_path) - if have_lib_result: - logging.info(f"* {library} is installed!") - else: - if config.PACKAGE_MANAGER_COMMAND_INSTALL: - message = f"Your {config.OS_NAME} install is missing the library: {library}. To continue, the script will attempt to install the library by using {config.PACKAGE_MANAGER_COMMAND_INSTALL}. Proceed?" # noqa: E501 - if msg.cli_continue_question(message, "", ""): - install_packages(config.PACKAGES) - else: - msg.logos_error( - f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the library: {library}. Please install the package associated with {library} for {config.OS_NAME}.") # noqa: E501 - - def check_dependencies(app=None): if config.TARGETVERSION: targetversion = int(config.TARGETVERSION) @@ -775,9 +223,9 @@ def check_dependencies(app=None): app.root.event_generate('<>') if targetversion == 10: - install_dependencies(config.PACKAGES, config.BADPACKAGES, app=app) + system.install_dependencies(config.PACKAGES, config.BADPACKAGES, app=app) elif targetversion == 9: - install_dependencies( + system.install_dependencies( config.PACKAGES, config.BADPACKAGES, config.L9PACKAGES, @@ -948,34 +396,6 @@ def get_winetricks_options(): return winetricks_options -def install_winetricks( - installdir, - app=None, - version=config.WINETRICKS_VERSION, -): - msg.logos_msg(f"Installing winetricks v{version}…") - base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 - zip_name = f"{version}.zip" - network.logos_reuse_download( - f"{base_url}/{version}", - zip_name, - config.MYDOWNLOADS, - app=app, - ) - wtzip = f"{config.MYDOWNLOADS}/{zip_name}" - logging.debug(f"Extracting winetricks script into {installdir}…") - with zipfile.ZipFile(wtzip) as z: - for zi in z.infolist(): - if zi.is_dir(): - continue - zi.filename = Path(zi.filename).name - if zi.filename == 'winetricks': - z.extract(zi, path=installdir) - break - os.chmod(f"{installdir}/winetricks", 0o755) - logging.debug("Winetricks installed.") - - def get_pids_using_file(file_path, mode=None): pids = set() for proc in psutil.process_iter(['pid', 'open_files']): @@ -1031,12 +451,6 @@ def find_installed_product(): return exe -def log_current_persistent_config(): - logging.debug("Current persistent config:") - for k in config.core_config_keys: - logging.debug(f"{k}: {config.__dict__.get(k)}") - - def enough_disk_space(dest_dir, bytes_required): free_bytes = shutil.disk_usage(dest_dir).free logging.debug(f"{free_bytes=}; {bytes_required=}") @@ -1359,7 +773,7 @@ def set_appimage_symlink(app=None): def update_to_latest_lli_release(app=None): status, _ = compare_logos_linux_installer_version() - if get_runmode() != 'binary': + if system.get_runmode() != 'binary': logging.error("Can't update LogosLinuxInstaller when run as a script.") elif status == 0: network.update_lli_binary(app=app) From d961391a438adbef73b7178ed560d457873fd875 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 15 Jul 2024 00:26:11 -0400 Subject: [PATCH 038/253] Fix Debian packaging --- system.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/system.py b/system.py index 88356610..4ff98fd3 100644 --- a/system.py +++ b/system.py @@ -178,11 +178,15 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" config.QUERY_PREFIX = '.i ' + if distro.id() == "debian": + binutils = "binutils binutils-common binutils-x86-64-linux-gnu libbinutils libctf-nobfd0 libctf0 libgprofng0" + else: + binutils = "binutils" if distro.id() == "ubuntu" and distro.version() < "24.04": fuse = "fuse" else: fuse = "fuse3" - config.PACKAGES = f"binutils cabextract {fuse} wget winbind" + config.PACKAGES = f"{binutils} cabextract {fuse} wget winbind" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora From 9dfcb75e1909939f7ee4dc21e0bddfe17de8773a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 15 Jul 2024 01:26:50 -0400 Subject: [PATCH 039/253] Fix system.run_command(); Fix pkg status setting --- system.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/system.py b/system.py index 4ff98fd3..d7bcaeed 100644 --- a/system.py +++ b/system.py @@ -18,6 +18,9 @@ def run_command(command, retries=1, delay=0, stdin=None, shell=False): if retries < 1: retries = 1 + if isinstance(command, str) and not shell: + command = command.split() + for attempt in range(retries): try: logging.debug(f"Attempting to execute {command}") @@ -42,6 +45,7 @@ def run_command(command, retries=1, delay=0, stdin=None, shell=False): return None logging.error(f"Failed to execute after {retries} attempts: '{command}'") + return None def reboot(): @@ -333,10 +337,10 @@ def install_packages(packages, elements, app=None): result = run_command(command, retries=5, delay=15) if elements is not None: - elements[index] = ( - package, - "Installed" if result.returncode == 0 else "Failed") - + if result and result.returncode == 0: + elements[index] = (package, "Installed") + else: + elements[index] = (package, "Failed") if app is not None and config.DIALOG == "curses" and elements is not None: app.report_dependencies( f"Installing Packages ({index + 1}/{total_packages})", @@ -357,10 +361,10 @@ def remove_packages(packages, elements, app=None): result = run_command(command, retries=5, delay=15) if elements is not None: - elements[index] = ( - package, - "Removed" if result.returncode == 0 else "Failed") - + if result and result.returncode == 0: + elements[index] = (package, "Removed") + else: + elements[index] = (package, "Failed") if app is not None and config.DIALOG == "curses" and elements is not None: app.report_dependencies( f"Removing Packages ({index + 1}/{total_packages})", @@ -508,13 +512,13 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if config.PACKAGE_MANAGER_COMMAND_INSTALL: if missing_packages and conflicting_packages: message = f"Your {config.OS_NAME} computer requires installing and removing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}) and will remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 - logging.critical(message) + #logging.critical(message) elif missing_packages: message = f"Your {config.OS_NAME} computer requires installing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}). Proceed?" # noqa: E501 - logging.critical(message) + #logging.critical(message) elif conflicting_packages: message = f"Your {config.OS_NAME} computer requires removing some software. To continue, the program will attempt to remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 - logging.critical(message) + #logging.critical(message) else: logging.debug("No missing or conflicting dependencies found.") From b4c01c426787cd9c5e9c387cf2ea232ddba5dedc Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 15 Jul 2024 17:26:36 -0400 Subject: [PATCH 040/253] Fix check_libs(); fix Fedora packages --- system.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/system.py b/system.py index d7bcaeed..8dd1a201 100644 --- a/system.py +++ b/system.py @@ -530,7 +530,11 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): preinstall_dependencies() # libfuse: for AppImage use. This is the only known needed library. - check_libs(["libfuse"]) + if config.OS_NAME == "fedora": + fuse = "fuse" + else: + fuse = "libfuse" + check_libs([f"{fuse}"], app=app) if missing_packages: download_packages(missing_packages, elements, app) @@ -564,7 +568,7 @@ def have_lib(library, ld_library_path): return False -def check_libs(libraries): +def check_libs(libraries, app=None): ld_library_path = os.environ.get('LD_LIBRARY_PATH', '') for library in libraries: have_lib_result = have_lib(library, ld_library_path) @@ -573,8 +577,17 @@ def check_libs(libraries): else: if config.PACKAGE_MANAGER_COMMAND_INSTALL: message = f"Your {config.OS_NAME} install is missing the library: {library}. To continue, the script will attempt to install the library by using {config.PACKAGE_MANAGER_COMMAND_INSTALL}. Proceed?" # noqa: E501 - if msg.cli_continue_question(message, "", ""): - install_packages(config.PACKAGES) + #if msg.cli_continue_question(message, "", ""): + elements = {} + + if config.DIALOG == "curses" and app is not None and elements is not None: + for p in libraries: + elements[p] = "Unchecked" + + if config.DIALOG == "curses" and app is not None: + app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) + + install_packages(config.PACKAGES, elements, app=app) else: msg.logos_error( f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the library: {library}. Please install the package associated with {library} for {config.OS_NAME}.") # noqa: E501 From 4450496c1780d79aba17162f1ad90602cf52451d Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 16 Jul 2024 11:26:27 -0400 Subject: [PATCH 041/253] Make tui_app.resize_curses() work better --- config.py | 1 + tui_app.py | 94 +++++++++++++++++++++++++++------------------------ tui_curses.py | 30 ++++++++-------- 3 files changed, 66 insertions(+), 59 deletions(-) diff --git a/config.py b/config.py index 11b541a4..861788f3 100644 --- a/config.py +++ b/config.py @@ -104,6 +104,7 @@ total_pages = 0 options_per_page = 8 use_python_dialog = False +resizing = False def get_config_file_dict(config_file_path): diff --git a/tui_app.py b/tui_app.py index fb416612..d05f9dd5 100644 --- a/tui_app.py +++ b/tui_app.py @@ -80,7 +80,7 @@ def __init__(self, stdscr): self.window_width = "" self.update_tty_dimensions() self.main_window_height = 9 - self.menu_window_height = 14 + self.menu_window_height = 6 + config.options_per_page self.tui_screens = [] self.menu_options = [] self.threads = [] @@ -152,19 +152,24 @@ def end(self, signal, frame): sys.exit(0) def resize_curses(self): + config.resizing = True curses.endwin() - self.stdscr = curses.initscr() - curses.curs_set(0) - self.stdscr.clear() - self.stdscr.noutrefresh() + self.menu_window.clear() + self.menu_window.refresh() self.update_tty_dimensions() + available_height = self.window_height - self.menu_window_height - 6 + config.options_per_page = max(available_height, 3) + self.menu_window_height = 6 + config.options_per_page + logging.debug(f"DEV: Options: {config.options_per_page}") + logging.debug(f"DEV: Main: {self.main_window_height}") + logging.debug(f"DEV: Menu: {self.menu_window_height}") self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) - self.console = tui_screen.ConsoleScreen(self.main_window, 0, self.status_q, self.status_e, self.title) - self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 7, 0) - self.main_window.noutrefresh() - self.menu_window.noutrefresh() - curses.doupdate() + self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) + self.init_curses() + self.menu_window.refresh() + self.menu_window.clear() msg.status("Resizing window.", self) + config.resizing = False def display(self): signal.signal(signal.SIGINT, self.end) @@ -173,39 +178,40 @@ def display(self): self.active_screen = self.menu_screen while self.llirunning: - if isinstance(self.active_screen, tui_screen.CursesScreen): - self.main_window.erase() - self.menu_window.erase() - self.stdscr.timeout(100) - self.console.display() - - self.active_screen.display() - - #if (not isinstance(self.active_screen, tui_screen.TextScreen) - # and not isinstance(self.active_screen, tui_screen.TextDialog)): - if self.choice_q.qsize() > 0: - self.choice_processor( - self.menu_window, - self.active_screen.get_screen_id(), - self.choice_q.get()) - - if self.screen_q.qsize() > 0: - self.screen_q.get() - self.switch_q.put(1) - - if self.switch_q.qsize() > 0: - self.switch_q.get() - self.switch_screen(config.use_python_dialog) - - if len(self.tui_screens) == 0: - self.active_screen = self.menu_screen - else: - self.active_screen = self.tui_screens[-1] - - if isinstance(self.active_screen, tui_screen.CursesScreen): - self.main_window.noutrefresh() - self.menu_window.noutrefresh() - curses.doupdate() + if not config.resizing: + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.main_window.erase() + self.menu_window.erase() + self.stdscr.timeout(100) + self.console.display() + + self.active_screen.display() + + #if (not isinstance(self.active_screen, tui_screen.TextScreen) + # and not isinstance(self.active_screen, tui_screen.TextDialog)): + if self.choice_q.qsize() > 0: + self.choice_processor( + self.menu_window, + self.active_screen.get_screen_id(), + self.choice_q.get()) + + if self.screen_q.qsize() > 0: + self.screen_q.get() + self.switch_q.put(1) + + if self.switch_q.qsize() > 0: + self.switch_q.get() + self.switch_screen(config.use_python_dialog) + + if len(self.tui_screens) == 0: + self.active_screen = self.menu_screen + else: + self.active_screen = self.tui_screens[-1] + + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.main_window.noutrefresh() + self.menu_window.noutrefresh() + curses.doupdate() def run(self): try: @@ -234,7 +240,7 @@ def task_processor(self, evt=None, task=None): utils.start_thread(self.get_config(config.use_python_dialog)) elif task == 'DONE': self.finish_install() - elif task == 'TUI-RESIZE': + elif task == 'RESIZE': self.resize_curses() elif task == 'TUI-UPDATE-MENU': self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) diff --git a/tui_curses.py b/tui_curses.py index 63163343..f363d59c 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -147,22 +147,14 @@ def do_menu_down(app): def menu(app, question_text, options): - stdscr = app.get_menu_window() - current_option = config.current_option - current_page = config.current_page - options_per_page = config.options_per_page - config.total_pages = (len(options) - 1) // options_per_page + 1 - - app.menu_options = options - - while True: + def draw(app): stdscr.erase() question_start_y, question_lines = text_centered(app, question_text) # Display the options, centered options_start_y = question_start_y + len(question_lines) + 2 - for i in range(options_per_page): - index = current_page * options_per_page + i + for i in range(config.options_per_page): + index = config.current_page * config.options_per_page + i if index < len(options): option = options[index] if type(option) is list: @@ -193,7 +185,7 @@ def menu(app, question_text, options): y = options_start_y + i + j x = max(0, app.window_width // 2 - len(line) // 2) if y < app.menu_window_height: - if index == current_option: + if index == config.current_option: stdscr.addstr(y, x, line, curses.A_REVERSE) else: stdscr.addstr(y, x, line) @@ -203,18 +195,26 @@ def menu(app, question_text, options): # Display pagination information page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(options)}" + logging.debug(f"DEV: Options: {config.options_per_page}") + logging.debug(f"DEV: Main: {app.main_window_height}") + logging.debug(f"DEV: Menu: {app.menu_window_height}") stdscr.addstr(app.menu_window_height - 1, 2, page_info, curses.A_BOLD) # Refresh the windows stdscr.noutrefresh() + stdscr = app.get_menu_window() + options_per_page = config.options_per_page + config.total_pages = (len(options) - 1) // options_per_page + 1 + + app.menu_options = options + + while True: + draw(app) # Get user input thread = utils.start_thread(menu_keyboard, True, app) - thread.join() - stdscr.noutrefresh() - return From 065c8ad05a8cc58aa70c3e89d32e17d9f32975e4 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 16 Jul 2024 11:35:23 -0400 Subject: [PATCH 042/253] Make Dialog Import Conditional --- tui_screen.py | 7 +++---- utils.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tui_screen.py b/tui_screen.py index 7497751e..70edb0be 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -1,15 +1,14 @@ import logging -import queue - import time from pathlib import Path import curses +import sys -import msg import config import installer import tui_curses -import tui_dialog +if 'dialog' in sys.modules: + import tui_dialog import tui_screen import utils diff --git a/utils.py b/utils.py index d1ad0d3f..b6cda635 100644 --- a/utils.py +++ b/utils.py @@ -21,7 +21,8 @@ import msg import network import system -import tui_dialog +if 'dialog' in sys.modules: + import tui_dialog import wine #TODO: Move config commands to config.py From 030bec1291827be5396192f205384f055ce53cfa Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 16 Jul 2024 11:39:09 -0400 Subject: [PATCH 043/253] Clean up dev logging --- tui_app.py | 3 --- tui_curses.py | 3 --- tui_screen.py | 2 -- 3 files changed, 8 deletions(-) diff --git a/tui_app.py b/tui_app.py index d05f9dd5..7fae62ca 100644 --- a/tui_app.py +++ b/tui_app.py @@ -160,9 +160,6 @@ def resize_curses(self): available_height = self.window_height - self.menu_window_height - 6 config.options_per_page = max(available_height, 3) self.menu_window_height = 6 + config.options_per_page - logging.debug(f"DEV: Options: {config.options_per_page}") - logging.debug(f"DEV: Main: {self.main_window_height}") - logging.debug(f"DEV: Menu: {self.menu_window_height}") self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) self.init_curses() diff --git a/tui_curses.py b/tui_curses.py index f363d59c..d70f79e4 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -195,9 +195,6 @@ def draw(app): # Display pagination information page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(options)}" - logging.debug(f"DEV: Options: {config.options_per_page}") - logging.debug(f"DEV: Main: {app.main_window_height}") - logging.debug(f"DEV: Menu: {app.menu_window_height}") stdscr.addstr(app.menu_window_height - 1, 2, page_info, curses.A_BOLD) # Refresh the windows diff --git a/tui_screen.py b/tui_screen.py index 70edb0be..32564504 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -9,8 +9,6 @@ import tui_curses if 'dialog' in sys.modules: import tui_dialog -import tui_screen -import utils class Screen: From ab661ac1b05b4af1d6b32dc443440cba036624d9 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 16 Jul 2024 14:24:46 -0400 Subject: [PATCH 044/253] Fix threading issues --- main.py | 38 +++++++++++++++++++++++++++++--------- tui_app.py | 11 ++++++----- tui_curses.py | 15 +++++++++------ utils.py | 2 ++ 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/main.py b/main.py index ccac3940..f0ec4d0e 100755 --- a/main.py +++ b/main.py @@ -3,6 +3,12 @@ import os import argparse import curses +import sys +import threading + +processes = {} +threads = [] +stop_event = threading.Event() import config import control @@ -15,9 +21,6 @@ import utils import wine -processes = {} - - def get_parser(): desc = "Installs FaithLife Bible Software with Wine on Linux." parser = argparse.ArgumentParser(description=desc) @@ -260,12 +263,26 @@ def run_control_panel(): if config.DIALOG is None or config.DIALOG == 'tk': gui_app.control_panel_app() else: - #try: + try: curses.wrapper(tui_app.control_panel_app) - #except curses.error as e: - # logging.error(f"Curses error in run_control_panel(): {e}") - #except Exception as e: - # logging.error(f"An error occurred in run_control_panel(): {e}") + except KeyboardInterrupt: + stop_event.set() + for thread in threads: + thread.join() + raise + except SystemExit: + logging.info("Caught SystemExit, exiting gracefully...") + try: + close() + except Exception as e: + raise + except curses.error as e: + logging.error(f"Curses error in run_control_panel(): {e}") + raise + except Exception as e: + logging.error(f"An error occurred in run_control_panel(): {e}") + curses.endwin() + raise def main(): @@ -386,6 +403,9 @@ def close(): if __name__ == '__main__': - main() + try: + main() + except KeyboardInterrupt: + close() close() diff --git a/tui_app.py b/tui_app.py index 7fae62ca..e3638beb 100644 --- a/tui_app.py +++ b/tui_app.py @@ -19,6 +19,8 @@ import utils import wine +from main import threads + console_message = "" # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. @@ -83,8 +85,6 @@ def __init__(self, stdscr): self.menu_window_height = 6 + config.options_per_page self.tui_screens = [] self.menu_options = [] - self.threads = [] - self.threads_started = [] self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) self.console = None @@ -144,9 +144,10 @@ def end_curses(self): def end(self, signal, frame): logging.debug("Exiting…") - self.stdscr.clear() - curses.endwin() - for thread in self.threads_started: + if self.stdscr is not None: + self.stdscr.clear() + curses.endwin() + for thread in threads: thread.join() sys.exit(0) diff --git a/tui_curses.py b/tui_curses.py index d70f79e4..9d2358c9 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -7,6 +7,8 @@ import msg import utils +from main import stop_event + def wrap_text(app, text): # Turn text into wrapped text, line by line, centered @@ -207,12 +209,13 @@ def draw(app): app.menu_options = options while True: - draw(app) - # Get user input - thread = utils.start_thread(menu_keyboard, True, app) - thread.join() - stdscr.noutrefresh() - return + while not stop_event.is_set(): + draw(app) + # Get user input + thread = utils.start_thread(menu_keyboard, False, app) + thread.join() + stdscr.noutrefresh() + return def menu_keyboard(app): diff --git a/utils.py b/utils.py index b6cda635..a19f9c1e 100644 --- a/utils.py +++ b/utils.py @@ -27,6 +27,7 @@ #TODO: Move config commands to config.py +from main import threads def get_calling_function_name(): if 'inspect' in sys.modules: @@ -834,6 +835,7 @@ def grep(regexp, filepath): def start_thread(task, daemon_bool=True, *args): thread = threading.Thread(name=f"{task}", target=task, daemon=daemon_bool, args=args) + threads.append(thread) thread.start() return thread From d1cc3a791a4f92b2f1813411697454272c87333a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 16 Jul 2024 15:38:02 -0400 Subject: [PATCH 045/253] Fix #126 --- tui_curses.py | 101 +++++++++++++++++++++++++++++++------------------- tui_screen.py | 11 +++--- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/tui_curses.py b/tui_curses.py index 9d2358c9..397d1300 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -10,6 +10,24 @@ from main import stop_event +class CursesDialog(): + def __init__(self, app): + self.app = app + self.stdscr = app.get_menu_window() + + def __str__(self): + return f"Curses Dialog" + + def draw(self): + pass + + def input(self): + pass + + def run(self): + pass + + def wrap_text(app, text): # Turn text into wrapped text, line by line, centered wrapped_text = textwrap.fill(text, app.window_width - 4) @@ -71,54 +89,59 @@ def spinner(app, index, start_y=0): return i -def get_user_input(app, question_text, default_text): - stdscr = app.get_menu_window() - curses.echo() - curses.curs_set(1) - user_input = input_keyboard(app, question_text, default_text) - curses.curs_set(0) - curses.noecho() - - return user_input - - -def input_keyboard(app, question_text, default): - stdscr = app.get_menu_window() - done = False - choice = "" +class UserInputDialog(CursesDialog): + def __init__(self, app, question_text, default_text): + super().__init__(app) + self.question_text = question_text + self.default_text = default_text + self.user_input = "" + self.submit = False + self.question_start_y = None + self.question_lines = None + + def __str__(self): + return f"UserInput Curses Dialog" + + def draw(self): + curses.echo() + curses.curs_set(1) + self.stdscr.clear() + self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) + self.input() + curses.curs_set(0) + curses.noecho() + self.stdscr.refresh() - stdscr.clear() - question_start_y, question_lines = text_centered(app, question_text) + def input(self): + self.stdscr.addstr(self.question_start_y + len(self.question_lines) + 2, 10, self.user_input) + key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.user_input)) - try: - while done is False: - curses.echo() - key = stdscr.getch(question_start_y + len(question_lines) + 2, 10 + len(choice)) + try: if key == -1: # If key not found, keep processing. pass elif key == ord('\n'): # Enter key - if choice is None or not choice: - choice = default - logging.debug(f"Selected Path: {choice}") - done = True + self.submit = True elif key == curses.KEY_RESIZE: - utils.send_task(app, 'RESIZE') + utils.send_task(self.app, 'RESIZE') elif key == curses.KEY_BACKSPACE or key == 127: - if len(choice) > 0: - choice = choice[:-1] - stdscr.addstr(question_start_y + len(question_lines) + 2, 10, ' ' * (len(choice) + 1)) - stdscr.addstr(question_start_y + len(question_lines) + 2, 10, choice) + if len(self.user_input) > 0: + self.user_input = self.user_input[:-1] else: - choice += chr(key) - stdscr.addch(question_start_y + len(question_lines) + 2, 10 + len(choice) - 1, chr(key)) - stdscr.refresh() - curses.noecho() + self.user_input += chr(key) + except KeyboardInterrupt: + signal.signal(signal.SIGINT, self.app.end) - stdscr.refresh() - if done: - return choice - except KeyboardInterrupt: - signal.signal(signal.SIGINT, app.end) + logging.debug(f"DEV: {self.user_input}") + + def run(self): + if not self.submit: + self.draw() + return "Processing" + else: + if self.user_input is None or not self.user_input or self.user_input == "": + self.user_input = self.default_text + logging.debug(f"Selected Path: {self.user_input}") + return self.user_input def do_menu_up(app): diff --git a/tui_screen.py b/tui_screen.py index 32564504..b97dca87 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -129,17 +129,18 @@ def __init__(self, app, screen_id, queue, event, question, default): self.stdscr = self.app.get_menu_window() self.question = question self.default = default + self.user_input_dialog = tui_curses.UserInputDialog( + self.app, + self.question, + self.default + ) def __str__(self): return f"Curses Input Screen" def display(self): self.stdscr.erase() - self.choice = tui_curses.get_user_input( - self.app, - self.question, - self.default - ) + self.choice = self.user_input_dialog.run() self.submit_choice_to_queue() self.stdscr.noutrefresh() curses.doupdate() From a652f4e7fea6b2d1a7201b1a79e5a1bb1f31b36e Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 16 Jul 2024 17:47:43 -0400 Subject: [PATCH 046/253] Create tui_curses.MenuDialog --- main.py | 8 +- tui_app.py | 15 ++- tui_curses.py | 254 ++++++++++++++++++++++++-------------------------- tui_screen.py | 19 ++-- 4 files changed, 145 insertions(+), 151 deletions(-) diff --git a/main.py b/main.py index f0ec4d0e..8c6412f3 100755 --- a/main.py +++ b/main.py @@ -8,7 +8,6 @@ processes = {} threads = [] -stop_event = threading.Event() import config import control @@ -266,9 +265,6 @@ def run_control_panel(): try: curses.wrapper(tui_app.control_panel_app) except KeyboardInterrupt: - stop_event.set() - for thread in threads: - thread.join() raise except SystemExit: logging.info("Caught SystemExit, exiting gracefully...") @@ -276,12 +272,12 @@ def run_control_panel(): close() except Exception as e: raise + raise except curses.error as e: logging.error(f"Curses error in run_control_panel(): {e}") raise except Exception as e: logging.error(f"An error occurred in run_control_panel(): {e}") - curses.endwin() raise @@ -395,6 +391,8 @@ def main(): def close(): logging.debug("Closing Logos on Linux.") + for thread in threads: + thread.join() if len(processes) > 0: wine.end_wine_processes() else: diff --git a/tui_app.py b/tui_app.py index e3638beb..b874fb8b 100644 --- a/tui_app.py +++ b/tui_app.py @@ -139,18 +139,15 @@ def end_curses(self): curses.echo() except curses.error as e: logging.error(f"Curses error in end_curses: {e}") + raise except Exception as e: logging.error(f"An error occurred in end_curses(): {e}") + raise def end(self, signal, frame): logging.debug("Exiting…") - if self.stdscr is not None: - self.stdscr.clear() - curses.endwin() - for thread in threads: - thread.join() - - sys.exit(0) + self.llirunning = False + curses.endwin() def resize_curses(self): config.resizing = True @@ -215,6 +212,9 @@ def run(self): try: self.init_curses() self.display() + except KeyboardInterrupt: + self.end_curses() + signal.signal(signal.SIGINT, self.end) finally: self.end_curses() signal.signal(signal.SIGINT, self.end) @@ -255,7 +255,6 @@ def choice_processor(self, stdscr, screen_id, choice): msg.logos_warn("Exiting installation.") self.tui_screens = [] self.llirunning = False - sys.exit(0) elif choice.startswith("Install"): config.INSTALL_STEPS_COUNT = 0 config.INSTALL_STEP = 0 diff --git a/tui_curses.py b/tui_curses.py index 397d1300..2e009bd4 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -7,26 +7,6 @@ import msg import utils -from main import stop_event - - -class CursesDialog(): - def __init__(self, app): - self.app = app - self.stdscr = app.get_menu_window() - - def __str__(self): - return f"Curses Dialog" - - def draw(self): - pass - - def input(self): - pass - - def run(self): - pass - def wrap_text(app, text): # Turn text into wrapped text, line by line, centered @@ -61,6 +41,14 @@ def text_centered(app, text, start_y=0): return text_start_y, text_lines +def spinner(app, index, start_y=0): + spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"] + i = index + text_centered(app, spinner_chars[i], start_y) + i = (i + 1) % len(spinner_chars) + return i + + def confirm(app, question_text, height=None, width=None): stdscr = app.get_menu_window() question_text = question_text + " [Y/n]: " @@ -80,13 +68,22 @@ def confirm(app, question_text, height=None, width=None): stdscr.addstr(y, 0, "Type Y[es] or N[o]. ") -def spinner(app, index, start_y=0): - spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"] - i = index - text_centered(app, spinner_chars[i], start_y) - i = (i + 1) % len(spinner_chars) +class CursesDialog: + def __init__(self, app): + self.app = app + self.stdscr = self.app.get_menu_window() - return i + def __str__(self): + return f"Curses Dialog" + + def draw(self): + pass + + def input(self): + pass + + def run(self): + pass class UserInputDialog(CursesDialog): @@ -131,57 +128,42 @@ def input(self): except KeyboardInterrupt: signal.signal(signal.SIGINT, self.app.end) - logging.debug(f"DEV: {self.user_input}") - def run(self): if not self.submit: self.draw() return "Processing" else: - if self.user_input is None or not self.user_input or self.user_input == "": + if self.user_input is None or self.user_input == "": self.user_input = self.default_text logging.debug(f"Selected Path: {self.user_input}") return self.user_input -def do_menu_up(app): - if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: - # Move to the previous page - config.current_page -= 1 - config.current_option = min(len(app.menu_options) - 1, (config.current_page + 1) * config.options_per_page - 1) - elif config.current_option == 0: - if config.total_pages == 1: - config.current_option = len(app.menu_options) - 1 - else: - config.current_page = config.total_pages - 1 - config.current_option = len(app.menu_options) - 1 - else: - config.current_option = max(0, config.current_option - 1) - - -def do_menu_down(app): - if config.current_option == (config.current_page + 1) * config.options_per_page - 1 and config.current_page < config.total_pages - 1: - # Move to the next page - config.current_page += 1 - config.current_option = min(len(app.menu_options) - 1, config.current_page * config.options_per_page) - elif config.current_option == len(app.menu_options) - 1: - config.current_page = 0 - config.current_option = 0 - else: - config.current_option = min(len(app.menu_options) - 1, config.current_option + 1) +class MenuDialog(CursesDialog): + def __init__(self, app, question_text, options): + super().__init__(app) + self.user_input = "Processing" + self.submit = False + self.question_text = question_text + self.options = options + self.question_start_y = None + self.question_lines = None + def __str__(self): + return f"Menu Curses Dialog" -def menu(app, question_text, options): - def draw(app): - stdscr.erase() - question_start_y, question_lines = text_centered(app, question_text) + def draw(self): + self.stdscr.erase() + self.app.active_screen.set_options(self.options) + config.total_pages = (len(self.options) - 1) // config.options_per_page + 1 + self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) # Display the options, centered - options_start_y = question_start_y + len(question_lines) + 2 + options_start_y = self.question_start_y + len(self.question_lines) + 2 for i in range(config.options_per_page): index = config.current_page * config.options_per_page + i - if index < len(options): - option = options[index] + if index < len(self.options): + option = self.options[index] if type(option) is list: option_lines = [] wine_binary_code = option[0] @@ -189,99 +171,107 @@ def draw(app): wine_binary_path = option[1] wine_binary_description = option[2] wine_binary_path_wrapped = textwrap.wrap( - f"Binary Path: {wine_binary_path}", app.window_width - 4) + f"Binary Path: {wine_binary_path}", self.app.window_width - 4) option_lines.extend(wine_binary_path_wrapped) wine_binary_desc_wrapped = textwrap.wrap( - f"Description: {wine_binary_description}", app.window_width - 4) + f"Description: {wine_binary_description}", self.app.window_width - 4) option_lines.extend(wine_binary_desc_wrapped) else: wine_binary_path = option[1] wine_binary_description = option[2] wine_binary_path_wrapped = textwrap.wrap( - f"{wine_binary_path}", app.window_width - 4) + f"{wine_binary_path}", self.app.window_width - 4) option_lines.extend(wine_binary_path_wrapped) wine_binary_desc_wrapped = textwrap.wrap( - f"{wine_binary_description}", app.window_width - 4) + f"{wine_binary_description}", self.app.window_width - 4) option_lines.extend(wine_binary_desc_wrapped) else: - option_lines = textwrap.wrap(option, app.window_width - 4) + option_lines = textwrap.wrap(option, self.app.window_width - 4) for j, line in enumerate(option_lines): y = options_start_y + i + j - x = max(0, app.window_width // 2 - len(line) // 2) - if y < app.menu_window_height: + x = max(0, self.app.window_width // 2 - len(line) // 2) + if y < self.app.menu_window_height: if index == config.current_option: - stdscr.addstr(y, x, line, curses.A_REVERSE) + self.stdscr.addstr(y, x, line, curses.A_REVERSE) else: - stdscr.addstr(y, x, line) + self.stdscr.addstr(y, x, line) if type(option) is list: options_start_y += (len(option_lines)) # Display pagination information - page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(options)}" - stdscr.addstr(app.menu_window_height - 1, 2, page_info, curses.A_BOLD) + page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(self.options)}" + self.stdscr.addstr(self.app.menu_window_height - 1, 2, page_info, curses.A_BOLD) + + def do_menu_up(self): + if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: + # Move to the previous page + config.current_page -= 1 + config.current_option = min(len(self.app.menu_options) - 1, (config.current_page + 1) * config.options_per_page - 1) + elif config.current_option == 0: + if config.total_pages == 1: + config.current_option = len(self.app.menu_options) - 1 + else: + config.current_page = config.total_pages - 1 + config.current_option = len(self.app.menu_options) - 1 + else: + config.current_option = max(0, config.current_option - 1) + + def do_menu_down(self): + if config.current_option == (config.current_page + 1) * config.options_per_page - 1 and config.current_page < config.total_pages - 1: + # Move to the next page + config.current_page += 1 + config.current_option = min(len(self.app.menu_options) - 1, config.current_page * config.options_per_page) + elif config.current_option == len(self.app.menu_options) - 1: + config.current_page = 0 + config.current_option = 0 + else: + config.current_option = min(len(self.app.menu_options) - 1, config.current_option + 1) - # Refresh the windows - stdscr.noutrefresh() + def input(self): + if len(self.app.tui_screens) > 0: + self.stdscr = self.app.tui_screens[-1].get_stdscr() + else: + self.stdscr = self.app.menu_screen.get_stdscr() + key = self.stdscr.getch() - stdscr = app.get_menu_window() - options_per_page = config.options_per_page - config.total_pages = (len(options) - 1) // options_per_page + 1 + try: + if key == -1: # If key not found, keep processing. + pass + elif key == curses.KEY_RESIZE: + utils.send_task(self.app, 'RESIZE') + elif key == curses.KEY_UP or key == 259: # Up arrow + self.do_menu_up() + elif key == curses.KEY_DOWN or key == 258: # Down arrow + self.do_menu_down() + elif key == 27: # Sometimes the up/down arrow key is represented by a series of three keys. + next_key = self.stdscr.getch() + if next_key == 91: + final_key = self.stdscr.getch() + if final_key == 65: + self.do_menu_up() + elif final_key == 66: + self.do_menu_down() + elif key == ord('\n') or key == 10: # Enter key + self.user_input = self.options[config.current_option] + elif key == ord('\x1b'): + signal.signal(signal.SIGINT, self.app.end) + else: + msg.status("Input unknown.", self.app) + pass + except KeyboardInterrupt: + signal.signal(signal.SIGINT, self.app.end) - app.menu_options = options + self.stdscr.noutrefresh() - while True: - while not stop_event.is_set(): - draw(app) - # Get user input - thread = utils.start_thread(menu_keyboard, False, app) - thread.join() - stdscr.noutrefresh() - return - - -def menu_keyboard(app): - if len(app.tui_screens) > 0: - stdscr = app.tui_screens[-1].get_stdscr() - else: - stdscr = app.menu_screen.get_stdscr() - options = app.menu_options - key = stdscr.getch() - choice = "" - - try: - if key == -1: # If key not found, keep processing. - pass - elif key == curses.KEY_RESIZE: - utils.send_task(app, 'RESIZE') - elif key == curses.KEY_UP or key == 259: # Up arrow - do_menu_up(app) - elif key == curses.KEY_DOWN or key == 258: # Down arrow - do_menu_down(app) - elif key == 27: # Sometimes the up/down arrow key is represented by a series of three keys. - next_key = stdscr.getch() - if next_key == 91: - final_key = stdscr.getch() - if final_key == 65: - do_menu_up(app) - elif final_key == 66: - do_menu_down(app) - elif key == ord('\n'): # Enter key - choice = options[config.current_option] - # Reset for next menu - config.current_option = 0 - config.current_page = 0 - elif key == ord('\x1b'): - signal.signal(signal.SIGINT, app.end) - else: - msg.status("Input unknown.", app) - pass - except KeyboardInterrupt: - signal.signal(signal.SIGINT, app.end) - - stdscr.refresh() - if choice: - app.active_screen.choice = choice - else: - return "Processing" + def run(self): + #thread = utils.start_thread(self.input, False) + #thread.join() + self.draw() + self.input() + return self.user_input + + def set_options(self, new_options): + self.options = new_options + self.app.menu_options = new_options diff --git a/tui_screen.py b/tui_screen.py index b97dca87..a02d0b07 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -14,6 +14,7 @@ class Screen: def __init__(self, app, screen_id, queue, event): self.app = app + self.stdscr = "" self.screen_id = screen_id self.choice = "Processing" self.queue = queue @@ -107,12 +108,15 @@ def __str__(self): def display(self): self.stdscr.erase() - tui_curses.menu( + self.choice = tui_curses.MenuDialog( self.app, self.question, self.options - ) - self.submit_choice_to_queue() + ).run() + if self.choice is not None and not self.choice == "" and not self.choice == "Processing": + config.current_option = 0 + config.current_page = 0 + self.submit_choice_to_queue() self.stdscr.noutrefresh() curses.doupdate() @@ -121,6 +125,7 @@ def get_question(self): def set_options(self, new_options): self.options = new_options + self.app.menu_options = new_options class InputScreen(CursesScreen): @@ -129,7 +134,7 @@ def __init__(self, app, screen_id, queue, event, question, default): self.stdscr = self.app.get_menu_window() self.question = question self.default = default - self.user_input_dialog = tui_curses.UserInputDialog( + self.dialog = tui_curses.UserInputDialog( self.app, self.question, self.default @@ -140,8 +145,10 @@ def __str__(self): def display(self): self.stdscr.erase() - self.choice = self.user_input_dialog.run() - self.submit_choice_to_queue() + self.choice = self.dialog.run() + if not self.choice == "Processing": + logging.debug(f"DEV: {self.choice}") + self.submit_choice_to_queue() self.stdscr.noutrefresh() curses.doupdate() From eec6dc0e0f71426a54c1a962244652ffdc1ce7d1 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 16 Jul 2024 18:03:20 -0400 Subject: [PATCH 047/253] Clean up dialog importing --- main.py | 2 +- tui_app.py | 2 -- tui_dialog.py | 4 +++- tui_screen.py | 5 ++++- utils.py | 4 +++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 8c6412f3..67cbe58d 100755 --- a/main.py +++ b/main.py @@ -327,7 +327,7 @@ def main(): if config.DIALOG == 'tk': config.GUI = True - if config.DIALOG == 'curses': + if config.DIALOG == 'curses' and "dialog" in sys.modules: config.use_python_dialog = system.test_dialog_version() if config.use_python_dialog is None: diff --git a/tui_app.py b/tui_app.py index b874fb8b..19e982d5 100644 --- a/tui_app.py +++ b/tui_app.py @@ -19,8 +19,6 @@ import utils import wine -from main import threads - console_message = "" # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. diff --git a/tui_dialog.py b/tui_dialog.py index d3237bd6..56b7446d 100644 --- a/tui_dialog.py +++ b/tui_dialog.py @@ -1,6 +1,8 @@ import curses -from dialog import Dialog import logging +import sys +if "dialog" in sys.modules: + from dialog import Dialog def text(app, text, height=None, width=None, title=None, backtitle=None, colors=True): diff --git a/tui_screen.py b/tui_screen.py index a02d0b07..c8e1e78a 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -6,9 +6,12 @@ import config import installer +import system import tui_curses -if 'dialog' in sys.modules: +if system.have_dep("dialog"): import tui_dialog +else: + logging.error(f"tui_screen.py: dialog not installed.") class Screen: diff --git a/utils.py b/utils.py index a19f9c1e..0fc6eb39 100644 --- a/utils.py +++ b/utils.py @@ -21,8 +21,10 @@ import msg import network import system -if 'dialog' in sys.modules: +if system.have_dep("dialog"): import tui_dialog +else: + logging.error(f"utils.py: dialog not installed.") import wine #TODO: Move config commands to config.py From 71603dd6b89939f0bdfa2156489559f3e3157d6f Mon Sep 17 00:00:00 2001 From: n8marti Date: Tue, 23 Jul 2024 06:15:26 -0500 Subject: [PATCH 048/253] Clarify name --- .github/workflows/autobuild-main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/autobuild-main.yml b/.github/workflows/autobuild-main.yml index f082198a..74ed164c 100644 --- a/.github/workflows/autobuild-main.yml +++ b/.github/workflows/autobuild-main.yml @@ -1,4 +1,4 @@ -name: Build & release to test-builds repo +name: Auto-build & release to test-builds repo on: push: branches: From af213480bee2e52d0f4e9fe6eb7d79a53cd6f470 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 29 Jun 2024 00:20:39 -0400 Subject: [PATCH 049/253] Install ICU data files. Fix #22. - Fix incorrect commands with Ubuntu prereq installer - Update maximum allowed Logos version --- installer.py | 14 +++++++++++++- network.py | 3 ++- utils.py | 14 ++++++++++++++ wine.py | 23 ++++++++++++++++++----- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/installer.py b/installer.py index c4effc37..22cebede 100644 --- a/installer.py +++ b/installer.py @@ -455,10 +455,22 @@ def ensure_wineprefix_init(app=None): logging.debug(f"> {init_file} exists?: {init_file.is_file()}") -def ensure_winetricks_applied(app=None): +def ensure_icu_data_files(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_wineprefix_init(app=app) config.INSTALL_STEP += 1 + status = "Ensuring ICU data files installed…" + update_install_feedback(status, app=app) + logging.debug('- ICU data files') + # TODO: Need a test to skip + wine.installICUDataFiles() + logging.debug('+ ICU data files') + + +def ensure_winetricks_applied(app=None): + config.INSTALL_STEPS_COUNT += 1 + ensure_icu_data_files(app=app) + config.INSTALL_STEP += 1 status = "Ensuring winetricks & other settings are applied…" update_install_feedback(status, app=app) logging.debug('- disable winemenubuilder') diff --git a/network.py b/network.py index 9165361a..a80cc755 100644 --- a/network.py +++ b/network.py @@ -535,7 +535,7 @@ def get_logos_releases(app=None): # if len(releases) == 5: # break - filtered_releases = utils.filter_versions(releases, 30, 1) + filtered_releases = utils.filter_versions(releases, 36, 1) logging.debug(f"Available releases: {', '.join(releases)}") logging.debug(f"Filtered releases: {', '.join(filtered_releases)}") @@ -547,6 +547,7 @@ def get_logos_releases(app=None): app.releases_e.set() return filtered_releases + def update_lli_binary(app=None): lli_file_path = os.path.realpath(sys.argv[0]) lli_download_path = Path(config.MYDOWNLOADS) / "LogosLinuxInstaller" diff --git a/utils.py b/utils.py index 0fc6eb39..dc51f43d 100644 --- a/utils.py +++ b/utils.py @@ -11,6 +11,7 @@ import stat import subprocess import sys +import tarfile import threading import tkinter as tk from packaging import version @@ -848,3 +849,16 @@ def str_array_to_string(text, delimeter="\n"): return processed_text except TypeError: return text + + +def untar_file(file_path, output_dir): + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + try: + with tarfile.open(file_path, 'r:gz') as tar: + tar.extractall(path=output_dir) + logging.debug(f"Successfully extracted '{file_path}' to '{output_dir}'") + except tarfile.TarError as e: + logging.error(f"Error extracting '{file_path}': {e}") + diff --git a/wine.py b/wine.py index 9dc4e8aa..32ec41b2 100644 --- a/wine.py +++ b/wine.py @@ -265,6 +265,13 @@ def winetricks_install(*args): heavy_wineserver_wait() +def installD3DCompiler(): + cmd = ['d3dcompiler_47'] + if config.WINETRICKS_UNATTENDED is None: + cmd.insert(0, '-q') + winetricks_install(*cmd) + + def installFonts(): msg.logos_msg("Configuring fonts…") fonts = ['corefonts', 'tahoma'] @@ -278,11 +285,17 @@ def installFonts(): winetricks_install('-q', 'settings', 'fontsmooth=rgb') -def installD3DCompiler(): - cmd = ['d3dcompiler_47'] - if config.WINETRICKS_UNATTENDED is None: - cmd.insert(0, '-q') - winetricks_install(*cmd) +def installICUDataFiles(): + releases_url = "https://api.github.com/repos/FaithLife-Community/icu/releases" # noqa: E501 + json_data = utils.get_latest_release_data(releases_url) + icu_url = utils.get_latest_release_url(json_data) + icu_tag_name = utils.get_latest_release_version_tag_name(json_data) # noqa: E501 + if icu_url is None: + logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 + return + icu_filename = os.path.basename(logoslinuxinstaller_url) # noqa: #501 + utils.logos_reuse_download(icu_url, "icu.tar.gz", config.MYDOWNLOADS) + utils.untar_file(f"{config.MYDOWNLOADS}/icu.tar.gz", f"{config.APPDIR}/wine64_bottle/drive_c") def get_registry_value(reg_path, name): From 5b2dd1e2c56d33baebef5617e833ecde40bdbeea Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 2 Jul 2024 08:35:06 +0100 Subject: [PATCH 050/253] fix variable name --- wine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wine.py b/wine.py index 32ec41b2..0f7fc0ad 100644 --- a/wine.py +++ b/wine.py @@ -293,7 +293,7 @@ def installICUDataFiles(): if icu_url is None: logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 return - icu_filename = os.path.basename(logoslinuxinstaller_url) # noqa: #501 + icu_filename = os.path.basename(icu_url) utils.logos_reuse_download(icu_url, "icu.tar.gz", config.MYDOWNLOADS) utils.untar_file(f"{config.MYDOWNLOADS}/icu.tar.gz", f"{config.APPDIR}/wine64_bottle/drive_c") From dd32f8ae76d8bb78c499ef6af2ec2a39a797cca2 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 2 Jul 2024 09:41:48 +0100 Subject: [PATCH 051/253] set drive_c location relative to INSTALLDIR --- wine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wine.py b/wine.py index 0f7fc0ad..9112fbe2 100644 --- a/wine.py +++ b/wine.py @@ -295,7 +295,8 @@ def installICUDataFiles(): return icu_filename = os.path.basename(icu_url) utils.logos_reuse_download(icu_url, "icu.tar.gz", config.MYDOWNLOADS) - utils.untar_file(f"{config.MYDOWNLOADS}/icu.tar.gz", f"{config.APPDIR}/wine64_bottle/drive_c") + drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" + utils.untar_file(f"{config.MYDOWNLOADS}/icu.tar.gz", drive_c) def get_registry_value(reg_path, name): From 215341ea4a27e135b1f8f7895beab0c454d07601 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 2 Jul 2024 18:02:20 +0100 Subject: [PATCH 052/253] pass app to ICU installer to handle progress tracking --- installer.py | 2 +- wine.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/installer.py b/installer.py index 22cebede..84d89cbc 100644 --- a/installer.py +++ b/installer.py @@ -463,7 +463,7 @@ def ensure_icu_data_files(app=None): update_install_feedback(status, app=app) logging.debug('- ICU data files') # TODO: Need a test to skip - wine.installICUDataFiles() + wine.installICUDataFiles(app=app) logging.debug('+ ICU data files') diff --git a/wine.py b/wine.py index 9112fbe2..f09f63b2 100644 --- a/wine.py +++ b/wine.py @@ -285,16 +285,21 @@ def installFonts(): winetricks_install('-q', 'settings', 'fontsmooth=rgb') -def installICUDataFiles(): +def installICUDataFiles(app=None): releases_url = "https://api.github.com/repos/FaithLife-Community/icu/releases" # noqa: E501 json_data = utils.get_latest_release_data(releases_url) icu_url = utils.get_latest_release_url(json_data) - icu_tag_name = utils.get_latest_release_version_tag_name(json_data) # noqa: E501 + # icu_tag_name = utils.get_latest_release_version_tag_name(json_data) if icu_url is None: logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 return icu_filename = os.path.basename(icu_url) - utils.logos_reuse_download(icu_url, "icu.tar.gz", config.MYDOWNLOADS) + utils.logos_reuse_download( + icu_url, + icu_filename, + config.MYDOWNLOADS, + app=app + ) drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" utils.untar_file(f"{config.MYDOWNLOADS}/icu.tar.gz", drive_c) From 0f73e57cfe7620b3647c779a96185604d50c6104 Mon Sep 17 00:00:00 2001 From: John Goodman M0RVJ Date: Wed, 3 Jul 2024 10:09:08 +0100 Subject: [PATCH 053/253] Update wine.py corrected icu-win.tar.gz filename from icu.tar.gz --- wine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wine.py b/wine.py index f09f63b2..7eef48ab 100644 --- a/wine.py +++ b/wine.py @@ -301,7 +301,7 @@ def installICUDataFiles(app=None): app=app ) drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" - utils.untar_file(f"{config.MYDOWNLOADS}/icu.tar.gz", drive_c) + utils.untar_file(f"{config.MYDOWNLOADS}/icu-win.tar.gz", drive_c) def get_registry_value(reg_path, name): From 9d92e7d7de91ce7fe3d9e725bc3c879f5ca069fd Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 3 Jul 2024 10:22:19 -0400 Subject: [PATCH 054/253] Add copytree --- wine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wine.py b/wine.py index 7eef48ab..57f9e003 100644 --- a/wine.py +++ b/wine.py @@ -2,6 +2,7 @@ import os import psutil import re +import shutils import signal import subprocess import time @@ -302,6 +303,7 @@ def installICUDataFiles(app=None): ) drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" utils.untar_file(f"{config.MYDOWNLOADS}/icu-win.tar.gz", drive_c) + shutil.copytree(f"{drive_c}/icu-win/windows", f"{drive_c}/windows", dirs_exist_ok = True) def get_registry_value(reg_path, name): From dcec6c7e843e66793549252b11d3e7953f6d2e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Freilichtb=C3=BChne?= <52177341+Freilichtbuehne@users.noreply.github.com> Date: Sat, 6 Jul 2024 14:05:10 +0200 Subject: [PATCH 055/253] Fix import typo and missing directory --- wine.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wine.py b/wine.py index 57f9e003..9af0c6e2 100644 --- a/wine.py +++ b/wine.py @@ -2,7 +2,7 @@ import os import psutil import re -import shutils +import shutil import signal import subprocess import time @@ -303,7 +303,13 @@ def installICUDataFiles(app=None): ) drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" utils.untar_file(f"{config.MYDOWNLOADS}/icu-win.tar.gz", drive_c) - shutil.copytree(f"{drive_c}/icu-win/windows", f"{drive_c}/windows", dirs_exist_ok = True) + + # Ensure the target directory exists + icu_win_dir = f"{drive_c}/icu-win/windows" + if not os.path.exists(icu_win_dir): + os.makedirs(icu_win_dir) + + shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok = True) def get_registry_value(reg_path, name): From fbff0b3d7fbf908171c7df6a68343adf8b567054 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 6 Jul 2024 19:16:17 -0400 Subject: [PATCH 056/253] better handling of changed dropdown options --- gui_app.py | 63 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/gui_app.py b/gui_app.py index 4b7a05df..98cfb823 100644 --- a/gui_app.py +++ b/gui_app.py @@ -183,6 +183,9 @@ def __init__(self, new_win, root, **kwargs): self.start_ensure_config() def start_ensure_config(self): + # Ensure progress counter is reset. + config.INSTALL_STEP = 1 + config.INSTALL_STEPS_COUNT = 0 self.config_thread = Thread( target=installer.ensure_installation_config, kwargs={'app': self}, @@ -230,7 +233,6 @@ def todo(self, evt=None, task=None): self.gui.release_check_button, self.gui.wine_dropdown, self.gui.wine_check_button, - # self.gui.tricks_dropdown, self.gui.okay_button, ] self.set_input_widgets_state('disabled', widgets=widgets) @@ -258,16 +260,7 @@ def todo(self, evt=None, task=None): self.gui.okay_button, ] self.set_input_widgets_state('disabled', widgets=widgets) - # if not self.gui.releasevar.get(): - if not self.gui.release_dropdown['values']: - # No previous choice. - self.start_releases_check() - else: - # Check if previous choice was for other TARGETVERSION. - if config.TARGETVERSION == '9' and not self.gui.release_dropdown['values'][0].startswith('9'): # noqa: E501 - self.start_releases_check() - if config.TARGETVERSION == '10' and self.gui.release_dropdown['values'][0].startswith('9'): # noqa: E501 - self.start_releases_check() + self.start_releases_check() elif task == 'WINE_EXE': # Disable all input widgets after Wine Exe. widgets = [ @@ -294,15 +287,30 @@ def todo(self, evt=None, task=None): utils.write_config(config.CONFIG_FILE) def set_product(self, evt=None): - if self.gui.productvar.get()[0] == 'C': # ignore default text + if self.gui.productvar.get().startswith('C'): # ignore default text return self.gui.flproduct = self.gui.productvar.get() self.gui.product_dropdown.selection_clear() if evt: # manual override; reset dependent variables + logging.debug(f"User changed FLPRODUCT to '{self.gui.flproduct}'") config.FLPRODUCT = None + config.FLPRODUCTi = None + config.VERBUM_PATH = None + + config.TARGETVERSION = None + self.gui.versionvar.set('') + + config.TARGET_RELEASE_VERSION = None + self.gui.releasevar.set('') + config.INSTALLDIR = None + config.APPDIR_BINDIR = None + config.WINE_EXE = None - config.WINETRICKSBIN = None + self.gui.winevar.set('') + config.SELECTED_APPIMAGE_FILENAME = None + config.WINEBIN_CODE = None + self.start_ensure_config() else: self.product_q.put(self.gui.flproduct) @@ -311,13 +319,20 @@ def set_version(self, evt=None): self.gui.targetversion = self.gui.versionvar.get() self.gui.version_dropdown.selection_clear() if evt: # manual override; reset dependent variables - logging.debug(f"Change TARGETVERSION to {self.gui.targetversion}") + logging.debug(f"User changed TARGETVERSION to '{self.gui.targetversion}'") # noqa: E501 config.TARGETVERSION = None self.gui.releasevar.set('') config.TARGET_RELEASE_VERSION = None + self.gui.releasevar.set('') + config.INSTALLDIR = None + config.APPDIR_BINDIR = None + config.WINE_EXE = None - config.WINETRICKSBIN = None + self.gui.winevar.set('') + config.SELECTED_APPIMAGE_FILENAME = None + config.WINEBIN_CODE = None + self.start_ensure_config() else: self.version_q.put(self.gui.targetversion) @@ -352,6 +367,16 @@ def set_release(self, evt=None): self.gui.release_dropdown.selection_clear() if evt: # manual override config.TARGET_RELEASE_VERSION = None + logging.debug(f"User changed TARGET_RELEASE_VERSION to '{self.gui.logos_release_version}'") # noqa: E501 + + config.INSTALLDIR = None + config.APPDIR_BINDIR = None + + config.WINE_EXE = None + self.gui.winevar.set('') + config.SELECTED_APPIMAGE_FILENAME = None + config.WINEBIN_CODE = None + self.start_ensure_config() else: self.release_q.put(self.gui.logos_release_version) @@ -410,7 +435,11 @@ def set_wine(self, evt=None): self.gui.wine_exe = self.gui.winevar.get() self.gui.wine_dropdown.selection_clear() if evt: # manual override + logging.debug(f"User changed WINE_EXE to '{self.gui.wine_exe}'") config.WINE_EXE = None + config.SELECTED_APPIMAGE_FILENAME = None + config.WINEBIN_CODE = None + self.start_ensure_config() else: self.wine_q.put(self.gui.wine_exe) @@ -492,7 +521,9 @@ def update_wine_check_progress(self, evt=None): if evt and self.wines_q.empty(): return self.gui.wine_dropdown['values'] = self.wines_q.get() - self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) + if not self.gui.winevar.get(): + # If no value selected, default to 1st item in list. + self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) self.set_wine() self.stop_indeterminate_progress() self.gui.wine_check_button.state(['!disabled']) From 362bfe48abf46d2b05365f8886f8653e52d295ad Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 6 Jul 2024 19:40:31 -0400 Subject: [PATCH 057/253] fix bad winetricksbin value in GUI --- gui_app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui_app.py b/gui_app.py index 98cfb823..8e681e15 100644 --- a/gui_app.py +++ b/gui_app.py @@ -194,6 +194,7 @@ def start_ensure_config(self): self.config_thread.start() def get_winetricks_options(self): + config.WINETRICKSBIN = None # override config file b/c "Download" accounts for that # noqa: E501 self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() self.gui.tricksvar.set(self.gui.tricks_dropdown['values'][0]) From 0fb8f4133369b6d354302d28528d5858209d96bb Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 6 Jul 2024 20:26:11 -0400 Subject: [PATCH 058/253] Add optargs --install-(d3d-compiler|fonts|icu) --- main.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/main.py b/main.py index 67cbe58d..ed711860 100755 --- a/main.py +++ b/main.py @@ -148,6 +148,18 @@ def get_parser(): '--run-winetricks', action='store_true', help='start Winetricks window', ) + cmd.add_argument( + '--install-d3d-compiler', action='store_true', + help='Install d3dcompiler through Winetricks', + ) + cmd.add_argument( + '--install-fonts', action='store_true', + help='Install fonts through Winetricks', + ) + cmd.add_argument( + '--install-icu', action='store_true', + help='Install ICU data files for Logos 30+', + ) cmd.add_argument( '--toggle-app-logging', action='store_true', help='enable/disable app logs', @@ -228,6 +240,9 @@ def parse_args(args, parser): 'set_appimage': utils.set_appimage_symlink, 'get_winetricks': control.set_winetricks, 'run_winetricks': wine.run_winetricks, + 'install_d3d_compiler': wine.installD3DCompiler, + 'install_fonts': wine.installFonts, + 'install_icu': wine.installICUDataFiles, 'toggle_app_logging': wine.switch_logging, 'create_shortcuts': installer.ensure_launcher_shortcuts, 'remove_install_dir': control.remove_install_dir, From 3ccaf8315d0e1f65c41592f6b6e3d51a4b8c827e Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 16 Jul 2024 09:06:34 -0400 Subject: [PATCH 059/253] send correct TODO task to GUI --- installer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/installer.py b/installer.py index 84d89cbc..6f593c8b 100644 --- a/installer.py +++ b/installer.py @@ -533,7 +533,7 @@ def ensure_product_installed(app=None): if not utils.find_installed_product(): wine.install_msi() config.LOGOS_EXE = utils.find_installed_product() - utils.send_task(app, 'DONE') + utils.send_task(app, 'CONFIG') if config.DIALOG == 'curses': app.finished_e.wait() @@ -585,7 +585,10 @@ def ensure_config_file(app=None): ): logging.info("Updating config file.") utils.write_config(config.CONFIG_FILE) - utils.send_task(app, "TUI-UPDATE-MENU") + if config.DIALOG == 'tk': + utils.send_task(app, 'DONE') + else: + utils.send_task(app, "TUI-UPDATE-MENU") logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 From ff118558bbfe134d7e8ac4f825f9a123071fdd1a Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 16 Jul 2024 09:42:12 -0400 Subject: [PATCH 060/253] kill logging state thread if app closes --- gui_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui_app.py b/gui_app.py index 8e681e15..c709a25c 100644 --- a/gui_app.py +++ b/gui_app.py @@ -666,7 +666,8 @@ def __init__(self, root, *args, **kwargs): if utils.app_is_installed(): t = Thread( target=wine.get_app_logging_state, - kwargs={'app': self, 'init': True} + kwargs={'app': self, 'init': True}, + daemon=True, ) t.start() self.gui.statusvar.set('Getting current app logging status…') From 57809934c3d646e85af7272c69d77402a2eea802 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 16 Jul 2024 10:04:07 -0400 Subject: [PATCH 061/253] get logos ver from "libraries" in Logos.deps.json --- utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/utils.py b/utils.py index dc51f43d..3385d4a3 100644 --- a/utils.py +++ b/utils.py @@ -253,16 +253,18 @@ def file_exists(file_path): def get_current_logos_version(): - path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" + path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" # noqa: E501 file_paths = glob.glob(path_regex) + logos_version_number = None if file_paths: logos_version_file = file_paths[0] with open(logos_version_file, 'r') as json_file: json_data = json.load(json_file) + for key in json_data.get('libraries', dict()): + if key.startswith('Logos') and '/' in key: + logos_version_number = key.split('/')[1] - dependencies = json_data["targets"]['.NETCoreApp,Version=v6.0/win10-x64']['Logos/1.0.0']['dependencies'] - logos_version_number = dependencies.get("LogosUpdater.Reference") - + logging.debug(f"{logos_version_number=}") if logos_version_number is not None: return logos_version_number else: From 2f1bacf5d362fd955e9b33906e4e5eaf4f67bde9 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 29 Jun 2024 00:27:24 -0400 Subject: [PATCH 062/253] Update Version and Changelog --- CHANGELOG.md | 4 ++++ config.py | 2 +- utils.py | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ca0eb0..f7c80987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +- 4.0.0-alpha.13 + - Fix #22. [T. Bleher, J. Goodman, N. Marti, S. Freilichtbuenhe, M. Malevic, T. Wright] + - Fix package installer and TUI app [T. H. Wright] + - Introduce network.py and system.py - 4.0.0-alpha.12 - Fix TUI app's installer [T. H. Wright] - 4.0.0-alpha.11 diff --git a/config.py b/config.py index 861788f3..ffcc5ebf 100644 --- a/config.py +++ b/config.py @@ -59,7 +59,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.12" +LLI_CURRENT_VERSION = "4.0.0-alpha.13" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING diff --git a/utils.py b/utils.py index 3385d4a3..0483b6ad 100644 --- a/utils.py +++ b/utils.py @@ -863,4 +863,3 @@ def untar_file(file_path, output_dir): logging.debug(f"Successfully extracted '{file_path}' to '{output_dir}'") except tarfile.TarError as e: logging.error(f"Error extracting '{file_path}': {e}") - From bbdfb0b83628908f1e97c24df43df6afc974b20f Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 17 Jul 2024 15:38:35 -0400 Subject: [PATCH 063/253] Fix install errors --- installer.py | 1 + system.py | 27 ++++++++++++++++++++++----- tui_curses.py | 2 +- tui_screen.py | 1 - wine.py | 50 ++++++++++++++++++++++++++++++-------------------- 5 files changed, 54 insertions(+), 27 deletions(-) diff --git a/installer.py b/installer.py index 6f593c8b..040046b1 100644 --- a/installer.py +++ b/installer.py @@ -487,6 +487,7 @@ def ensure_winetricks_applied(app=None): usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): + #FIXME: This command is failing. reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') with open(reg_file, 'w') as f: f.write(r'''REGEDIT4 diff --git a/system.py b/system.py index 8dd1a201..f8c9d5e9 100644 --- a/system.py +++ b/system.py @@ -14,7 +14,19 @@ import network -def run_command(command, retries=1, delay=0, stdin=None, shell=False): +#TODO: Add a Popen variant to run_command to replace functions in control.py and wine.py +def run_command(command, retries=1, delay=0, **kwargs): + check = kwargs.get("check", True) + text = kwargs.get("text", True) + capture_output = kwargs.get("capture_output", True) + shell = kwargs.get("shell", False) + env = kwargs.get("env", None) + cwd = kwargs.get("cwd", None) + encoding = kwargs.get("encoding", None) + stdin = kwargs.get("stdin", None) + stdout = kwargs.get("stdout", None) + stderr = kwargs.get("stderr", None) + if retries < 1: retries = 1 @@ -26,11 +38,16 @@ def run_command(command, retries=1, delay=0, stdin=None, shell=False): logging.debug(f"Attempting to execute {command}") result = subprocess.run( command, - stdin=stdin, - check=True, - text=True, + check=check, + text=text, shell=shell, - capture_output=True + capture_output=capture_output, + stdin=stdin, + stdout=stdout, + stderr=stderr, + encoding=encoding, + cwd=cwd, + env=env ) return result except subprocess.CalledProcessError as e: diff --git a/tui_curses.py b/tui_curses.py index 2e009bd4..291ee56f 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -48,7 +48,7 @@ def spinner(app, index, start_y=0): i = (i + 1) % len(spinner_chars) return i - +#FIXME: Display flickers. def confirm(app, question_text, height=None, width=None): stdscr = app.get_menu_window() question_text = question_text + " [Y/n]: " diff --git a/tui_screen.py b/tui_screen.py index c8e1e78a..3cf39fdd 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -150,7 +150,6 @@ def display(self): self.stdscr.erase() self.choice = self.dialog.run() if not self.choice == "Processing": - logging.debug(f"DEV: {self.choice}") self.submit_choice_to_queue() self.stdscr.noutrefresh() curses.doupdate() diff --git a/wine.py b/wine.py index 9af0c6e2..28e33ade 100644 --- a/wine.py +++ b/wine.py @@ -10,6 +10,8 @@ import config import msg +import network +import system import utils from main import processes @@ -68,6 +70,7 @@ def heavy_wineserver_wait(): utils.wait_process_using_dir(config.WINEPREFIX) wait_on([f"{config.WINESERVER_EXE}", "-w"]) + def get_wine_release(binary): cmd = [binary, "--version"] try: @@ -183,18 +186,18 @@ def initializeWineBottle(app=None): def wine_reg_install(REG_FILE): msg.logos_msg(f"Installing registry file: {REG_FILE}") env = get_wine_env() - p = subprocess.run( + result = system.run_command( [config.WINE_EXE, "regedit.exe", REG_FILE], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, - text=True, cwd=config.WORKDIR, + capture_output=False ) - if p.returncode == 0: - logging.info(f"{REG_FILE} installed.") - elif p.returncode != 0: + if result is None or result.returncode != 0: msg.logos_error(f"Failed to install reg file: {REG_FILE}") + elif result.returncode == 0: + logging.info(f"{REG_FILE} installed.") light_wineserver_wait() @@ -212,8 +215,12 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): env = get_wine_env() if config.WINECMD_ENCODING is None: # Get wine system's cmd.exe encoding for proper decoding to UTF8 later. - codepages = get_registry_value('HKCU\\Software\\Wine\\Fonts', 'Codepages').split(',') # noqa: E501 - config.WINECMD_ENCODING = codepages[-1] + registry_value = get_registry_value('HKCU\\Software\\Wine\\Fonts', 'Codepages') + if registry_value is not None: + codepages = registry_value.split(',') # noqa: E501 + config.WINECMD_ENCODING = codepages[-1] + else: + logging.error("wine.wine_proc: wine.get_registry_value returned None.") logging.debug(f"run_wine_proc: {winecmd}; {exe=}; {exe_args=}") wine_env_vars = {k: v for k, v in env.items() if k.startswith('WINE')} logging.debug(f"wine environment: {wine_env_vars}") @@ -242,7 +249,10 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): try: logging.info(line.decode().rstrip()) except UnicodeDecodeError: - logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 + if config.WINECMD_ENCODING is not None: + logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 + else: + logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") returncode = process.wait() if returncode != 0: @@ -288,14 +298,14 @@ def installFonts(): def installICUDataFiles(app=None): releases_url = "https://api.github.com/repos/FaithLife-Community/icu/releases" # noqa: E501 - json_data = utils.get_latest_release_data(releases_url) - icu_url = utils.get_latest_release_url(json_data) + json_data = network.get_latest_release_data(releases_url) + icu_url = network.get_latest_release_url(json_data) # icu_tag_name = utils.get_latest_release_version_tag_name(json_data) if icu_url is None: logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 return icu_filename = os.path.basename(icu_url) - utils.logos_reuse_download( + network.logos_reuse_download( icu_url, icu_filename, config.MYDOWNLOADS, @@ -316,14 +326,14 @@ def get_registry_value(reg_path, name): value = None env = get_wine_env() cmd = [config.WINE_EXE, 'reg', 'query', reg_path, '/v', name] - stdout = subprocess.run( - cmd, capture_output=True, - text=True, encoding=config.WINECMD_ENCODING, - env=env).stdout - for line in stdout.splitlines(): - if line.strip().startswith(name): - value = line.split()[-1].strip() - break + result = system.run_command(cmd, encoding=config.WINECMD_ENCODING, env=env) + if result.stdout is not None: + for line in result.stdout.splitlines(): + if line.strip().startswith(name): + value = line.split()[-1].strip() + break + else: + logging.critical(f"wine.get_registry_value: Failed to get registry value: {reg_path}") return value @@ -464,7 +474,7 @@ def run_logos(): wine_release = get_wine_release(config.WINE_EXE) #TODO: Find a way to incorporate check_wine_version_and_branch() - if logos_release[0] < 30 and logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): + if 30 > logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): txt = "Can't run Logos 10+ with Wine below 7.18." logging.critical(txt) msg.status(txt) From de9d4cdb41c004fbdde3d76592927af4f5641550 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 18 Jul 2024 15:27:34 -0400 Subject: [PATCH 064/253] initial icu gui components --- gui.py | 93 +++++++++++++++++++++++++++++++----------------------- gui_app.py | 12 +++++++ wine.py | 4 ++- 3 files changed, 69 insertions(+), 40 deletions(-) diff --git a/gui.py b/gui.py index aa66a48f..20aa0f01 100644 --- a/gui.py +++ b/gui.py @@ -187,6 +187,12 @@ def __init__(self, root, *args, **kwargs): variable=self.actionsvar, value='remove-index-files', ) + self.install_icu_radio = Radiobutton( + self, + text="Install/Update ICU files", + variable=self.actionsvar, + value='install-icu', + ) self.actions_button = Button(self, text="Run action") self.actions_button.state(['disabled']) s2 = Separator(self, orient='horizontal') @@ -237,46 +243,55 @@ def __init__(self, root, *args, **kwargs): self.progress.state(['disabled']) # Place widgets. - self.app_label.grid(column=0, row=0, sticky='w', pady=2) - self.app_button.grid(column=1, row=0, sticky='w', pady=2) - + row = 0 + self.app_label.grid(column=0, row=row, sticky='w', pady=2) + self.app_button.grid(column=1, row=row, sticky='w', pady=2) + row += 1 s1.grid(column=0, row=1, columnspan=3, sticky='we', pady=2) - self.actions_label.grid(column=0, row=2, sticky='e', padx=20, pady=2) - self.actions_button.grid(column=0, row=4, sticky='e', padx=20, pady=2) - self.run_indexing_radio.grid(column=1, row=2, sticky='w', pady=2, columnspan=2) # noqa: E501 - self.remove_library_catalog_radio.grid(column=1, row=3, sticky='w', pady=2, columnspan=2) # noqa: E501 - self.remove_index_files_radio.grid(column=1, row=4, sticky='w', pady=2, columnspan=2) # noqa: E501 - s2.grid(column=0, row=5, columnspan=3, sticky='we', pady=2) - - self.config_label.grid(column=0, row=6, sticky='w', pady=2) - self.config_button.grid(column=1, row=6, sticky='w', pady=2) - - self.deps_label.grid(column=0, row=7, sticky='w', pady=2) - self.deps_button.grid(column=1, row=7, sticky='w', pady=2) - - self.backups_label.grid(column=0, row=8, sticky='w', pady=2) - self.backup_button.grid(column=1, row=8, sticky='w', pady=2) - self.restore_button.grid(column=2, row=8, sticky='w', pady=2) - - self.update_lli_label.grid(column=0, row=9, sticky='w', pady=2) - self.update_lli_button.grid(column=1, row=9, sticky='w', pady=2) - - self.latest_appimage_label.grid(column=0, row=10, sticky='w', pady=2) - self.latest_appimage_button.grid(column=1, row=10, sticky='w', pady=2) - - self.set_appimage_label.grid(column=0, row=11, sticky='w', pady=2) - self.set_appimage_button.grid(column=1, row=11, sticky='w', pady=2) - - self.winetricks_label.grid(column=0, row=12, sticky='w', pady=2) - self.run_winetricks_button.grid(column=1, row=12, sticky='w', pady=2) - self.get_winetricks_button.grid(column=2, row=12, sticky='w', pady=2) - - self.logging_label.grid(column=0, row=13, sticky='w', pady=2) - self.logging_button.grid(column=1, row=13, sticky='w', pady=2) - - s3.grid(column=0, row=14, columnspan=3, sticky='we', pady=2) - self.message_label.grid(column=0, row=15, columnspan=3, sticky='we', pady=2) # noqa: E501 - self.progress.grid(column=0, row=16, columnspan=3, sticky='we', pady=2) + row += 1 + self.actions_label.grid(column=0, row=row, sticky='e', padx=20, pady=2) + self.run_indexing_radio.grid(column=1, row=row, sticky='w', pady=2, columnspan=2) # noqa: E501 + row += 1 + self.remove_library_catalog_radio.grid(column=1, row=row, sticky='w', pady=2, columnspan=2) # noqa: E501 + row += 1 + self.actions_button.grid(column=0, row=row, sticky='e', padx=20, pady=2) # noqa: E501 + self.remove_index_files_radio.grid(column=1, row=row, sticky='w', pady=2, columnspan=2) # noqa: E501 + row += 1 + self.install_icu_radio.grid(column=1, row=row, sticky='w', pady=2, columnspan=2) # noqa: E501 + row += 1 + s2.grid(column=0, row=row, columnspan=3, sticky='we', pady=2) + row += 1 + self.config_label.grid(column=0, row=row, sticky='w', pady=2) + self.config_button.grid(column=1, row=row, sticky='w', pady=2) + row += 1 + self.deps_label.grid(column=0, row=row, sticky='w', pady=2) + self.deps_button.grid(column=1, row=row, sticky='w', pady=2) + row += 1 + self.backups_label.grid(column=0, row=row, sticky='w', pady=2) + self.backup_button.grid(column=1, row=row, sticky='w', pady=2) + self.restore_button.grid(column=2, row=row, sticky='w', pady=2) + row += 1 + self.update_lli_label.grid(column=0, row=row, sticky='w', pady=2) + self.update_lli_button.grid(column=1, row=row, sticky='w', pady=2) + row += 1 + self.latest_appimage_label.grid(column=0, row=row, sticky='w', pady=2) + self.latest_appimage_button.grid(column=1, row=row, sticky='w', pady=2) + row += 1 + self.set_appimage_label.grid(column=0, row=row, sticky='w', pady=2) + self.set_appimage_button.grid(column=1, row=row, sticky='w', pady=2) + row += 1 + self.winetricks_label.grid(column=0, row=row, sticky='w', pady=2) + self.run_winetricks_button.grid(column=1, row=row, sticky='w', pady=2) + self.get_winetricks_button.grid(column=2, row=row, sticky='w', pady=2) + row += 1 + self.logging_label.grid(column=0, row=row, sticky='w', pady=2) + self.logging_button.grid(column=1, row=row, sticky='w', pady=2) + row += 1 + s3.grid(column=0, row=row, columnspan=3, sticky='we', pady=2) + row += 1 + self.message_label.grid(column=0, row=row, columnspan=3, sticky='we', pady=2) # noqa: E501 + row += 1 + self.progress.grid(column=0, row=row, columnspan=3, sticky='we', pady=2) # noqa: E501 class ToolTip: diff --git a/gui_app.py b/gui_app.py index c709a25c..91b5b153 100644 --- a/gui_app.py +++ b/gui_app.py @@ -219,6 +219,7 @@ def set_input_widgets_state(self, state, widgets='all'): w.state(state) def todo(self, evt=None, task=None): + logging.debug(f"GUI todo: {task=}") widgets = [] if not task: if not self.todo_q.empty(): @@ -700,6 +701,8 @@ def on_action_radio_clicked(self, evt=None): self.gui.actioncmd = self.remove_library_catalog elif self.gui.actionsvar.get() == 'remove-index-files': self.gui.actioncmd = self.remove_indexes + elif self.gui.actionsvar.get() == 'install-icu': + self.gui.actioncmd = self.install_icu def run_indexing(self, evt=None): t = Thread(target=wine.run_indexing) @@ -716,6 +719,15 @@ def remove_indexes(self, evt=None): ) t.start() + def install_icu(self, evt=None): + self.gui.statusvar.set("Installing ICU files…") + t = Thread( + target=wine.installICUDataFiles, + kwargs={'app': self}, + daemon=True, + ) + t.start() + def run_backup(self, evt=None): # Get backup folder. if config.BACKUPDIR is None: diff --git a/wine.py b/wine.py index 28e33ade..d0339b73 100644 --- a/wine.py +++ b/wine.py @@ -319,7 +319,9 @@ def installICUDataFiles(app=None): if not os.path.exists(icu_win_dir): os.makedirs(icu_win_dir) - shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok = True) + shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok=True) + if app and not hasattr(app, 'todo_q'): + app.root.event_generate(app.message_event) def get_registry_value(reg_path, name): From 36e42d8de049352519cd62691b12c488e7b6b291 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 23 Jul 2024 08:32:18 -0500 Subject: [PATCH 065/253] fix depedency check for dpkg --- system.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/system.py b/system.py index f8c9d5e9..8860922e 100644 --- a/system.py +++ b/system.py @@ -282,14 +282,29 @@ def query_packages(packages, elements=None, mode="install", app=None): for p in packages: status = "Unchecked" + l_num = 0 for line in package_list.split('\n'): - if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": - status = "Installed" - break - elif line.strip().startswith(p) and mode == "remove": - conflicting_packages.append(p) - status = "Conflicting" - break + l_num += 1 + if config.PACKAGE_MANAGER_COMMAND_QUERY.startswith('dpkg'): + parts = line.strip().split() + if l_num < 6 or len(parts) < 2: # skip header, etc. + continue + state = parts[0] + pkg = parts[1].split(':')[0] # remove :arch if present + if pkg == p and state[1] == 'i': + if mode == 'install': + status = "Installed" + elif mode == 'remove': + status == 'Conflicting' + break + else: + if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": + status = "Installed" + break + elif line.strip().startswith(p) and mode == "remove": + conflicting_packages.append(p) + status = "Conflicting" + break if status == "Unchecked": if mode == "install": @@ -309,6 +324,7 @@ def query_packages(packages, elements=None, mode="install", app=None): dialog=config.use_python_dialog) txt = 'None' + exit() if mode == "install": if missing_packages: txt = f"Missing packages: {' '.join(missing_packages)}" @@ -523,8 +539,9 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) if config.PACKAGE_MANAGER_COMMAND_QUERY: - missing_packages, elements = query_packages(package_list, elements, app=app) - conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", app=app) + logging.debug("Querying packages...") + missing_packages, elements = query_packages(package_list, elements, app=app) # noqa: E501 + conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", app=app) # noqa: E501 if config.PACKAGE_MANAGER_COMMAND_INSTALL: if missing_packages and conflicting_packages: From 12727489176ff550fab6fb6d57750b0203216951 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 23 Jul 2024 08:36:41 -0500 Subject: [PATCH 066/253] remove errant "exit()" --- system.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system.py b/system.py index 8860922e..fbe0f845 100644 --- a/system.py +++ b/system.py @@ -324,7 +324,6 @@ def query_packages(packages, elements=None, mode="install", app=None): dialog=config.use_python_dialog) txt = 'None' - exit() if mode == "install": if missing_packages: txt = f"Missing packages: {' '.join(missing_packages)}" From e6b1ab79ceaf9f36a8e7e7cb93b1d86b14b868b0 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 29 Jun 2024 00:20:39 -0400 Subject: [PATCH 067/253] Install ICU data files. Fix #22. - Fix incorrect commands with Ubuntu prereq installer - Update maximum allowed Logos version --- utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utils.py b/utils.py index 0483b6ad..3385d4a3 100644 --- a/utils.py +++ b/utils.py @@ -863,3 +863,4 @@ def untar_file(file_path, output_dir): logging.debug(f"Successfully extracted '{file_path}' to '{output_dir}'") except tarfile.TarError as e: logging.error(f"Error extracting '{file_path}': {e}") + From 956e475fb33c5dfb0920853af4b53f0446a6101d Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 3 Jul 2024 10:22:19 -0400 Subject: [PATCH 068/253] Add copytree --- wine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wine.py b/wine.py index d0339b73..7b661cbc 100644 --- a/wine.py +++ b/wine.py @@ -313,6 +313,7 @@ def installICUDataFiles(app=None): ) drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" utils.untar_file(f"{config.MYDOWNLOADS}/icu-win.tar.gz", drive_c) + shutil.copytree(f"{drive_c}/icu-win/windows", f"{drive_c}/windows", dirs_exist_ok = True) # Ensure the target directory exists icu_win_dir = f"{drive_c}/icu-win/windows" From 67ef9fef3fe64269731bbfc946af80f07c866521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Freilichtb=C3=BChne?= <52177341+Freilichtbuehne@users.noreply.github.com> Date: Sat, 6 Jul 2024 14:05:10 +0200 Subject: [PATCH 069/253] Fix import typo and missing directory --- wine.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wine.py b/wine.py index 7b661cbc..c4e61578 100644 --- a/wine.py +++ b/wine.py @@ -313,7 +313,13 @@ def installICUDataFiles(app=None): ) drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" utils.untar_file(f"{config.MYDOWNLOADS}/icu-win.tar.gz", drive_c) - shutil.copytree(f"{drive_c}/icu-win/windows", f"{drive_c}/windows", dirs_exist_ok = True) + + # Ensure the target directory exists + icu_win_dir = f"{drive_c}/icu-win/windows" + if not os.path.exists(icu_win_dir): + os.makedirs(icu_win_dir) + + shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok = True) # Ensure the target directory exists icu_win_dir = f"{drive_c}/icu-win/windows" From 793e167471b6f3c34e7b0ce06d7eb1579df94b85 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 18 Jul 2024 15:27:34 -0400 Subject: [PATCH 070/253] initial icu gui components --- wine.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wine.py b/wine.py index c4e61578..8a28db01 100644 --- a/wine.py +++ b/wine.py @@ -319,7 +319,9 @@ def installICUDataFiles(app=None): if not os.path.exists(icu_win_dir): os.makedirs(icu_win_dir) - shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok = True) + shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok=True) + if app and not hasattr(app, 'todo_q'): + app.root.event_generate(app.message_event) # Ensure the target directory exists icu_win_dir = f"{drive_c}/icu-win/windows" From 94fd048c0c6d51f141ffaa8320d1b7ba096f8d07 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 23 Jul 2024 08:32:18 -0500 Subject: [PATCH 071/253] fix depedency check for dpkg --- system.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system.py b/system.py index fbe0f845..8860922e 100644 --- a/system.py +++ b/system.py @@ -324,6 +324,7 @@ def query_packages(packages, elements=None, mode="install", app=None): dialog=config.use_python_dialog) txt = 'None' + exit() if mode == "install": if missing_packages: txt = f"Missing packages: {' '.join(missing_packages)}" From 1b31e3be007fced353ba5fc98a92eb9ce08a73e9 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 22 Jul 2024 07:27:03 -0500 Subject: [PATCH 072/253] simplify debian and ubuntu required packages list --- system.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/system.py b/system.py index 8860922e..ddcdb396 100644 --- a/system.py +++ b/system.py @@ -199,15 +199,7 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" config.QUERY_PREFIX = '.i ' - if distro.id() == "debian": - binutils = "binutils binutils-common binutils-x86-64-linux-gnu libbinutils libctf-nobfd0 libctf0 libgprofng0" - else: - binutils = "binutils" - if distro.id() == "ubuntu" and distro.version() < "24.04": - fuse = "fuse" - else: - fuse = "fuse3" - config.PACKAGES = f"{binutils} cabextract {fuse} wget winbind" + config.PACKAGES = "binutils cabextract fuse3 wget winbind" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora From 2a5c0631609862d1a896754729d6942a4df17e48 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 22 Jul 2024 07:38:30 -0500 Subject: [PATCH 073/253] pep8 cleanup --- system.py | 106 +++++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/system.py b/system.py index ddcdb396..a9d5de81 100644 --- a/system.py +++ b/system.py @@ -53,12 +53,12 @@ def run_command(command, retries=1, delay=0, **kwargs): except subprocess.CalledProcessError as e: logging.error(f"Error occurred while executing {command}: {e}") if "lock" in str(e): - logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") + logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") # noqa: E501 time.sleep(delay) else: raise e except Exception as e: - logging.error(f"An unexpected error occurred when running {command}: {e}") + logging.error(f"An unexpected error occurred when running {command}: {e}") # noqa: E501 return None logging.error(f"Failed to execute after {retries} attempts: '{command}'") @@ -195,7 +195,7 @@ def get_package_manager(): # Check for package manager and associated packages if shutil.which('apt') is not None: # debian, ubuntu config.PACKAGE_MANAGER_COMMAND_INSTALL = "apt install -y" - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "apt install --download-only -y" + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "apt install --download-only -y" # noqa: E501 config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" config.QUERY_PREFIX = '.i ' @@ -204,7 +204,7 @@ def get_package_manager(): config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora config.PACKAGE_MANAGER_COMMAND_INSTALL = "dnf install -y" - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "dnf install --downloadonly -y" + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "dnf install --downloadonly -y" # noqa: E501 config.PACKAGE_MANAGER_COMMAND_REMOVE = "dnf remove -y" config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed" config.QUERY_PREFIX = '' @@ -213,7 +213,7 @@ def get_package_manager(): config.BADPACKAGES = "appiamgelauncher" elif shutil.which('pamac') is not None: # manjaro config.PACKAGE_MANAGER_COMMAND_INSTALL = "pamac install --no-upgrade --no-confirm" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pamac install --download-only --no-confirm" + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pamac install --download-only --no-confirm" # noqa: E501 config.PACKAGE_MANAGER_COMMAND_REMOVE = "pamac remove --no-confirm" config.PACKAGE_MANAGER_COMMAND_QUERY = "pamac list -i" config.QUERY_PREFIX = '' @@ -227,9 +227,9 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_QUERY = "pacman -Q" config.QUERY_PREFIX = '' if config.OS_NAME == "steamos": # steamOS - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" #noqa: #E501 + config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: #E501 else: # arch - config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 + config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" # Add more conditions for other package managers as needed @@ -310,7 +310,7 @@ def query_packages(packages, elements=None, mode="install", app=None): if app is not None and config.DIALOG == "curses": app.report_dependencies( - f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", + f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", # noqa: E501 100 * (packages.index(p) + 1) // len(packages), elements, dialog=config.use_python_dialog) @@ -335,7 +335,7 @@ def download_packages(packages, elements, app=None): if packages: total_packages = len(packages) - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_DOWNLOAD} {' '.join(packages)}" + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_DOWNLOAD} {' '.join(packages)}" # noqa: E501 logging.debug(f"download_packages cmd: {command}") command_args = shlex.split(command) result = run_command(command_args, retries=5, delay=15) @@ -345,9 +345,11 @@ def download_packages(packages, elements, app=None): if elements is not None: elements[index] = (package, status) - if app is not None and config.DIALOG == "curses" and elements is not None: - app.report_dependencies(f"Downloading Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog) + if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 + app.report_dependencies( + f"Downloading Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog # noqa: E501 + ) def install_packages(packages, elements, app=None): @@ -357,7 +359,7 @@ def install_packages(packages, elements, app=None): if packages: total_packages = len(packages) for index, package in enumerate(packages): - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" # noqa: E501 logging.debug(f"install_packages cmd: {command}") result = run_command(command, retries=5, delay=15) @@ -366,7 +368,7 @@ def install_packages(packages, elements, app=None): elements[index] = (package, "Installed") else: elements[index] = (package, "Failed") - if app is not None and config.DIALOG == "curses" and elements is not None: + if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 app.report_dependencies( f"Installing Packages ({index + 1}/{total_packages})", 100 * (index + 1) // total_packages, @@ -381,7 +383,7 @@ def remove_packages(packages, elements, app=None): if packages: total_packages = len(packages) for index, package in enumerate(packages): - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {package}" + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {package}" # noqa: E501 logging.debug(f"remove_packages cmd: {command}") result = run_command(command, retries=5, delay=15) @@ -390,7 +392,7 @@ def remove_packages(packages, elements, app=None): elements[index] = (package, "Removed") else: elements[index] = (package, "Failed") - if app is not None and config.DIALOG == "curses" and elements is not None: + if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 app.report_dependencies( f"Removing Packages ({index + 1}/{total_packages})", 100 * (index + 1) // total_packages, @@ -416,7 +418,7 @@ def check_dialog_version(): except subprocess.CalledProcessError as e: print(f"Error running command: {e.stderr}") except FileNotFoundError: - print("The 'dialog' command is not found. Please ensure it is installed and in your PATH.") + print("The 'dialog' command is not found. Please ensure it is installed and in your PATH.") # noqa: E501 return None @@ -435,7 +437,7 @@ def parse_date(version): if version is not None: minimum_version = parse_date(minimum_version) current_version = parse_date(version) - logging.debug(f"Minimum dialog version: {minimum_version}. Installed version: {current_version}.") + logging.debug(f"Minimum dialog version: {minimum_version}. Installed version: {current_version}.") # noqa: E501 return current_version > minimum_version else: return None @@ -443,16 +445,16 @@ def parse_date(version): def preinstall_dependencies_ubuntu(): try: - dpkg_output = run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) - mkdir_output = run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) - wget_key_output = run_command( - [config.SUPERUSER_COMMAND, "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", "https://dl.winehq.org/wine-builds/winehq.key"]) + run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) # noqa: E501 + run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) # noqa: E501 + url = "https://dl.winehq.org/wine-builds/winehq.key" + run_command([config.SUPERUSER_COMMAND, "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", url]) # noqa: E501 lsb_release_output = run_command(["lsb_release", "-a"]) - codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[1].strip() - wget_sources_output = run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", - f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources"]) - apt_update_output = run_command([config.SUPERUSER_COMMAND, "apt", "update"]) - apt_install_output = run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) + codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[1].strip() # noqa: E501 + url = f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources" # noqa: E501 + run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", url]) # noqa: E501 + run_command([config.SUPERUSER_COMMAND, "apt", "update"]) + run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) # noqa: E501 except subprocess.CalledProcessError as e: print(f"An error occurred: {e}") print(f"Command output: {e.output}") @@ -460,35 +462,35 @@ def preinstall_dependencies_ubuntu(): def preinstall_dependencies_steamos(): command = [config.SUPERUSER_COMMAND, "steamos-readonly", "disable"] - steamsos_readonly_output = run_command(command) + run_command(command) command = [config.SUPERUSER_COMMAND, "pacman-key", "--init"] - pacman_key_init_output = run_command(command) - command = [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"] - pacman_key_populate_output = run_command(command) + run_command(command) + command = [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"] # noqa: E501 + run_command(command) def postinstall_dependencies_steamos(): - command =[ + command = [ config.SUPERUSER_COMMAND, "sed", '-i', 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 '/etc/nsswitch.conf' ] - sed_output = run_command(command) - command =[config.SUPERUSER_COMMAND, "locale-gen"] - locale_gen_output = run_command(command) - command =[ + run_command(command) + command = [config.SUPERUSER_COMMAND, "locale-gen"] + run_command(command) + command = [ config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "avahi-daemon" ] - systemctl_avahi_daemon_output = run_command(command) - command =[config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"] - systemctl_cups = run_command(command) + run_command(command) + command = [config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"] # noqa: E501 + run_command(command) command = [config.SUPERUSER_COMMAND, "steamos-readonly", "enable"] - steamos_readonly_output = run_command(command) + run_command(command) def preinstall_dependencies(): @@ -503,7 +505,7 @@ def postinstall_dependencies(): postinstall_dependencies_steamos() -def install_dependencies(packages, badpackages, logos9_packages=None, app=None): +def install_dependencies(packages, badpackages, logos9_packages=None, app=None): # noqa: E501 missing_packages = {} conflicting_packages = {} package_list = [] @@ -523,12 +525,12 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if config.DIALOG == "curses" and app is not None and elements is not None: for p in package_list: elements[p] = "Unchecked" - if config.DIALOG == "curses" and app is not None and bad_elements is not None: + if config.DIALOG == "curses" and app is not None and bad_elements is not None: # noqa: E501 for p in bad_package_list: bad_elements[p] = "Unchecked" if config.DIALOG == "curses" and app is not None: - app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) + app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) # noqa: E501 if config.PACKAGE_MANAGER_COMMAND_QUERY: logging.debug("Querying packages...") @@ -538,13 +540,13 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if config.PACKAGE_MANAGER_COMMAND_INSTALL: if missing_packages and conflicting_packages: message = f"Your {config.OS_NAME} computer requires installing and removing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}) and will remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 - #logging.critical(message) + # logging.critical(message) elif missing_packages: message = f"Your {config.OS_NAME} computer requires installing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}). Proceed?" # noqa: E501 - #logging.critical(message) + # logging.critical(message) elif conflicting_packages: message = f"Your {config.OS_NAME} computer requires removing some software. To continue, the program will attempt to remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 - #logging.critical(message) + # logging.critical(message) else: logging.debug("No missing or conflicting dependencies found.") @@ -569,8 +571,8 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if conflicting_packages: # AppImage Launcher is the only known conflicting package. remove_packages(conflicting_packages, bad_elements, app) - #config.REBOOT_REQUIRED = True - #TODO: Verify with user before executing + # config.REBOOT_REQUIRED = True + # TODO: Verify with user before executing postinstall_dependencies() @@ -602,16 +604,16 @@ def check_libs(libraries, app=None): logging.info(f"* {library} is installed!") else: if config.PACKAGE_MANAGER_COMMAND_INSTALL: - message = f"Your {config.OS_NAME} install is missing the library: {library}. To continue, the script will attempt to install the library by using {config.PACKAGE_MANAGER_COMMAND_INSTALL}. Proceed?" # noqa: E501 - #if msg.cli_continue_question(message, "", ""): + # message = f"Your {config.OS_NAME} install is missing the library: {library}. To continue, the script will attempt to install the library by using {config.PACKAGE_MANAGER_COMMAND_INSTALL}. Proceed?" # noqa: E501 + # if msg.cli_continue_question(message, "", ""): elements = {} - if config.DIALOG == "curses" and app is not None and elements is not None: + if config.DIALOG == "curses" and app is not None and elements is not None: # noqa: E501 for p in libraries: elements[p] = "Unchecked" if config.DIALOG == "curses" and app is not None: - app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) + app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) # noqa: E501 install_packages(config.PACKAGES, elements, app=app) else: From 5cedb2401bbc1d91182df772e96855eda0d99507 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 22 Jul 2024 08:31:52 -0500 Subject: [PATCH 074/253] look for pkexec first, even in TUI --- system.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system.py b/system.py index a9d5de81..e7665e19 100644 --- a/system.py +++ b/system.py @@ -182,7 +182,9 @@ def get_superuser_command(): else: msg.logos_error("No superuser command found. Please install pkexec.") # noqa: E501 else: - if shutil.which('sudo'): + if shutil.which('pkexec'): + config.SUPERUSER_COMMAND = "pkexec" + elif shutil.which('sudo'): config.SUPERUSER_COMMAND = "sudo" elif shutil.which('doas'): config.SUPERUSER_COMMAND = "doas" From ecf364515c78a1691c3898ce981c5d9b29531086 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 23 Jul 2024 08:36:41 -0500 Subject: [PATCH 075/253] remove errant "exit()" --- system.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system.py b/system.py index e7665e19..d2b7328b 100644 --- a/system.py +++ b/system.py @@ -318,7 +318,6 @@ def query_packages(packages, elements=None, mode="install", app=None): dialog=config.use_python_dialog) txt = 'None' - exit() if mode == "install": if missing_packages: txt = f"Missing packages: {' '.join(missing_packages)}" From 60dc5fde62d4349374faa072dc3ef9f0a4db1106 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 23 Jul 2024 11:21:13 -0400 Subject: [PATCH 076/253] Fix #135 --- main.py | 20 ++++++++++---------- msg.py | 23 ++++++----------------- tui_app.py | 1 - 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/main.py b/main.py index ed711860..3f375a37 100755 --- a/main.py +++ b/main.py @@ -301,8 +301,16 @@ def main(): cli_args = parser.parse_args() # parsing early lets 'help' run immediately # Set runtime config. - # Initialize logging. - msg.initialize_logging(config.LOG_LEVEL) + # Set DIALOG and GUI variables and initialize logging. + if config.DIALOG is None: + system.get_dialog() + # Initialize logging. + msg.initialize_logging(config.LOG_LEVEL) + else: + config.DIALOG = config.DIALOG.lower() + if config.DIALOG == 'tk': + config.GUI = True + msg.initialize_curses_logging() current_log_level = config.LOG_LEVEL # Set default config; incl. defining CONFIG_FILE. @@ -334,14 +342,6 @@ def main(): if config.LOG_LEVEL != current_log_level: msg.update_log_level(config.LOG_LEVEL) - # Set DIALOG and GUI variables. - if config.DIALOG is None: - system.get_dialog() - else: - config.DIALOG = config.DIALOG.lower() - if config.DIALOG == 'tk': - config.GUI = True - if config.DIALOG == 'curses' and "dialog" in sys.modules: config.use_python_dialog = system.test_dialog_version() diff --git a/msg.py b/msg.py index 33a0c6b8..f33a30ec 100644 --- a/msg.py +++ b/msg.py @@ -10,19 +10,19 @@ logging.console_log = [] + class CursesHandler(logging.Handler): - def __init__(self, screen): + def __init__(self): logging.Handler.__init__(self) - self.screen = screen + def emit(self, record): try: msg = self.format(record) - screen = self.screen status(msg) - screen.refresh() except: raise + def get_log_level_name(level): name = None levels = { @@ -78,18 +78,7 @@ def initialize_logging(stderr_log_level): ) -def initialize_curses_logging(stdscr): - ''' - Log levels: - Level Value Description - CRITICAL 50 the program can't continue - ERROR 40 the program has not been able to do something - WARNING 30 something unexpected happened (maybe neg. effect) - INFO 20 confirmation that things are working as expected - DEBUG 10 detailed, dev-level information - NOTSET 0 all events are handled - ''' - +def initialize_curses_logging(): # Ensure log file parent folders exist. log_parent = Path(config.LOGOS_LOG).parent if not log_parent.is_dir(): @@ -98,7 +87,7 @@ def initialize_curses_logging(stdscr): # Define logging handlers. file_h = logging.FileHandler(config.LOGOS_LOG, encoding='UTF8') file_h.setLevel(logging.DEBUG) - curses_h = CursesHandler(stdscr) + curses_h = CursesHandler() handlers = [ curses_h, ] diff --git a/tui_app.py b/tui_app.py index 19e982d5..53084153 100644 --- a/tui_app.py +++ b/tui_app.py @@ -166,7 +166,6 @@ def resize_curses(self): def display(self): signal.signal(signal.SIGINT, self.end) - msg.initialize_curses_logging(self.stdscr) msg.status(self.console_message, self) self.active_screen = self.menu_screen From 58fe814e9b887b56bd1eb9f8169693581834ed01 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 23 Jul 2024 10:24:06 -0500 Subject: [PATCH 077/253] handle non-zero status for registry lookup --- wine.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/wine.py b/wine.py index 8a28db01..19a474d2 100644 --- a/wine.py +++ b/wine.py @@ -337,14 +337,24 @@ def get_registry_value(reg_path, name): value = None env = get_wine_env() cmd = [config.WINE_EXE, 'reg', 'query', reg_path, '/v', name] - result = system.run_command(cmd, encoding=config.WINECMD_ENCODING, env=env) + err_msg = f"Failed to get registry value: {reg_path}\\{name}" + try: + result = system.run_command( + cmd, + encoding=config.WINECMD_ENCODING, + env=env + ) + except subprocess.CalledProcessError as e: + if 'non-zero exit status' in str(e): + logging.warning(err_msg) + return None if result.stdout is not None: for line in result.stdout.splitlines(): if line.strip().startswith(name): value = line.split()[-1].strip() break else: - logging.critical(f"wine.get_registry_value: Failed to get registry value: {reg_path}") + logging.critical(err_msg) return value From ed0b0141660c3d3e0d651a0481fbf4130fabdd97 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 23 Jul 2024 10:49:45 -0500 Subject: [PATCH 078/253] Revert "Fix #135" This reverts commit d72ec015d2c49812af08b58dee6e90e02f5a66e7. --- main.py | 20 ++++++++++---------- msg.py | 23 +++++++++++++++++------ tui_app.py | 1 + 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/main.py b/main.py index 3f375a37..ed711860 100755 --- a/main.py +++ b/main.py @@ -301,16 +301,8 @@ def main(): cli_args = parser.parse_args() # parsing early lets 'help' run immediately # Set runtime config. - # Set DIALOG and GUI variables and initialize logging. - if config.DIALOG is None: - system.get_dialog() - # Initialize logging. - msg.initialize_logging(config.LOG_LEVEL) - else: - config.DIALOG = config.DIALOG.lower() - if config.DIALOG == 'tk': - config.GUI = True - msg.initialize_curses_logging() + # Initialize logging. + msg.initialize_logging(config.LOG_LEVEL) current_log_level = config.LOG_LEVEL # Set default config; incl. defining CONFIG_FILE. @@ -342,6 +334,14 @@ def main(): if config.LOG_LEVEL != current_log_level: msg.update_log_level(config.LOG_LEVEL) + # Set DIALOG and GUI variables. + if config.DIALOG is None: + system.get_dialog() + else: + config.DIALOG = config.DIALOG.lower() + if config.DIALOG == 'tk': + config.GUI = True + if config.DIALOG == 'curses' and "dialog" in sys.modules: config.use_python_dialog = system.test_dialog_version() diff --git a/msg.py b/msg.py index f33a30ec..33a0c6b8 100644 --- a/msg.py +++ b/msg.py @@ -10,19 +10,19 @@ logging.console_log = [] - class CursesHandler(logging.Handler): - def __init__(self): + def __init__(self, screen): logging.Handler.__init__(self) - + self.screen = screen def emit(self, record): try: msg = self.format(record) + screen = self.screen status(msg) + screen.refresh() except: raise - def get_log_level_name(level): name = None levels = { @@ -78,7 +78,18 @@ def initialize_logging(stderr_log_level): ) -def initialize_curses_logging(): +def initialize_curses_logging(stdscr): + ''' + Log levels: + Level Value Description + CRITICAL 50 the program can't continue + ERROR 40 the program has not been able to do something + WARNING 30 something unexpected happened (maybe neg. effect) + INFO 20 confirmation that things are working as expected + DEBUG 10 detailed, dev-level information + NOTSET 0 all events are handled + ''' + # Ensure log file parent folders exist. log_parent = Path(config.LOGOS_LOG).parent if not log_parent.is_dir(): @@ -87,7 +98,7 @@ def initialize_curses_logging(): # Define logging handlers. file_h = logging.FileHandler(config.LOGOS_LOG, encoding='UTF8') file_h.setLevel(logging.DEBUG) - curses_h = CursesHandler() + curses_h = CursesHandler(stdscr) handlers = [ curses_h, ] diff --git a/tui_app.py b/tui_app.py index 53084153..19e982d5 100644 --- a/tui_app.py +++ b/tui_app.py @@ -166,6 +166,7 @@ def resize_curses(self): def display(self): signal.signal(signal.SIGINT, self.end) + msg.initialize_curses_logging(self.stdscr) msg.status(self.console_message, self) self.active_screen = self.menu_screen From 33c50d2bc4a695f3e4da2a0d759670f66dac5463 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 23 Jul 2024 11:26:11 -0500 Subject: [PATCH 079/253] fix logging recursion error --- msg.py | 49 ++++++++++++++----------------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/msg.py b/msg.py index 33a0c6b8..4a59666c 100644 --- a/msg.py +++ b/msg.py @@ -10,19 +10,21 @@ logging.console_log = [] + class CursesHandler(logging.Handler): def __init__(self, screen): - logging.Handler.__init__(self) + super().__init__() self.screen = screen + def emit(self, record): try: msg = self.format(record) - screen = self.screen status(msg) - screen.refresh() - except: + self.screen.refresh() + except Exception: raise + def get_log_level_name(level): name = None levels = { @@ -58,10 +60,12 @@ def initialize_logging(stderr_log_level): # Define logging handlers. file_h = logging.FileHandler(config.LOGOS_LOG, encoding='UTF8') + file_h.name = "logfile" file_h.setLevel(logging.DEBUG) # stdout_h = logging.StreamHandler(sys.stdout) # stdout_h.setLevel(stdout_log_level) stderr_h = logging.StreamHandler(sys.stderr) + stderr_h.name = "terminal" stderr_h.setLevel(stderr_log_level) handlers = [ file_h, @@ -79,37 +83,13 @@ def initialize_logging(stderr_log_level): def initialize_curses_logging(stdscr): - ''' - Log levels: - Level Value Description - CRITICAL 50 the program can't continue - ERROR 40 the program has not been able to do something - WARNING 30 something unexpected happened (maybe neg. effect) - INFO 20 confirmation that things are working as expected - DEBUG 10 detailed, dev-level information - NOTSET 0 all events are handled - ''' - - # Ensure log file parent folders exist. - log_parent = Path(config.LOGOS_LOG).parent - if not log_parent.is_dir(): - log_parent.mkdir(parents=True) - - # Define logging handlers. - file_h = logging.FileHandler(config.LOGOS_LOG, encoding='UTF8') - file_h.setLevel(logging.DEBUG) + current_logger = logging.getLogger() + for h in current_logger.handlers: + if h.name == 'terminal': + current_logger.removeHandler(h) + break curses_h = CursesHandler(stdscr) - handlers = [ - curses_h, - ] - - # Set initial config. - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s %(levelname)s: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - handlers=handlers, - ) + current_logger.addHandler(curses_h) def update_log_level(new_level): @@ -251,7 +231,6 @@ def progress(percent, app=None): def status(text, app=None): timestamp = time.strftime("%Y-%m-%dT%H:%M:%S") """Handles status messages for both TUI and GUI.""" - logging.debug(f"Status: {text}") if config.DIALOG == 'tk': app.status_q.put(text) app.root.event_generate('<>') From 097804eb87bfac9bf930234f7eafdc6ac5841202 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 23 Jul 2024 13:37:41 -0400 Subject: [PATCH 080/253] Clean up terminal --- msg.py | 10 ++++------ tui_app.py | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/msg.py b/msg.py index 4a59666c..0794af67 100644 --- a/msg.py +++ b/msg.py @@ -12,15 +12,13 @@ class CursesHandler(logging.Handler): - def __init__(self, screen): + def __init__(self): super().__init__() - self.screen = screen def emit(self, record): try: msg = self.format(record) status(msg) - self.screen.refresh() except Exception: raise @@ -82,13 +80,13 @@ def initialize_logging(stderr_log_level): ) -def initialize_curses_logging(stdscr): +def initialize_curses_logging(): current_logger = logging.getLogger() for h in current_logger.handlers: if h.name == 'terminal': - current_logger.removeHandler(h) + #current_logger.removeHandler(h) break - curses_h = CursesHandler(stdscr) + curses_h = CursesHandler() current_logger.addHandler(curses_h) diff --git a/tui_app.py b/tui_app.py index 19e982d5..164aee79 100644 --- a/tui_app.py +++ b/tui_app.py @@ -166,7 +166,7 @@ def resize_curses(self): def display(self): signal.signal(signal.SIGINT, self.end) - msg.initialize_curses_logging(self.stdscr) + msg.initialize_curses_logging() msg.status(self.console_message, self) self.active_screen = self.menu_screen @@ -597,4 +597,3 @@ def get_menu_window(self): def control_panel_app(stdscr): os.environ.setdefault('ESCDELAY', '100') TUI(stdscr).run() - From 876f0204fec5344742e7a6dbeb54765aeeed47a6 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 24 Jul 2024 08:28:43 -0500 Subject: [PATCH 081/253] ensure "Run action" button is activated when install ICU radio clicked --- gui_app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui_app.py b/gui_app.py index 91b5b153..9a25e78a 100644 --- a/gui_app.py +++ b/gui_app.py @@ -596,6 +596,9 @@ def __init__(self, root, *args, **kwargs): self.gui.remove_index_files_radio.config( command=self.on_action_radio_clicked ) + self.gui.install_icu_radio.config( + command=self.on_action_radio_clicked + ) self.gui.actions_button.config(command=self.gui.actioncmd) self.gui.loggingstatevar.set('Enable') From c2ec13b22adfa381f64e458515ebbb78c55f4a1f Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 24 Jul 2024 09:47:19 -0500 Subject: [PATCH 082/253] remove duplicated config vars --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index ffcc5ebf..308c8cbb 100644 --- a/config.py +++ b/config.py @@ -7,7 +7,7 @@ # Define and set variables that are required in the config file. core_config_keys = [ "FLPRODUCT", "FLPRODUCTi", "TARGETVERSION", "TARGET_RELEASE_VERSION", - "current_logos_version", "FLPRODUCT", "TARGETVERSION", + "current_logos_version", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION" From 5bed8f42143620e339a0bc860db3961de8b4d423 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 25 Jul 2024 07:05:30 -0500 Subject: [PATCH 083/253] remove error logging when dialog pkg not found --- tui_screen.py | 2 -- utils.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/tui_screen.py b/tui_screen.py index 3cf39fdd..c150b85f 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -10,8 +10,6 @@ import tui_curses if system.have_dep("dialog"): import tui_dialog -else: - logging.error(f"tui_screen.py: dialog not installed.") class Screen: diff --git a/utils.py b/utils.py index 3385d4a3..24d594b0 100644 --- a/utils.py +++ b/utils.py @@ -24,8 +24,6 @@ import system if system.have_dep("dialog"): import tui_dialog -else: - logging.error(f"utils.py: dialog not installed.") import wine #TODO: Move config commands to config.py From 01fc1ad7a6024e6ae31675d48fbdfd2e11bd9288 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 25 Jul 2024 07:29:28 -0500 Subject: [PATCH 084/253] fix "Run action" button handling --- gui.py | 1 - gui_app.py | 15 ++++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/gui.py b/gui.py index 20aa0f01..ea0e2f02 100644 --- a/gui.py +++ b/gui.py @@ -167,7 +167,6 @@ def __init__(self, root, *args, **kwargs): # -> Run indexing, Remove library catalog, Remove all index files s1 = Separator(self, orient='horizontal') self.actionsvar = StringVar() - self.actioncmd = None self.actions_label = Label(self, text="App actions: ") self.run_indexing_radio = Radiobutton( self, diff --git a/gui_app.py b/gui_app.py index 9a25e78a..c23f46c1 100644 --- a/gui_app.py +++ b/gui_app.py @@ -580,6 +580,7 @@ def __init__(self, root, *args, **kwargs): self.root.title("Faithlife Bible Software Control Panel") self.root.resizable(False, False) self.gui = gui.ControlGui(self.root) + self.actioncmd = None text = self.gui.update_lli_label.cget('text') ver = config.LLI_CURRENT_VERSION @@ -599,7 +600,7 @@ def __init__(self, root, *args, **kwargs): self.gui.install_icu_radio.config( command=self.on_action_radio_clicked ) - self.gui.actions_button.config(command=self.gui.actioncmd) + self.gui.actions_button.config(command=self.run_action_cmd) self.gui.loggingstatevar.set('Enable') self.gui.logging_button.config( @@ -695,17 +696,21 @@ def run_logos(self, evt=None): t = Thread(target=wine.run_logos) t.start() + def run_action_cmd(self, evt=None): + self.actioncmd() + def on_action_radio_clicked(self, evt=None): + logging.debug("gui_app.ControlPanel.on_action_radio_clicked START") if utils.app_is_installed(): self.gui.actions_button.state(['!disabled']) if self.gui.actionsvar.get() == 'run-indexing': - self.gui.actioncmd = self.run_indexing + self.actioncmd = self.run_indexing elif self.gui.actionsvar.get() == 'remove-library-catalog': - self.gui.actioncmd = self.remove_library_catalog + self.actioncmd = self.remove_library_catalog elif self.gui.actionsvar.get() == 'remove-index-files': - self.gui.actioncmd = self.remove_indexes + self.actioncmd = self.remove_indexes elif self.gui.actionsvar.get() == 'install-icu': - self.gui.actioncmd = self.install_icu + self.actioncmd = self.install_icu def run_indexing(self, evt=None): t = Thread(target=wine.run_indexing) From 3ce4782ffdfbe2e5dc5f407739085032ed8d75e7 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 25 Jul 2024 07:56:28 -0500 Subject: [PATCH 085/253] fix status message events --- control.py | 4 ++-- gui_app.py | 9 ++++----- wine.py | 14 +++----------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/control.py b/control.py index 5855bfde..fbe7677d 100644 --- a/control.py +++ b/control.py @@ -252,8 +252,8 @@ def remove_all_index_files(app=None): logging.error(f"Error removing {file_to_remove}: {e}") msg.logos_msg("======= Removing all LogosBible index files done! =======") - if app is not None: - app.root.event_generate(app.message_event) + if app and hasattr(app, 'status_evt'): + app.root.event_generate(app.status_evt) sys.exit(0) diff --git a/gui_app.py b/gui_app.py index c23f46c1..607a6e79 100644 --- a/gui_app.py +++ b/gui_app.py @@ -157,10 +157,8 @@ def __init__(self, new_win, root, **kwargs): self.check_evt = "<>" self.root.bind(self.check_evt, self.update_file_check_progress) self.status_q = Queue() - self.root.bind( - "<>", - self.update_status_text - ) + self.status_evt = "<>" + self.root.bind(self.status_evt, self.update_status_text) self.progress_q = Queue() self.root.bind( "<>", @@ -641,8 +639,9 @@ def __init__(self, root, *args, **kwargs): self.root.bind('<>', self.initialize_logging_button) self.root.bind('<>', self.update_logging_button) self.status_q = Queue() + self.status_evt = '<>' + self.root.bind(self.status_evt, self.update_status_text) self.root.bind('<>', self.clear_status_text) - self.root.bind('<>', self.update_status_text) self.progress_q = Queue() self.root.bind( '<>', diff --git a/wine.py b/wine.py index 19a474d2..06a6433e 100644 --- a/wine.py +++ b/wine.py @@ -320,17 +320,9 @@ def installICUDataFiles(app=None): os.makedirs(icu_win_dir) shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok=True) - if app and not hasattr(app, 'todo_q'): - app.root.event_generate(app.message_event) - - # Ensure the target directory exists - icu_win_dir = f"{drive_c}/icu-win/windows" - if not os.path.exists(icu_win_dir): - os.makedirs(icu_win_dir) - - shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok=True) - if app and not hasattr(app, 'todo_q'): - app.root.event_generate(app.message_event) + if app and hasattr(app, 'status_evt'): + app.status_q.put("ICU files copied.") + app.root.event_generate(app.status_evt) def get_registry_value(reg_path, name): From 796eb789350dc5c084f0bf07fedeafeb3044ddd5 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 25 Jul 2024 08:12:16 -0500 Subject: [PATCH 086/253] add ICU files test before installing --- installer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/installer.py b/installer.py index 040046b1..ac4e66d0 100644 --- a/installer.py +++ b/installer.py @@ -462,9 +462,11 @@ def ensure_icu_data_files(app=None): status = "Ensuring ICU data files installed…" update_install_feedback(status, app=app) logging.debug('- ICU data files') - # TODO: Need a test to skip - wine.installICUDataFiles(app=app) - logging.debug('+ ICU data files') + + icu_license_path = f"{config.WINEPREFIX}/drive_c/windows/globalization/ICU/LICENSE-ICU.txt" # noqa: E501 + if not utils.file_exists(icu_license_path): + wine.installICUDataFiles(app=app) + logging.debug('> ICU data files installed') def ensure_winetricks_applied(app=None): From 5555c9c44fbbf5809b3ebc66ad1dbb10b95ef4cb Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 25 Jul 2024 13:48:45 -0500 Subject: [PATCH 087/253] re-raise caught exceptions --- main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index ed711860..6e66d331 100755 --- a/main.py +++ b/main.py @@ -286,14 +286,14 @@ def run_control_panel(): try: close() except Exception as e: - raise + raise e raise except curses.error as e: logging.error(f"Curses error in run_control_panel(): {e}") - raise + raise e except Exception as e: logging.error(f"An error occurred in run_control_panel(): {e}") - raise + raise e def main(): From fcac6d7af93617df4357c7ebbc6c939c1921646b Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 25 Jul 2024 14:12:13 -0500 Subject: [PATCH 088/253] use try/except for importing dialog --- main.py | 4 ++++ tui_dialog.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 6e66d331..c62abf49 100755 --- a/main.py +++ b/main.py @@ -11,6 +11,10 @@ import config import control +try: + import dialog # noqa: F401 +except ImportError: + pass import gui_app import installer import msg diff --git a/tui_dialog.py b/tui_dialog.py index 56b7446d..7227842e 100644 --- a/tui_dialog.py +++ b/tui_dialog.py @@ -1,8 +1,9 @@ import curses import logging -import sys -if "dialog" in sys.modules: +try: from dialog import Dialog +except ImportError: + pass def text(app, text, height=None, width=None, title=None, backtitle=None, colors=True): From 3fd30f7c85942e943ed630f807c8c529a9985b02 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 25 Jul 2024 14:12:50 -0500 Subject: [PATCH 089/253] pep8 fixes --- main.py | 21 ++++++++++----------- network.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index c62abf49..17d41159 100755 --- a/main.py +++ b/main.py @@ -1,29 +1,28 @@ #!/usr/bin/env python3 -import logging -import os import argparse -import curses -import sys -import threading - -processes = {} -threads = [] - import config import control +import curses try: import dialog # noqa: F401 except ImportError: pass import gui_app import installer +import logging import msg import network +import os +import sys import system import tui_app import utils import wine +processes = {} +threads = [] + + def get_parser(): desc = "Installs FaithLife Bible Software with Wine on Linux." parser = argparse.ArgumentParser(description=desc) @@ -350,13 +349,13 @@ def main(): config.use_python_dialog = system.test_dialog_version() if config.use_python_dialog is None: - logging.debug("The 'dialog' package was not found. Falling back to Python Curses.") + logging.debug("The 'dialog' package was not found. Falling back to Python Curses.") # noqa: E501 config.use_python_dialog = False elif config.use_python_dialog: logging.debug("Dialog version is up-to-date.") config.use_python_dialog = True else: - logging.error("Dialog version is outdated. The program will fall back to Curses.") + logging.error("Dialog version is outdated. The program will fall back to Curses.") # noqa: E501 config.use_python_dialog = False # Log persistent config. diff --git a/network.py b/network.py index a80cc755..02a23a6b 100644 --- a/network.py +++ b/network.py @@ -376,7 +376,7 @@ def get_latest_release_data(releases_url): if data: try: json_data = json.loads(data.decode()) - logging.debug(f"{json_data=}") + # logging.debug(f"{json_data=}") # omit to make log more readable except json.JSONDecodeError as e: logging.error(f"Error decoding JSON response: {e}") return None From e4abfcb51cd2077e4c8ec265942b6b1096dd7190 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 27 Jul 2024 18:55:14 -0400 Subject: [PATCH 090/253] Fix TUI not exiting after Logos runs --- tui_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tui_app.py b/tui_app.py index 164aee79..4b0a8b77 100644 --- a/tui_app.py +++ b/tui_app.py @@ -261,6 +261,8 @@ def choice_processor(self, stdscr, screen_id, choice): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": wine.run_logos() + self.active_screen.running = 0 + self.active_screen.choice = "Processing" elif choice == "Run Indexing": wine.run_indexing() elif choice == "Remove Library Catalog": From e213630b6c77ee9d736371845c9c625db17a8a76 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 27 Jul 2024 19:08:14 -0400 Subject: [PATCH 091/253] Detect when Logos is running --- tui_app.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tui_app.py b/tui_app.py index 4b0a8b77..8aa2551b 100644 --- a/tui_app.py +++ b/tui_app.py @@ -260,9 +260,10 @@ def choice_processor(self, stdscr, screen_id, choice): elif choice.startswith("Update Logos Linux Installer"): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": - wine.run_logos() self.active_screen.running = 0 self.active_screen.choice = "Processing" + self.stack_text(12, self.todo_q, self.todo_e, "Logos is running…", dialog=config.use_python_dialog) + self.choice_q.put("0") elif choice == "Run Indexing": wine.run_indexing() elif choice == "Remove Library Catalog": @@ -366,6 +367,10 @@ def choice_processor(self, stdscr, screen_id, choice): pass elif screen_id == 11: pass + if screen_id == 12: + if choice: + wine.run_logos() + self.tui_screens = [] def switch_screen(self, dialog): if self.active_screen is not None and self.active_screen != self.menu_screen: From 1abaead5e58741240fcf9f69e3b5e20b06a4f04c Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 29 Jul 2024 22:16:11 -0400 Subject: [PATCH 092/253] Partial fix for TUI logging and config --- installer.py | 58 +++++++++++++++++++++++----------------------------- msg.py | 35 +++++++++++-------------------- network.py | 15 ++++++-------- tui_app.py | 6 +++++- 4 files changed, 49 insertions(+), 65 deletions(-) diff --git a/installer.py b/installer.py index ac4e66d0..210eae56 100644 --- a/installer.py +++ b/installer.py @@ -459,7 +459,7 @@ def ensure_icu_data_files(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_wineprefix_init(app=app) config.INSTALL_STEP += 1 - status = "Ensuring ICU data files installed…" + status = "Ensuring ICU data files are installed…" update_install_feedback(status, app=app) logging.debug('- ICU data files') @@ -576,22 +576,14 @@ def ensure_config_file(app=None): utils.send_task(app, 'CONFIG') if config.DIALOG == 'curses': app.config_e.wait() - choice = app.config_q.get() - if choice == "Yes": - logging.info("Updating config file.") - utils.write_config(config.CONFIG_FILE) - else: - logging.info("Config file left unchanged.") elif msg.logos_acknowledge_question( f"Update config file at {config.CONFIG_FILE}?", "The existing config file was not overwritten." ): logging.info("Updating config file.") utils.write_config(config.CONFIG_FILE) - if config.DIALOG == 'tk': + if app: utils.send_task(app, 'DONE') - else: - utils.send_task(app, "TUI-UPDATE-MENU") logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 @@ -622,20 +614,19 @@ def ensure_launcher_shortcuts(app=None): ensure_launcher_executable(app=app) config.INSTALL_STEP += 1 runmode = system.get_runmode() - if runmode != 'binary': - return - update_install_feedback("Creating launcher shortcuts…", app=app) - - app_dir = Path(config.INSTALLDIR) / 'data' - logos_icon_path = app_dir / config.LOGOS_ICON_FILENAME # noqa: E501 - if not logos_icon_path.is_file(): - app_dir.mkdir(exist_ok=True) - shutil.copy(config.LOGOS_ICON_URL, logos_icon_path) - else: - logging.info(f"Icon found at {logos_icon_path}.") + if runmode == 'binary': + update_install_feedback("Creating launcher shortcuts…", app=app) + + app_dir = Path(config.INSTALLDIR) / 'data' + logos_icon_path = app_dir / config.LOGOS_ICON_FILENAME # noqa: E501 + if not logos_icon_path.is_file(): + app_dir.mkdir(exist_ok=True) + shutil.copy(config.LOGOS_ICON_URL, logos_icon_path) + else: + logging.info(f"Icon found at {logos_icon_path}.") - desktop_files = [ - ( + desktop_files = [ + ( f"{config.FLPRODUCT}Bible.desktop", f"""[Desktop Entry] Name={config.FLPRODUCT}Bible @@ -646,8 +637,8 @@ def ensure_launcher_shortcuts(app=None): Type=Application Categories=Education; """ - ), - ( + ), + ( f"{config.FLPRODUCT}Bible-ControlPanel.desktop", f"""[Desktop Entry] Name={config.FLPRODUCT}Bible Control Panel @@ -658,19 +649,22 @@ def ensure_launcher_shortcuts(app=None): Type=Application Categories=Education; """ - ), - ] - for f, c in desktop_files: - create_desktop_file(f, c) - fpath = Path.home() / '.local' / 'share' / 'applications' / f - logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") + ), + ] + for f, c in desktop_files: + create_desktop_file(f, c) + fpath = Path.home() / '.local' / 'share' / 'applications' / f + logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") + + if config.DIALOG == 'curses': + utils.send_task(app, "TUI-UPDATE-MENU") def update_install_feedback(text, app=None): percent = get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) logging.debug(f"Install step {config.INSTALL_STEP} of {config.INSTALL_STEPS_COUNT}") # noqa: E501 - msg.status(text, app=app) msg.progress(percent, app=app) + msg.status(text, app=app) def get_progress_pct(current, total): diff --git a/msg.py b/msg.py index 0794af67..ca9fabed 100644 --- a/msg.py +++ b/msg.py @@ -11,18 +11,6 @@ logging.console_log = [] -class CursesHandler(logging.Handler): - def __init__(self): - super().__init__() - - def emit(self, record): - try: - msg = self.format(record) - status(msg) - except Exception: - raise - - def get_log_level_name(level): name = None levels = { @@ -84,10 +72,7 @@ def initialize_curses_logging(): current_logger = logging.getLogger() for h in current_logger.handlers: if h.name == 'terminal': - #current_logger.removeHandler(h) break - curses_h = CursesHandler() - current_logger.addHandler(curses_h) def update_log_level(new_level): @@ -105,7 +90,7 @@ def cli_msg(message, end='\n'): def logos_msg(message, end='\n'): if config.DIALOG == 'curses': - logging.debug(message) + pass else: cli_msg(message, end) @@ -216,12 +201,15 @@ def get_progress_str(percent): def progress(percent, app=None): """Updates progressbar values for TUI and GUI.""" - logging.debug(f"Progress: {percent}%") if config.DIALOG == 'tk' and app: app.progress_q.put(percent) app.root.event_generate('<>') + logging.info(f"Progress: {percent}%") elif config.DIALOG == 'curses': - status(f"Progress: {get_progress_str(percent)}", app) + if app: + status(f"Progress: {percent}%", app) + else: + status(f"Progress: {get_progress_str(percent)}", app) else: logos_msg(get_progress_str(percent)) # provisional @@ -229,11 +217,12 @@ def progress(percent, app=None): def status(text, app=None): timestamp = time.strftime("%Y-%m-%dT%H:%M:%S") """Handles status messages for both TUI and GUI.""" - if config.DIALOG == 'tk': - app.status_q.put(text) - app.root.event_generate('<>') - elif config.DIALOG == 'curses': - if app is not None: + if app is not None: + if config.DIALOG == 'tk': + app.status_q.put(text) + app.root.event_generate('<>') + logging.info(f"{text}") + elif config.DIALOG == 'curses': app.status_q.put(f"{timestamp} {text}") app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) else: diff --git a/network.py b/network.py index 02a23a6b..d93e63d9 100644 --- a/network.py +++ b/network.py @@ -3,6 +3,7 @@ import logging import os import queue +import requests import shutil import sys import threading @@ -12,8 +13,6 @@ from urllib.parse import urlparse from xml.etree import ElementTree as ET -import requests - import config import msg import utils @@ -323,22 +322,20 @@ def net_get(url, target=None, app=None, evt=None, q=None): def verify_downloaded_file(url, file_path, app=None, evt=None): if app: + msg.status(f"Verifying {file_path}…", app) if config.DIALOG == "tk": app.root.event_generate('<>') - app.status_q.put(f"Verifying {file_path}…") app.root.event_generate('<>') - else: - app.status_q.put(f"Verifying {file_path}…") res = False - msg = f"{file_path} is the wrong size." + txt = f"{file_path} is the wrong size." right_size = same_size(url, file_path) if right_size: - msg = f"{file_path} has the wrong MD5 sum." + txt = f"{file_path} has the wrong MD5 sum." right_md5 = same_md5(url, file_path) if right_md5: - msg = f"{file_path} is verified." + txt = f"{file_path} is verified." res = True - logging.info(msg) + logging.info(txt) if app: if config.DIALOG == "tk": if not evt: diff --git a/tui_app.py b/tui_app.py index 8aa2551b..2ff654ab 100644 --- a/tui_app.py +++ b/tui_app.py @@ -360,7 +360,11 @@ def choice_processor(self, stdscr, screen_id, choice): self.finished_e.set() elif screen_id == 9: if choice: - self.config_q.put(choice) + if choice == "Yes": + logging.info("Updating config file.") + utils.write_config(config.CONFIG_FILE) + else: + logging.info("Config file left unchanged.") self.config_e.set() self.tui_screens = [] elif screen_id == 10: From bf8f278aa5b4e2a83f1ffe1317741c4a08b85a8b Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 29 Jul 2024 22:19:22 -0400 Subject: [PATCH 093/253] Swap runmode check, add debugging --- installer.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/installer.py b/installer.py index 210eae56..ce8ae68e 100644 --- a/installer.py +++ b/installer.py @@ -592,21 +592,22 @@ def ensure_launcher_executable(app=None): ensure_config_file(app=app) config.INSTALL_STEP += 1 runmode = system.get_runmode() - if runmode != 'binary': - return - update_install_feedback( - f"Copying launcher to {config.INSTALLDIR}…", - app=app - ) + if runmode == 'binary': + update_install_feedback( + f"Copying launcher to {config.INSTALLDIR}…", + app=app + ) - # Copy executable to config.INSTALLDIR. - launcher_exe = Path(f"{config.INSTALLDIR}/LogosLinuxInstaller") - if launcher_exe.is_file(): - logging.debug("Removing existing launcher binary.") - launcher_exe.unlink() - logging.info(f"Creating launcher binary by copying this installer binary to {launcher_exe}.") # noqa: E501 - shutil.copy(sys.executable, launcher_exe) - logging.debug(f"> File exists?: {launcher_exe}: {launcher_exe.is_file()}") # noqa: E501 + # Copy executable to config.INSTALLDIR. + launcher_exe = Path(f"{config.INSTALLDIR}/LogosLinuxInstaller") + if launcher_exe.is_file(): + logging.debug("Removing existing launcher binary.") + launcher_exe.unlink() + logging.info(f"Creating launcher binary by copying this installer binary to {launcher_exe}.") # noqa: E501 + shutil.copy(sys.executable, launcher_exe) + logging.debug(f"> File exists?: {launcher_exe}: {launcher_exe.is_file()}") # noqa: E501 + else: + logging.debug(f"Running from source. Skipping launcher creation.") def ensure_launcher_shortcuts(app=None): @@ -655,6 +656,8 @@ def ensure_launcher_shortcuts(app=None): create_desktop_file(f, c) fpath = Path.home() / '.local' / 'share' / 'applications' / f logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") + else: + logging.debug(f"Running from source. Skipping launcher creation.") if config.DIALOG == 'curses': utils.send_task(app, "TUI-UPDATE-MENU") From 45597b997a27d81824f2aff0b3d2a10d9e670fd4 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 30 Jul 2024 02:47:19 -0400 Subject: [PATCH 094/253] Fix TUI config writing and launcher creation. - Add `-W|--skip-winetricks` option to make dev work speedier for testing the end of the install routine. --- config.py | 1 + installer.py | 91 ++++++++++++++++++++++++++-------------------------- main.py | 15 ++++++--- tui_app.py | 25 +++++++-------- wine.py | 3 +- 5 files changed, 72 insertions(+), 63 deletions(-) diff --git a/config.py b/config.py index 308c8cbb..c6846bcb 100644 --- a/config.py +++ b/config.py @@ -35,6 +35,7 @@ 'SELECTED_APPIMAGE_FILENAME': None, 'SKIP_DEPENDENCIES': False, 'SKIP_FONTS': False, + 'SKIP_WINETRICKS': False, 'VERBOSE': False, 'WINE_EXE': None, 'WINEBIN_CODE': None, diff --git a/installer.py b/installer.py index ce8ae68e..6d8fc2b2 100644 --- a/installer.py +++ b/installer.py @@ -482,48 +482,49 @@ def ensure_winetricks_applied(app=None): logging.debug('- settings fontsmooth=rgb') logging.debug('- d3dcompiler_47') - usr_reg = None - sys_reg = None - workdir = Path(f"{config.WORKDIR}") - workdir.mkdir(parents=True, exist_ok=True) - usr_reg = Path(f"{config.WINEPREFIX}/user.reg") - sys_reg = Path(f"{config.WINEPREFIX}/system.reg") - if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - #FIXME: This command is failing. - reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') - with open(reg_file, 'w') as f: - f.write(r'''REGEDIT4 + if not config.SKIP_WINETRICKS: + usr_reg = None + sys_reg = None + workdir = Path(f"{config.WORKDIR}") + workdir.mkdir(parents=True, exist_ok=True) + usr_reg = Path(f"{config.WINEPREFIX}/user.reg") + sys_reg = Path(f"{config.WINEPREFIX}/system.reg") + if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): + #FIXME: This command is failing. + reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') + with open(reg_file, 'w') as f: + f.write(r'''REGEDIT4 [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "winemenubuilder.exe"="" ''') - wine.wine_reg_install(reg_file) - - if not utils.grep(r'"renderer"="gdi"', usr_reg): - wine.winetricks_install("-q", "settings", "renderer=gdi") - - if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 - wine.installFonts() - - if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): - wine.installD3DCompiler() - - if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): - args = ["settings", "win10"] - if not config.WINETRICKS_UNATTENDED: - args.insert(0, "-q") - wine.winetricks_install(*args) - - if config.TARGETVERSION == '9': - msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") - exe_args = [ - 'add', - f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 - "/v", "Version", - "/t", "REG_SZ", - "/d", "vista", "/f", - ] - wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + wine.wine_reg_install(reg_file) + + if not utils.grep(r'"renderer"="gdi"', usr_reg): + wine.winetricks_install("-q", "settings", "renderer=gdi") + + if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 + wine.installFonts() + + if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): + wine.installD3DCompiler() + + if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): + args = ["settings", "win10"] + if not config.WINETRICKS_UNATTENDED: + args.insert(0, "-q") + wine.winetricks_install(*args) + + if config.TARGETVERSION == '9': + msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") + exe_args = [ + 'add', + f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 + "/v", "Version", + "/t", "REG_SZ", + "/d", "vista", "/f", + ] + wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) logging.debug("> Done.") @@ -536,9 +537,7 @@ def ensure_product_installed(app=None): if not utils.find_installed_product(): wine.install_msi() config.LOGOS_EXE = utils.find_installed_product() - utils.send_task(app, 'CONFIG') - if config.DIALOG == 'curses': - app.finished_e.wait() + config.current_logos_version = config.TARGET_RELEASE_VERSION # Clean up temp files, etc. utils.clean_all() @@ -567,10 +566,12 @@ def ensure_config_file(app=None): logging.info("Comparing its contents with current config.") current_config_file_dict = config.get_config_file_dict(config.CONFIG_FILE) # noqa: E501 different = False + for key in config.core_config_keys: if current_config_file_dict.get(key) != config.__dict__.get(key): different = True break + if different: if app: utils.send_task(app, 'CONFIG') @@ -582,8 +583,10 @@ def ensure_config_file(app=None): ): logging.info("Updating config file.") utils.write_config(config.CONFIG_FILE) + if app: utils.send_task(app, 'DONE') + logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 @@ -607,7 +610,7 @@ def ensure_launcher_executable(app=None): shutil.copy(sys.executable, launcher_exe) logging.debug(f"> File exists?: {launcher_exe}: {launcher_exe.is_file()}") # noqa: E501 else: - logging.debug(f"Running from source. Skipping launcher creation.") + update_install_feedback(f"Running from source. Skipping launcher creation.", app=app) def ensure_launcher_shortcuts(app=None): @@ -658,9 +661,7 @@ def ensure_launcher_shortcuts(app=None): logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") else: logging.debug(f"Running from source. Skipping launcher creation.") - - if config.DIALOG == 'curses': - utils.send_task(app, "TUI-UPDATE-MENU") + update_install_feedback(f"Running from source. Skipping launcher creation.", app=app) def update_install_feedback(text, app=None): diff --git a/main.py b/main.py index 17d41159..bf63e542 100755 --- a/main.py +++ b/main.py @@ -36,10 +36,6 @@ def get_parser(): # Define options that affect runtime config. cfg = parser.add_argument_group(title="runtime config options") - cfg.add_argument( - '-F', '--skip-fonts', action='store_true', - help='skip font installations', - ) cfg.add_argument( '-a', '--check-for-updates', action='store_true', help='force a check for updates' @@ -48,6 +44,14 @@ def get_parser(): '-K', '--skip-dependencies', action='store_true', help='skip dependencies check and installation', ) + cfg.add_argument( + '-F', '--skip-fonts', action='store_true', + help='skip font installations', + ) + cfg.add_argument( + '-W', '--skip-winetricks', action='store_true', + help='skip winetricks installations. For development purposes only!!!', + ) cfg.add_argument( '-V', '--verbose', action='store_true', help='enable verbose mode', @@ -205,6 +209,9 @@ def parse_args(args, parser): if args.skip_fonts: config.SKIP_FONTS = True + if args.skip_winetricks: + config.SKIP_WINETRICKS = True + if network.check_for_updates: config.CHECK_UPDATES = True diff --git a/tui_app.py b/tui_app.py index 2ff654ab..0cd489d6 100644 --- a/tui_app.py +++ b/tui_app.py @@ -235,12 +235,13 @@ def task_processor(self, evt=None, task=None): elif task == 'CONFIG': utils.start_thread(self.get_config(config.use_python_dialog)) elif task == 'DONE': - self.finish_install() - elif task == 'RESIZE': - self.resize_curses() - elif task == 'TUI-UPDATE-MENU': + self.subtitle = f"Logos Version: {config.current_logos_version}" + self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) + self.switch_q.put(1) + elif task == 'RESIZE': + self.resize_curses() def choice_processor(self, stdscr, screen_id, choice): if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): @@ -262,7 +263,7 @@ def choice_processor(self, stdscr, screen_id, choice): elif choice == f"Run {config.FLPRODUCT}": self.active_screen.running = 0 self.active_screen.choice = "Processing" - self.stack_text(12, self.todo_q, self.todo_e, "Logos is running…", dialog=config.use_python_dialog) + self.screen_q.put(self.stack_text(12, self.todo_q, self.todo_e, "Logos is running…", dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice == "Run Indexing": wine.run_indexing() @@ -355,9 +356,7 @@ def choice_processor(self, stdscr, screen_id, choice): self.tricksbin_q.put(config.WINETRICKSBIN) self.tricksbin_e.set() elif screen_id == 8: - if config.install_finished: - self.finished_q.put(True) - self.finished_e.set() + pass elif screen_id == 9: if choice: if choice == "Yes": @@ -365,8 +364,9 @@ def choice_processor(self, stdscr, screen_id, choice): utils.write_config(config.CONFIG_FILE) else: logging.info("Config file left unchanged.") + self.config_q.put(True) self.config_e.set() - self.tui_screens = [] + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Finishing install…", dialog=config.use_python_dialog)) elif screen_id == 10: pass elif screen_id == 11: @@ -374,7 +374,9 @@ def choice_processor(self, stdscr, screen_id, choice): if screen_id == 12: if choice: wine.run_logos() - self.tui_screens = [] + self.switch_q.put(1) + if screen_id == 13: + pass def switch_screen(self, dialog): if self.active_screen is not None and self.active_screen != self.menu_screen: @@ -464,9 +466,6 @@ def get_config(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog)) - def finish_install(self): - utils.send_task(self, 'TUI-UPDATE-MENU') - def report_waiting(self, text, dialog): #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) logging.console_log.append(text) diff --git a/wine.py b/wine.py index 06a6433e..440c33ad 100644 --- a/wine.py +++ b/wine.py @@ -484,7 +484,8 @@ def get_wine_env(): def run_logos(): logos_release = utils.convert_logos_release(config.current_logos_version) - wine_release = get_wine_release(config.WINE_EXE) + wine_release, _ = get_wine_release(config.WINE_EXE) + logging.debug(f"DEV: {wine_release}") #TODO: Find a way to incorporate check_wine_version_and_branch() if 30 > logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): From 429f8764f1a66aac6a90faeaf0fb5de31da24b49 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 30 Jul 2024 09:23:05 -0400 Subject: [PATCH 095/253] Fix #140. Fix vertical redraw. --- config.py | 2 +- main.py | 3 +- tui_app.py | 135 +++++++++++++++++++++++++++++++------------------- tui_curses.py | 2 - 4 files changed, 86 insertions(+), 56 deletions(-) diff --git a/config.py b/config.py index c6846bcb..d8eb2662 100644 --- a/config.py +++ b/config.py @@ -36,6 +36,7 @@ 'SKIP_DEPENDENCIES': False, 'SKIP_FONTS': False, 'SKIP_WINETRICKS': False, + 'use_python_dialog': None, 'VERBOSE': False, 'WINE_EXE': None, 'WINEBIN_CODE': None, @@ -104,7 +105,6 @@ current_page = 0 total_pages = 0 options_per_page = 8 -use_python_dialog = False resizing = False diff --git a/main.py b/main.py index bf63e542..768b252e 100755 --- a/main.py +++ b/main.py @@ -352,7 +352,7 @@ def main(): if config.DIALOG == 'tk': config.GUI = True - if config.DIALOG == 'curses' and "dialog" in sys.modules: + if config.DIALOG == 'curses' and "dialog" in sys.modules and config.use_python_dialog is None: config.use_python_dialog = system.test_dialog_version() if config.use_python_dialog is None: @@ -364,6 +364,7 @@ def main(): else: logging.error("Dialog version is outdated. The program will fall back to Curses.") # noqa: E501 config.use_python_dialog = False + logging.debug(f"Use Python Dialog?: {config.use_python_dialog}") # Log persistent config. utils.log_current_persistent_config() diff --git a/tui_app.py b/tui_app.py index 0cd489d6..cf497521 100644 --- a/tui_app.py +++ b/tui_app.py @@ -76,8 +76,8 @@ def __init__(self, stdscr): self.appimage_e = threading.Event() # Window and Screen Management - self.window_height = "" - self.window_width = "" + self.window_height = 0 + self.window_width = 0 self.update_tty_dimensions() self.main_window_height = 9 self.menu_window_height = 6 + config.options_per_page @@ -85,26 +85,30 @@ def __init__(self, stdscr): self.menu_options = [] self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) + self.resize_window = curses.newwin(2, curses.COLS, 0, 0) self.console = None self.menu_screen = None self.active_screen = None + def set_curses_style(self): + curses.start_color() + curses.use_default_colors() + curses.init_color(curses.COLOR_BLUE, 0, 510, 1000) # Logos Blue + curses.init_color(curses.COLOR_CYAN, 906, 906, 906) # Logos Gray + curses.init_color(curses.COLOR_WHITE, 988, 988, 988) # Logos White + curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_CYAN) + curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE) + curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLUE) + curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) + curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_BLUE) + self.stdscr.bkgd(' ', curses.color_pair(3)) + self.main_window.bkgd(' ', curses.color_pair(3)) + self.menu_window.bkgd(' ', curses.color_pair(3)) + def init_curses(self): try: if curses.has_colors(): - curses.start_color() - curses.use_default_colors() - curses.init_color(curses.COLOR_BLUE, 0, 510, 1000) # Logos Blue - curses.init_color(curses.COLOR_CYAN, 906, 906, 906) # Logos Gray - curses.init_color(curses.COLOR_WHITE, 988, 988, 988) # Logos White - curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_CYAN) - curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE) - curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLUE) - curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) - curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_BLUE) - self.stdscr.bkgd(' ', curses.color_pair(3)) - self.main_window.bkgd(' ', curses.color_pair(3)) - self.menu_window.bkgd(' ', curses.color_pair(3)) + self.set_curses_style() curses.curs_set(0) curses.noecho() @@ -158,53 +162,80 @@ def resize_curses(self): self.menu_window_height = 6 + config.options_per_page self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) + self.resize_window = curses.newwin(2, curses.COLS, 0, 0) self.init_curses() self.menu_window.refresh() self.menu_window.clear() msg.status("Resizing window.", self) config.resizing = False + def signal_resize(self, signum, frame): + self.resize_curses() + self.choice_q.put("resize") + + if config.use_python_dialog: + if isinstance(self.active_screen, tui_screen.TextDialog) and self.active_screen.text == "Screen Too Small": + self.choice_q.put("Return to Main Menu") + else: + if self.active_screen.get_screen_id == 14: + if self.window_height >= 18: + self.switch_q.put(1) + + def draw_resize_screen(self): + title_lines = tui_curses.wrap_text(self, "Screen too small.") + self.resize_window = curses.newwin(len(title_lines), curses.COLS, 0, 0) + self.resize_window.erase() + for i, line in enumerate(title_lines): + if i < self.window_height: + self.resize_window.addstr(i + 0, 2, line, curses.A_BOLD) + self.resize_window.noutrefresh() + curses.doupdate() + def display(self): + signal.signal(signal.SIGWINCH, self.signal_resize) signal.signal(signal.SIGINT, self.end) msg.initialize_curses_logging() msg.status(self.console_message, self) self.active_screen = self.menu_screen while self.llirunning: - if not config.resizing: - if isinstance(self.active_screen, tui_screen.CursesScreen): - self.main_window.erase() - self.menu_window.erase() - self.stdscr.timeout(100) - self.console.display() - - self.active_screen.display() - - #if (not isinstance(self.active_screen, tui_screen.TextScreen) - # and not isinstance(self.active_screen, tui_screen.TextDialog)): - if self.choice_q.qsize() > 0: - self.choice_processor( - self.menu_window, - self.active_screen.get_screen_id(), - self.choice_q.get()) - - if self.screen_q.qsize() > 0: - self.screen_q.get() - self.switch_q.put(1) - - if self.switch_q.qsize() > 0: - self.switch_q.get() - self.switch_screen(config.use_python_dialog) - - if len(self.tui_screens) == 0: - self.active_screen = self.menu_screen - else: - self.active_screen = self.tui_screens[-1] - - if isinstance(self.active_screen, tui_screen.CursesScreen): - self.main_window.noutrefresh() - self.menu_window.noutrefresh() - curses.doupdate() + if self.window_height >= (self.main_window_height + self.menu_window_height): + if not config.resizing: + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.main_window.erase() + self.menu_window.erase() + self.stdscr.timeout(100) + self.console.display() + + self.active_screen.display() + + #if (not isinstance(self.active_screen, tui_screen.TextScreen) + # and not isinstance(self.active_screen, tui_screen.TextDialog)): + if self.choice_q.qsize() > 0: + self.choice_processor( + self.menu_window, + self.active_screen.get_screen_id(), + self.choice_q.get()) + + if self.screen_q.qsize() > 0: + self.screen_q.get() + self.switch_q.put(1) + + if self.switch_q.qsize() > 0: + self.switch_q.get() + self.switch_screen(config.use_python_dialog) + + if len(self.tui_screens) == 0: + self.active_screen = self.menu_screen + else: + self.active_screen = self.tui_screens[-1] + + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.main_window.noutrefresh() + self.menu_window.noutrefresh() + curses.doupdate() + else: + self.draw_resize_screen() def run(self): try: @@ -240,8 +271,6 @@ def task_processor(self, evt=None, task=None): self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) - elif task == 'RESIZE': - self.resize_curses() def choice_processor(self, stdscr, screen_id, choice): if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): @@ -377,9 +406,11 @@ def choice_processor(self, stdscr, screen_id, choice): self.switch_q.put(1) if screen_id == 13: pass + if screen_id == 14: + pass def switch_screen(self, dialog): - if self.active_screen is not None and self.active_screen != self.menu_screen: + if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: self.tui_screens.pop(0) if self.active_screen == self.menu_screen: self.menu_screen.choice = "Processing" diff --git a/tui_curses.py b/tui_curses.py index 291ee56f..c2f78760 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -118,8 +118,6 @@ def input(self): pass elif key == ord('\n'): # Enter key self.submit = True - elif key == curses.KEY_RESIZE: - utils.send_task(self.app, 'RESIZE') elif key == curses.KEY_BACKSPACE or key == 127: if len(self.user_input) > 0: self.user_input = self.user_input[:-1] From 72586ae4e56b2003c026ef0fa7527ba49b3bd407 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 30 Jul 2024 09:09:35 -0500 Subject: [PATCH 096/253] add error output to failed shell command --- system.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system.py b/system.py index d2b7328b..a491dfb4 100644 --- a/system.py +++ b/system.py @@ -264,6 +264,7 @@ def query_packages(packages, elements=None, mode="install", app=None): result = run_command(command, shell=True) except Exception as e: logging.error(f"Error occurred while executing command: {e}") + logging.error(result.stderr) package_list = result.stdout From 3cf72afe6a60c00e00ecce547bb7eb685797e363 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 29 Jun 2024 00:27:24 -0400 Subject: [PATCH 097/253] Update Version and Changelog --- CHANGELOG.md | 2 +- utils.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c80987..cfc1ff3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ - 4.0.0-alpha.13 - Fix #22. [T. Bleher, J. Goodman, N. Marti, S. Freilichtbuenhe, M. Malevic, T. Wright] - - Fix package installer and TUI app [T. H. Wright] + - Fix package installer and TUI app. Also fix #135, #136, #140. [T. H. Wright, N. Marti] - Introduce network.py and system.py - 4.0.0-alpha.12 - Fix TUI app's installer [T. H. Wright] diff --git a/utils.py b/utils.py index 24d594b0..4f5393b1 100644 --- a/utils.py +++ b/utils.py @@ -861,4 +861,3 @@ def untar_file(file_path, output_dir): logging.debug(f"Successfully extracted '{file_path}' to '{output_dir}'") except tarfile.TarError as e: logging.error(f"Error extracting '{file_path}': {e}") - From 4110a2fc17a3477a7612301d1b078702629a83b9 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 1 Aug 2024 11:18:36 -0500 Subject: [PATCH 098/253] add missing status message to GUI status_q --- network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/network.py b/network.py index d93e63d9..1218f167 100644 --- a/network.py +++ b/network.py @@ -325,6 +325,7 @@ def verify_downloaded_file(url, file_path, app=None, evt=None): msg.status(f"Verifying {file_path}…", app) if config.DIALOG == "tk": app.root.event_generate('<>') + app.status_q.put(f"Verifying {file_path}…") app.root.event_generate('<>') res = False txt = f"{file_path} is the wrong size." From 4df9706e0f783a2ca47f8873b55bbb5f7cd89154 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 1 Aug 2024 18:24:48 -0500 Subject: [PATCH 099/253] correctly default to recommended binary --- gui_app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui_app.py b/gui_app.py index 607a6e79..3fd566ef 100644 --- a/gui_app.py +++ b/gui_app.py @@ -405,9 +405,10 @@ def start_find_appimage_files(self, release_version): self.appimage_thread.start() def start_wine_versions_check(self, release_version): - if not self.appimages: - self.start_find_appimage_files(release_version) - return + if self.appimages is None: + self.appimages = [] + # self.start_find_appimage_files(release_version) + # return # Setup queue, signal, thread. self.wines_q = Queue() self.wine_evt = "<>" From 3551d91aa118ab8fa46e013687e5121249fa7f2a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 1 Aug 2024 23:35:48 -0400 Subject: [PATCH 100/253] Add log rotation. Fix #132. --- msg.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/msg.py b/msg.py index ca9fabed..7407cee8 100644 --- a/msg.py +++ b/msg.py @@ -1,6 +1,9 @@ +import gzip import logging +from logging.handlers import RotatingFileHandler import os import signal +import shutil import sys import time @@ -11,6 +14,29 @@ logging.console_log = [] +class GzippedRotatingFileHandler(RotatingFileHandler): + def doRollover(self): + super().doRollover() + + if self.backupCount > 0: + for i in range(self.backupCount - 1, 0, -1): + source = f"{self.baseFilename}.{i}.gz" + destination = f"{self.baseFilename}.{i + 1}.gz" + if os.path.exists(source): + if os.path.exists(destination): + os.remove(destination) + os.rename(source, destination) + + last_log = self.baseFilename + ".1" + gz_last_log = self.baseFilename + ".1.gz" + + if os.path.exists(last_log) and os.path.getsize(last_log) > 0: + with open(last_log, 'rb') as f_in: + with gzip.open(gz_last_log, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + os.remove(last_log) + + def get_log_level_name(level): name = None levels = { @@ -45,7 +71,7 @@ def initialize_logging(stderr_log_level): log_parent.mkdir(parents=True) # Define logging handlers. - file_h = logging.FileHandler(config.LOGOS_LOG, encoding='UTF8') + file_h = GzippedRotatingFileHandler(config.LOGOS_LOG, maxBytes=10*1024*1024, backupCount=5, encoding='UTF8') file_h.name = "logfile" file_h.setLevel(logging.DEBUG) # stdout_h = logging.StreamHandler(sys.stdout) From b472f4f85443e5ce64f05565df2cc8133c9a4fed Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 2 Aug 2024 07:08:02 -0500 Subject: [PATCH 101/253] remove JSON debug code --- network.py | 1 - 1 file changed, 1 deletion(-) diff --git a/network.py b/network.py index 1218f167..13460475 100644 --- a/network.py +++ b/network.py @@ -374,7 +374,6 @@ def get_latest_release_data(releases_url): if data: try: json_data = json.loads(data.decode()) - # logging.debug(f"{json_data=}") # omit to make log more readable except json.JSONDecodeError as e: logging.error(f"Error decoding JSON response: {e}") return None From fb222b39ca712b3788b060c5b9f704de6a5e07b4 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 1 Aug 2024 12:20:19 -0500 Subject: [PATCH 102/253] Continued work on fixing deps install - remove sudo for package download - skip wine repo and install on ubuntu-like systems - Revert "remove sudo for package download" - Revert commit c045e5cd3668fc023b89d72599311988c4f60ce9. - install deps in one command in GUI - add status messages to GUI - add GUI progress - pep8 fix --- system.py | 50 ++++++++++++++++++++++++++++++++++---------------- utils.py | 2 +- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/system.py b/system.py index a491dfb4..d33a5efa 100644 --- a/system.py +++ b/system.py @@ -359,23 +359,28 @@ def install_packages(packages, elements, app=None): return if packages: - total_packages = len(packages) - for index, package in enumerate(packages): - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" # noqa: E501 + if config.DIALOG == 'tk': + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {' '.join(packages)}" # noqa: E501 logging.debug(f"install_packages cmd: {command}") result = run_command(command, retries=5, delay=15) - - if elements is not None: - if result and result.returncode == 0: - elements[index] = (package, "Installed") - else: - elements[index] = (package, "Failed") - if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 - app.report_dependencies( - f"Installing Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, - elements, - dialog=config.use_python_dialog) + else: + total_packages = len(packages) + for index, package in enumerate(packages): + command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" # noqa: E501 + logging.debug(f"install_packages cmd: {command}") + result = run_command(command, retries=5, delay=15) + + if elements is not None: + if result and result.returncode == 0: + elements[index] = (package, "Installed") + else: + elements[index] = (package, "Failed") + if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 + app.report_dependencies( + f"Installing Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, + elements, + dialog=config.use_python_dialog) def remove_packages(packages, elements, app=None): @@ -497,7 +502,8 @@ def postinstall_dependencies_steamos(): def preinstall_dependencies(): if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mint": - preinstall_dependencies_ubuntu() + # preinstall_dependencies_ubuntu() + pass elif config.OS_NAME == "Steam": preinstall_dependencies_steamos() @@ -557,6 +563,10 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): # Do we need a TK continue question? I see we have a CLI and curses one # in msg.py + if app and config.DIALOG == 'tk': + app.root.event_generate('<>') + app.status_q.put("Installing pre-install dependencies…") + app.root.event_generate('<>') preinstall_dependencies() # libfuse: for AppImage use. This is the only known needed library. @@ -567,7 +577,15 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): check_libs([f"{fuse}"], app=app) if missing_packages: + if app and config.DIALOG == 'tk': + app.root.event_generate('<>') + app.status_q.put("Downloading packages…") + app.root.event_generate('<>') download_packages(missing_packages, elements, app) + if app and config.DIALOG == 'tk': + app.root.event_generate('<>') + app.status_q.put("Installing packages…") + app.root.event_generate('<>') install_packages(missing_packages, elements, app) if conflicting_packages: diff --git a/utils.py b/utils.py index 4f5393b1..9e65f2a8 100644 --- a/utils.py +++ b/utils.py @@ -226,7 +226,7 @@ def check_dependencies(app=None): app.root.event_generate('<>') if targetversion == 10: - system.install_dependencies(config.PACKAGES, config.BADPACKAGES, app=app) + system.install_dependencies(config.PACKAGES, config.BADPACKAGES, app=app) # noqa: E501 elif targetversion == 9: system.install_dependencies( config.PACKAGES, From a41b2e55d1c842a28ff8ca4b64cda07f7a086a79 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 2 Aug 2024 11:20:23 -0400 Subject: [PATCH 103/253] Add TUI pw dialogs. - Default TUI to sudo/doas - Add password storage and retrieval - Rework deps routine. - Switch to PAM; use input= Fix package installation - This required setting system.run_command calls to use a list instead of a str and `shell=True` - Add lots of logging - Modify msg.status calls to specify `app` - Minor pep8 fixes Logos 10 Indexer: Set to Vista. Fix #154, #156 Disable Logos version filtering since 30+ is working --- config.py | 1 + gui.py | 20 +++ gui_app.py | 1 + installer.py | 19 ++- msg.py | 2 +- network.py | 9 +- system.py | 366 ++++++++++++++++++++++++++++---------------------- tui_app.py | 37 ++++- tui_curses.py | 35 +++++ tui_dialog.py | 95 +++++++++++-- tui_screen.py | 37 +++++ utils.py | 3 +- wine.py | 7 +- 13 files changed, 431 insertions(+), 201 deletions(-) diff --git a/config.py b/config.py index d8eb2662..fe430f3a 100644 --- a/config.py +++ b/config.py @@ -53,6 +53,7 @@ # Set other run-time variables not set in the env. ACTION = 'app' APPIMAGE_FILE_PATH = None +authenticated = False BADPACKAGES = None DEFAULT_CONFIG_PATH = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.json") # noqa: E501 GUI = None diff --git a/gui.py b/gui.py index ea0e2f02..a8d89378 100644 --- a/gui.py +++ b/gui.py @@ -2,6 +2,7 @@ from tkinter import BooleanVar from tkinter import font from tkinter import IntVar +from tkinter import simpledialog from tkinter import StringVar from tkinter.ttk import Button from tkinter.ttk import Checkbutton @@ -332,3 +333,22 @@ def hide_tooltip(self, event=None): if self.tooltip_visible: self.tooltip_window.destroy() self.tooltip_visible = False + + +def input_prompt(root, title, prompt): + # Prompt for the password + input = simpledialog.askstring(title, prompt, show='*', parent=root) + return input + +class PromptGui(Frame): + def __init__(self, root, title="", prompt="", **kwargs): + super(PromptGui, self).__init__(root, **kwargs) + self.options = {"title": title, "prompt": prompt} + if title is not None: + self.options['title'] = title + if prompt is not None: + self.options['prompt'] = prompt + + def draw_prompt(self): + store_button = Button(self.root, text="Store Password", command=lambda: input_prompt(self.root, self.options)) + store_button.pack(pady=20) diff --git a/gui_app.py b/gui_app.py index 3fd566ef..c7a0667e 100644 --- a/gui_app.py +++ b/gui_app.py @@ -693,6 +693,7 @@ def run_installer(self, evt=None): self.root.icon = config.LOGOS_ICON_URL def run_logos(self, evt=None): + #TODO: Add reference to App here so the status message is sent to the GUI? See msg.status and wine.run_logos t = Thread(target=wine.run_logos) t.start() diff --git a/installer.py b/installer.py index 6d8fc2b2..7313f7d9 100644 --- a/installer.py +++ b/installer.py @@ -515,16 +515,15 @@ def ensure_winetricks_applied(app=None): args.insert(0, "-q") wine.winetricks_install(*args) - if config.TARGETVERSION == '9': - msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") - exe_args = [ - 'add', - f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 - "/v", "Version", - "/t", "REG_SZ", - "/d", "vista", "/f", - ] - wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") + exe_args = [ + 'add', + f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 + "/v", "Version", + "/t", "REG_SZ", + "/d", "vista", "/f", + ] + wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) logging.debug("> Done.") diff --git a/msg.py b/msg.py index 7407cee8..db2d6acd 100644 --- a/msg.py +++ b/msg.py @@ -247,10 +247,10 @@ def status(text, app=None): if config.DIALOG == 'tk': app.status_q.put(text) app.root.event_generate('<>') - logging.info(f"{text}") elif config.DIALOG == 'curses': app.status_q.put(f"{timestamp} {text}") app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) + logging.info(f"{text}") else: '''Prints message to stdout regardless of log level.''' logos_msg(text) diff --git a/network.py b/network.py index 13460475..10473088 100644 --- a/network.py +++ b/network.py @@ -532,9 +532,12 @@ def get_logos_releases(app=None): # if len(releases) == 5: # break - filtered_releases = utils.filter_versions(releases, 36, 1) - logging.debug(f"Available releases: {', '.join(releases)}") - logging.debug(f"Filtered releases: {', '.join(filtered_releases)}") + # Disabled filtering: with Logos 30+, all versions are known to be working. + # Keeping code if it needs to be reactivated. + # filtered_releases = utils.filter_versions(releases, 36, 1) + # logging.debug(f"Available releases: {', '.join(releases)}") + # logging.debug(f"Filtered releases: {', '.join(filtered_releases)}") + filtered_releases = releases if app: app.releases_q.put(filtered_releases) diff --git a/system.py b/system.py index d33a5efa..e4059d5b 100644 --- a/system.py +++ b/system.py @@ -1,6 +1,9 @@ +import getpass +import keyring import logging import distro import os +import pam import shlex import shutil import subprocess @@ -10,12 +13,14 @@ from pathlib import Path import config +import gui import msg import network +import utils #TODO: Add a Popen variant to run_command to replace functions in control.py and wine.py -def run_command(command, retries=1, delay=0, **kwargs): +def run_command(command, retries=1, delay=0, verify=False, **kwargs): check = kwargs.get("check", True) text = kwargs.get("text", True) capture_output = kwargs.get("capture_output", True) @@ -23,6 +28,7 @@ def run_command(command, retries=1, delay=0, **kwargs): env = kwargs.get("env", None) cwd = kwargs.get("cwd", None) encoding = kwargs.get("encoding", None) + input = kwargs.get("input", None) stdin = kwargs.get("stdin", None) stdout = kwargs.get("stdout", None) stderr = kwargs.get("stderr", None) @@ -35,13 +41,13 @@ def run_command(command, retries=1, delay=0, **kwargs): for attempt in range(retries): try: - logging.debug(f"Attempting to execute {command}") result = subprocess.run( command, check=check, text=text, shell=shell, capture_output=capture_output, + input=input, stdin=stdin, stdout=stdout, stderr=stderr, @@ -93,6 +99,27 @@ def tl(library): return False +def set_password(app=None): + p = pam.pam() + username = getpass.getuser() + utils.send_task(app, "PASSWORD") + app.password_e.wait() + password = app.password_q.get() + if p.authenticate(username, password): + keyring.set_password("Logos", username, password) + config.authenticated = True + else: + msg.status("Incorrect password. Try again.", app) + logging.debug("Incorrect password. Try again.") + + +def get_password(app=None): + while not config.authenticated: + set_password(app) + msg.status("I have the power!", app) + return keyring.get_password("Logos", getpass.getuser()) + + def get_dialog(): if not os.environ.get('DISPLAY'): msg.logos_error("The installer does not work unless you are running a display") # noqa: E501 @@ -115,59 +142,6 @@ def get_dialog(): def get_os(): - # TODO: Remove if we can verify these are no longer needed commented code. - - # Try reading /etc/os-release - # try: - # with open('/etc/os-release', 'r') as f: - # os_release_content = f.read() - # match = re.search( - # r'^ID=(\S+).*?VERSION_ID=(\S+)', - # os_release_content, re.MULTILINE - # ) - # if match: - # config.OS_NAME = match.group(1) - # config.OS_RELEASE = match.group(2) - # return config.OS_NAME, config.OS_RELEASE - # except FileNotFoundError: - # pass - - # Try using lsb_release command - # try: - # config.OS_NAME = platform.linux_distribution()[0] - # config.OS_RELEASE = platform.linux_distribution()[1] - # return config.OS_NAME, config.OS_RELEASE - # except AttributeError: - # pass - - # Try reading /etc/lsb-release - # try: - # with open('/etc/lsb-release', 'r') as f: - # lsb_release_content = f.read() - # match = re.search( - # r'^DISTRIB_ID=(\S+).*?DISTRIB_RELEASE=(\S+)', - # lsb_release_content, - # re.MULTILINE - # ) - # if match: - # config.OS_NAME = match.group(1) - # config.OS_RELEASE = match.group(2) - # return config.OS_NAME, config.OS_RELEASE - # except FileNotFoundError: - # pass - - # Try reading /etc/debian_version - # try: - # with open('/etc/debian_version', 'r') as f: - # config.OS_NAME = 'Debian' - # config.OS_RELEASE = f.read().strip() - # return config.OS_NAME, config.OS_RELEASE - # except FileNotFoundError: - # pass - - # Add more conditions for other distributions as needed - - # Fallback to platform module config.OS_NAME = distro.id() # FIXME: Not working. Returns "Linux". logging.info(f"OS name: {config.OS_NAME}") config.OS_RELEASE = distro.version() @@ -182,12 +156,12 @@ def get_superuser_command(): else: msg.logos_error("No superuser command found. Please install pkexec.") # noqa: E501 else: - if shutil.which('pkexec'): - config.SUPERUSER_COMMAND = "pkexec" - elif shutil.which('sudo'): + if shutil.which('sudo'): config.SUPERUSER_COMMAND = "sudo" elif shutil.which('doas'): config.SUPERUSER_COMMAND = "doas" + elif shutil.which('pkexec'): + config.SUPERUSER_COMMAND = "pkexec" else: msg.logos_error("No superuser command found. Please install sudo or doas.") # noqa: E501 logging.debug(f"{config.SUPERUSER_COMMAND=}") @@ -196,37 +170,37 @@ def get_superuser_command(): def get_package_manager(): # Check for package manager and associated packages if shutil.which('apt') is not None: # debian, ubuntu - config.PACKAGE_MANAGER_COMMAND_INSTALL = "apt install -y" - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "apt install --download-only -y" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = "apt remove -y" - config.PACKAGE_MANAGER_COMMAND_QUERY = "dpkg -l" + config.PACKAGE_MANAGER_COMMAND_INSTALL = ["apt", "install", "-y"] + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["apt", "install", "--download-only", "-y"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_REMOVE = ["apt", "remove", "-y"] + config.PACKAGE_MANAGER_COMMAND_QUERY = ["dpkg", "-l"] config.QUERY_PREFIX = '.i ' config.PACKAGES = "binutils cabextract fuse3 wget winbind" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('dnf') is not None: # rhel, fedora - config.PACKAGE_MANAGER_COMMAND_INSTALL = "dnf install -y" - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "dnf install --downloadonly -y" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = "dnf remove -y" - config.PACKAGE_MANAGER_COMMAND_QUERY = "dnf list installed" + config.PACKAGE_MANAGER_COMMAND_INSTALL = ["dnf", "install", "-y"] + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["dnf", "install", "--downloadonly", "-y"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_REMOVE = ["dnf", "remove", "-y"] + config.PACKAGE_MANAGER_COMMAND_QUERY = ["dnf", "list", "installed"] config.QUERY_PREFIX = '' config.PACKAGES = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appiamgelauncher" elif shutil.which('pamac') is not None: # manjaro - config.PACKAGE_MANAGER_COMMAND_INSTALL = "pamac install --no-upgrade --no-confirm" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pamac install --download-only --no-confirm" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = "pamac remove --no-confirm" - config.PACKAGE_MANAGER_COMMAND_QUERY = "pamac list -i" + config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pamac", "install", "--no-upgrade", "--no-confirm"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pamac", "install", "--no-upgrade", "--download-only", "--no-confirm"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pamac", "remove", "--no-confirm"] + config.PACKAGE_MANAGER_COMMAND_QUERY = ["pamac", "list", "-i"] config.QUERY_PREFIX = '' config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "appimagelauncher" elif shutil.which('pacman') is not None: # arch, steamOS - config.PACKAGE_MANAGER_COMMAND_INSTALL = r"pacman -Syu --overwrite * --noconfirm --needed" # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = "pacman -Sw -y" - config.PACKAGE_MANAGER_COMMAND_REMOVE = r"pacman -R --no-confirm" - config.PACKAGE_MANAGER_COMMAND_QUERY = "pacman -Q" + config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pacman", "-Syu", "--overwrite", r"*", "--noconfirm", "--needed"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pacman", "-Sw", "-y"] + config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pacman", "-R", "--no-confirm"] + config.PACKAGE_MANAGER_COMMAND_QUERY = ["pacman", "-Q"] config.QUERY_PREFIX = '' if config.OS_NAME == "steamos": # steamOS config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: #E501 @@ -261,14 +235,13 @@ def query_packages(packages, elements=None, mode="install", app=None): command = config.PACKAGE_MANAGER_COMMAND_QUERY try: - result = run_command(command, shell=True) + result = run_command(command) except Exception as e: logging.error(f"Error occurred while executing command: {e}") logging.error(result.stderr) package_list = result.stdout - logging.debug(f"Checking packages: {packages} in package list.") if app is not None: if elements is None: elements = {} # Initialize elements if not provided @@ -280,7 +253,7 @@ def query_packages(packages, elements=None, mode="install", app=None): l_num = 0 for line in package_list.split('\n'): l_num += 1 - if config.PACKAGE_MANAGER_COMMAND_QUERY.startswith('dpkg'): + if config.PACKAGE_MANAGER_COMMAND_QUERY[0] == 'dpkg': parts = line.strip().split() if l_num < 6 or len(parts) < 2: # skip header, etc. continue @@ -308,42 +281,50 @@ def query_packages(packages, elements=None, mode="install", app=None): elif mode == "remove": status = "Not Installed" - logging.debug(f"Setting {p}: {status}") elements[p] = status if app is not None and config.DIALOG == "curses": app.report_dependencies( - f"Checking Packages {(packages.index(p) + 1)}/{len(packages)}", # noqa: E501 + f"Checking Package: {(packages.index(p) + 1)}/{len(packages)}", # noqa: E501 100 * (packages.index(p) + 1) // len(packages), elements, dialog=config.use_python_dialog) + logging.debug(f"Setting Status of {p}: {status}") - txt = 'None' if mode == "install": if missing_packages: txt = f"Missing packages: {' '.join(missing_packages)}" - logging.info(f"Missing packages: {txt}") + logging.info(f"{txt}") return missing_packages, elements elif mode == "remove": if conflicting_packages: txt = f"Conflicting packages: {' '.join(conflicting_packages)}" - logging.info(f"Conflicting packages: {txt}") + logging.info(f"Conflicting packages: {txt}") return conflicting_packages, elements def download_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return + #TODO: As can be seen in this commit, there is good potential for reusing this code block in install_dependencies(). + password = get_password(app) + if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": + superuser_stdin = ["sudo", "-S"] + else: + superuser_stdin = ["doas", "-n"] if packages: + msg.status(f"Downloading Missing Packages: {packages}", app) total_packages = len(packages) - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_DOWNLOAD} {' '.join(packages)}" # noqa: E501 - logging.debug(f"download_packages cmd: {command}") - command_args = shlex.split(command) - result = run_command(command_args, retries=5, delay=15) - for index, package in enumerate(packages): - status = "Downloaded" if result.returncode == 0 else "Failed" + logging.debug(f"Downloading package: {package}") + command = superuser_stdin + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD + [package] # noqa: E501 + result = run_command(command, input=password, retries=5, delay=15, verify=True) + + if not isinstance(result, bool) and result.returncode == 0: + status = "Downloaded" + else: + status = "Failed" if elements is not None: elements[index] = (package, status) @@ -353,46 +334,57 @@ def download_packages(packages, elements, app=None): 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog # noqa: E501 ) + app.password_e.clear() + def install_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return + password = get_password(app) + if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": + superuser_stdin = ["sudo", "-S"] + else: + superuser_stdin = ["doas", "-n"] if packages: - if config.DIALOG == 'tk': - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {' '.join(packages)}" # noqa: E501 - logging.debug(f"install_packages cmd: {command}") - result = run_command(command, retries=5, delay=15) - else: - total_packages = len(packages) - for index, package in enumerate(packages): - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_INSTALL} {package}" # noqa: E501 - logging.debug(f"install_packages cmd: {command}") - result = run_command(command, retries=5, delay=15) - - if elements is not None: - if result and result.returncode == 0: - elements[index] = (package, "Installed") - else: - elements[index] = (package, "Failed") - if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 - app.report_dependencies( - f"Installing Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, - elements, - dialog=config.use_python_dialog) + msg.status(f"Installing Missing Packages: {packages}", app) + total_packages = len(packages) + for index, package in enumerate(packages): + logging.debug(f"Installing package: {package}") + command = superuser_stdin + config.PACKAGE_MANAGER_COMMAND_INSTALL + [package] # noqa: E501 + result = run_command(command, input=password, retries=5, delay=15, verify=True) + + if elements is not None: + if result and result.returncode == 0: + elements[index] = (package, "Installed") + else: + elements[index] = (package, "Failed") + if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 + app.report_dependencies( + f"Installing Packages ({index + 1}/{total_packages})", + 100 * (index + 1) // total_packages, + elements, + dialog=config.use_python_dialog) + + app.password_e.clear() def remove_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return + password = get_password(app) + if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": + superuser_stdin = ["sudo", "-S"] + else: + superuser_stdin = ["doas", "-n"] if packages: + msg.status(f"Removing Conflicting Packages: {packages}", app) total_packages = len(packages) for index, package in enumerate(packages): - command = f"{config.SUPERUSER_COMMAND} {config.PACKAGE_MANAGER_COMMAND_REMOVE} {package}" # noqa: E501 - logging.debug(f"remove_packages cmd: {command}") - result = run_command(command, retries=5, delay=15) + logging.debug(f"Removing package: {package}") + command = superuser_stdin + config.PACKAGE_MANAGER_COMMAND_REMOVE + [package] # noqa: E501 + result = run_command(command, input=password, retries=5, delay=15, verify=True) if elements is not None: if result and result.returncode == 0: @@ -401,11 +393,13 @@ def remove_packages(packages, elements, app=None): elements[index] = (package, "Failed") if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 app.report_dependencies( - f"Removing Packages ({index + 1}/{total_packages})", + f"Removing Conflicting Packages ({index + 1}/{total_packages})", 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog) + app.password_e.clear() + def have_dep(cmd): if shutil.which(cmd) is not None: @@ -450,67 +444,110 @@ def parse_date(version): return None -def preinstall_dependencies_ubuntu(): +def preinstall_dependencies_ubuntu(app=None): + password = get_password(app) + if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": + superuser_stdin = ["sudo", "-S"] + else: + superuser_stdin = ["doas", "-n"] try: - run_command([config.SUPERUSER_COMMAND, "dpkg", "--add-architecture", "i386"]) # noqa: E501 - run_command([config.SUPERUSER_COMMAND, "mkdir", "-pm755", "/etc/apt/keyrings"]) # noqa: E501 + logging.debug("Adding wine staging repositories…") + run_command(superuser_stdin + ["dpkg", "--add-architecture", "i386"], input=password, verify=True) # noqa: E501 + run_command(superuser_stdin + ["mkdir", "-pm755", "/etc/apt/keyrings"], input=password, verify=True) # noqa: E501 url = "https://dl.winehq.org/wine-builds/winehq.key" - run_command([config.SUPERUSER_COMMAND, "wget", "-O", "/etc/apt/keyrings/winehq-archive.key", url]) # noqa: E501 + run_command(superuser_stdin + ["wget", "-O", "/etc/apt/keyrings/winehq-archive.key", url], input=password, + verify=True) # noqa: E501 lsb_release_output = run_command(["lsb_release", "-a"]) - codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[1].strip() # noqa: E501 + codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[ + 1].strip() # noqa: E501 url = f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources" # noqa: E501 - run_command([config.SUPERUSER_COMMAND, "wget", "-NP", "/etc/apt/sources.list.d/", url]) # noqa: E501 - run_command([config.SUPERUSER_COMMAND, "apt", "update"]) - run_command([config.SUPERUSER_COMMAND, "apt", "install", "--install-recommends", "winehq-staging"]) # noqa: E501 + run_command(superuser_stdin + ["wget", "-NP", "/etc/apt/sources.list.d/", url], input=password, verify=True) # noqa: E501 + run_command(superuser_stdin + ["apt", "update"], input=password, verify=True) + run_command(superuser_stdin + ["apt", "install", "--install-recommends", "winehq-staging"], input=password, + verify=True) # noqa: E501 except subprocess.CalledProcessError as e: - print(f"An error occurred: {e}") - print(f"Command output: {e.output}") + logging.error(f"An error occurred: {e}") + logging.error(f"Command output: {e.output}") + app.password_e.clear() -def preinstall_dependencies_steamos(): - command = [config.SUPERUSER_COMMAND, "steamos-readonly", "disable"] - run_command(command) - command = [config.SUPERUSER_COMMAND, "pacman-key", "--init"] - run_command(command) - command = [config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux"] # noqa: E501 - run_command(command) +def preinstall_dependencies_steamos(app=None): + password = get_password(app) + if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": + superuser_stdin = ["sudo", "-S"] + else: + superuser_stdin = ["doas", "-n"] + try: + logging.debug("Disabling read only…") + command = superuser_stdin + ["steamos-readonly", "disable"] + run_command(command, input=password, verify=True) + logging.debug("Updating pacman keys…") + command = superuser_stdin + ["pacman-key", "--init"] + run_command(command, input=password, verify=True) + command = superuser_stdin + ["pacman-key", "--populate", "archlinux"] # noqa: E501 + run_command(command, input=password, verify=True) + except subprocess.CalledProcessError as e: + logging.error(f"An error occurred: {e}") + logging.error(f"Command output: {e.output}") + app.password_e.clear() -def postinstall_dependencies_steamos(): - command = [ +def postinstall_dependencies_steamos(app=None): + password = get_password(app) + if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": + superuser_stdin = ["sudo", "-S"] + else: + superuser_stdin = ["doas", "-n"] + try: + logging.debug("Updating DNS settings…") + command = superuser_stdin + [ config.SUPERUSER_COMMAND, "sed", '-i', 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 '/etc/nsswitch.conf' ] - run_command(command) - command = [config.SUPERUSER_COMMAND, "locale-gen"] - run_command(command) - command = [ - config.SUPERUSER_COMMAND, + run_command(command, input=password, verify=True) + logging.debug("Updating locales…") + command = [config.SUPERUSER_COMMAND, "locale-gen"] + run_command(command, input=password, verify=True) + logging.debug("Enabling avahi…") + command = superuser_stdin + [ "systemctl", "enable", "--now", "avahi-daemon" ] - run_command(command) - command = [config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups"] # noqa: E501 - run_command(command) - command = [config.SUPERUSER_COMMAND, "steamos-readonly", "enable"] - run_command(command) + run_command(command, input=password, verify=True) + logging.debug("Enabling cups…") + command = superuser_stdin + ["systemctl", "enable", "--now", "cups"] # noqa: E501 + run_command(command, input=password, verify=True) + logging.debug("Enabling read only…") + command = superuser_stdin + ["steamos-readonly", "enable"] + run_command(command, input=password, verify=True) + except subprocess.CalledProcessError as e: + logging.error(f"An error occurred: {e}") + logging.error(f"Command output: {e.output}") + app.password_e.clear() -def preinstall_dependencies(): +def preinstall_dependencies(app=None): + logging.debug(f"Performing pre-install dependencies…") + #TODO: Use a dict for distro derivatives so that we can add all Ubuntu derivatives here or in similar other places if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mint": # preinstall_dependencies_ubuntu() pass elif config.OS_NAME == "Steam": - preinstall_dependencies_steamos() + preinstall_dependencies_steamos(app) + else: + logging.debug(f"No pre-install dependencies required.") -def postinstall_dependencies(): +def postinstall_dependencies(app=None): + logging.debug(f"Performing post-install dependencies…") if config.OS_NAME == "Steam": - postinstall_dependencies_steamos() + postinstall_dependencies_steamos(app) + else: + logging.debug(f"No post-install dependencies required.") def install_dependencies(packages, badpackages, logos9_packages=None, app=None): # noqa: E501 @@ -541,9 +578,10 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) # noqa: E501 if config.PACKAGE_MANAGER_COMMAND_QUERY: - logging.debug("Querying packages...") + logging.debug("Querying packages…") missing_packages, elements = query_packages(package_list, elements, app=app) # noqa: E501 - conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", app=app) # noqa: E501 + conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", + app=app) # noqa: E501 if config.PACKAGE_MANAGER_COMMAND_INSTALL: if missing_packages and conflicting_packages: @@ -565,9 +603,8 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if app and config.DIALOG == 'tk': app.root.event_generate('<>') - app.status_q.put("Installing pre-install dependencies…") - app.root.event_generate('<>') - preinstall_dependencies() + msg.status("Installing pre-install dependencies…", app) + preinstall_dependencies(app) # libfuse: for AppImage use. This is the only known needed library. if config.OS_NAME == "fedora": @@ -579,29 +616,33 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if missing_packages: if app and config.DIALOG == 'tk': app.root.event_generate('<>') - app.status_q.put("Downloading packages…") - app.root.event_generate('<>') + msg.status("Downloading packages…", app) download_packages(missing_packages, elements, app) if app and config.DIALOG == 'tk': app.root.event_generate('<>') - app.status_q.put("Installing packages…") - app.root.event_generate('<>') + msg.status("Installing packages…", app) install_packages(missing_packages, elements, app) + else: + logging.debug("No missing packages detected.") if conflicting_packages: # AppImage Launcher is the only known conflicting package. remove_packages(conflicting_packages, bad_elements, app) # config.REBOOT_REQUIRED = True # TODO: Verify with user before executing + else: + logging.debug("No conflicting packages detected.") - postinstall_dependencies() + postinstall_dependencies(app) if config.REBOOT_REQUIRED: reboot() else: msg.logos_error( - f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the command(s) {missing_packages}. Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 + f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. " + f"Your computer is missing the command(s) {missing_packages}. " + f"Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 def have_lib(library, ld_library_path): @@ -633,7 +674,8 @@ def check_libs(libraries, app=None): elements[p] = "Unchecked" if config.DIALOG == "curses" and app is not None: - app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) # noqa: E501 + app.report_dependencies("Checking Packages", 0, elements, + dialog=config.use_python_dialog) # noqa: E501 install_packages(config.PACKAGES, elements, app=app) else: @@ -642,9 +684,9 @@ def check_libs(libraries, app=None): def install_winetricks( - installdir, - app=None, - version=config.WINETRICKS_VERSION, + installdir, + app=None, + version=config.WINETRICKS_VERSION, ): msg.logos_msg(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 diff --git a/tui_app.py b/tui_app.py index cf497521..466be970 100644 --- a/tui_app.py +++ b/tui_app.py @@ -72,6 +72,8 @@ def __init__(self, stdscr): self.finished_e = threading.Event() self.config_q = Queue() self.config_e = threading.Event() + self.password_q = Queue() + self.password_e = threading.Event() self.appimage_q = Queue() self.appimage_e = threading.Event() @@ -263,6 +265,8 @@ def task_processor(self, evt=None, task=None): utils.start_thread(self.get_winetricksbin(config.use_python_dialog)) elif task == 'INSTALLING': utils.start_thread(self.get_waiting(config.use_python_dialog)) + elif task == 'INSTALLING_PW': + utils.start_thread(self.get_waiting(config.use_python_dialog, screen_id=15)) elif task == 'CONFIG': utils.start_thread(self.get_config(config.use_python_dialog)) elif task == 'DONE': @@ -271,6 +275,8 @@ def task_processor(self, evt=None, task=None): self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) + elif task == 'PASSWORD': + utils.start_thread(self.get_password(config.use_python_dialog)) def choice_processor(self, stdscr, screen_id, choice): if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): @@ -400,14 +406,18 @@ def choice_processor(self, stdscr, screen_id, choice): pass elif screen_id == 11: pass - if screen_id == 12: + elif screen_id == 12: if choice: - wine.run_logos() + wine.run_logos(self) self.switch_q.put(1) - if screen_id == 13: + elif screen_id == 13: pass - if screen_id == 14: + elif screen_id == 14: pass + elif screen_id == 15: + if choice: + self.password_q.put(choice) + self.password_e.set() def switch_screen(self, dialog): if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: @@ -482,12 +492,12 @@ def get_winetricksbin(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog)) - def get_waiting(self, dialog): + def get_waiting(self, dialog, screen_id=8): self.tricksbin_e.wait() text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) - self.screen_q.put(self.stack_text(8, self.status_q, self.status_e, processed_text, wait=True, percent=percent, + self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, wait=True, percent=percent, dialog=dialog)) def get_config(self, dialog): @@ -497,6 +507,11 @@ def get_config(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog)) + def get_password(self, dialog): + question = (f"Logos Linux Installer needs to run a command as root. " + f"Please provide your password to provide escalation privileges.") + self.screen_q.put(self.stack_password(15, self.password_q, self.password_e, question, dialog=dialog)) + def report_waiting(self, text, dialog): #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) logging.console_log.append(text) @@ -508,7 +523,7 @@ def report_dependencies(self, text, percent, elements, dialog): # Without this delay, the reporting works too quickly and instead appears all at once. time.sleep(0.1) else: - #TODO + msg.status(f"{text}", self) pass def which_dialog_options(self, labels, dialog=False): @@ -585,6 +600,14 @@ def stack_input(self, screen_id, queue, event, question, default, dialog=False): utils.append_unique(self.tui_screens, tui_screen.InputScreen(self, screen_id, queue, event, question, default)) + def stack_password(self, screen_id, queue, event, question, default="", dialog=False): + if dialog: + utils.append_unique(self.tui_screens, + tui_screen.PasswordDialog(self, screen_id, queue, event, question, default)) + else: + utils.append_unique(self.tui_screens, + tui_screen.PasswordScreen(self, screen_id, queue, event, question, default)) + def stack_confirm(self, screen_id, queue, event, question, options, dialog=False): if dialog: utils.append_unique(self.tui_screens, diff --git a/tui_curses.py b/tui_curses.py index c2f78760..78ffe239 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -137,6 +137,41 @@ def run(self): return self.user_input +class PasswordDialog(UserInputDialog): + def __init__(self, app, question_text, default_text): + super().__init__(app, question_text, default_text) + + self.obfuscation = "" + + def run(self): + if not self.submit: + self.draw() + return "Processing" + else: + if self.user_input is None or self.user_input == "": + self.user_input = self.default_text + return self.user_input + + def input(self): + self.stdscr.addstr(self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation) + key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.obfuscation)) + + try: + if key == -1: # If key not found, keep processing. + pass + elif key == ord('\n'): # Enter key + self.submit = True + elif key == curses.KEY_BACKSPACE or key == 127: + if len(self.user_input) > 0: + self.user_input = self.user_input[:-1] + self.obfuscation = '*' * len(self.user_input[:-1]) + else: + self.user_input += chr(key) + self.obfuscation = '*' * (len(self.obfuscation) + 1) + except KeyboardInterrupt: + signal.signal(signal.SIGINT, self.app.end) + + class MenuDialog(CursesDialog): def __init__(self, app, question_text, options): super().__init__(app) diff --git a/tui_dialog.py b/tui_dialog.py index 7227842e..8955d0f9 100644 --- a/tui_dialog.py +++ b/tui_dialog.py @@ -69,19 +69,67 @@ def tasklist_progress_bar(app, text, percent, elements, height=None, width=None, raise -def confirm(app, question_text, height=None, width=None): +def input(app, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): dialog = Dialog() - check = dialog.yesno(question_text, height, width) + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle + code, input = dialog.inputbox(question_text, init=init, **options) + return code, input + + +def password(app, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): + dialog = Dialog() + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle + code, password = dialog.passwordbox(question_text, init=init, insecure=True, **options) + return code, password + + +def confirm(app, question_text, height=None, width=None, title=None, backtitle=None, colors=True): + dialog = Dialog() + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle + check = dialog.yesno(question_text, **options) return check -def directory_picker(app, path_dir): +def directory_picker(app, path_dir, height=None, width=None, title=None, backtitle=None, colors=True): str_dir = str(path_dir) try: dialog = Dialog() + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle curses.curs_set(1) - _, path = dialog.dselect(str_dir) + _, path = dialog.dselect(str_dir, **options) curses.curs_set(0) except Exception as e: logging.error("An error occurred:", e) @@ -90,12 +138,17 @@ def directory_picker(app, path_dir): return path -def menu(app, question_text, options, height=None, width=None, menu_height=8): - tag_to_description = {tag: description for tag, description in options} +def menu(app, question_text, choices, height=None, width=None, menu_height=8, title=None, backtitle=None, colors=True): + tag_to_description = {tag: description for tag, description in choices} dialog = Dialog(dialog="dialog") + options = {'colors': colors} + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle - menu_options = [(tag, description) for i, (tag, description) in enumerate(options)] - code, tag = dialog.menu(question_text, height, width, menu_height, choices=menu_options) + menu_options = [(tag, description) for i, (tag, description) in enumerate(choices)] + code, tag = dialog.menu(question_text, height, width, menu_height, menu_options, **options) selected_description = tag_to_description.get(tag) if code == dialog.OK: @@ -104,11 +157,20 @@ def menu(app, question_text, options, height=None, width=None, menu_height=8): return None, None, "Return to Main Menu" -def buildlist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=None): +def buildlist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # items is an interable of (tag, item, status) dialog = Dialog(dialog="dialog") + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle - code, tags = dialog.buildlist(text, height, width, list_height, items, title, backtitle, colors) + code, tags = dialog.buildlist(text, list_height=list_height, items=items, **options) if code == dialog.OK: return code, tags @@ -116,11 +178,20 @@ def buildlist(app, text, items=[], height=None, width=None, list_height=None, ti return None -def checklist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=None): +def checklist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # items is an iterable of (tag, item, status) dialog = Dialog(dialog="dialog") + options = {'colors': colors} + if height is not None: + options['height'] = height + if width is not None: + options['width'] = width + if title is not None: + options['title'] = title + if backtitle is not None: + options['backtitle'] = backtitle - code, tags = dialog.checklist(text, items, height, width, list_height, title, backtitle, colors) + code, tags = dialog.checklist(text, choices=items, list_height=list_height, **options) if code == dialog.OK: return code, tags diff --git a/tui_screen.py b/tui_screen.py index c150b85f..b3c70dd2 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -8,6 +8,7 @@ import installer import system import tui_curses +import utils if system.have_dep("dialog"): import tui_dialog @@ -159,6 +160,28 @@ def get_default(self): return self.default +class PasswordScreen(InputScreen): + def __init__(self, app, screen_id, queue, event, question, default): + super().__init__(app, screen_id, queue, event, question, default) + self.dialog = tui_curses.PasswordDialog( + self.app, + self.question, + self.default + ) + + def __str__(self): + return f"Curses Password Screen" + + def display(self): + self.stdscr.erase() + self.choice = self.dialog.run() + if not self.choice == "Processing": + self.submit_choice_to_queue() + utils.send_task(self.app, "INSTALLING_PW") + self.stdscr.noutrefresh() + curses.doupdate() + + class TextScreen(CursesScreen): def __init__(self, app, screen_id, queue, event, text, wait): super().__init__(app, screen_id, queue, event) @@ -235,6 +258,20 @@ def get_default(self): return self.default +class PasswordDialog(InputDialog): + def __init__(self, app, screen_id, queue, event, question, default): + super().__init__(app, screen_id, queue, event, question, default) + + def __str__(self): + return f"PyDialog Password Screen" + + def display(self): + if self.running == 0: + self.running = 1 + _, self.choice = tui_dialog.password(self.app, self.question, init=self.default) + self.submit_choice_to_queue() + utils.send_task(self.app, "INSTALLING_PW") + class ConfirmDialog(DialogScreen): def __init__(self, app, screen_id, queue, event, question, yes_label="Yes", no_label="No"): super().__init__(app, screen_id, queue, event) diff --git a/utils.py b/utils.py index 9e65f2a8..c1041853 100644 --- a/utils.py +++ b/utils.py @@ -219,9 +219,8 @@ def check_dependencies(app=None): targetversion = int(config.TARGETVERSION) else: targetversion = 10 - logging.info(f"Checking Logos {str(targetversion)} dependencies…") + msg.status(f"Checking Logos {str(targetversion)} dependencies…", app) if app: - app.status_q.put(f"Checking Logos {str(targetversion)} dependencies…") if config.DIALOG == "tk": app.root.event_generate('<>') diff --git a/wine.py b/wine.py index 440c33ad..9fab2a7e 100644 --- a/wine.py +++ b/wine.py @@ -482,20 +482,19 @@ def get_wine_env(): return wine_env -def run_logos(): +def run_logos(app=None): logos_release = utils.convert_logos_release(config.current_logos_version) wine_release, _ = get_wine_release(config.WINE_EXE) - logging.debug(f"DEV: {wine_release}") #TODO: Find a way to incorporate check_wine_version_and_branch() if 30 > logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): txt = "Can't run Logos 10+ with Wine below 7.18." logging.critical(txt) - msg.status(txt) + msg.status(txt, app) if logos_release[0] > 29 and wine_release[0] < 9 and wine_release[1] < 10: txt = "Can't run Logos 30+ with Wine below 9.10." logging.critical(txt) - msg.status(txt) + msg.status(txt, app) else: run_wine_proc(config.WINE_EXE, exe=config.LOGOS_EXE) run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) From 4a543ebd590431ec1bae85d3e6b8f6f917eba749 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 7 Aug 2024 07:31:05 -0500 Subject: [PATCH 104/253] update build requirements --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index ba3bd95e..7f098401 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,10 @@ certifi==2023.11.17 charset-normalizer==3.3.2 distro==1.9.0 idna==3.6 +keyring==25.3.0 packaging==23.2 +pam==0.2.0 +six==1.16.0 psutil==5.9.7 pythondialog==3.5.3 requests==2.31.0 From cfdb2be3c06ee8f7ca135bb8455fbeac85305806 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 7 Aug 2024 13:17:24 -0400 Subject: [PATCH 105/253] Add config.superuser_stdnin_command --- config.py | 1 + system.py | 57 ++++++++++++++++++------------------------------------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/config.py b/config.py index fe430f3a..52b637e8 100644 --- a/config.py +++ b/config.py @@ -97,6 +97,7 @@ RECOMMENDED_WINE64_APPIMAGE_VERSION = None RECOMMENDED_WINE64_APPIMAGE_BRANCH = None SUPERUSER_COMMAND = None +superuser_stdnin_command = None VERBUM_PATH = None WINETRICKS_URL = "https://raw.githubusercontent.com/Winetricks/winetricks/5904ee355e37dff4a3ab37e1573c56cffe6ce223/src/winetricks" # noqa: E501 WINETRICKS_VERSION = '20220411' diff --git a/system.py b/system.py index e4059d5b..97bb1a75 100644 --- a/system.py +++ b/system.py @@ -158,10 +158,13 @@ def get_superuser_command(): else: if shutil.which('sudo'): config.SUPERUSER_COMMAND = "sudo" + config.superuser_stdnin_command = ["sudo", "-S"] elif shutil.which('doas'): config.SUPERUSER_COMMAND = "doas" + config.superuser_stdnin_command = ["doas", "-n"] elif shutil.which('pkexec'): config.SUPERUSER_COMMAND = "pkexec" + config.superuser_stdnin_command = ["sudo", "-S"] else: msg.logos_error("No superuser command found. Please install sudo or doas.") # noqa: E501 logging.debug(f"{config.SUPERUSER_COMMAND=}") @@ -308,17 +311,13 @@ def download_packages(packages, elements, app=None): return #TODO: As can be seen in this commit, there is good potential for reusing this code block in install_dependencies(). password = get_password(app) - if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": - superuser_stdin = ["sudo", "-S"] - else: - superuser_stdin = ["doas", "-n"] if packages: msg.status(f"Downloading Missing Packages: {packages}", app) total_packages = len(packages) for index, package in enumerate(packages): logging.debug(f"Downloading package: {package}") - command = superuser_stdin + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD + [package] # noqa: E501 + command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD + [package] # noqa: E501 result = run_command(command, input=password, retries=5, delay=15, verify=True) if not isinstance(result, bool) and result.returncode == 0: @@ -341,17 +340,13 @@ def install_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return password = get_password(app) - if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": - superuser_stdin = ["sudo", "-S"] - else: - superuser_stdin = ["doas", "-n"] if packages: msg.status(f"Installing Missing Packages: {packages}", app) total_packages = len(packages) for index, package in enumerate(packages): logging.debug(f"Installing package: {package}") - command = superuser_stdin + config.PACKAGE_MANAGER_COMMAND_INSTALL + [package] # noqa: E501 + command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_INSTALL + [package] # noqa: E501 result = run_command(command, input=password, retries=5, delay=15, verify=True) if elements is not None: @@ -373,17 +368,13 @@ def remove_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return password = get_password(app) - if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": - superuser_stdin = ["sudo", "-S"] - else: - superuser_stdin = ["doas", "-n"] if packages: msg.status(f"Removing Conflicting Packages: {packages}", app) total_packages = len(packages) for index, package in enumerate(packages): logging.debug(f"Removing package: {package}") - command = superuser_stdin + config.PACKAGE_MANAGER_COMMAND_REMOVE + [package] # noqa: E501 + command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_REMOVE + [package] # noqa: E501 result = run_command(command, input=password, retries=5, delay=15, verify=True) if elements is not None: @@ -446,14 +437,10 @@ def parse_date(version): def preinstall_dependencies_ubuntu(app=None): password = get_password(app) - if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": - superuser_stdin = ["sudo", "-S"] - else: - superuser_stdin = ["doas", "-n"] try: logging.debug("Adding wine staging repositories…") - run_command(superuser_stdin + ["dpkg", "--add-architecture", "i386"], input=password, verify=True) # noqa: E501 - run_command(superuser_stdin + ["mkdir", "-pm755", "/etc/apt/keyrings"], input=password, verify=True) # noqa: E501 + run_command(config.superuser_stdnin_command + ["dpkg", "--add-architecture", "i386"], input=password, verify=True) # noqa: E501 + run_command(config.superuser_stdnin_command + ["mkdir", "-pm755", "/etc/apt/keyrings"], input=password, verify=True) # noqa: E501 url = "https://dl.winehq.org/wine-builds/winehq.key" run_command(superuser_stdin + ["wget", "-O", "/etc/apt/keyrings/winehq-archive.key", url], input=password, verify=True) # noqa: E501 @@ -461,9 +448,9 @@ def preinstall_dependencies_ubuntu(app=None): codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[ 1].strip() # noqa: E501 url = f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources" # noqa: E501 - run_command(superuser_stdin + ["wget", "-NP", "/etc/apt/sources.list.d/", url], input=password, verify=True) # noqa: E501 - run_command(superuser_stdin + ["apt", "update"], input=password, verify=True) - run_command(superuser_stdin + ["apt", "install", "--install-recommends", "winehq-staging"], input=password, + run_command(config.superuser_stdnin_command + ["wget", "-NP", "/etc/apt/sources.list.d/", url], input=password, verify=True) # noqa: E501 + run_command(config.superuser_stdnin_command + ["apt", "update"], input=password, verify=True) + run_command(config.superuser_stdnin_command + ["apt", "install", "--install-recommends", "winehq-staging"], input=password, verify=True) # noqa: E501 except subprocess.CalledProcessError as e: logging.error(f"An error occurred: {e}") @@ -473,18 +460,14 @@ def preinstall_dependencies_ubuntu(app=None): def preinstall_dependencies_steamos(app=None): password = get_password(app) - if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": - superuser_stdin = ["sudo", "-S"] - else: - superuser_stdin = ["doas", "-n"] try: logging.debug("Disabling read only…") - command = superuser_stdin + ["steamos-readonly", "disable"] + command = config.superuser_stdnin_command + ["steamos-readonly", "disable"] run_command(command, input=password, verify=True) logging.debug("Updating pacman keys…") - command = superuser_stdin + ["pacman-key", "--init"] + command = config.superuser_stdnin_command + ["pacman-key", "--init"] run_command(command, input=password, verify=True) - command = superuser_stdin + ["pacman-key", "--populate", "archlinux"] # noqa: E501 + command = config.superuser_stdnin_command + ["pacman-key", "--populate", "archlinux"] # noqa: E501 run_command(command, input=password, verify=True) except subprocess.CalledProcessError as e: logging.error(f"An error occurred: {e}") @@ -494,13 +477,9 @@ def preinstall_dependencies_steamos(app=None): def postinstall_dependencies_steamos(app=None): password = get_password(app) - if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": - superuser_stdin = ["sudo", "-S"] - else: - superuser_stdin = ["doas", "-n"] try: logging.debug("Updating DNS settings…") - command = superuser_stdin + [ + command = config.superuser_stdnin_command + [ config.SUPERUSER_COMMAND, "sed", '-i', 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 @@ -511,7 +490,7 @@ def postinstall_dependencies_steamos(app=None): command = [config.SUPERUSER_COMMAND, "locale-gen"] run_command(command, input=password, verify=True) logging.debug("Enabling avahi…") - command = superuser_stdin + [ + command = config.superuser_stdnin_command + [ "systemctl", "enable", "--now", @@ -519,10 +498,10 @@ def postinstall_dependencies_steamos(app=None): ] run_command(command, input=password, verify=True) logging.debug("Enabling cups…") - command = superuser_stdin + ["systemctl", "enable", "--now", "cups"] # noqa: E501 + command = config.superuser_stdnin_command + ["systemctl", "enable", "--now", "cups"] # noqa: E501 run_command(command, input=password, verify=True) logging.debug("Enabling read only…") - command = superuser_stdin + ["steamos-readonly", "enable"] + command = config.superuser_stdnin_command + ["steamos-readonly", "enable"] run_command(command, input=password, verify=True) except subprocess.CalledProcessError as e: logging.error(f"An error occurred: {e}") From 98d6c882b7b0e2da693ed5f0c8641f405eb61c7b Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 7 Aug 2024 14:09:20 -0500 Subject: [PATCH 106/253] log error code and reason if password fails REVERT LATER: show username and password in log if failed login --- system.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system.py b/system.py index 97bb1a75..291078cf 100644 --- a/system.py +++ b/system.py @@ -109,6 +109,9 @@ def set_password(app=None): keyring.set_password("Logos", username, password) config.authenticated = True else: + logging.error(f"{p.reason} (return code: {p.code})") + # WARNING: Remove the next line before merging into main! Testing only! + logging.debug(f"{username=}; {password=}") msg.status("Incorrect password. Try again.", app) logging.debug("Incorrect password. Try again.") From ea6e020230b04ad7ce24fdb8e78289325ec35cd9 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 7 Aug 2024 21:28:35 -0400 Subject: [PATCH 107/253] Update requirements; fix var Fix pydialog buildlist --- requirements.txt | 2 ++ system.py | 42 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7f098401..20d705cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,8 @@ certifi==2023.11.17 charset-normalizer==3.3.2 distro==1.9.0 idna==3.6 +dbus-python==1.3.2 +secretstorage==3.3.3 keyring==25.3.0 packaging==23.2 pam==0.2.0 diff --git a/system.py b/system.py index 291078cf..a3e305aa 100644 --- a/system.py +++ b/system.py @@ -318,9 +318,9 @@ def download_packages(packages, elements, app=None): if packages: msg.status(f"Downloading Missing Packages: {packages}", app) total_packages = len(packages) - for index, package in enumerate(packages): - logging.debug(f"Downloading package: {package}") - command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD + [package] # noqa: E501 + for p in packages: + logging.debug(f"Downloading package: {p}") + command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD + [p] # noqa: E501 result = run_command(command, input=password, retries=5, delay=15, verify=True) if not isinstance(result, bool) and result.returncode == 0: @@ -328,12 +328,12 @@ def download_packages(packages, elements, app=None): else: status = "Failed" if elements is not None: - elements[index] = (package, status) + elements[p] = status if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 app.report_dependencies( - f"Downloading Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, elements, dialog=config.use_python_dialog # noqa: E501 + f"Downloading Packages ({packages.index(p) + 1}/{total_packages})", + 100 * (packages.index(p) + 1) // total_packages, elements, dialog=config.use_python_dialog # noqa: E501 ) app.password_e.clear() @@ -347,20 +347,20 @@ def install_packages(packages, elements, app=None): if packages: msg.status(f"Installing Missing Packages: {packages}", app) total_packages = len(packages) - for index, package in enumerate(packages): - logging.debug(f"Installing package: {package}") - command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_INSTALL + [package] # noqa: E501 + for p in packages: + logging.debug(f"Installing package: {p}") + command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_INSTALL + [p] # noqa: E501 result = run_command(command, input=password, retries=5, delay=15, verify=True) if elements is not None: if result and result.returncode == 0: - elements[index] = (package, "Installed") + elements[p] = "Installed" else: - elements[index] = (package, "Failed") + elements[p] = "Failed" if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 app.report_dependencies( - f"Installing Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, + f"Installing Packages ({packages.index(p) + 1}/{total_packages})", + 100 * (packages.index(p) + 1) // total_packages, elements, dialog=config.use_python_dialog) @@ -375,20 +375,20 @@ def remove_packages(packages, elements, app=None): if packages: msg.status(f"Removing Conflicting Packages: {packages}", app) total_packages = len(packages) - for index, package in enumerate(packages): - logging.debug(f"Removing package: {package}") - command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_REMOVE + [package] # noqa: E501 + for p in packages: + logging.debug(f"Removing package: {p}") + command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_REMOVE + [p] # noqa: E501 result = run_command(command, input=password, retries=5, delay=15, verify=True) if elements is not None: if result and result.returncode == 0: - elements[index] = (package, "Removed") + elements[p] = "Removed" else: - elements[index] = (package, "Failed") + elements[p] = "Failed" if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 app.report_dependencies( - f"Removing Conflicting Packages ({index + 1}/{total_packages})", - 100 * (index + 1) // total_packages, + f"Removing Conflicting Packages ({packages.index(p) + 1}/{total_packages})", + 100 * (packages.index(p) + 1) // total_packages, elements, dialog=config.use_python_dialog) @@ -445,7 +445,7 @@ def preinstall_dependencies_ubuntu(app=None): run_command(config.superuser_stdnin_command + ["dpkg", "--add-architecture", "i386"], input=password, verify=True) # noqa: E501 run_command(config.superuser_stdnin_command + ["mkdir", "-pm755", "/etc/apt/keyrings"], input=password, verify=True) # noqa: E501 url = "https://dl.winehq.org/wine-builds/winehq.key" - run_command(superuser_stdin + ["wget", "-O", "/etc/apt/keyrings/winehq-archive.key", url], input=password, + run_command(config.superuser_stdnin_command + ["wget", "-O", "/etc/apt/keyrings/winehq-archive.key", url], input=password, verify=True) # noqa: E501 lsb_release_output = run_command(["lsb_release", "-a"]) codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[ From 6a595de7b147973f8122554ff6f7ed99359d5b59 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 8 Aug 2024 10:02:19 -0500 Subject: [PATCH 108/253] Continued work fixing deps install - add dbus package to build server - run apt-get with sudo - install dbus-1 deps --- .github/workflows/build-branch.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index e215b6bc..e39394f6 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -46,6 +46,7 @@ jobs: - name: Install dependencies run: | # apt-get install python3-tk + sudo apt-get install build-essential libpython3-dev libdbus-1-dev pip install --upgrade pip pip install -r requirements.txt pip install pyinstaller From 3eb18b2aaa8aae0f9605799ba69cb9744af79ac1 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 8 Aug 2024 12:38:50 -0400 Subject: [PATCH 109/253] Remove tui_app.report_dependencies --- system.py | 31 ------------------------------- tui_app.py | 10 ---------- 2 files changed, 41 deletions(-) diff --git a/system.py b/system.py index a3e305aa..3b27662c 100644 --- a/system.py +++ b/system.py @@ -289,12 +289,6 @@ def query_packages(packages, elements=None, mode="install", app=None): elements[p] = status - if app is not None and config.DIALOG == "curses": - app.report_dependencies( - f"Checking Package: {(packages.index(p) + 1)}/{len(packages)}", # noqa: E501 - 100 * (packages.index(p) + 1) // len(packages), - elements, - dialog=config.use_python_dialog) logging.debug(f"Setting Status of {p}: {status}") if mode == "install": @@ -330,12 +324,6 @@ def download_packages(packages, elements, app=None): if elements is not None: elements[p] = status - if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 - app.report_dependencies( - f"Downloading Packages ({packages.index(p) + 1}/{total_packages})", - 100 * (packages.index(p) + 1) // total_packages, elements, dialog=config.use_python_dialog # noqa: E501 - ) - app.password_e.clear() @@ -357,12 +345,6 @@ def install_packages(packages, elements, app=None): elements[p] = "Installed" else: elements[p] = "Failed" - if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 - app.report_dependencies( - f"Installing Packages ({packages.index(p) + 1}/{total_packages})", - 100 * (packages.index(p) + 1) // total_packages, - elements, - dialog=config.use_python_dialog) app.password_e.clear() @@ -385,12 +367,6 @@ def remove_packages(packages, elements, app=None): elements[p] = "Removed" else: elements[p] = "Failed" - if app is not None and config.DIALOG == "curses" and elements is not None: # noqa: E501 - app.report_dependencies( - f"Removing Conflicting Packages ({packages.index(p) + 1}/{total_packages})", - 100 * (packages.index(p) + 1) // total_packages, - elements, - dialog=config.use_python_dialog) app.password_e.clear() @@ -556,9 +532,6 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): for p in bad_package_list: bad_elements[p] = "Unchecked" - if config.DIALOG == "curses" and app is not None: - app.report_dependencies("Checking Packages", 0, elements, dialog=config.use_python_dialog) # noqa: E501 - if config.PACKAGE_MANAGER_COMMAND_QUERY: logging.debug("Querying packages…") missing_packages, elements = query_packages(package_list, elements, app=app) # noqa: E501 @@ -655,10 +628,6 @@ def check_libs(libraries, app=None): for p in libraries: elements[p] = "Unchecked" - if config.DIALOG == "curses" and app is not None: - app.report_dependencies("Checking Packages", 0, elements, - dialog=config.use_python_dialog) # noqa: E501 - install_packages(config.PACKAGES, elements, app=app) else: msg.logos_error( diff --git a/tui_app.py b/tui_app.py index 466be970..0a969734 100644 --- a/tui_app.py +++ b/tui_app.py @@ -516,16 +516,6 @@ def report_waiting(self, text, dialog): #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) logging.console_log.append(text) - def report_dependencies(self, text, percent, elements, dialog): - if elements is not None: - if dialog: - self.screen_q.put(self.stack_tasklist(11, self.deps_q, self.deps_e, text, elements, percent, dialog=dialog)) - # Without this delay, the reporting works too quickly and instead appears all at once. - time.sleep(0.1) - else: - msg.status(f"{text}", self) - pass - def which_dialog_options(self, labels, dialog=False): options = [] option_number = 1 From 9a5fa788edb97c2651da8a7708035a62c9dd7d10 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 9 Aug 2024 06:51:32 -0500 Subject: [PATCH 110/253] Continued work fixing deps install - Revert "install dbus-1 deps" - This reverts commit f8d5a2ad2f88047b5dc332f3adba01e8f5ae2c3e. - Revert "run apt-get with sudo" - This reverts commit b7c0af97b1493aa741cdde39cc57b0d6e0c25c8b. - Revert "add dbus package to build server" - This reverts commit 8ead8599fdf4f17c7fcc16fde9ac28e9996b29bf. - Revert "Fix pydialog buildlist" - This reverts commit fc1a97e37b418d8bbded734874041befd9831c69. - Revert "Update requirements; fix var" - This reverts commit bdfdf160eef9dee72a8389ebebb857289bf63cbf. - Revert "REVERT LATER: show username and password in log if failed login" - This reverts commit 0f1d71374b2ac6c23aa0f4a8aa5cf76fa4f80c77. - Revert "log error code and reason if password fails" - This reverts commit fc8ebbabc986033ab9035934a3f65d49683385e7. - Revert "Add config.superuser_stdnin_command" - This reverts commit 3494b7a104c7253784bbcb00cf3ac598be8be983. - Revert "update build requirements" - This reverts commit de941ca043a7d041b418e2ca03ec3d382c1b5f35. - use one call each for install/remove packages - comment out keyring and pam code - fix pkg mgr commands - make pkexec 1st superuser option - remove extra GUI event generation - various cleanup --- .github/workflows/build-branch.yml | 1 - config.py | 1 - requirements.txt | 5 - system.py | 266 +++++++++++------------------ tui_app.py | 12 +- utils.py | 34 ++-- 6 files changed, 128 insertions(+), 191 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index e39394f6..e215b6bc 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -46,7 +46,6 @@ jobs: - name: Install dependencies run: | # apt-get install python3-tk - sudo apt-get install build-essential libpython3-dev libdbus-1-dev pip install --upgrade pip pip install -r requirements.txt pip install pyinstaller diff --git a/config.py b/config.py index 52b637e8..fe430f3a 100644 --- a/config.py +++ b/config.py @@ -97,7 +97,6 @@ RECOMMENDED_WINE64_APPIMAGE_VERSION = None RECOMMENDED_WINE64_APPIMAGE_BRANCH = None SUPERUSER_COMMAND = None -superuser_stdnin_command = None VERBUM_PATH = None WINETRICKS_URL = "https://raw.githubusercontent.com/Winetricks/winetricks/5904ee355e37dff4a3ab37e1573c56cffe6ce223/src/winetricks" # noqa: E501 WINETRICKS_VERSION = '20220411' diff --git a/requirements.txt b/requirements.txt index 20d705cd..ba3bd95e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,12 +3,7 @@ certifi==2023.11.17 charset-normalizer==3.3.2 distro==1.9.0 idna==3.6 -dbus-python==1.3.2 -secretstorage==3.3.3 -keyring==25.3.0 packaging==23.2 -pam==0.2.0 -six==1.16.0 psutil==5.9.7 pythondialog==3.5.3 requests==2.31.0 diff --git a/system.py b/system.py index 3b27662c..81510d28 100644 --- a/system.py +++ b/system.py @@ -1,10 +1,6 @@ -import getpass -import keyring import logging import distro import os -import pam -import shlex import shutil import subprocess import sys @@ -13,13 +9,12 @@ from pathlib import Path import config -import gui import msg import network -import utils -#TODO: Add a Popen variant to run_command to replace functions in control.py and wine.py +# TODO: Add a Popen variant to run_command to replace functions in control.py +# and wine.py def run_command(command, retries=1, delay=0, verify=False, **kwargs): check = kwargs.get("check", True) text = kwargs.get("text", True) @@ -99,30 +94,6 @@ def tl(library): return False -def set_password(app=None): - p = pam.pam() - username = getpass.getuser() - utils.send_task(app, "PASSWORD") - app.password_e.wait() - password = app.password_q.get() - if p.authenticate(username, password): - keyring.set_password("Logos", username, password) - config.authenticated = True - else: - logging.error(f"{p.reason} (return code: {p.code})") - # WARNING: Remove the next line before merging into main! Testing only! - logging.debug(f"{username=}; {password=}") - msg.status("Incorrect password. Try again.", app) - logging.debug("Incorrect password. Try again.") - - -def get_password(app=None): - while not config.authenticated: - set_password(app) - msg.status("I have the power!", app) - return keyring.get_password("Logos", getpass.getuser()) - - def get_dialog(): if not os.environ.get('DISPLAY'): msg.logos_error("The installer does not work unless you are running a display") # noqa: E501 @@ -159,15 +130,12 @@ def get_superuser_command(): else: msg.logos_error("No superuser command found. Please install pkexec.") # noqa: E501 else: - if shutil.which('sudo'): + if shutil.which('pkexec'): + config.SUPERUSER_COMMAND = "pkexec" + elif shutil.which('sudo'): config.SUPERUSER_COMMAND = "sudo" - config.superuser_stdnin_command = ["sudo", "-S"] elif shutil.which('doas'): config.SUPERUSER_COMMAND = "doas" - config.superuser_stdnin_command = ["doas", "-n"] - elif shutil.which('pkexec'): - config.SUPERUSER_COMMAND = "pkexec" - config.superuser_stdnin_command = ["sudo", "-S"] else: msg.logos_error("No superuser command found. Please install sudo or doas.") # noqa: E501 logging.debug(f"{config.SUPERUSER_COMMAND=}") @@ -196,7 +164,7 @@ def get_package_manager(): elif shutil.which('pamac') is not None: # manjaro config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pamac", "install", "--no-upgrade", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pamac", "install", "--no-upgrade", "--download-only", "--no-confirm"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pamac", "remove", "--no-confirm"] + config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pamac", "remove", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = ["pamac", "list", "-i"] config.QUERY_PREFIX = '' config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 @@ -205,7 +173,7 @@ def get_package_manager(): elif shutil.which('pacman') is not None: # arch, steamOS config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pacman", "-Syu", "--overwrite", r"*", "--noconfirm", "--needed"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pacman", "-Sw", "-y"] - config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pacman", "-R", "--no-confirm"] + config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pacman", "-R", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = ["pacman", "-Q"] config.QUERY_PREFIX = '' if config.OS_NAME == "steamos": # steamOS @@ -272,7 +240,7 @@ def query_packages(packages, elements=None, mode="install", app=None): status == 'Conflicting' break else: - if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": + if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": # noqa: E501 status = "Installed" break elif line.strip().startswith(p) and mode == "remove": @@ -303,72 +271,35 @@ def query_packages(packages, elements=None, mode="install", app=None): return conflicting_packages, elements -def download_packages(packages, elements, app=None): - if config.SKIP_DEPENDENCIES: - return - #TODO: As can be seen in this commit, there is good potential for reusing this code block in install_dependencies(). - password = get_password(app) - - if packages: - msg.status(f"Downloading Missing Packages: {packages}", app) - total_packages = len(packages) - for p in packages: - logging.debug(f"Downloading package: {p}") - command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD + [p] # noqa: E501 - result = run_command(command, input=password, retries=5, delay=15, verify=True) - - if not isinstance(result, bool) and result.returncode == 0: - status = "Downloaded" - else: - status = "Failed" - if elements is not None: - elements[p] = status - - app.password_e.clear() - - def install_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return - password = get_password(app) + # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 + # superuser_stdin = ["sudo", "-S"] + # else: + # superuser_stdin = ["doas", "-n"] if packages: msg.status(f"Installing Missing Packages: {packages}", app) - total_packages = len(packages) - for p in packages: - logging.debug(f"Installing package: {p}") - command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_INSTALL + [p] # noqa: E501 - result = run_command(command, input=password, retries=5, delay=15, verify=True) - - if elements is not None: - if result and result.returncode == 0: - elements[p] = "Installed" - else: - elements[p] = "Failed" - - app.password_e.clear() + command = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_INSTALL, *packages] # noqa: E501 + # TODO: Handle non-zero exit status. + result = run_command(command) def remove_packages(packages, elements, app=None): if config.SKIP_DEPENDENCIES: return - password = get_password(app) + # password = get_password(app) + # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 + # superuser_stdin = ["sudo", "-S"] + # else: + # superuser_stdin = ["doas", "-n"] if packages: msg.status(f"Removing Conflicting Packages: {packages}", app) - total_packages = len(packages) - for p in packages: - logging.debug(f"Removing package: {p}") - command = config.superuser_stdnin_command + config.PACKAGE_MANAGER_COMMAND_REMOVE + [p] # noqa: E501 - result = run_command(command, input=password, retries=5, delay=15, verify=True) - - if elements is not None: - if result and result.returncode == 0: - elements[p] = "Removed" - else: - elements[p] = "Failed" - - app.password_e.clear() + command = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE, *packages] # noqa: E501 + # TODO: Handle non-zero exit status. + result = run_command(command) def have_dep(cmd): @@ -414,98 +345,89 @@ def parse_date(version): return None -def preinstall_dependencies_ubuntu(app=None): - password = get_password(app) - try: - logging.debug("Adding wine staging repositories…") - run_command(config.superuser_stdnin_command + ["dpkg", "--add-architecture", "i386"], input=password, verify=True) # noqa: E501 - run_command(config.superuser_stdnin_command + ["mkdir", "-pm755", "/etc/apt/keyrings"], input=password, verify=True) # noqa: E501 - url = "https://dl.winehq.org/wine-builds/winehq.key" - run_command(config.superuser_stdnin_command + ["wget", "-O", "/etc/apt/keyrings/winehq-archive.key", url], input=password, - verify=True) # noqa: E501 - lsb_release_output = run_command(["lsb_release", "-a"]) - codename = [line for line in lsb_release_output.stdout.split('\n') if "Description" in line][0].split()[ - 1].strip() # noqa: E501 - url = f"https://dl.winehq.org/wine-builds/ubuntu/dists/{codename}/winehq-{codename}.sources" # noqa: E501 - run_command(config.superuser_stdnin_command + ["wget", "-NP", "/etc/apt/sources.list.d/", url], input=password, verify=True) # noqa: E501 - run_command(config.superuser_stdnin_command + ["apt", "update"], input=password, verify=True) - run_command(config.superuser_stdnin_command + ["apt", "install", "--install-recommends", "winehq-staging"], input=password, - verify=True) # noqa: E501 - except subprocess.CalledProcessError as e: - logging.error(f"An error occurred: {e}") - logging.error(f"Command output: {e.output}") - app.password_e.clear() - - def preinstall_dependencies_steamos(app=None): - password = get_password(app) + # password = get_password(app) + # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 + # superuser_stdin = ["sudo", "-S"] + # else: + # superuser_stdin = ["doas", "-n"] try: - logging.debug("Disabling read only…") - command = config.superuser_stdnin_command + ["steamos-readonly", "disable"] - run_command(command, input=password, verify=True) - logging.debug("Updating pacman keys…") - command = config.superuser_stdnin_command + ["pacman-key", "--init"] - run_command(command, input=password, verify=True) - command = config.superuser_stdnin_command + ["pacman-key", "--populate", "archlinux"] # noqa: E501 - run_command(command, input=password, verify=True) + logging.debug("Disabling read only, updating pacman keys…") + command = [ + config.SUPERUSER_COMMAND, "steamos-readonly", "disable", "&&", + config.SUPERUSER_COMMAND, "pacman-key", "--init", "&&", + config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux" + ] + run_command(command, verify=True) + # logging.debug("Updating pacman keys…") + # command = superuser_stdin + ["pacman-key", "--init"] + # run_command(command, input=password, verify=True) + # command = superuser_stdin + ["pacman-key", "--populate", "archlinux"] # noqa: E501 + # run_command(command, input=password, verify=True) except subprocess.CalledProcessError as e: logging.error(f"An error occurred: {e}") logging.error(f"Command output: {e.output}") - app.password_e.clear() def postinstall_dependencies_steamos(app=None): - password = get_password(app) + # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 + # superuser_stdin = ["sudo", "-S"] + # else: + # superuser_stdin = ["doas", "-n"] try: - logging.debug("Updating DNS settings…") - command = config.superuser_stdnin_command + [ - config.SUPERUSER_COMMAND, - "sed", '-i', + logging.debug("Updating DNS settings & locales, enabling services & read-only system…") # noqa: E501 + command = [ + config.SUPERUSER_COMMAND, "sed", '-i', 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 - '/etc/nsswitch.conf' + '/etc/nsswitch.conf', '&&', + config.SUPERUSER_COMMAND, "locale-gen", '&&', + config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "avahi-daemon", "&&", # noqa: E501 + config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups", "&&", # noqa: E501 + config.SUPERUSER_COMMAND, "steamos-readonly", "enable", ] - run_command(command, input=password, verify=True) - logging.debug("Updating locales…") - command = [config.SUPERUSER_COMMAND, "locale-gen"] - run_command(command, input=password, verify=True) - logging.debug("Enabling avahi…") - command = config.superuser_stdnin_command + [ - "systemctl", - "enable", - "--now", - "avahi-daemon" - ] - run_command(command, input=password, verify=True) - logging.debug("Enabling cups…") - command = config.superuser_stdnin_command + ["systemctl", "enable", "--now", "cups"] # noqa: E501 - run_command(command, input=password, verify=True) - logging.debug("Enabling read only…") - command = config.superuser_stdnin_command + ["steamos-readonly", "enable"] - run_command(command, input=password, verify=True) + # command = superuser_stdin + [ + # config.SUPERUSER_COMMAND, + # "sed", '-i', + # 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 + # '/etc/nsswitch.conf' + # ] + # run_command(command, input=password, verify=True) + # logging.debug("Updating locales…") + # command = [config.SUPERUSER_COMMAND, "locale-gen"] + # run_command(command, input=password, verify=True) + # logging.debug("Enabling avahi…") + # command = superuser_stdin + [ + # "systemctl", + # "enable", + # "--now", + # "avahi-daemon" + # ] + # run_command(command, input=password, verify=True) + # logging.debug("Enabling cups…") + # command = superuser_stdin + ["systemctl", "enable", "--now", "cups"] # noqa: E501 + # run_command(command, input=password, verify=True) + # logging.debug("Enabling read only…") + # command = superuser_stdin + ["steamos-readonly", "enable"] + run_command(command, verify=True) except subprocess.CalledProcessError as e: logging.error(f"An error occurred: {e}") logging.error(f"Command output: {e.output}") - app.password_e.clear() def preinstall_dependencies(app=None): - logging.debug(f"Performing pre-install dependencies…") - #TODO: Use a dict for distro derivatives so that we can add all Ubuntu derivatives here or in similar other places - if config.OS_NAME == "Ubuntu" or config.OS_NAME == "Linux Mint": - # preinstall_dependencies_ubuntu() - pass - elif config.OS_NAME == "Steam": + logging.debug("Performing pre-install dependencies…") + if config.OS_NAME == "Steam": preinstall_dependencies_steamos(app) else: - logging.debug(f"No pre-install dependencies required.") + logging.debug("No pre-install dependencies required.") def postinstall_dependencies(app=None): - logging.debug(f"Performing post-install dependencies…") + logging.debug("Performing post-install dependencies…") if config.OS_NAME == "Steam": postinstall_dependencies_steamos(app) else: - logging.debug(f"No post-install dependencies required.") + logging.debug("No post-install dependencies required.") def install_dependencies(packages, badpackages, logos9_packages=None, app=None): # noqa: E501 @@ -534,19 +456,27 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if config.PACKAGE_MANAGER_COMMAND_QUERY: logging.debug("Querying packages…") - missing_packages, elements = query_packages(package_list, elements, app=app) # noqa: E501 - conflicting_packages, bad_elements = query_packages(bad_package_list, bad_elements, "remove", - app=app) # noqa: E501 + missing_packages, elements = query_packages( + package_list, + elements, + app=app + ) + conflicting_packages, bad_elements = query_packages( + bad_package_list, + bad_elements, + "remove", + app=app + ) if config.PACKAGE_MANAGER_COMMAND_INSTALL: if missing_packages and conflicting_packages: - message = f"Your {config.OS_NAME} computer requires installing and removing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}) and will remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 + message = f"Your {config.OS_NAME} computer requires installing and removing some software. To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}\nand will remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}\nProceed?" # noqa: E501 # logging.critical(message) elif missing_packages: - message = f"Your {config.OS_NAME} computer requires installing some software. To continue, the program will attempt to install the package(s): {missing_packages} by using ({config.PACKAGE_MANAGER_COMMAND_INSTALL}). Proceed?" # noqa: E501 + message = f"Your {config.OS_NAME} computer requires installing some software. To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}\nProceed?" # noqa: E501 # logging.critical(message) elif conflicting_packages: - message = f"Your {config.OS_NAME} computer requires removing some software. To continue, the program will attempt to remove the package(s): {conflicting_packages} by using ({config.PACKAGE_MANAGER_COMMAND_REMOVE}). Proceed?" # noqa: E501 + message = f"Your {config.OS_NAME} computer requires removing some software. To continue, the program will attempt to remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}\nProceed?" # noqa: E501 # logging.critical(message) else: logging.debug("No missing or conflicting dependencies found.") @@ -571,8 +501,8 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): if missing_packages: if app and config.DIALOG == 'tk': app.root.event_generate('<>') - msg.status("Downloading packages…", app) - download_packages(missing_packages, elements, app) + # msg.status("Downloading packages…", app) + # download_packages(missing_packages, elements, app) if app and config.DIALOG == 'tk': app.root.event_generate('<>') msg.status("Installing packages…", app) @@ -595,7 +525,7 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): else: msg.logos_error( - f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. " + f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. " # noqa: E501 f"Your computer is missing the command(s) {missing_packages}. " f"Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 diff --git a/tui_app.py b/tui_app.py index 0a969734..a3a75af2 100644 --- a/tui_app.py +++ b/tui_app.py @@ -275,8 +275,8 @@ def task_processor(self, evt=None, task=None): self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) - elif task == 'PASSWORD': - utils.start_thread(self.get_password(config.use_python_dialog)) + # elif task == 'PASSWORD': + # utils.start_thread(self.get_password(config.use_python_dialog)) def choice_processor(self, stdscr, screen_id, choice): if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): @@ -507,10 +507,10 @@ def get_config(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog)) - def get_password(self, dialog): - question = (f"Logos Linux Installer needs to run a command as root. " - f"Please provide your password to provide escalation privileges.") - self.screen_q.put(self.stack_password(15, self.password_q, self.password_e, question, dialog=dialog)) + # def get_password(self, dialog): + # question = (f"Logos Linux Installer needs to run a command as root. " + # f"Please provide your password to provide escalation privileges.") + # self.screen_q.put(self.stack_password(15, self.password_q, self.password_e, question, dialog=dialog)) def report_waiting(self, text, dialog): #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) diff --git a/utils.py b/utils.py index c1041853..48979538 100644 --- a/utils.py +++ b/utils.py @@ -26,10 +26,11 @@ import tui_dialog import wine -#TODO: Move config commands to config.py +# TODO: Move config commands to config.py from main import threads + def get_calling_function_name(): if 'inspect' in sys.modules: stack = inspect.stack() @@ -220,9 +221,6 @@ def check_dependencies(app=None): else: targetversion = 10 msg.status(f"Checking Logos {str(targetversion)} dependencies…", app) - if app: - if config.DIALOG == "tk": - app.root.event_generate('<>') if targetversion == 10: system.install_dependencies(config.PACKAGES, config.BADPACKAGES, app=app) # noqa: E501 @@ -268,7 +266,7 @@ def get_current_logos_version(): logging.debug("Couldn't determine installed Logos version.") return None else: - logging.debug(f"Logos.deps.json not found.") + logging.debug("Logos.deps.json not found.") def convert_logos_release(logos_release): @@ -283,7 +281,12 @@ def convert_logos_release(logos_release): release = 0 point = 0 - logos_release_arr = [int(ver_major), int(ver_minor), int(release), int(point)] + logos_release_arr = [ + int(ver_major), + int(ver_minor), + int(release), + int(point), + ] return logos_release_arr @@ -659,7 +662,10 @@ def find_appimage_files(release_version, app=None): appimage_paths = Path(d).rglob('wine*.appimage', case_sensitive=False) for p in appimage_paths: if p is not None and check_appimage(p): - output1, output2 = wine.check_wine_version_and_branch(release_version, p) + output1, output2 = wine.check_wine_version_and_branch( + release_version, + p, + ) if output1 is not None and output1: appimages.append(str(p)) else: @@ -699,7 +705,10 @@ def find_wine_binary_files(release_version): binaries.append(binary_path) for binary in binaries[:]: - output1, output2 = wine.check_wine_version_and_branch(release_version, binary) + output1, output2 = wine.check_wine_version_and_branch( + release_version, + binary, + ) if output1 is not None and output1: continue else: @@ -836,7 +845,12 @@ def grep(regexp, filepath): def start_thread(task, daemon_bool=True, *args): - thread = threading.Thread(name=f"{task}", target=task, daemon=daemon_bool, args=args) + thread = threading.Thread( + name=f"{task}", + target=task, + daemon=daemon_bool, + args=args + ) threads.append(thread) thread.start() return thread @@ -857,6 +871,6 @@ def untar_file(file_path, output_dir): try: with tarfile.open(file_path, 'r:gz') as tar: tar.extractall(path=output_dir) - logging.debug(f"Successfully extracted '{file_path}' to '{output_dir}'") + logging.debug(f"Successfully extracted '{file_path}' to '{output_dir}'") # noqa: E501 except tarfile.TarError as e: logging.error(f"Error extracting '{file_path}': {e}") From 71ea280a86fef0a5454748ae1e2baf4d0785a49d Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 9 Aug 2024 18:42:16 -0400 Subject: [PATCH 111/253] Reduce to a single command - This removes install_dep and remove_dep as all they were doing by the end was assigning a var. - This makes it so we only run one cmd by executing all of the needed commands as a one-time script through `sh -c`. --- system.py | 274 ++++++++++++++++++++---------------------------------- 1 file changed, 99 insertions(+), 175 deletions(-) diff --git a/system.py b/system.py index 81510d28..c7a3079b 100644 --- a/system.py +++ b/system.py @@ -15,7 +15,7 @@ # TODO: Add a Popen variant to run_command to replace functions in control.py # and wine.py -def run_command(command, retries=1, delay=0, verify=False, **kwargs): +def run_command(command, retries=1, delay=0, **kwargs): check = kwargs.get("check", True) text = kwargs.get("text", True) capture_output = kwargs.get("capture_output", True) @@ -198,14 +198,10 @@ def get_runmode(): return 'script' -def query_packages(packages, elements=None, mode="install", app=None): +def query_packages(packages, mode="install", app=None): result = "" - if config.SKIP_DEPENDENCIES: - return - missing_packages = [] conflicting_packages = [] - command = config.PACKAGE_MANAGER_COMMAND_QUERY try: @@ -213,17 +209,13 @@ def query_packages(packages, elements=None, mode="install", app=None): except Exception as e: logging.error(f"Error occurred while executing command: {e}") logging.error(result.stderr) - package_list = result.stdout + status = {package: "Unchecked" for package in packages} + if app is not None: - if elements is None: - elements = {} # Initialize elements if not provided - elif isinstance(elements, list): - elements = {element[0]: element[1] for element in elements} for p in packages: - status = "Unchecked" l_num = 0 for line in package_list.split('\n'): l_num += 1 @@ -235,71 +227,36 @@ def query_packages(packages, elements=None, mode="install", app=None): pkg = parts[1].split(':')[0] # remove :arch if present if pkg == p and state[1] == 'i': if mode == 'install': - status = "Installed" + status[p] = "Installed" elif mode == 'remove': - status == 'Conflicting' + status[p] = 'Conflicting' break else: if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": # noqa: E501 - status = "Installed" + status[p] = "Installed" break elif line.strip().startswith(p) and mode == "remove": conflicting_packages.append(p) - status = "Conflicting" + status[p] = "Conflicting" break - if status == "Unchecked": + if status[p] == "Unchecked": if mode == "install": missing_packages.append(p) - status = "Missing" + status[p] = "Missing" elif mode == "remove": - status = "Not Installed" - - elements[p] = status - - logging.debug(f"Setting Status of {p}: {status}") + status[p] = "Not Installed" if mode == "install": if missing_packages: txt = f"Missing packages: {' '.join(missing_packages)}" logging.info(f"{txt}") - return missing_packages, elements + return missing_packages elif mode == "remove": if conflicting_packages: txt = f"Conflicting packages: {' '.join(conflicting_packages)}" logging.info(f"Conflicting packages: {txt}") - return conflicting_packages, elements - - -def install_packages(packages, elements, app=None): - if config.SKIP_DEPENDENCIES: - return - # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 - # superuser_stdin = ["sudo", "-S"] - # else: - # superuser_stdin = ["doas", "-n"] - - if packages: - msg.status(f"Installing Missing Packages: {packages}", app) - command = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_INSTALL, *packages] # noqa: E501 - # TODO: Handle non-zero exit status. - result = run_command(command) - - -def remove_packages(packages, elements, app=None): - if config.SKIP_DEPENDENCIES: - return - # password = get_password(app) - # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 - # superuser_stdin = ["sudo", "-S"] - # else: - # superuser_stdin = ["doas", "-n"] - - if packages: - msg.status(f"Removing Conflicting Packages: {packages}", app) - command = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE, *packages] # noqa: E501 - # TODO: Handle non-zero exit status. - result = run_command(command) + return conflicting_packages def have_dep(cmd): @@ -345,126 +302,82 @@ def parse_date(version): return None -def preinstall_dependencies_steamos(app=None): - # password = get_password(app) - # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 - # superuser_stdin = ["sudo", "-S"] - # else: - # superuser_stdin = ["doas", "-n"] - try: - logging.debug("Disabling read only, updating pacman keys…") - command = [ - config.SUPERUSER_COMMAND, "steamos-readonly", "disable", "&&", - config.SUPERUSER_COMMAND, "pacman-key", "--init", "&&", - config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux" - ] - run_command(command, verify=True) - # logging.debug("Updating pacman keys…") - # command = superuser_stdin + ["pacman-key", "--init"] - # run_command(command, input=password, verify=True) - # command = superuser_stdin + ["pacman-key", "--populate", "archlinux"] # noqa: E501 - # run_command(command, input=password, verify=True) - except subprocess.CalledProcessError as e: - logging.error(f"An error occurred: {e}") - logging.error(f"Command output: {e.output}") - - -def postinstall_dependencies_steamos(app=None): - # if config.SUPERUSER_COMMAND == "sudo" or config.SUPERUSER_COMMAND == "pkexec": # noqa: E501 - # superuser_stdin = ["sudo", "-S"] - # else: - # superuser_stdin = ["doas", "-n"] - try: - logging.debug("Updating DNS settings & locales, enabling services & read-only system…") # noqa: E501 - command = [ - config.SUPERUSER_COMMAND, "sed", '-i', - 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 - '/etc/nsswitch.conf', '&&', - config.SUPERUSER_COMMAND, "locale-gen", '&&', - config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "avahi-daemon", "&&", # noqa: E501 - config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups", "&&", # noqa: E501 - config.SUPERUSER_COMMAND, "steamos-readonly", "enable", - ] - # command = superuser_stdin + [ - # config.SUPERUSER_COMMAND, - # "sed", '-i', - # 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 - # '/etc/nsswitch.conf' - # ] - # run_command(command, input=password, verify=True) - # logging.debug("Updating locales…") - # command = [config.SUPERUSER_COMMAND, "locale-gen"] - # run_command(command, input=password, verify=True) - # logging.debug("Enabling avahi…") - # command = superuser_stdin + [ - # "systemctl", - # "enable", - # "--now", - # "avahi-daemon" - # ] - # run_command(command, input=password, verify=True) - # logging.debug("Enabling cups…") - # command = superuser_stdin + ["systemctl", "enable", "--now", "cups"] # noqa: E501 - # run_command(command, input=password, verify=True) - # logging.debug("Enabling read only…") - # command = superuser_stdin + ["steamos-readonly", "enable"] - run_command(command, verify=True) - except subprocess.CalledProcessError as e: - logging.error(f"An error occurred: {e}") - logging.error(f"Command output: {e.output}") +def preinstall_dependencies_steamos(): + logging.debug("Disabling read only, updating pacman keys…") + command = [ + config.SUPERUSER_COMMAND, "steamos-readonly", "disable", "&&", + config.SUPERUSER_COMMAND, "pacman-key", "--init", "&&", + config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux" + ] + return command + + +def postinstall_dependencies_steamos(): + logging.debug("Updating DNS settings & locales, enabling services & read-only system…") # noqa: E501 + command = [ + config.SUPERUSER_COMMAND, "sed", '-i', + 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 + '/etc/nsswitch.conf', '&&', + config.SUPERUSER_COMMAND, "locale-gen", '&&', + config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "avahi-daemon", "&&", # noqa: E501 + config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups", "&&", # noqa: E501 + config.SUPERUSER_COMMAND, "steamos-readonly", "enable", + ] + return command def preinstall_dependencies(app=None): + command = [] logging.debug("Performing pre-install dependencies…") if config.OS_NAME == "Steam": - preinstall_dependencies_steamos(app) + command = preinstall_dependencies_steamos() else: logging.debug("No pre-install dependencies required.") + return command def postinstall_dependencies(app=None): + command = [] logging.debug("Performing post-install dependencies…") if config.OS_NAME == "Steam": postinstall_dependencies_steamos(app) else: logging.debug("No post-install dependencies required.") + return command + +def install_dependencies(packages, bad_packages, logos9_packages=None, app=None): # noqa: E501 + if config.SKIP_DEPENDENCIES: + return -def install_dependencies(packages, badpackages, logos9_packages=None, app=None): # noqa: E501 + command = [] + preinstall_command = [] + install_command = [] + remove_command = [] + postinstall_command = [] missing_packages = {} conflicting_packages = {} package_list = [] - elements = {} - bad_elements = {} + bad_package_list = [] if packages: package_list = packages.split() - bad_package_list = [] - if badpackages: - bad_package_list = badpackages.split() + if bad_packages: + bad_package_list = bad_packages.split() if logos9_packages: package_list.extend(logos9_packages.split()) - if config.DIALOG == "curses" and app is not None and elements is not None: - for p in package_list: - elements[p] = "Unchecked" - if config.DIALOG == "curses" and app is not None and bad_elements is not None: # noqa: E501 - for p in bad_package_list: - bad_elements[p] = "Unchecked" - if config.PACKAGE_MANAGER_COMMAND_QUERY: logging.debug("Querying packages…") - missing_packages, elements = query_packages( + missing_packages = query_packages( package_list, - elements, app=app ) - conflicting_packages, bad_elements = query_packages( + conflicting_packages = query_packages( bad_package_list, - bad_elements, - "remove", + mode="remove", app=app ) @@ -486,49 +399,70 @@ def install_dependencies(packages, badpackages, logos9_packages=None, app=None): # Do we need a TK continue question? I see we have a CLI and curses one # in msg.py - if app and config.DIALOG == 'tk': - app.root.event_generate('<>') - msg.status("Installing pre-install dependencies…", app) - preinstall_dependencies(app) + preinstall_command = preinstall_dependencies() # libfuse: for AppImage use. This is the only known needed library. if config.OS_NAME == "fedora": fuse = "fuse" else: fuse = "libfuse" - check_libs([f"{fuse}"], app=app) + + install_fuse = check_libs([f"{fuse}"], app=app) + if not install_fuse: + missing_packages.append(fuse) if missing_packages: - if app and config.DIALOG == 'tk': - app.root.event_generate('<>') - # msg.status("Downloading packages…", app) - # download_packages(missing_packages, elements, app) - if app and config.DIALOG == 'tk': - app.root.event_generate('<>') - msg.status("Installing packages…", app) - install_packages(missing_packages, elements, app) + install_command = config.PACKAGE_MANAGER_COMMAND_INSTALL + missing_packages # noqa: E501 else: logging.debug("No missing packages detected.") if conflicting_packages: - # AppImage Launcher is the only known conflicting package. - remove_packages(conflicting_packages, bad_elements, app) - # config.REBOOT_REQUIRED = True # TODO: Verify with user before executing + # AppImage Launcher is the only known conflicting package. + remove_command = config.PACKAGE_MANAGER_COMMAND_REMOVE + conflicting_packages # noqa: E501 + config.REBOOT_REQUIRED = True + logging.info("System reboot required.") else: logging.debug("No conflicting packages detected.") - postinstall_dependencies(app) - - if config.REBOOT_REQUIRED: - reboot() + postinstall_command = postinstall_dependencies(app) + + if preinstall_command: + command.extend(preinstall_command) + if install_command: + if preinstall_command: + command.append('&&') + command.extend(install_command) + if remove_command: + if preinstall_command or install_command: + command.append('&&') + command.extend(remove_command) + if postinstall_command: + if preinstall_command or install_command or remove_command: + command.append('&&') + command.extend(postinstall_command) + if app and config.DIALOG == 'tk': + app.root.event_generate('<>') + msg.status("Installing dependencies…", app) + final_command = [f"{config.SUPERUSER_COMMAND}", 'sh', '-c'] + command + try: + logging.debug(f"Attempting to run this command: {final_command}") + run_command(final_command) + except subprocess.CalledProcessError as e: + logging.error(f"An error occurred: {e}") + logging.error(f"Command output: {e.output}") else: msg.logos_error( f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. " # noqa: E501 f"Your computer is missing the command(s) {missing_packages}. " f"Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 + # TODO: Verify with user before executing + if config.REBOOT_REQUIRED: + pass + #reboot() + def have_lib(library, ld_library_path): roots = ['/usr/lib', '/lib'] @@ -548,20 +482,10 @@ def check_libs(libraries, app=None): have_lib_result = have_lib(library, ld_library_path) if have_lib_result: logging.info(f"* {library} is installed!") + return True else: - if config.PACKAGE_MANAGER_COMMAND_INSTALL: - # message = f"Your {config.OS_NAME} install is missing the library: {library}. To continue, the script will attempt to install the library by using {config.PACKAGE_MANAGER_COMMAND_INSTALL}. Proceed?" # noqa: E501 - # if msg.cli_continue_question(message, "", ""): - elements = {} - - if config.DIALOG == "curses" and app is not None and elements is not None: # noqa: E501 - for p in libraries: - elements[p] = "Unchecked" - - install_packages(config.PACKAGES, elements, app=app) - else: - msg.logos_error( - f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. Your computer is missing the library: {library}. Please install the package associated with {library} for {config.OS_NAME}.") # noqa: E501 + logging.info(f"* {library} is not installed!") + return False def install_winetricks( From 125d7831a4497497648690f599d12340f1df30bd Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 10 Aug 2024 07:11:02 -0500 Subject: [PATCH 112/253] Continued work fixing deps install - return command from steam postinst deps, etc. - run pkexec command with shell=True - add conflicting package to list if found --- system.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/system.py b/system.py index c7a3079b..cff4779d 100644 --- a/system.py +++ b/system.py @@ -245,6 +245,7 @@ def query_packages(packages, mode="install", app=None): missing_packages.append(p) status[p] = "Missing" elif mode == "remove": + conflicting_packages.append(p) status[p] = "Not Installed" if mode == "install": @@ -307,7 +308,7 @@ def preinstall_dependencies_steamos(): command = [ config.SUPERUSER_COMMAND, "steamos-readonly", "disable", "&&", config.SUPERUSER_COMMAND, "pacman-key", "--init", "&&", - config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux" + config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux", ] return command @@ -340,7 +341,7 @@ def postinstall_dependencies(app=None): command = [] logging.debug("Performing post-install dependencies…") if config.OS_NAME == "Steam": - postinstall_dependencies_steamos(app) + command = postinstall_dependencies_steamos() else: logging.debug("No post-install dependencies required.") return command @@ -430,25 +431,28 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if preinstall_command: command.extend(preinstall_command) if install_command: - if preinstall_command: + if command: command.append('&&') command.extend(install_command) if remove_command: - if preinstall_command or install_command: + if command: command.append('&&') command.extend(remove_command) if postinstall_command: - if preinstall_command or install_command or remove_command: + if command: command.append('&&') command.extend(postinstall_command) if app and config.DIALOG == 'tk': app.root.event_generate('<>') msg.status("Installing dependencies…", app) - final_command = [f"{config.SUPERUSER_COMMAND}", 'sh', '-c'] + command + final_command = [ + f"{config.SUPERUSER_COMMAND}", 'sh', '-c', "'", *command, "'" + ] try: - logging.debug(f"Attempting to run this command: {final_command}") - run_command(final_command) + command_str = ' '.join(final_command) + logging.debug(f"Attempting to run this command: {command_str}") + run_command(command_str, shell=True) except subprocess.CalledProcessError as e: logging.error(f"An error occurred: {e}") logging.error(f"Command output: {e.output}") From 8fc670db5a230414326a4852903ef84ae81745d4 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 12 Aug 2024 17:00:23 -0400 Subject: [PATCH 113/253] Modify conflicting pkg add --- system.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/system.py b/system.py index cff4779d..071ce3d2 100644 --- a/system.py +++ b/system.py @@ -229,23 +229,22 @@ def query_packages(packages, mode="install", app=None): if mode == 'install': status[p] = "Installed" elif mode == 'remove': + conflicting_packages.append(p) status[p] = 'Conflicting' break else: if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": # noqa: E501 status[p] = "Installed" - break elif line.strip().startswith(p) and mode == "remove": conflicting_packages.append(p) status[p] = "Conflicting" - break + break if status[p] == "Unchecked": if mode == "install": missing_packages.append(p) status[p] = "Missing" elif mode == "remove": - conflicting_packages.append(p) status[p] = "Not Installed" if mode == "install": From 4d17504877efeef92eff8e98497a8cefd17a2a4a Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 13 Aug 2024 08:43:15 -0500 Subject: [PATCH 114/253] Continued work fixing deps install - return install_deps func if no command to run - add error message box for GUI - exit on error if AppImageLauncher installed - add missing space - auto-remove AppImageLauncher if installed - fix appimagelauncher removal command - fix clobbered variable - ask user confirmation before removing AppImageLauncher - add secondary text to continue question - fix title and text for GUI question - add note about system reboot in log - use CLI continue question for removing appimagelauncher if not tk - add explanation to cli_question - use minimal set of arch deps - remove --overwrite from pacman command for arch - Revert "remove --overwrite from pacman command for arch" - This reverts commit 8cd077c26da216367609a02f95c8d38d21af2f2e. - return False for appimage wine version check if fuse not yet installed - Revert "return False for appimage wine version check if fuse not yet installed" - This reverts commit aefb28c5aaa2acc4de5fbe51a13ad5700c1e5a21. - use glob instead of rglob for listing appimages - escape '*' in pacman command - force non-sh pkexec for arch dep installs - Revert "escape '*' in pacman command" - This reverts commit ab2ec57882e9ffd648067fcc96c92fe2919aee73. - skip extra check for fuse package - fix variable definition - Revert "force non-sh pkexec for arch dep installs" - This reverts commit 470d198a50a1d58bf023067a491f0d450e5eebb5. - put quotes around command - use normal str instead of raw for '*' - double-escape asterisk - add error message for fedora and arch dep handling - clean up suggested dep install command - clean swap pkexec out for sudo - add detail text to GUI logos_error - log package status for debugging - add detail for general logos_error - simplify hasattr checks - build showerror parameters according to passed values - fix parent param - auto-add command to clipboard in GUI - destroy error window on exit - fix typo - various pep8 fixes - add debugging info to query_packages - add more debug output - move "break" statement - add some audio/video pkgs - update arch package list - remove extra debug logging - update fedora packages - use rpm command for dnf workaround --- control.py | 6 +-- gui.py | 33 ++++++++++++++--- gui_app.py | 7 ++-- main.py | 12 ++++++ msg.py | 49 +++++++++++++++++------- system.py | 107 +++++++++++++++++++++++++++++++++++++++++------------ utils.py | 2 +- wine.py | 20 ++++++---- 8 files changed, 179 insertions(+), 57 deletions(-) diff --git a/control.py b/control.py index fbe7677d..252a2d7e 100644 --- a/control.py +++ b/control.py @@ -20,7 +20,7 @@ import network import system import tui_curses -import tui_app +# import tui_app import utils # import wine @@ -252,7 +252,7 @@ def remove_all_index_files(app=None): logging.error(f"Error removing {file_to_remove}: {e}") msg.logos_msg("======= Removing all LogosBible index files done! =======") - if app and hasattr(app, 'status_evt'): + if hasattr(app, 'status_evt'): app.root.event_generate(app.status_evt) sys.exit(0) @@ -291,7 +291,7 @@ def set_winetricks(): "1: Use local winetricks.", "2: Download winetricks from the Internet" ] - winetricks_choice = tui_curses.menu(options, title, question_text) + winetricks_choice = tui_curses.menu(options, title, question_text) # noqa: E501 logging.debug(f"winetricks_choice: {winetricks_choice}") if winetricks_choice.startswith("1"): diff --git a/gui.py b/gui.py index a8d89378..716b16bb 100644 --- a/gui.py +++ b/gui.py @@ -2,6 +2,7 @@ from tkinter import BooleanVar from tkinter import font from tkinter import IntVar +from tkinter import messagebox from tkinter import simpledialog from tkinter import StringVar from tkinter.ttk import Button @@ -335,11 +336,6 @@ def hide_tooltip(self, event=None): self.tooltip_visible = False -def input_prompt(root, title, prompt): - # Prompt for the password - input = simpledialog.askstring(title, prompt, show='*', parent=root) - return input - class PromptGui(Frame): def __init__(self, root, title="", prompt="", **kwargs): super(PromptGui, self).__init__(root, **kwargs) @@ -350,5 +346,30 @@ def __init__(self, root, title="", prompt="", **kwargs): self.options['prompt'] = prompt def draw_prompt(self): - store_button = Button(self.root, text="Store Password", command=lambda: input_prompt(self.root, self.options)) + store_button = Button( + self.root, + text="Store Password", + command=lambda: input_prompt(self.root, self.options) + ) store_button.pack(pady=20) + + +def show_error(message, title="Fatal Error", detail=None, app=None, parent=None): # noqa: E501 + kwargs = {'message': message} + if parent and hasattr(app, parent): + kwargs['parent'] = app.__dict__.get(parent) + if detail: + kwargs['detail'] = detail + messagebox.showerror(title, **kwargs) + if hasattr(app, 'root'): + app.root.destroy() + + +def ask_question(question, secondary): + return messagebox.askquestion(question, secondary) + + +def input_prompt(root, title, prompt): + # Prompt for the password + input = simpledialog.askstring(title, prompt, show='*', parent=root) + return input diff --git a/gui_app.py b/gui_app.py index c7a0667e..88e00193 100644 --- a/gui_app.py +++ b/gui_app.py @@ -688,12 +688,13 @@ def configure_app_button(self, evt=None): def run_installer(self, evt=None): classname = "LogosLinuxInstaller" - self.new_win = Toplevel() - InstallerWindow(self.new_win, self.root, class_=classname) + self.installer_win = Toplevel() + InstallerWindow(self.installer_win, self.root, class_=classname) self.root.icon = config.LOGOS_ICON_URL def run_logos(self, evt=None): - #TODO: Add reference to App here so the status message is sent to the GUI? See msg.status and wine.run_logos + # TODO: Add reference to App here so the status message is sent to the + # GUI? See msg.status and wine.run_logos t = Thread(target=wine.run_logos) t.start() diff --git a/main.py b/main.py index 768b252e..92157bba 100755 --- a/main.py +++ b/main.py @@ -13,6 +13,7 @@ import msg import network import os +import shutil import sys import system import tui_app @@ -388,6 +389,17 @@ def main(): logging.info(f"{config.LLI_TITLE}, {config.LLI_CURRENT_VERSION} by {config.LLI_AUTHOR}.") # noqa: E501 logging.debug(f"Installer log file: {config.LOGOS_LOG}") + # Check for AppImageLauncher + if shutil.which('AppImageLauncher'): + question_text = "Remove AppImageLauncher?" + secondary = ( + "LogosLinuxInstaller is not compatible with AppImageLauncher.\n" + "Remove AppImageLauncher now? A reboot will be required." + ) + no_text = "User declined to remove AppImageLauncher." + msg.logos_continue_question(question_text, no_text, secondary) + system.remove_appimagelauncher() + network.check_for_updates() # Check if app is installed. diff --git a/msg.py b/msg.py index db2d6acd..74e8c416 100644 --- a/msg.py +++ b/msg.py @@ -10,6 +10,8 @@ from pathlib import Path import config +from gui import ask_question +from gui import show_error logging.console_log = [] @@ -142,17 +144,25 @@ def logos_warn(message): logos_msg(message) -def logos_error(message, secondary=None): +def logos_error(message, secondary=None, detail=None, app=None, parent=None): + if detail is None: + message = f"{message}\n{detail}" + logging.critical(message) WIKI_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki" # noqa: E501 TELEGRAM_LINK = "https://t.me/linux_logos" MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 - if config.DIALOG == 'curses' and secondary != "info": - logging.critical(message) + if config.DIALOG == 'tk': + show_error( + message, + detail=f"{detail}\n{help_message}", + app=app, + parent=parent + ) + elif config.DIALOG == 'curses' and secondary != "info": status(message) status(help_message) elif secondary != "info": - logging.critical(message) logos_msg(message) else: logos_msg(message) @@ -163,13 +173,17 @@ def logos_error(message, secondary=None): except FileNotFoundError: # no pid file when testing functions pass os.kill(os.getpgid(os.getpid()), signal.SIGKILL) + + if hasattr(app, 'destroy'): + app.destroy() sys.exit(1) -def cli_question(QUESTION_TEXT): +def cli_question(question_text, secondary): while True: try: - yn = input(f"{QUESTION_TEXT} [Y/n]: ") + cli_msg(secondary) + yn = input(f"{question_text} [Y/n]: ") except KeyboardInterrupt: print() logos_error("Cancelled with Ctrl+C") @@ -182,9 +196,14 @@ def cli_question(QUESTION_TEXT): logos_msg("Type Y[es] or N[o].") -def cli_continue_question(QUESTION_TEXT, NO_TEXT, SECONDARY): - if not cli_question(QUESTION_TEXT): - logos_error(NO_TEXT, SECONDARY) +def cli_continue_question(question_text, no_text, secondary): + if not cli_question(question_text, secondary): + logos_error(no_text) + + +def gui_continue_question(question_text, no_text, secondary): + if ask_question(question_text, secondary) == 'no': + logos_error(no_text) def cli_acknowledge_question(QUESTION_TEXT, NO_TEXT): @@ -204,11 +223,15 @@ def cli_ask_filepath(question_text): return answer.strip('"').strip("'") -def logos_continue_question(QUESTION_TEXT, NO_TEXT, SECONDARY): - if config.DIALOG == 'curses': +def logos_continue_question(question_text, no_text, secondary, app=None): + if config.DIALOG == 'tk': + gui_continue_question(question_text, no_text, secondary) + elif app is None: + cli_continue_question(question_text, no_text, secondary) + elif config.DIALOG == 'curses': pass else: - cli_continue_question(QUESTION_TEXT, NO_TEXT, SECONDARY) + logos_error(f"Unhandled question: {question_text}") def logos_acknowledge_question(QUESTION_TEXT, NO_TEXT): @@ -249,7 +272,7 @@ def status(text, app=None): app.root.event_generate('<>') elif config.DIALOG == 'curses': app.status_q.put(f"{timestamp} {text}") - app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) + app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) # noqa: E501 logging.info(f"{text}") else: '''Prints message to stdout regardless of log level.''' diff --git a/system.py b/system.py index 071ce3d2..64a58418 100644 --- a/system.py +++ b/system.py @@ -52,7 +52,7 @@ def run_command(command, retries=1, delay=0, **kwargs): ) return result except subprocess.CalledProcessError as e: - logging.error(f"Error occurred while executing {command}: {e}") + logging.error(f"Error occurred while executing \"{command}\": {e}") if "lock" in str(e): logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") # noqa: E501 time.sleep(delay) @@ -151,16 +151,21 @@ def get_package_manager(): config.QUERY_PREFIX = '.i ' config.PACKAGES = "binutils cabextract fuse3 wget winbind" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appimagelauncher" + config.BADPACKAGES = "" # appimagelauncher handled separately elif shutil.which('dnf') is not None: # rhel, fedora config.PACKAGE_MANAGER_COMMAND_INSTALL = ["dnf", "install", "-y"] config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["dnf", "install", "--downloadonly", "-y"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_REMOVE = ["dnf", "remove", "-y"] - config.PACKAGE_MANAGER_COMMAND_QUERY = ["dnf", "list", "installed"] + # config.PACKAGE_MANAGER_COMMAND_QUERY = ["dnf", "list", "installed"] + config.PACKAGE_MANAGER_COMMAND_QUERY = ["rpm", "-qa"] # workaround config.QUERY_PREFIX = '' - config.PACKAGES = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 + # config.PACKAGES = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 + config.PACKAGES = ( + "fuse3 fuse3-libs " # appimages + "mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract " # wine # noqa: E501 + ) config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appiamgelauncher" + config.BADPACKAGES = "" # appimagelauncher handled separately elif shutil.which('pamac') is not None: # manjaro config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pamac", "install", "--no-upgrade", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pamac", "install", "--no-upgrade", "--download-only", "--no-confirm"] # noqa: E501 @@ -169,9 +174,9 @@ def get_package_manager(): config.QUERY_PREFIX = '' config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appimagelauncher" + config.BADPACKAGES = "" # appimagelauncher handled separately elif shutil.which('pacman') is not None: # arch, steamOS - config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pacman", "-Syu", "--overwrite", r"*", "--noconfirm", "--needed"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pacman", "-Syu", "--overwrite", "\\*", "--noconfirm", "--needed"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pacman", "-Sw", "-y"] config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pacman", "-R", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = ["pacman", "-Q"] @@ -179,9 +184,18 @@ def get_package_manager(): if config.OS_NAME == "steamos": # steamOS config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: #E501 else: # arch - config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 + # config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" # noqa: E501 + config.PACKAGES = ( + "fuse2 fuse3 " # appimages + "binutils cabextract wget libwbclient " # wine + "openjpeg2 libxcomposite libxinerama " # display + "ocl-icd vulkan-icd-loader " # hardware + "alsa-plugins gst-plugins-base-libs libpulse openal " # audio + "libva mpg123 v4l-utils " # video + "libxslt sqlite " # misc + ) config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "appimagelauncher" + config.BADPACKAGES = "" # appimagelauncher handled separately # Add more conditions for other package managers as needed # Add logging output. @@ -208,16 +222,18 @@ def query_packages(packages, mode="install", app=None): result = run_command(command) except Exception as e: logging.error(f"Error occurred while executing command: {e}") - logging.error(result.stderr) + logging.error(e.output) package_list = result.stdout + logging.debug(f"packages to check: {packages}") status = {package: "Unchecked" for package in packages} if app is not None: - for p in packages: + logging.debug(f"Checking for: {p}") l_num = 0 for line in package_list.split('\n'): + # logging.debug(f"{line=}") l_num += 1 if config.PACKAGE_MANAGER_COMMAND_QUERY[0] == 'dpkg': parts = line.strip().split() @@ -234,11 +250,13 @@ def query_packages(packages, mode="install", app=None): break else: if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": # noqa: E501 + logging.debug(f"'{p}' installed: {line}") status[p] = "Installed" + break elif line.strip().startswith(p) and mode == "remove": conflicting_packages.append(p) status[p] = "Conflicting" - break + break if status[p] == "Unchecked": if mode == "install": @@ -246,6 +264,9 @@ def query_packages(packages, mode="install", app=None): status[p] = "Missing" elif mode == "remove": status[p] = "Not Installed" + logging.debug(f"{p} status: {status.get(p)}") + + logging.debug(f"Packages status: {status}") if mode == "install": if missing_packages: @@ -302,6 +323,23 @@ def parse_date(version): return None +def remove_appimagelauncher(app=None): + pkg = "appimagelauncher" + cmd = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE] + cmd.append(pkg) + msg.status("Removing AppImageLauncher…", app) + try: + logging.debug(f"Running command: {cmd}") + run_command(cmd) + except subprocess.CalledProcessError as e: + logging.error(f"An error occurred: {e}") + logging.error(f"Command output: {e.output}") + msg.logos_error("Failed to uninstall AppImageLauncher.") + sys.exit(1) + logging.info("System reboot is required.") + sys.exit() + + def preinstall_dependencies_steamos(): logging.debug("Disabling read only, updating pacman keys…") command = [ @@ -407,9 +445,10 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) else: fuse = "libfuse" - install_fuse = check_libs([f"{fuse}"], app=app) - if not install_fuse: - missing_packages.append(fuse) + fuse_lib_installed = check_libs([f"{fuse}"], app=app) + logging.debug(f"{fuse_lib_installed=}") + # if not fuse_lib_installed: + # missing_packages.append(fuse) if missing_packages: install_command = config.PACKAGE_MANAGER_COMMAND_INSTALL + missing_packages # noqa: E501 @@ -441,6 +480,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if command: command.append('&&') command.extend(postinstall_command) + if not command: # nothing to run; avoid running empty pkexec command + return if app and config.DIALOG == 'tk': app.root.event_generate('<>') @@ -448,13 +489,33 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) final_command = [ f"{config.SUPERUSER_COMMAND}", 'sh', '-c', "'", *command, "'" ] - try: - command_str = ' '.join(final_command) - logging.debug(f"Attempting to run this command: {command_str}") - run_command(command_str, shell=True) - except subprocess.CalledProcessError as e: - logging.error(f"An error occurred: {e}") - logging.error(f"Command output: {e.output}") + command_str = ' '.join(final_command) + # TODO: Fix fedora/arch handling. + if config.OS_NAME in ['fedora', 'arch']: + sudo_command = command_str.replace("pkexec", "sudo") + message = "The system needs to install/remove packages." + detail = ( + "Please run the following command in a terminal, then restart " + f"LogosLinuxInstaller:\n{sudo_command}\n" + ) + if hasattr(app, 'root'): + detail += "\nThe command has been copied to the clipboard." + app.root.clipboard_clear() + app.root.clipboard_append(sudo_command) + app.root.update() + msg.logos_error( + message, + detail=detail, + app=app, + parent='installer_win' + ) + else: + try: + logging.debug(f"Attempting to run this command: {command_str}") + run_command(command_str, shell=True) + except subprocess.CalledProcessError as e: + logging.error(f"An error occurred: {e}") + logging.error(f"Command output: {e.output}") else: msg.logos_error( f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. " # noqa: E501 @@ -464,7 +525,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) # TODO: Verify with user before executing if config.REBOOT_REQUIRED: pass - #reboot() + # reboot() def have_lib(library, ld_library_path): diff --git a/utils.py b/utils.py index 48979538..36da8d8e 100644 --- a/utils.py +++ b/utils.py @@ -659,7 +659,7 @@ def find_appimage_files(release_version, app=None): raise RuntimeError("Python 3.12 or higher is required for .rglob() flag `case-sensitive` ") # noqa: E501 for d in directories: - appimage_paths = Path(d).rglob('wine*.appimage', case_sensitive=False) + appimage_paths = Path(d).glob('wine*.appimage', case_sensitive=False) for p in appimage_paths: if p is not None and check_appimage(p): output1, output2 = wine.check_wine_version_and_branch( diff --git a/wine.py b/wine.py index 9fab2a7e..05b80b6d 100644 --- a/wine.py +++ b/wine.py @@ -215,12 +215,16 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): env = get_wine_env() if config.WINECMD_ENCODING is None: # Get wine system's cmd.exe encoding for proper decoding to UTF8 later. - registry_value = get_registry_value('HKCU\\Software\\Wine\\Fonts', 'Codepages') + registry_value = get_registry_value( + 'HKCU\\Software\\Wine\\Fonts', + 'Codepages' + ) if registry_value is not None: codepages = registry_value.split(',') # noqa: E501 config.WINECMD_ENCODING = codepages[-1] else: - logging.error("wine.wine_proc: wine.get_registry_value returned None.") + m = "wine.wine_proc: wine.get_registry_value returned None." + logging.error(m) logging.debug(f"run_wine_proc: {winecmd}; {exe=}; {exe_args=}") wine_env_vars = {k: v for k, v in env.items() if k.startswith('WINE')} logging.debug(f"wine environment: {wine_env_vars}") @@ -252,7 +256,7 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): if config.WINECMD_ENCODING is not None: logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 else: - logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") + logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 returncode = process.wait() if returncode != 0: @@ -320,7 +324,7 @@ def installICUDataFiles(app=None): os.makedirs(icu_win_dir) shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok=True) - if app and hasattr(app, 'status_evt'): + if hasattr(app, 'status_evt'): app.status_q.put("ICU files copied.") app.root.event_generate(app.status_evt) @@ -485,9 +489,9 @@ def get_wine_env(): def run_logos(app=None): logos_release = utils.convert_logos_release(config.current_logos_version) wine_release, _ = get_wine_release(config.WINE_EXE) - - #TODO: Find a way to incorporate check_wine_version_and_branch() - if 30 > logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): + + # TODO: Find a way to incorporate check_wine_version_and_branch() + if 30 > logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): # noqa: E501 txt = "Can't run Logos 10+ with Wine below 7.18." logging.critical(txt) msg.status(txt, app) @@ -515,7 +519,7 @@ def run_indexing(): def end_wine_processes(): for process_name, process in processes.items(): if isinstance(process, subprocess.Popen): - logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") + logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") # noqa: E501 try: process.terminate() process.wait(timeout=10) From 1af159274f836fe17d7480f3eec20523bad766cd Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 20 Aug 2024 21:12:25 -0400 Subject: [PATCH 115/253] Continued work on deps install - Fix up msg.logos_continue_question. - Add Curses ConfirmDialog - Allow Curses to wrap text with newlines - Properly detect if user cancels pkexec - Handle TUI manual installation required --- installer.py | 3 ++ main.py | 5 ++- msg.py | 4 +- system.py | 111 +++++++++++++++++++++++++++++++++++++------------- tui_app.py | 78 +++++++++++++++++++++++++---------- tui_curses.py | 14 +++++-- tui_dialog.py | 39 ++++++++++++------ tui_screen.py | 40 +++++++++++++++++- 8 files changed, 224 insertions(+), 70 deletions(-) diff --git a/installer.py b/installer.py index 7313f7d9..53b22876 100644 --- a/installer.py +++ b/installer.py @@ -271,6 +271,9 @@ def ensure_sys_deps(app=None): if not config.SKIP_DEPENDENCIES: utils.check_dependencies(app) + if app: + if config.DIALOG == "curses": + app.installdeps_e.wait() logging.debug("> Done.") else: logging.debug("> Skipped.") diff --git a/main.py b/main.py index 92157bba..37adbe83 100755 --- a/main.py +++ b/main.py @@ -391,10 +391,11 @@ def main(): # Check for AppImageLauncher if shutil.which('AppImageLauncher'): - question_text = "Remove AppImageLauncher?" + question_text = "Remove AppImageLauncher? A reboot will be required." secondary = ( + "Your system currently has AppImageLauncher installed.\n" "LogosLinuxInstaller is not compatible with AppImageLauncher.\n" - "Remove AppImageLauncher now? A reboot will be required." + "For more information, see: https://github.com/FaithLife-Community/LogosLinuxInstaller/issues/114" ) no_text = "User declined to remove AppImageLauncher." msg.logos_continue_question(question_text, no_text, secondary) diff --git a/msg.py b/msg.py index 74e8c416..7762f93c 100644 --- a/msg.py +++ b/msg.py @@ -12,6 +12,7 @@ import config from gui import ask_question from gui import show_error +import utils logging.console_log = [] @@ -144,6 +145,7 @@ def logos_warn(message): logos_msg(message) +#TODO: I think detail is doing the same thing as secondary. def logos_error(message, secondary=None, detail=None, app=None, parent=None): if detail is None: message = f"{message}\n{detail}" @@ -229,7 +231,7 @@ def logos_continue_question(question_text, no_text, secondary, app=None): elif app is None: cli_continue_question(question_text, no_text, secondary) elif config.DIALOG == 'curses': - pass + app.screen_q.put(app.stack_confirm(16, app.confirm_q, app.confirm_e, question_text, no_text, secondary, dialog=config.use_python_dialog)) else: logos_error(f"Unhandled question: {question_text}") diff --git a/system.py b/system.py index 64a58418..8792257a 100644 --- a/system.py +++ b/system.py @@ -52,7 +52,7 @@ def run_command(command, retries=1, delay=0, **kwargs): ) return result except subprocess.CalledProcessError as e: - logging.error(f"Error occurred while executing \"{command}\": {e}") + logging.error(f"Error occurred in run_command() while executing \"{command}\": {e}") if "lock" in str(e): logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") # noqa: E501 time.sleep(delay) @@ -332,8 +332,11 @@ def remove_appimagelauncher(app=None): logging.debug(f"Running command: {cmd}") run_command(cmd) except subprocess.CalledProcessError as e: - logging.error(f"An error occurred: {e}") - logging.error(f"Command output: {e.output}") + if e.returncode == 127: + logging.error("User cancelled appimagelauncher removal.") + else: + logging.error(f"An error occurred: {e}") + logging.error(f"Command output: {e.output}") msg.logos_error("Failed to uninstall AppImageLauncher.") sys.exit(1) logging.info("System reboot is required.") @@ -388,6 +391,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if config.SKIP_DEPENDENCIES: return + install_deps_failed = False + manual_install_required = False command = [] preinstall_command = [] install_command = [] @@ -420,17 +425,32 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) ) if config.PACKAGE_MANAGER_COMMAND_INSTALL: - if missing_packages and conflicting_packages: - message = f"Your {config.OS_NAME} computer requires installing and removing some software. To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}\nand will remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}\nProceed?" # noqa: E501 - # logging.critical(message) + if config.OS_NAME in ['fedora', 'arch']: + message = False + elif missing_packages and conflicting_packages: + message = f"Your {config.OS_NAME} computer requires installing and removing some software.\nProceed?" # noqa: E501 + no_message = "User refused to install and remove software via the application" # noqa: E501 + secondary = f"To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}\nand will remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}" # noqa: E501 elif missing_packages: - message = f"Your {config.OS_NAME} computer requires installing some software. To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}\nProceed?" # noqa: E501 - # logging.critical(message) + message = f"Your {config.OS_NAME} computer requires installing some software.\nProceed?" # noqa: E501 + no_message = "User refused to install software via the application." # noqa: E501 + secondary = f"To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}" # noqa: E501 elif conflicting_packages: - message = f"Your {config.OS_NAME} computer requires removing some software. To continue, the program will attempt to remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}\nProceed?" # noqa: E501 - # logging.critical(message) + message = f"Your {config.OS_NAME} computer requires removing some software.\nProceed?" # noqa: E501 + no_message = "User refused to remove software via the application." # noqa: E501 + secondary = f"To continue, the program will attempt to remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}" # noqa: E501 else: + message = None + + if message is None: logging.debug("No missing or conflicting dependencies found.") + elif not message: + logging.error("Your distro requires manual dependency installation.") + else: + msg.logos_continue_question(message, no_message, secondary, app) + if app: + if config.DIALOG == "curses": + app.confirm_e.wait() # TODO: Need to send continue question to user based on DIALOG. # All we do above is create a message that we never send. @@ -492,40 +512,75 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) command_str = ' '.join(final_command) # TODO: Fix fedora/arch handling. if config.OS_NAME in ['fedora', 'arch']: + manual_install_required = True sudo_command = command_str.replace("pkexec", "sudo") - message = "The system needs to install/remove packages." + message = "The system needs to install/remove packages, but it requires manual intervention." detail = ( "Please run the following command in a terminal, then restart " f"LogosLinuxInstaller:\n{sudo_command}\n" ) - if hasattr(app, 'root'): - detail += "\nThe command has been copied to the clipboard." - app.root.clipboard_clear() - app.root.clipboard_append(sudo_command) - app.root.update() - msg.logos_error( - message, - detail=detail, - app=app, - parent='installer_win' - ) - else: + if app: + if config.DIALOG == "tk": + if hasattr(app, 'root'): + detail += "\nThe command has been copied to the clipboard." + app.root.clipboard_clear() + app.root.clipboard_append(sudo_command) + app.root.update() + msg.logos_error( + message, + detail=detail, + app=app, + parent='installer_win' + ) + install_deps_failed = True + else: + msg.logos_error(message + "\n" + detail) + install_deps_failed = True + + if manual_install_required and app and config.DIALOG == "curses": + app.screen_q.put(app.stack_confirm(17, app.todo_q, app.todo_e, + f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\nLogosLinuxInstaller:\n{sudo_command}\n", # noqa: E501 + "User cancelled dependency installation.", # noqa: E501 + message, # noqa: E501 + options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) + app.manualinstall_e.wait() + + + if not install_deps_failed and not manual_install_required: try: logging.debug(f"Attempting to run this command: {command_str}") run_command(command_str, shell=True) except subprocess.CalledProcessError as e: - logging.error(f"An error occurred: {e}") - logging.error(f"Command output: {e.output}") + if e.returncode == 127: + logging.error("User cancelled dependency installation.") + else: + logging.error(f"An error occurred in install_dependencies(): {e}") + logging.error(f"Command output: {e.output}") + install_deps_failed = True else: msg.logos_error( f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. " # noqa: E501 f"Your computer is missing the command(s) {missing_packages}. " f"Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 - # TODO: Verify with user before executing if config.REBOOT_REQUIRED: - pass - # reboot() + question = "Should the program reboot the host now?" # noqa: E501 + no_text = "The user has chosen not to reboot." + secondary = "The system has installed or removed a package that requires a reboot." + if msg.logos_continue_question(question, no_text, secondary): + reboot() + else: + logging.error("Cannot proceed until reboot. Exiting.") + sys.exit(1) + + if install_deps_failed: + if app: + if config.DIALOG == "curses": + app.choice_q.put("Return to Main Menu") + else: + if app: + if config.DIALOG == "curses": + app.installdeps_e.set() def have_lib(library, ld_library_path): diff --git a/tui_app.py b/tui_app.py index a3a75af2..3b5689ed 100644 --- a/tui_app.py +++ b/tui_app.py @@ -60,6 +60,10 @@ def __init__(self, stdscr): self.releases_e = threading.Event() self.release_q = Queue() self.release_e = threading.Event() + self.manualinstall_q = Queue() + self.manualinstall_e = threading.Event() + self.installdeps_q = Queue() + self.installdeps_e = threading.Event() self.installdir_q = Queue() self.installdir_e = threading.Event() self.wine_q = Queue() @@ -72,6 +76,8 @@ def __init__(self, stdscr): self.finished_e = threading.Event() self.config_q = Queue() self.config_e = threading.Event() + self.confirm_q = Queue() + self.confirm_e = threading.Event() self.password_q = Queue() self.password_e = threading.Event() self.appimage_q = Queue() @@ -275,8 +281,6 @@ def task_processor(self, evt=None, task=None): self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) - # elif task == 'PASSWORD': - # utils.start_thread(self.get_password(config.use_python_dialog)) def choice_processor(self, stdscr, screen_id, choice): if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): @@ -345,48 +349,54 @@ def choice_processor(self, stdscr, screen_id, choice): appimage_filename = choice config.SELECTED_APPIMAGE_FILENAME = appimage_filename utils.set_appimage_symlink() + self.menu_screen.choice = "Processing" self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) self.appimage_e.set() elif screen_id == 2: - if str(choice).startswith("Logos"): - config.FLPRODUCT = "Logos" - self.product_q.put(config.FLPRODUCT) - self.product_e.set() - elif str(choice).startswith("Verbum"): - config.FLPRODUCT = "Verbum" + if choice: + if str(choice).startswith("Logos"): + config.FLPRODUCT = "Logos" + elif str(choice).startswith("Verbum"): + config.FLPRODUCT = "Verbum" + self.menu_screen.choice = "Processing" self.product_q.put(config.FLPRODUCT) self.product_e.set() elif screen_id == 3: - if "10" in choice: - config.TARGETVERSION = "10" - self.version_q.put(config.TARGETVERSION) - self.version_e.set() - elif "9" in choice: - config.TARGETVERSION = "9" + if choice: + if "10" in choice: + config.TARGETVERSION = "10" + elif "9" in choice: + config.TARGETVERSION = "9" + self.menu_screen.choice = "Processing" self.version_q.put(config.TARGETVERSION) self.version_e.set() elif screen_id == 4: if choice: config.TARGET_RELEASE_VERSION = choice + self.menu_screen.choice = "Processing" self.release_q.put(config.TARGET_RELEASE_VERSION) self.release_e.set() elif screen_id == 5: if choice: config.INSTALLDIR = choice config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + self.menu_screen.choice = "Processing" self.installdir_q.put(config.INSTALLDIR) self.installdir_e.set() elif screen_id == 6: config.WINE_EXE = choice if choice: + self.menu_screen.choice = "Processing" self.wine_q.put(config.WINE_EXE) self.wine_e.set() elif screen_id == 7: winetricks_options = utils.get_winetricks_options() if choice.startswith("Download"): + self.menu_screen.choice = "Processing" self.tricksbin_q.put("Download") self.tricksbin_e.set() else: + self.menu_screen.choice = "Processing" config.WINETRICKSBIN = winetricks_options[0] self.tricksbin_q.put(config.WINETRICKSBIN) self.tricksbin_e.set() @@ -399,6 +409,7 @@ def choice_processor(self, stdscr, screen_id, choice): utils.write_config(config.CONFIG_FILE) else: logging.info("Config file left unchanged.") + self.menu_screen.choice = "Processing" self.config_q.put(True) self.config_e.set() self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Finishing install…", dialog=config.use_python_dialog)) @@ -408,6 +419,7 @@ def choice_processor(self, stdscr, screen_id, choice): pass elif screen_id == 12: if choice: + self.menu_screen.choice = "Processing" wine.run_logos(self) self.switch_q.put(1) elif screen_id == 13: @@ -416,8 +428,28 @@ def choice_processor(self, stdscr, screen_id, choice): pass elif screen_id == 15: if choice: + self.menu_screen.choice = "Processing" self.password_q.put(choice) self.password_e.set() + elif screen_id == 16: + if choice == "No": + self.menu_screen.choice = "Processing" + self.choice_q.put("Return to Main Menu") + else: + self.menu_screen.choice = "Processing" + self.confirm_e.set() + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, + "Installing dependencies…\n", wait=True, + dialog=config.use_python_dialog)) + elif screen_id == 17: + if choice == "Yes": + self.menu_screen.choice = "Processing" + self.manualinstall_e.set() + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, + "Installing dependencies…\n", wait=True, + dialog=config.use_python_dialog)) + else: + self.llirunning = False def switch_screen(self, dialog): if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: @@ -497,14 +529,15 @@ def get_waiting(self, dialog, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) - self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, wait=True, percent=percent, - dialog=dialog)) + self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, + wait=True, percent=percent, dialog=dialog)) def get_config(self, dialog): question = f"Update config file at {config.CONFIG_FILE}?" labels = ["Yes", "No"] options = self.which_dialog_options(labels, dialog) self.menu_options = options + #TODO: Switch to msg.logos_continue_message self.screen_q.put(self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog)) # def get_password(self, dialog): @@ -598,13 +631,16 @@ def stack_password(self, screen_id, queue, event, question, default="", dialog=F utils.append_unique(self.tui_screens, tui_screen.PasswordScreen(self, screen_id, queue, event, question, default)) - def stack_confirm(self, screen_id, queue, event, question, options, dialog=False): + def stack_confirm(self, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"], dialog=False): if dialog: - utils.append_unique(self.tui_screens, - tui_screen.ConfirmDialog(self, screen_id, queue, event, question, options)) + yes_label = options[0] + no_label = options[1] + utils.append_unique(self.tui_screens, tui_screen.ConfirmDialog(self, screen_id, queue, event, + question, no_text, secondary, + yes_label=yes_label, no_label=no_label)) else: - #TODO: curses version - pass + utils.append_unique(self.tui_screens, tui_screen.ConfirmScreen(self, screen_id, queue, event, + question, no_text, secondary, options)) def stack_text(self, screen_id, queue, event, text, wait=False, percent=None, dialog=False): if dialog: diff --git a/tui_curses.py b/tui_curses.py index 78ffe239..31f58819 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -10,8 +10,13 @@ def wrap_text(app, text): # Turn text into wrapped text, line by line, centered - wrapped_text = textwrap.fill(text, app.window_width - 4) - lines = wrapped_text.split('\n') + if "\n" in text: + lines = text.splitlines() + wrapped_lines = [textwrap.fill(line, app.window_width - 4) for line in lines] + lines = '\n'.join(wrapped_lines) + else: + wrapped_text = textwrap.fill(text, app.window_width - 4) + lines = wrapped_text.split('\n') return lines @@ -30,7 +35,10 @@ def title(app, title_text, title_start_y_adj): def text_centered(app, text, start_y=0): stdscr = app.get_menu_window() - text_lines = wrap_text(app, text) + if "\n" in text: + text_lines = wrap_text(app, text).splitlines() + else: + text_lines = wrap_text(app, text) text_start_y = start_y text_width = max(len(line) for line in text_lines) for i, line in enumerate(text_lines): diff --git a/tui_dialog.py b/tui_dialog.py index 8955d0f9..121d2d17 100644 --- a/tui_dialog.py +++ b/tui_dialog.py @@ -7,7 +7,8 @@ def text(app, text, height=None, width=None, title=None, backtitle=None, colors=True): - d = Dialog() + dialog = Dialog() + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -17,11 +18,12 @@ def text(app, text, height=None, width=None, title=None, backtitle=None, colors= options['title'] = title if backtitle is not None: options['backtitle'] = backtitle - d.infobox(text, **options) + dialog.infobox(text, **options) def progress_bar(app, text, percent, height=None, width=None, title=None, backtitle=None, colors=True): - d = Dialog() + dialog = Dialog() + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -32,22 +34,25 @@ def progress_bar(app, text, percent, height=None, width=None, title=None, backti if backtitle is not None: options['backtitle'] = backtitle - d.gauge_start(text=text, percent=percent, **options) + dialog.gauge_start(text=text, percent=percent, **options) #FIXME: Not working. See tui_screen.py#262. def update_progress_bar(app, percent, text='', update_text=False): - d = Dialog() - d.gauge_update(percent, text, update_text) + dialog = Dialog() + dialog.autowidgetsize = True + dialog.gauge_update(percent, text, update_text) def stop_progress_bar(app): - d = Dialog() - d.gauge_stop() + dialog = Dialog() + dialog.autowidgetsize = True + dialog.gauge_stop() def tasklist_progress_bar(app, text, percent, elements, height=None, width=None, title=None, backtitle=None, colors=None): - d = Dialog() + dialog = Dialog() + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -63,7 +68,7 @@ def tasklist_progress_bar(app, text, percent, elements, height=None, width=None, elements_list = [(k, v) for k, v in elements.items()] try: - d.mixedgauge(text=text, percent=percent, elements=elements_list, **options) + dialog.mixedgauge(text=text, percent=percent, elements=elements_list, **options) except Exception as e: logging.debug(f"Error in mixedgauge: {e}") raise @@ -71,6 +76,7 @@ def tasklist_progress_bar(app, text, percent, elements, height=None, width=None, def input(app, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): dialog = Dialog() + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -86,6 +92,7 @@ def input(app, question_text, height=None, width=None, init="", title=None, bac def password(app, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): dialog = Dialog() + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -99,8 +106,10 @@ def password(app, question_text, height=None, width=None, init="", title=None, return code, password -def confirm(app, question_text, height=None, width=None, title=None, backtitle=None, colors=True): +def confirm(app, question_text, yes_label="Yes", no_label="No", + height=None, width=None, title=None, backtitle=None, colors=True): dialog = Dialog() + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -110,8 +119,8 @@ def confirm(app, question_text, height=None, width=None, title=None, backtitle=N options['title'] = title if backtitle is not None: options['backtitle'] = backtitle - check = dialog.yesno(question_text, **options) - return check + check = dialog.yesno(question_text, height, width, yes_label=yes_label, no_label=no_label, **options) + return check # Returns "ok" or "cancel" def directory_picker(app, path_dir, height=None, width=None, title=None, backtitle=None, colors=True): @@ -119,6 +128,7 @@ def directory_picker(app, path_dir, height=None, width=None, title=None, backtit try: dialog = Dialog() + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -141,6 +151,7 @@ def directory_picker(app, path_dir, height=None, width=None, title=None, backtit def menu(app, question_text, choices, height=None, width=None, menu_height=8, title=None, backtitle=None, colors=True): tag_to_description = {tag: description for tag, description in choices} dialog = Dialog(dialog="dialog") + dialog.autowidgetsize = True options = {'colors': colors} if title is not None: options['title'] = title @@ -160,6 +171,7 @@ def menu(app, question_text, choices, height=None, width=None, menu_height=8, ti def buildlist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # items is an interable of (tag, item, status) dialog = Dialog(dialog="dialog") + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -181,6 +193,7 @@ def buildlist(app, text, items=[], height=None, width=None, list_height=None, ti def checklist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # items is an iterable of (tag, item, status) dialog = Dialog(dialog="dialog") + dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height diff --git a/tui_screen.py b/tui_screen.py index b3c70dd2..cae55bb3 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -130,6 +130,33 @@ def set_options(self, new_options): self.app.menu_options = new_options +class ConfirmScreen(MenuScreen): + def __init__(self, app, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"]): + super().__init__(app, screen_id, queue, event, question, options, + height=None, width=None, menu_height=8) + self.no_text = no_text + self.secondary = secondary + + def __str__(self): + return f"Curses Confirm Screen" + + def display(self): + self.stdscr.erase() + self.choice = tui_curses.MenuDialog( + self.app, + self.secondary + "\n" + self.question, + self.options + ).run() + if self.choice is not None and not self.choice == "" and not self.choice == "Processing": + config.current_option = 0 + config.current_page = 0 + if self.choice == "No": + logging.critical(self.no_text) + self.submit_choice_to_queue() + self.stdscr.noutrefresh() + curses.doupdate() + + class InputScreen(CursesScreen): def __init__(self, app, screen_id, queue, event, question, default): super().__init__(app, screen_id, queue, event) @@ -272,11 +299,14 @@ def display(self): self.submit_choice_to_queue() utils.send_task(self.app, "INSTALLING_PW") + class ConfirmDialog(DialogScreen): - def __init__(self, app, screen_id, queue, event, question, yes_label="Yes", no_label="No"): + def __init__(self, app, screen_id, queue, event, question, no_text, secondary, yes_label="Yes", no_label="No"): super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.question = question + self.no_text = no_text + self.secondary = secondary self.yes_label = yes_label self.no_label = no_label @@ -286,7 +316,13 @@ def __str__(self): def display(self): if self.running == 0: self.running = 1 - _, _, self.choice = tui_dialog.confirm(self.app, self.question, self.yes_label, self.no_label) + self.choice = tui_dialog.confirm(self.app, self.secondary + self.question, + self.yes_label, self.no_label) + if self.choice == "cancel": + self.choice = self.no_label + logging.critical(self.no_text) + else: + self.choice = self.yes_label self.submit_choice_to_queue() def get_question(self): From 7967170c0a781aa9c6723206a6ba5eea3cfc2297 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 23 Aug 2024 08:35:22 -0500 Subject: [PATCH 116/253] cleanup msg.logos_error --- msg.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/msg.py b/msg.py index 7762f93c..7b4a2e1b 100644 --- a/msg.py +++ b/msg.py @@ -12,7 +12,6 @@ import config from gui import ask_question from gui import show_error -import utils logging.console_log = [] @@ -145,10 +144,10 @@ def logos_warn(message): logos_msg(message) -#TODO: I think detail is doing the same thing as secondary. +# TODO: I think detail is doing the same thing as secondary. def logos_error(message, secondary=None, detail=None, app=None, parent=None): if detail is None: - message = f"{message}\n{detail}" + detail = '' logging.critical(message) WIKI_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki" # noqa: E501 TELEGRAM_LINK = "https://t.me/linux_logos" @@ -157,15 +156,16 @@ def logos_error(message, secondary=None, detail=None, app=None, parent=None): if config.DIALOG == 'tk': show_error( message, - detail=f"{detail}\n{help_message}", + detail=f"{detail}\n\n{help_message}", app=app, parent=parent ) - elif config.DIALOG == 'curses' and secondary != "info": - status(message) - status(help_message) - elif secondary != "info": - logos_msg(message) + elif config.DIALOG == 'curses': + if secondary != "info": + status(message) + status(help_message) + else: + logos_msg(message) else: logos_msg(message) @@ -231,7 +231,17 @@ def logos_continue_question(question_text, no_text, secondary, app=None): elif app is None: cli_continue_question(question_text, no_text, secondary) elif config.DIALOG == 'curses': - app.screen_q.put(app.stack_confirm(16, app.confirm_q, app.confirm_e, question_text, no_text, secondary, dialog=config.use_python_dialog)) + app.screen_q.put( + app.stack_confirm( + 16, + app.confirm_q, + app.confirm_e, + question_text, + no_text, + secondary, + dialog=config.use_python_dialog + ) + ) else: logos_error(f"Unhandled question: {question_text}") From fd807def24bb887823ff5a0c3b7315902484703a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 23 Aug 2024 10:26:18 -0400 Subject: [PATCH 117/253] Update Logos release --- CHANGELOG.md | 5 ++++- config.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc1ff3b..abddaf05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Changelog +- 4.0.0-alpha.14 + - Fix install routine [N. Marti, T. H. Wright] + - Fix #144, #154, #156 - 4.0.0-alpha.13 - - Fix #22. [T. Bleher, J. Goodman, N. Marti, S. Freilichtbuenhe, M. Malevic, T. Wright] + - Fix #22. [T. Bleher, J. Goodman, N. Marti, S. Freilichtbuenhe, M. Malevic, T. H. Wright] - Fix package installer and TUI app. Also fix #135, #136, #140. [T. H. Wright, N. Marti] - Introduce network.py and system.py - 4.0.0-alpha.12 diff --git a/config.py b/config.py index fe430f3a..bab1800a 100644 --- a/config.py +++ b/config.py @@ -62,7 +62,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.13" +LLI_CURRENT_VERSION = "4.0.0-alpha.14" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING From 317b6d771e6561349b6f7cd35b190371f6db634c Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sun, 25 Aug 2024 17:11:14 -0500 Subject: [PATCH 118/253] update README and dev scripts with full build deps --- README.md | 54 ++++++++++++++++++++++++---------------- scripts/ensure-python.sh | 7 ++++-- scripts/ensure-venv.sh | 5 +++- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e463054f..e94460d3 100644 --- a/README.md +++ b/README.md @@ -83,32 +83,44 @@ Your system might already include Python 3.12 built with Tcl/Tk. This will verif the installation: ``` $ python3 --version -Python 3.12.1 +Python 3.12.5 $ python3 -m tkinter # should open a basic Tk window ``` -If your Python version is < 3.12, then you might want to install 3.12 and tcl/tk using -your system's package manager or compile it from source using the following guide -or the script provided in `scripts/ensure-python.sh`. This is because the app is -built using 3.12 and might have errors if run with other versions. -``` -# install build dependencies; e.g. for debian-based systems: -$ apt install build-essential tcl-dev tk-dev libreadline-dev -# install & build python 3.12 -$ wget 'https://www.python.org/ftp/python/3.12.1/Python-3.12.1.tar.xz' -$ tar xf Python-3.12.1.tar.xz -$ cd Python-3.12.1 -Python-3.12.1$ ./configure --prefix=/opt --enable-shared -Python-3.12.1$ make -Python-3.12.1$ sudo make install -Python-3.12.1$ LD_LIBRARY_PATH=/opt/lib /opt/bin/python3.12 --version -Python 3.12.1 -``` -The script `scripts/ensure-python.sh` is not yet fully tested. Feedback is welcome! +If your Python version is < 3.12, then you might want to install 3.12 and tcl/tk +using your system's package manager or compile it from source using the +following guide or the script provided in `scripts/ensure-python.sh`. This is +because the app is built using 3.12 and might have errors if run with other +versions. + +**Install build dependencies** + +e.g. for debian-based systems: +``` +$ sudo apt-get install git build-essential gdb lcov pkg-config \ + libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \ + libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \ + lzma lzma-dev tk-dev uuid-dev zlib1g-dev +``` +*See Python's [Build dependencies](https://devguide.python.org/getting-started/setup-building/index.html#build-dependencies) section for further info.* + +**Install & build python 3.12** +``` +$ ver=$(wget -qO- https://www.python.org/ftp/python/ | grep -oE '3\.12\.[0-9]+' | sort -u | tail -n1) +$ wget "https://www.python.org/ftp/python/${ver}/Python-${ver}.tar.xz" +$ tar xf Python-${ver}.tar.xz +$ cd Python-${ver} +Python-3.12$ ./configure --prefix=/opt --enable-shared +Python-3.12$ make +Python-3.12$ sudo make install +Python-3.12$ LD_LIBRARY_PATH=/opt/lib /opt/bin/python3.12 --version +Python 3.12.5 +$ cd ~ +``` Both methods install python into /opt to avoid interfering with system python installations. ### Clone this repository ``` -$ git clone 'https://github.com/FaithLife-Community/LogosLinuxInstaller.git' +$ git clone 'https://github.com/FaithLife-Community/LogosLinuxInstaller.git' --depth=1 $ cd LogosLinuxInstaller LogosLinuxInstaller$ ``` @@ -122,7 +134,7 @@ LogosLinuxInstaller$ echo "LD_LIBRARY_PATH=/opt/lib" >> env/bin/activate # tell LogosLinuxInstaller$ echo "export LD_LIBRARY_PATH" >> env/bin/activate LogosLinuxInstaller$ source env/bin/activate # activate the env (env) LogosLinuxInstaller$ python --version # verify python version -Python 3.12.1 +Python 3.12.5 (env) LogosLinuxInstaller$ python -m tkinter # verify that tkinter test window opens (env) LogosLinuxInstaller$ pip install -r requirements.txt # install python packages (env) LogosLinuxInstaller$ ./main.py --help # run the script diff --git a/scripts/ensure-python.sh b/scripts/ensure-python.sh index be72e2f3..2e79edc1 100755 --- a/scripts/ensure-python.sh +++ b/scripts/ensure-python.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -python_ver='3.12.1' +python_ver=$(wget -qO- https://www.python.org/ftp/python/ | grep -oE '3\.12\.[0-9]+' | sort -u | tail -n1) prefix=/opt # Derived vars. @@ -18,7 +18,8 @@ fi # Warn about build deps. echo "Warning: You will likely need to install build dependencies for your system." -echo "e.g. Ubuntu requires: build-essential libreadline-dev tk-dev tcl-dev" +echo "e.g. Debian 12 requires:" +echo "build-essential gdb lcov pkg-config libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev lzma lzma-dev tk-dev uuid-dev zlib1g-dev" read -r -p "Continue? [Y/n] " ans if [[ ${ans,,} != 'y' ]]; then exit 1 @@ -51,6 +52,7 @@ sudo make install # Check install. if [[ ! -x "$python_exec_path" ]]; then echo "Error: Executable not found: $python_exec_path" + cd ~ exit 1 fi echo "Python $python_ver has been installed into $prefix" @@ -59,3 +61,4 @@ if [[ "$prefix" == '/opt' ]]; then echo "Running Python $python_ver directly requires LD_LIBRARY_PATH:" echo "LD_LIBRARY_PATH=${prefix}/lib $python_exec_path" fi +cd ~ diff --git a/scripts/ensure-venv.sh b/scripts/ensure-venv.sh index f21cf289..e66e5d1c 100755 --- a/scripts/ensure-venv.sh +++ b/scripts/ensure-venv.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -python_ver='3.12.1' +python_ver=$(wget -qO- https://www.python.org/ftp/python/ | grep -oE '3\.12\.[0-9]+' | sort -u | tail -n1) prefix=/opt venv=./env @@ -28,6 +28,9 @@ if [[ -d "$venv" ]]; then fi # Initialize venv. +if [[ $prefix == '/opt' ]]; then + LD_LIBRARY_PATH=${prefix}/lib +fi "$python_exec_path" -m venv "$venv" echo "LD_LIBRARY_PATH=${prefix}/lib" >> "${venv}/bin/activate" echo 'export LD_LIBRARY_PATH' >> "${venv}/bin/activate" From 11578a6055f4910e98a6f3b7906fcb0019d7c9bf Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sun, 25 Aug 2024 17:42:48 -0500 Subject: [PATCH 119/253] fix confirmation response --- scripts/ensure-python.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ensure-python.sh b/scripts/ensure-python.sh index 2e79edc1..367016d8 100755 --- a/scripts/ensure-python.sh +++ b/scripts/ensure-python.sh @@ -21,7 +21,7 @@ echo "Warning: You will likely need to install build dependencies for your syste echo "e.g. Debian 12 requires:" echo "build-essential gdb lcov pkg-config libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev lzma lzma-dev tk-dev uuid-dev zlib1g-dev" read -r -p "Continue? [Y/n] " ans -if [[ ${ans,,} != 'y' ]]; then +if [[ ${ans,,} != 'y' && $ans != '' ]]; then exit 1 fi From 27767a66c730432369f6b37008f4f897d1c11901 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sun, 25 Aug 2024 17:52:18 -0500 Subject: [PATCH 120/253] fix LD_LIBRARY_PATH --- scripts/ensure-venv.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ensure-venv.sh b/scripts/ensure-venv.sh index e66e5d1c..ec41e306 100755 --- a/scripts/ensure-venv.sh +++ b/scripts/ensure-venv.sh @@ -29,7 +29,7 @@ fi # Initialize venv. if [[ $prefix == '/opt' ]]; then - LD_LIBRARY_PATH=${prefix}/lib + export LD_LIBRARY_PATH=${prefix}/lib fi "$python_exec_path" -m venv "$venv" echo "LD_LIBRARY_PATH=${prefix}/lib" >> "${venv}/bin/activate" From 399c053d41c7fcd73767777c2e361cbd985c0c43 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sun, 25 Aug 2024 18:06:28 -0500 Subject: [PATCH 121/253] update and streamline README --- README.md | 106 ++++++++++++++++++++---------------------------------- 1 file changed, 38 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index e94460d3..7a52956b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This repository contains a Python program for installing and maintaining FaithLi This program is created and maintained by the FaithLife Community and is licensed under the MIT License. -## Logos Linux Installer +## LogosLinuxInstaller The main program is a distributable executable and contains Python itself and all necessary Python packages. @@ -21,63 +21,34 @@ To access the GUI version of the program, double click the executable in your fi The program can also be run from source and should be run from a Python virtual environment. See below. -By default the program installs Logos, but you can pass the `-C|--control-panel` optarg to access the Control Panel, which allows you to install Logos or do various maintenance functions on your install. -In time, you should be able to use the program to restore a backup. - -``` -Usage: ./LogosLinuxInstaller.sh -Installs ${FLPRODUCT} Bible Software with Wine on Linux. - -Options: - -h --help Prints this help message and exit. - -v --version Prints version information and exit. - -V --verbose Enable extra CLI verbosity. - -D --debug Makes Wine print out additional info. - -C --control-panel Open the Control Panel app. - -c --config Use the Logos on Linux config file when - setting environment variables. Defaults to: - \$HOME/.config/Logos_on_Linux/Logos_on_Linux.conf - Optionally can accept a config file provided by - the user. - -b --custom-binary-path Set a custom path to search for wine binaries - during the install. - -F --skip-fonts Skips installing corefonts and tahoma. - -s --shortcut Create or update the Logos shortcut, located in - HOME/.local/share/applications. - -d --dirlink Create a symlink to the Windows Logos directory - in your Logos on Linux install dir. - The symlink's name will be 'installation_dir'. - -e --edit-config Edit the Logos on Linux config file. - -i --indexing Run the Logos indexer in the - background. - --remove-all-index Removes all index and library catalog files. - --remove-library-catalog Removes all library catalog files. - -l --logs Turn Logos logs on or off. - -L --delete-install-log Delete the installation log file. - -R --check-resources Check Logos's resource usage while running. - -b --backup Saves Logos data to the config's - backup location. - -r --restore Restores Logos data from the config's - backup location. - -f --force-root Sets LOGOS_FORCE_ROOT to true, which permits - the root user to run the script. - -P --passive Install Logos non-interactively . - -k --make-skel Make a skeleton install only. -``` - -## Installation - -This section is a WIP. - -You can either run the program from the CLI for a CLI-only install, or you can double click the icon in your file browser or on your desktop for a GUI install. Then, follow the prompts. - -## Installing/running from Source +## Install Guide (for users) + +For an install guide with pictures and video, see the wiki's [Install Guide](https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki/Install-Guide). + +## Installing/running from Source (for developers) You can clone the repo and install the app from source. To do so, you will need to ensure a few prerequisites: -1. Install Python 3.12 and Tcl/Tk +1. Install build dependencies 1. Clone this repository +1. Build/install Python 3.12 and Tcl/Tk 1. Set up a virtual environment +### Install build dependencies + +e.g. for debian-based systems: +``` +sudo apt-get install git build-essential gdb lcov pkg-config \ + libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \ + libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \ + lzma lzma-dev python3-tk tk-dev uuid-dev zlib1g-dev +``` +*See Python's [Build dependencies](https://devguide.python.org/getting-started/setup-building/index.html#build-dependencies) section for further info.* + +### Clone this repository +``` +git clone 'https://github.com/FaithLife-Community/LogosLinuxInstaller.git' +``` + ### Install Python 3.12 and Tcl/Tk Your system might already include Python 3.12 built with Tcl/Tk. This will verify the installation: @@ -92,18 +63,12 @@ following guide or the script provided in `scripts/ensure-python.sh`. This is because the app is built using 3.12 and might have errors if run with other versions. -**Install build dependencies** - -e.g. for debian-based systems: +**Install & build python 3.12 using the script:** ``` -$ sudo apt-get install git build-essential gdb lcov pkg-config \ - libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \ - libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \ - lzma lzma-dev tk-dev uuid-dev zlib1g-dev +./LogosLinuxInstaller/scripts/ensure-python.sh ``` -*See Python's [Build dependencies](https://devguide.python.org/getting-started/setup-building/index.html#build-dependencies) section for further info.* -**Install & build python 3.12** +**Install & build python 3.12 manually:** ``` $ ver=$(wget -qO- https://www.python.org/ftp/python/ | grep -oE '3\.12\.[0-9]+' | sort -u | tail -n1) $ wget "https://www.python.org/ftp/python/${ver}/Python-${ver}.tar.xz" @@ -116,11 +81,10 @@ Python-3.12$ LD_LIBRARY_PATH=/opt/lib /opt/bin/python3.12 --version Python 3.12.5 $ cd ~ ``` -Both methods install python into /opt to avoid interfering with system python installations. +Both methods install python into `/opt` to avoid interfering with system python installations. -### Clone this repository +### Enter the repository folder ``` -$ git clone 'https://github.com/FaithLife-Community/LogosLinuxInstaller.git' --depth=1 $ cd LogosLinuxInstaller LogosLinuxInstaller$ ``` @@ -128,6 +92,14 @@ LogosLinuxInstaller$ ### Set up and use a virtual environment Use the following guide or the provided script at `scripts/ensure-venv.sh` to set up a virtual environment for running and/or building locally. + +**Using the script:** +``` +./scrips/ensure-venv.sh +``` + +**Manual setup:** + ``` LogosLinuxInstaller$ LD_LIBRARY_PATH=/opt/lib /opt/bin/python3.12 -m venv env # create a virtual env folder called "env" using python3.12's path LogosLinuxInstaller$ echo "LD_LIBRARY_PATH=/opt/lib" >> env/bin/activate # tell python where to find libs @@ -140,9 +112,7 @@ Python 3.12.5 (env) LogosLinuxInstaller$ ./main.py --help # run the script ``` -## Install Guide - -For an install guide with pictures and video, see the wiki's [Install Guide](https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki/Install-Guide). +## Install guide (possibly outdated) NOTE: You can run Logos on Linux using the Steam Proton Experimental binary, which often has the latest and greatest updates to make Logos run even smoother. The script should be able to find the binary automatically, unless your Steam install is located outside of your HOME directory. From 39b68135d253664f226f3f68ba7b2b5afed66d60 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 27 Aug 2024 15:45:26 -0500 Subject: [PATCH 122/253] use sys deps wiki link in README.md --- README.md | 151 +----------------------------------------------------- 1 file changed, 1 insertion(+), 150 deletions(-) diff --git a/README.md b/README.md index 7a52956b..c7b88114 100644 --- a/README.md +++ b/README.md @@ -116,154 +116,5 @@ Python 3.12.5 NOTE: You can run Logos on Linux using the Steam Proton Experimental binary, which often has the latest and greatest updates to make Logos run even smoother. The script should be able to find the binary automatically, unless your Steam install is located outside of your HOME directory. -If you want to install your distro's dependencies outside of the script, please see the following. +If you want to install your distro's dependencies outside of the script, please see the [System Dependencies wiki page](https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki/System-Dependencies). -NOTE: The following section is WIP. - -## Debian and Ubuntu - -### Install Dependencies - -``` -sudo apt install coreutils patch lsof wget findutils sed grep gawk winbind cabextract x11-apps bc binutils -``` - -If using wine from a repo, you must install wine staging. Run: - -``` -sudo dpkg --add-architecture i386 -sudo mkdir -pm755 /etc/apt/keyrings -sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key -CODENAME=$(lsb_release -a | grep Codename | awk '{print $2}') -sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/"${CODENAME}"/winehq-"${CODENAME}".sources -sudo apt update -sudo apt install --install-recommends winehq-staging -``` - -See https://wiki.winehq.org/Ubuntu for help. - -If using the AppImage, run: - -``` -sudo apt install fuse3 -``` - -## Arch - -### Install Dependencies - -``` -sudo pacman -S patch lsof wget sed grep gawk cabextract samba bc -``` - -If using wine from a repo, run: - -``` -sudo pacman -S wine -``` - -### Manjaro - -#### Install Dependencies - -``` -sudo pamac install patch lsof wget sed grep gawk cabextract samba bc -``` - -If using wine from a repo, run: - -``` -sudo pamac install wine -``` - -You may need to install pamac if you are not using Manjaro GNOME: - -``` -sudo pacman -S pamac-cli -``` - -### Steamdeck - -The steam deck has a locked down filesystem. There are some missing dependencies which cause irregular crashes in Logos. These can be installed following this sequence: - -1. Enter Desktop Mode -2. Use `passwd` to create a password for the deck user, unless you already did this. -3. Disable read-only mode: `sudo steamos-readonly disable` -4. Initialize pacman keyring: `sudo pacman-key --init` -5. Populate pacman keyring with the default Arch Linux keys: `sudo pacman-key --populate archlinux` -6. Get package lists: `sudo pacman -Fy` -7. Fix locale issues `sudo pacman -Syu glibc` -8. then `sudo locale-gen` -9. Install dependencies: `sudo pacman -S samba winbind cabextract appmenu-gtk-module patch bc lib32-libjpeg-turbo` - -Packages you install may be overwritten by the next Steam OS update, but you can easily reinstall them if that happens. - -After these steps you can go ahead and run the your install script. - -## RPM - -### Install Dependencies - -``` -sudo dnf install patch mod_auth_ntlm_winbind samba-winbind cabextract bc samba-winbind-clients -``` - -If using wine from a repo, run: - -``` -sudo dnf install winehq-staging -``` - -If using the AppImage, run: - -``` -sudo dnf install fuse3 -``` - -### CentOS - -### Install Dependencies - -``` -sudo yum install patch mod_auth_ntlm_winbind samba-winbind cabextract bc -``` - -If using wine from a repo, run: - -``` -sudo yum install winehq-staging -``` - -If using the AppImage, run: - -``` -sudo yum install fuse3 -``` - -## OpenSuse - -TODO - -``` -sudo zypper install … -``` - -## Alpine - -TODO - -``` -sudo apk add … -``` - -## BSD - -TODO. - -``` -doas pkg install … -``` - -## ChromeOS - -TODO. From 8e09f0aba03996bb5e4185209fc56a4d188d208a Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 23 Aug 2024 17:15:53 -0500 Subject: [PATCH 123/253] remove unused config.GUI variable --- main.py | 2 -- system.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/main.py b/main.py index 37adbe83..64342c03 100755 --- a/main.py +++ b/main.py @@ -350,8 +350,6 @@ def main(): system.get_dialog() else: config.DIALOG = config.DIALOG.lower() - if config.DIALOG == 'tk': - config.GUI = True if config.DIALOG == 'curses' and "dialog" in sys.modules and config.use_python_dialog is None: config.use_python_dialog = system.test_dialog_version() diff --git a/system.py b/system.py index 8792257a..222715db 100644 --- a/system.py +++ b/system.py @@ -99,7 +99,6 @@ def get_dialog(): msg.logos_error("The installer does not work unless you are running a display") # noqa: E501 DIALOG = os.getenv('DIALOG') - config.GUI = False # Set config.DIALOG. if DIALOG is not None: DIALOG = DIALOG.lower() @@ -110,9 +109,6 @@ def get_dialog(): config.DIALOG = 'curses' else: config.DIALOG = 'tk' - # Set config.GUI. - if config.DIALOG == 'tk': - config.GUI = True def get_os(): From e4e071af7e4790f930ce4de1b7bd44e903f10a41 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 24 Aug 2024 07:47:23 -0500 Subject: [PATCH 124/253] pep8 fixes --- installer.py | 64 +++++++++++++++++++++++++++++++++------------------- main.py | 4 ++-- msg.py | 9 ++++++-- system.py | 28 +++++++++++++---------- 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/installer.py b/installer.py index 53b22876..4660e5f6 100644 --- a/installer.py +++ b/installer.py @@ -11,11 +11,11 @@ import utils import wine -#TODO: Fix install progress if user returns to main menu? +# TODO: Fix install progress if user returns to main menu? # To replicate, start a TUI install, return/cancel on second step # Then launch a new install -#TODO: Reimplement `--install-app`? +# TODO: Reimplement `--install-app`? def ensure_product_choice(app=None): @@ -32,7 +32,8 @@ def ensure_product_choice(app=None): app.product_e.wait() config.FLPRODUCT = app.product_q.get() else: - logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) if config.FLPRODUCT == 'Logos': config.FLPRODUCTi = 'logos4' @@ -59,7 +60,8 @@ def ensure_version_choice(app=None): app.version_e.wait() config.TARGETVERSION = app.version_q.get() else: - logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) logging.debug(f"> {config.TARGETVERSION=}") @@ -79,7 +81,8 @@ def ensure_release_choice(app=None): config.TARGET_RELEASE_VERSION = app.release_q.get() logging.debug(f"{config.TARGET_RELEASE_VERSION=}") else: - logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) logging.debug(f"> {config.TARGET_RELEASE_VERSION=}") @@ -106,7 +109,8 @@ def ensure_install_dir_choice(app=None): config.INSTALLDIR = app.installdir_q.get() config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" else: - logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) logging.debug(f"> {config.INSTALLDIR=}") logging.debug(f"> {config.APPDIR_BINDIR=}") @@ -132,10 +136,12 @@ def ensure_wine_choice(app=None): app.wine_e.wait() config.WINE_EXE = app.wine_q.get() else: - logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. - logging.debug(f"Preparing to process WINE_EXE. Currently set to: {config.WINE_EXE}.") + m = f"Preparing to process WINE_EXE. Currently set to: {config.WINE_EXE}." + logging.debug(m) if config.WINE_EXE.lower().endswith('.appimage'): config.SELECTED_APPIMAGE_FILENAME = config.WINE_EXE if not config.WINEBIN_CODE: @@ -157,7 +163,7 @@ def ensure_winetricks_choice(app=None): logging.debug('- config.WINETRICKSBIN') if config.WINETRICKSBIN is None: - # Check if local winetricks version available; else, download it. + # Check if local winetricks version available; else, download it. config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" if app: @@ -166,7 +172,8 @@ def ensure_winetricks_choice(app=None): app.tricksbin_e.wait() winetricksbin = app.tricksbin_q.get() else: - logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) if not winetricksbin.startswith('Download'): config.WINETRICKSBIN = winetricksbin @@ -241,7 +248,7 @@ def ensure_install_dirs(app=None): if config.INSTALLDIR is None: config.INSTALLDIR = f"{os.getenv('HOME')}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" #noqa: E501 + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" bin_dir = Path(config.APPDIR_BINDIR) bin_dir.mkdir(parents=True, exist_ok=True) @@ -260,7 +267,8 @@ def ensure_install_dirs(app=None): if app: utils.send_task(app, 'INSTALLING') else: - logging.critical(f"{utils.get_calling_function_name()}: --install-app is broken") + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) def ensure_sys_deps(app=None): @@ -337,7 +345,7 @@ def ensure_wine_executables(app=None): elif config.WINEBIN_CODE in ["System", "Proton", "PlayOnLinux", "Custom"]: # noqa: E501 appimage_filename = "none.AppImage" else: - msg.logos_error(f"WINEBIN_CODE error. WINEBIN_CODE is {config.WINEBIN_CODE}. Installation canceled!") + msg.logos_error(f"WINEBIN_CODE error. WINEBIN_CODE is {config.WINEBIN_CODE}. Installation canceled!") # noqa: E501 appimage_link.unlink(missing_ok=True) # remove & replace appimage_link.symlink_to(f"./{appimage_filename}") @@ -493,8 +501,11 @@ def ensure_winetricks_applied(app=None): usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - #FIXME: This command is failing. - reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') + # FIXME: This command is failing. + reg_file = os.path.join( + config.WORKDIR, + 'disable-winemenubuilder.reg' + ) with open(reg_file, 'w') as f: f.write(r'''REGEDIT4 @@ -518,7 +529,8 @@ def ensure_winetricks_applied(app=None): args.insert(0, "-q") wine.winetricks_install(*args) - msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") + m = f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode." + msg.logos_msg(m) exe_args = [ 'add', f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 @@ -612,7 +624,10 @@ def ensure_launcher_executable(app=None): shutil.copy(sys.executable, launcher_exe) logging.debug(f"> File exists?: {launcher_exe}: {launcher_exe.is_file()}") # noqa: E501 else: - update_install_feedback(f"Running from source. Skipping launcher creation.", app=app) + update_install_feedback( + "Running from source. Skipping launcher creation.", + app=app + ) def ensure_launcher_shortcuts(app=None): @@ -633,8 +648,8 @@ def ensure_launcher_shortcuts(app=None): desktop_files = [ ( - f"{config.FLPRODUCT}Bible.desktop", - f"""[Desktop Entry] + f"{config.FLPRODUCT}Bible.desktop", + f"""[Desktop Entry] Name={config.FLPRODUCT}Bible Comment=A Bible Study Library with Built-In Tools Exec={config.INSTALLDIR}/LogosLinuxInstaller --run-installed-app @@ -645,8 +660,8 @@ def ensure_launcher_shortcuts(app=None): """ ), ( - f"{config.FLPRODUCT}Bible-ControlPanel.desktop", - f"""[Desktop Entry] + f"{config.FLPRODUCT}Bible-ControlPanel.desktop", + f"""[Desktop Entry] Name={config.FLPRODUCT}Bible Control Panel Comment=Perform various tasks for {config.FLPRODUCT} app Exec={config.INSTALLDIR}/LogosLinuxInstaller @@ -662,8 +677,11 @@ def ensure_launcher_shortcuts(app=None): fpath = Path.home() / '.local' / 'share' / 'applications' / f logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") else: - logging.debug(f"Running from source. Skipping launcher creation.") - update_install_feedback(f"Running from source. Skipping launcher creation.", app=app) + logging.debug("Running from source. Skipping launcher creation.") + update_install_feedback( + "Running from source. Skipping launcher creation.", + app=app + ) def update_install_feedback(text, app=None): diff --git a/main.py b/main.py index 64342c03..fa4ac33b 100755 --- a/main.py +++ b/main.py @@ -351,7 +351,7 @@ def main(): else: config.DIALOG = config.DIALOG.lower() - if config.DIALOG == 'curses' and "dialog" in sys.modules and config.use_python_dialog is None: + if config.DIALOG == 'curses' and "dialog" in sys.modules and config.use_python_dialog is None: # noqa: E501 config.use_python_dialog = system.test_dialog_version() if config.use_python_dialog is None: @@ -393,7 +393,7 @@ def main(): secondary = ( "Your system currently has AppImageLauncher installed.\n" "LogosLinuxInstaller is not compatible with AppImageLauncher.\n" - "For more information, see: https://github.com/FaithLife-Community/LogosLinuxInstaller/issues/114" + "For more information, see: https://github.com/FaithLife-Community/LogosLinuxInstaller/issues/114" # noqa: E501 ) no_text = "User declined to remove AppImageLauncher." msg.logos_continue_question(question_text, no_text, secondary) diff --git a/msg.py b/msg.py index 7b4a2e1b..e2af0588 100644 --- a/msg.py +++ b/msg.py @@ -73,7 +73,12 @@ def initialize_logging(stderr_log_level): log_parent.mkdir(parents=True) # Define logging handlers. - file_h = GzippedRotatingFileHandler(config.LOGOS_LOG, maxBytes=10*1024*1024, backupCount=5, encoding='UTF8') + file_h = GzippedRotatingFileHandler( + config.LOGOS_LOG, + maxBytes=10*1024*1024, + backupCount=5, + encoding='UTF8' + ) file_h.name = "logfile" file_h.setLevel(logging.DEBUG) # stdout_h = logging.StreamHandler(sys.stdout) @@ -287,5 +292,5 @@ def status(text, app=None): app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) # noqa: E501 logging.info(f"{text}") else: - '''Prints message to stdout regardless of log level.''' + # Prints message to stdout regardless of log level. logos_msg(text) diff --git a/system.py b/system.py index 222715db..a5523498 100644 --- a/system.py +++ b/system.py @@ -52,7 +52,7 @@ def run_command(command, retries=1, delay=0, **kwargs): ) return result except subprocess.CalledProcessError as e: - logging.error(f"Error occurred in run_command() while executing \"{command}\": {e}") + logging.error(f"Error occurred in run_command() while executing \"{command}\": {e}") # noqa: E501 if "lock" in str(e): logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") # noqa: E501 time.sleep(delay) @@ -441,7 +441,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if message is None: logging.debug("No missing or conflicting dependencies found.") elif not message: - logging.error("Your distro requires manual dependency installation.") + m = "Your distro requires manual dependency installation." + logging.error(m) else: msg.logos_continue_question(message, no_message, secondary, app) if app: @@ -510,7 +511,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if config.OS_NAME in ['fedora', 'arch']: manual_install_required = True sudo_command = command_str.replace("pkexec", "sudo") - message = "The system needs to install/remove packages, but it requires manual intervention." + message = "The system needs to install/remove packages, but it requires manual intervention." # noqa: E501 detail = ( "Please run the following command in a terminal, then restart " f"LogosLinuxInstaller:\n{sudo_command}\n" @@ -518,7 +519,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if app: if config.DIALOG == "tk": if hasattr(app, 'root'): - detail += "\nThe command has been copied to the clipboard." + detail += "\nThe command has been copied to the clipboard." # noqa: E501 app.root.clipboard_clear() app.root.clipboard_append(sudo_command) app.root.update() @@ -534,14 +535,17 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) install_deps_failed = True if manual_install_required and app and config.DIALOG == "curses": - app.screen_q.put(app.stack_confirm(17, app.todo_q, app.todo_e, - f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\nLogosLinuxInstaller:\n{sudo_command}\n", # noqa: E501 - "User cancelled dependency installation.", # noqa: E501 - message, # noqa: E501 - options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) + app.screen_q.put( + app.stack_confirm( + 17, + app.todo_q, + app.todo_e, + f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\nLogosLinuxInstaller:\n{sudo_command}\n", # noqa: E501 + "User cancelled dependency installation.", # noqa: E501 + message, + options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) # noqa: E501 app.manualinstall_e.wait() - if not install_deps_failed and not manual_install_required: try: logging.debug(f"Attempting to run this command: {command_str}") @@ -550,7 +554,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if e.returncode == 127: logging.error("User cancelled dependency installation.") else: - logging.error(f"An error occurred in install_dependencies(): {e}") + logging.error(f"An error occurred in install_dependencies(): {e}") # noqa: E501 logging.error(f"Command output: {e.output}") install_deps_failed = True else: @@ -562,7 +566,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if config.REBOOT_REQUIRED: question = "Should the program reboot the host now?" # noqa: E501 no_text = "The user has chosen not to reboot." - secondary = "The system has installed or removed a package that requires a reboot." + secondary = "The system has installed or removed a package that requires a reboot." # noqa: E501 if msg.logos_continue_question(question, no_text, secondary): reboot() else: From f6db2b502e0b0c782af582858bf90d03f08f4704 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 24 Aug 2024 08:32:13 -0500 Subject: [PATCH 125/253] pass args and kwargs to thread workers --- tui_app.py | 25 ++++++++++++++----------- tui_curses.py | 2 +- utils.py | 6 ++++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/tui_app.py b/tui_app.py index 3b5689ed..f2845008 100644 --- a/tui_app.py +++ b/tui_app.py @@ -258,23 +258,23 @@ def run(self): def task_processor(self, evt=None, task=None): if task == 'FLPRODUCT': - utils.start_thread(self.get_product(config.use_python_dialog)) + utils.start_thread(self.get_product, config.use_python_dialog) elif task == 'TARGETVERSION': - utils.start_thread(self.get_version(config.use_python_dialog)) + utils.start_thread(self.get_version, config.use_python_dialog) elif task == 'TARGET_RELEASE_VERSION': - utils.start_thread(self.get_release(config.use_python_dialog)) + utils.start_thread(self.get_release, config.use_python_dialog) elif task == 'INSTALLDIR': - utils.start_thread(self.get_installdir(config.use_python_dialog)) + utils.start_thread(self.get_installdir, config.use_python_dialog) elif task == 'WINE_EXE': - utils.start_thread(self.get_wine(config.use_python_dialog)) + utils.start_thread(self.get_wine, config.use_python_dialog) elif task == 'WINETRICKSBIN': - utils.start_thread(self.get_winetricksbin(config.use_python_dialog)) + utils.start_thread(self.get_winetricksbin, config.use_python_dialog) elif task == 'INSTALLING': - utils.start_thread(self.get_waiting(config.use_python_dialog)) + utils.start_thread(self.get_waiting, config.use_python_dialog) elif task == 'INSTALLING_PW': - utils.start_thread(self.get_waiting(config.use_python_dialog, screen_id=15)) + utils.start_thread(self.get_waiting, config.use_python_dialog, screen_id=15) elif task == 'CONFIG': - utils.start_thread(self.get_config(config.use_python_dialog)) + utils.start_thread(self.get_config, config.use_python_dialog) elif task == 'DONE': self.subtitle = f"Logos Version: {config.current_logos_version}" self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) @@ -296,7 +296,7 @@ def choice_processor(self, stdscr, screen_id, choice): elif choice.startswith("Install"): config.INSTALL_STEPS_COUNT = 0 config.INSTALL_STEP = 0 - utils.start_thread(installer.ensure_launcher_shortcuts, True, self) + utils.start_thread(installer.ensure_launcher_shortcuts, daemon_bool=True, app=self) elif choice.startswith("Update Logos Linux Installer"): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": @@ -480,7 +480,7 @@ def get_release(self, dialog): self.screen_q.put(self.stack_text(10, self.version_q, self.version_e, "Waiting to acquire Logos versions…", wait=True, dialog=dialog)) self.version_e.wait() question = f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?" # noqa: E501 - utils.start_thread(network.get_logos_releases, True, self) + utils.start_thread(network.get_logos_releases, daemon_bool=True, app=self) self.releases_e.wait() if config.TARGETVERSION == '10': @@ -525,6 +525,9 @@ def get_winetricksbin(self, dialog): self.screen_q.put(self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog)) def get_waiting(self, dialog, screen_id=8): + # FIXME: I think TUI install with existing config file fails here b/c + # the config file already defines the needed variable, so the event + # self.tricksbin_e is never triggered. self.tricksbin_e.wait() text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) diff --git a/tui_curses.py b/tui_curses.py index 31f58819..6ba48534 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -307,7 +307,7 @@ def input(self): self.stdscr.noutrefresh() def run(self): - #thread = utils.start_thread(self.input, False) + #thread = utils.start_thread(self.input, daemon_bool=False) #thread.join() self.draw() self.input() diff --git a/utils.py b/utils.py index 36da8d8e..13156aa2 100644 --- a/utils.py +++ b/utils.py @@ -844,13 +844,15 @@ def grep(regexp, filepath): return found -def start_thread(task, daemon_bool=True, *args): +def start_thread(task, *args, daemon_bool=True, **kwargs): thread = threading.Thread( name=f"{task}", target=task, daemon=daemon_bool, - args=args + args=args, + kwargs=kwargs ) + logging.debug(f"Starting thread: {task=}; {daemon_bool=} {args=}; {kwargs=}") # noqa: E501 threads.append(thread) thread.start() return thread From 6451b3c6f64524a1bc799c5bac9f9ce5bead3b53 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 26 Aug 2024 21:58:20 -0400 Subject: [PATCH 126/253] Fix #123 --- installer.py | 18 +++++++++++ system.py | 4 +++ tui_app.py | 91 +++++++++++++++++++++++++++++++--------------------- 3 files changed, 76 insertions(+), 37 deletions(-) diff --git a/installer.py b/installer.py index 4660e5f6..90d0cdea 100644 --- a/installer.py +++ b/installer.py @@ -34,6 +34,9 @@ def ensure_product_choice(app=None): else: m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) + else: + if config.DIALOG == 'curses': + app.set_product(config.FLPRODUCT) if config.FLPRODUCT == 'Logos': config.FLPRODUCTi = 'logos4' @@ -62,6 +65,9 @@ def ensure_version_choice(app=None): else: m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) + else: + if config.DIALOG == 'curses': + app.set_version(config.TARGETVERSION) logging.debug(f"> {config.TARGETVERSION=}") @@ -83,6 +89,9 @@ def ensure_release_choice(app=None): else: m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) + else: + if config.DIALOG == 'curses': + app.set_release(config.TARGET_RELEASE_VERSION) logging.debug(f"> {config.TARGET_RELEASE_VERSION=}") @@ -111,6 +120,9 @@ def ensure_install_dir_choice(app=None): else: m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) + else: + if config.DIALOG == 'curses': + app.set_installdir(config.INSTALLDIR) logging.debug(f"> {config.INSTALLDIR=}") logging.debug(f"> {config.APPDIR_BINDIR=}") @@ -138,6 +150,9 @@ def ensure_wine_choice(app=None): else: m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) + else: + if config.DIALOG == 'curses': + app.set_wine(config.WINE_EXE) # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. m = f"Preparing to process WINE_EXE. Currently set to: {config.WINE_EXE}." @@ -177,6 +192,9 @@ def ensure_winetricks_choice(app=None): if not winetricksbin.startswith('Download'): config.WINETRICKSBIN = winetricksbin + else: + if config.DIALOG == 'curses': + app.set_winetricksbin(config.WINETRICKSBIN) logging.debug(f"> {config.WINETRICKSBIN=}") diff --git a/system.py b/system.py index a5523498..be0a055f 100644 --- a/system.py +++ b/system.py @@ -498,6 +498,10 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) command.append('&&') command.extend(postinstall_command) if not command: # nothing to run; avoid running empty pkexec command + logging.debug(f"No dependency install required.") + if app: + if config.DIALOG == "curses": + app.installdeps_e.set() return if app and config.DIALOG == 'tk': diff --git a/tui_app.py b/tui_app.py index f2845008..6f86a5c7 100644 --- a/tui_app.py +++ b/tui_app.py @@ -354,52 +354,22 @@ def choice_processor(self, stdscr, screen_id, choice): self.appimage_e.set() elif screen_id == 2: if choice: - if str(choice).startswith("Logos"): - config.FLPRODUCT = "Logos" - elif str(choice).startswith("Verbum"): - config.FLPRODUCT = "Verbum" - self.menu_screen.choice = "Processing" - self.product_q.put(config.FLPRODUCT) - self.product_e.set() + self.set_product(choice) elif screen_id == 3: if choice: - if "10" in choice: - config.TARGETVERSION = "10" - elif "9" in choice: - config.TARGETVERSION = "9" - self.menu_screen.choice = "Processing" - self.version_q.put(config.TARGETVERSION) - self.version_e.set() + self.set_version(choice) elif screen_id == 4: if choice: - config.TARGET_RELEASE_VERSION = choice - self.menu_screen.choice = "Processing" - self.release_q.put(config.TARGET_RELEASE_VERSION) - self.release_e.set() + self.set_release(choice) elif screen_id == 5: if choice: - config.INSTALLDIR = choice - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" - self.menu_screen.choice = "Processing" - self.installdir_q.put(config.INSTALLDIR) - self.installdir_e.set() + self.set_installdir(choice) elif screen_id == 6: - config.WINE_EXE = choice if choice: - self.menu_screen.choice = "Processing" - self.wine_q.put(config.WINE_EXE) - self.wine_e.set() + self.set_wine(choice) elif screen_id == 7: - winetricks_options = utils.get_winetricks_options() - if choice.startswith("Download"): - self.menu_screen.choice = "Processing" - self.tricksbin_q.put("Download") - self.tricksbin_e.set() - else: - self.menu_screen.choice = "Processing" - config.WINETRICKSBIN = winetricks_options[0] - self.tricksbin_q.put(config.WINETRICKSBIN) - self.tricksbin_e.set() + if choice: + self.set_winetricksbin(choice) elif screen_id == 8: pass elif screen_id == 9: @@ -467,6 +437,15 @@ def get_product(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(2, self.product_q, self.product_e, question, options, dialog=dialog)) + def set_product(self, choice): + if str(choice).startswith("Logos"): + config.FLPRODUCT = "Logos" + elif str(choice).startswith("Verbum"): + config.FLPRODUCT = "Verbum" + self.menu_screen.choice = "Processing" + self.product_q.put(config.FLPRODUCT) + self.product_e.set() + def get_version(self, dialog): self.product_e.wait() question = f"Which version of {config.FLPRODUCT} should the script install?" # noqa: E501 @@ -475,6 +454,15 @@ def get_version(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(3, self.version_q, self.version_e, question, options, dialog=dialog)) + def set_version(self, choice): + if "10" in choice: + config.TARGETVERSION = "10" + elif "9" in choice: + config.TARGETVERSION = "9" + self.menu_screen.choice = "Processing" + self.version_q.put(config.TARGETVERSION) + self.version_e.set() + def get_release(self, dialog): labels = [] self.screen_q.put(self.stack_text(10, self.version_q, self.version_e, "Waiting to acquire Logos versions…", wait=True, dialog=dialog)) @@ -495,12 +483,25 @@ def get_release(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(4, self.release_q, self.release_e, question, options, dialog=dialog)) + def set_release(self, choice): + config.TARGET_RELEASE_VERSION = choice + self.menu_screen.choice = "Processing" + self.release_q.put(config.TARGET_RELEASE_VERSION) + self.release_e.set() + def get_installdir(self, dialog): self.release_e.wait() default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 question = f"Where should {config.FLPRODUCT} files be installed to? [{default}]: " # noqa: E501 self.screen_q.put(self.stack_input(5, self.installdir_q, self.installdir_e, question, default, dialog=dialog)) + def set_installdir(self, choice): + config.INSTALLDIR = choice + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + self.menu_screen.choice = "Processing" + self.installdir_q.put(config.INSTALLDIR) + self.installdir_e.set() + def get_wine(self, dialog): self.installdir_e.wait() self.screen_q.put(self.stack_text(10, self.wine_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog)) @@ -516,6 +517,12 @@ def get_wine(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, dialog=dialog)) + def set_wine(self, choice): + config.WINE_EXE = choice + self.menu_screen.choice = "Processing" + self.wine_q.put(config.WINE_EXE) + self.wine_e.set() + def get_winetricksbin(self, dialog): self.wine_e.wait() winetricks_options = utils.get_winetricks_options() @@ -524,6 +531,16 @@ def get_winetricksbin(self, dialog): self.menu_options = options self.screen_q.put(self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog)) + def set_winetricksbin(self, choice): + if choice.startswith("Download"): + self.tricksbin_q.put("Download") + else: + winetricks_options = utils.get_winetricks_options() + config.WINETRICKSBIN = winetricks_options[0] + self.tricksbin_q.put(config.WINETRICKSBIN) + self.menu_screen.choice = "Processing" + self.tricksbin_e.set() + def get_waiting(self, dialog, screen_id=8): # FIXME: I think TUI install with existing config file fails here b/c # the config file already defines the needed variable, so the event From 4c466e1d33c75b29381aead0b4231cc238e75f74 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 26 Aug 2024 23:33:54 -0400 Subject: [PATCH 127/253] Make config.WINE_EXE dynamic --- gui.py | 3 ++- gui_app.py | 3 ++- installer.py | 22 +++++++++--------- tui_app.py | 3 +-- utils.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++---- wine.py | 24 +++++++++++--------- 6 files changed, 89 insertions(+), 30 deletions(-) diff --git a/gui.py b/gui.py index 716b16bb..13f176f6 100644 --- a/gui.py +++ b/gui.py @@ -15,6 +15,7 @@ from tkinter.ttk import Separator import config +import utils class InstallerGui(Frame): @@ -30,7 +31,7 @@ def __init__(self, root, **kwargs): self.targetversion = config.TARGETVERSION self.logos_release_version = config.TARGET_RELEASE_VERSION self.default_config_path = config.DEFAULT_CONFIG_PATH - self.wine_exe = config.WINE_EXE + self.wine_exe = utils.get_wine_exe_path() self.winetricksbin = config.WINETRICKSBIN self.skip_fonts = config.SKIP_FONTS if self.skip_fonts is None: diff --git a/gui_app.py b/gui_app.py index 88e00193..189e6293 100644 --- a/gui_app.py +++ b/gui_app.py @@ -19,6 +19,7 @@ import gui import installer import network +import os import system import utils import wine @@ -443,7 +444,7 @@ def set_wine(self, evt=None): self.start_ensure_config() else: - self.wine_q.put(self.gui.wine_exe) + self.wine_q.put(utils.get_relative_path(utils.get_config_var(self.gui.wine_exe), config.INSTALLDIR)) def set_winetricks(self, evt=None): self.gui.winetricksbin = self.gui.tricksvar.get() diff --git a/installer.py b/installer.py index 90d0cdea..0d244a88 100644 --- a/installer.py +++ b/installer.py @@ -140,7 +140,7 @@ def ensure_wine_choice(app=None): logging.debug('- config.WINE_EXE') logging.debug('- config.WINEBIN_CODE') - if config.WINE_EXE is None: + if utils.get_wine_exe_path() is None: network.set_recommended_appimage_config() if app: utils.send_task(app, 'WINE_EXE') @@ -152,22 +152,22 @@ def ensure_wine_choice(app=None): logging.critical(m) else: if config.DIALOG == 'curses': - app.set_wine(config.WINE_EXE) + app.set_wine(utils.get_wine_exe_path()) # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. - m = f"Preparing to process WINE_EXE. Currently set to: {config.WINE_EXE}." + m = f"Preparing to process WINE_EXE. Currently set to: {utils.get_wine_exe_path()}." logging.debug(m) - if config.WINE_EXE.lower().endswith('.appimage'): - config.SELECTED_APPIMAGE_FILENAME = config.WINE_EXE + if str(utils.get_wine_exe_path()).lower().endswith('.appimage'): + config.SELECTED_APPIMAGE_FILENAME = str(utils.get_wine_exe_path()) if not config.WINEBIN_CODE: - config.WINEBIN_CODE = utils.get_winebin_code_and_desc(config.WINE_EXE)[0] # noqa: E501 + config.WINEBIN_CODE = utils.get_winebin_code_and_desc(utils.get_wine_exe_path())[0] # noqa: E501 logging.debug(f"> {config.SELECTED_APPIMAGE_FILENAME=}") logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_URL=}") logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME=}") logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_FILENAME=}") logging.debug(f"> {config.WINEBIN_CODE=}") - logging.debug(f"> {config.WINE_EXE=}") + logging.debug(f"> {utils.get_wine_exe_path()=}") def ensure_winetricks_choice(app=None): @@ -309,7 +309,7 @@ def ensure_appimage_download(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_sys_deps(app=app) config.INSTALL_STEP += 1 - if config.TARGETVERSION != '9' and not config.WINE_EXE.lower().endswith('appimage'): # noqa: E501 + if config.TARGETVERSION != '9' and not str(utils.get_wine_exe_path()).lower().endswith('appimage'): # noqa: E501 return update_install_feedback( "Ensuring wine AppImage is downloaded…", @@ -345,7 +345,7 @@ def ensure_wine_executables(app=None): logging.debug('- wineserver') # Add APPDIR_BINDIR to PATH. - if not os.access(config.WINE_EXE, os.X_OK): + if not os.access(utils.get_wine_exe_path(), os.X_OK): appdir_bindir = Path(config.APPDIR_BINDIR) os.environ['PATH'] = f"{config.APPDIR_BINDIR}:{os.getenv('PATH')}" # Ensure AppImage symlink. @@ -479,7 +479,7 @@ def ensure_wineprefix_init(app=None): f"{config.INSTALLDIR}/data", ) else: - if config.WINE_EXE: + if utils.get_wine_exe_path(): wine.initializeWineBottle() logging.debug(f"> {init_file} exists?: {init_file.is_file()}") @@ -556,7 +556,7 @@ def ensure_winetricks_applied(app=None): "/t", "REG_SZ", "/d", "vista", "/f", ] - wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + wine.run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) logging.debug("> Done.") diff --git a/tui_app.py b/tui_app.py index 6f86a5c7..7f1da021 100644 --- a/tui_app.py +++ b/tui_app.py @@ -518,9 +518,8 @@ def get_wine(self, dialog): self.screen_q.put(self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, dialog=dialog)) def set_wine(self, choice): - config.WINE_EXE = choice + self.wine_q.put(utils.get_relative_path(utils.get_config_var(choice), config.INSTALLDIR)) self.menu_screen.choice = "Processing" - self.wine_q.put(config.WINE_EXE) self.wine_e.set() def get_winetricksbin(self, dialog): diff --git a/utils.py b/utils.py index 13156aa2..dd084a4b 100644 --- a/utils.py +++ b/utils.py @@ -64,8 +64,8 @@ def set_runtime_config(): # Set runtime variables that are dependent on ones from config file. if config.INSTALLDIR and not config.WINEPREFIX: config.WINEPREFIX = f"{config.INSTALLDIR}/data/wine64_bottle" - if config.WINE_EXE and not config.WINESERVER_EXE: - bin_dir = Path(config.WINE_EXE).parent + if get_wine_exe_path() and not config.WINESERVER_EXE: + bin_dir = Path(get_wine_exe_path()).parent config.WINESERVER_EXE = str(bin_dir / 'wineserver') if config.FLPRODUCT and config.WINEPREFIX and not config.LOGOS_EXE: config.LOGOS_EXE = find_installed_product() @@ -85,6 +85,10 @@ def write_config(config_file_path): try: for key, value in config_data.items(): + if key == "WINE_EXE": + # We store the value of WINE_EXE as relative path if it is in the install directory. + if value is not None: + value = get_relative_path(get_config_var(value), config.INSTALLDIR) if isinstance(value, Path): config_data[key] = str(value) with open(config_file_path, 'w') as config_file: @@ -328,6 +332,8 @@ def get_winebin_code_and_desc(binary): # tell the GUI user that a particular AppImage/binary is recommended. # Below is my best guess for how to do this with the single element array… # Does it work? + if isinstance(binary, Path): + binary = str(binary) if binary == f"{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}": # noqa: E501 code = "Recommended" elif binary.lower().endswith('.appimage'): @@ -548,8 +554,8 @@ def compare_logos_linux_installer_version(): def compare_recommended_appimage_version(): wine_release = [] - if config.WINE_EXE is not None: - wine_release, error_message = wine.get_wine_release(config.WINE_EXE) + if get_wine_exe_path() is not None: + wine_release, error_message = wine.get_wine_release(get_wine_exe_path()) if wine_release is not None and wine_release is not False: current_version = '.'.join([str(n) for n in wine_release[:2]]) logging.debug(f"Current wine release: {current_version}") @@ -876,3 +882,53 @@ def untar_file(file_path, output_dir): logging.debug(f"Successfully extracted '{file_path}' to '{output_dir}'") # noqa: E501 except tarfile.TarError as e: logging.error(f"Error extracting '{file_path}': {e}") + + +def is_relative_path(path): + if isinstance(path, str): + path = Path(path) + return not path.is_absolute() + + +def get_relative_path(path, base_path): + if is_relative_path(path): + return path + else: + if isinstance(path, Path): + path = str(path) + if path.startswith(base_path): + return path[len(base_path):].lstrip(os.sep) + else: + return path + + +def create_dynamic_path(path, base_path): + if is_relative_path(path): + if isinstance(path, str): + path = Path(path) + if isinstance(path, str): + base_path = Path(base_path) + return base_path / path + else: + return Path(path) + + +def get_config_var(var): + if var is not None: + if callable(var): + return var() + return var + else: + return None + + +def get_wine_exe_path(path=None): + if path is not None: + path = get_relative_path(get_config_var(path), config.INSTALLDIR) + return Path(create_dynamic_path(path, config.INSTALLDIR)) + else: + if config.WINE_EXE is not None: + path = get_relative_path(get_config_var(config.WINE_EXE), config.INSTALLDIR) + return Path(create_dynamic_path(path, config.INSTALLDIR)) + else: + return None diff --git a/wine.py b/wine.py index 05b80b6d..c005589a 100644 --- a/wine.py +++ b/wine.py @@ -178,7 +178,7 @@ def initializeWineBottle(app=None): # Avoid wine-mono window orig_overrides = config.WINEDLLOVERRIDES config.WINEDLLOVERRIDES = f"{config.WINEDLLOVERRIDES};mscoree=" - run_wine_proc(config.WINE_EXE, exe='wineboot', exe_args=['--init']) + run_wine_proc(str(utils.get_wine_exe_path()), exe='wineboot', exe_args=['--init']) config.WINEDLLOVERRIDES = orig_overrides light_wineserver_wait() @@ -187,7 +187,7 @@ def wine_reg_install(REG_FILE): msg.logos_msg(f"Installing registry file: {REG_FILE}") env = get_wine_env() result = system.run_command( - [config.WINE_EXE, "regedit.exe", REG_FILE], + [str(utils.get_wine_exe_path()), "regedit.exe", REG_FILE], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, @@ -207,8 +207,8 @@ def install_msi(): exe_args = ["/i", f"{config.INSTALLDIR}/data/{config.LOGOS_EXECUTABLE}"] if config.PASSIVE is True: exe_args.append('/passive') - logging.info(f"Running: {config.WINE_EXE} msiexec {' '.join(exe_args)}") - run_wine_proc(config.WINE_EXE, exe="msiexec", exe_args=exe_args) + logging.info(f"Running: {utils.get_wine_exe_path()} msiexec {' '.join(exe_args)}") + run_wine_proc(str(utils.get_wine_exe_path()), exe="msiexec", exe_args=exe_args) def run_wine_proc(winecmd, exe=None, exe_args=list()): @@ -225,6 +225,8 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): else: m = "wine.wine_proc: wine.get_registry_value returned None." logging.error(m) + if isinstance(winecmd, Path): + winecmd = str(winecmd) logging.debug(f"run_wine_proc: {winecmd}; {exe=}; {exe_args=}") wine_env_vars = {k: v for k, v in env.items() if k.startswith('WINE')} logging.debug(f"wine environment: {wine_env_vars}") @@ -332,7 +334,7 @@ def installICUDataFiles(app=None): def get_registry_value(reg_path, name): value = None env = get_wine_env() - cmd = [config.WINE_EXE, 'reg', 'query', reg_path, '/v', name] + cmd = [str(utils.get_wine_exe_path()), 'reg', 'query', reg_path, '/v', name] err_msg = f"Failed to get registry value: {reg_path}\\{name}" try: result = system.run_command( @@ -397,7 +399,7 @@ def switch_logging(action=None, app=None): 'add', 'HKCU\\Software\\Logos4\\Logging', '/v', 'Enabled', '/t', 'REG_DWORD', '/d', value, '/f' ] - run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) run_wine_proc(config.WINESERVER_EXE, exe_args=['-w']) config.LOGS = state if app is not None: @@ -455,14 +457,14 @@ def get_wine_branch(binary): def get_wine_env(): wine_env = os.environ.copy() - winepath = Path(config.WINE_EXE) + winepath = utils.get_wine_exe_path() if winepath.name != 'wine64': # AppImage # Winetricks commands can fail if 'wine64' is not explicitly defined. # https://github.com/Winetricks/winetricks/issues/2084#issuecomment-1639259359 winepath = winepath.parent / 'wine64' wine_env_defaults = { 'WINE': str(winepath), - 'WINE_EXE': config.WINE_EXE, + 'WINE_EXE': str(utils.get_wine_exe_path()), 'WINEDEBUG': config.WINEDEBUG, 'WINEDLLOVERRIDES': config.WINEDLLOVERRIDES, 'WINELOADER': str(winepath), @@ -488,7 +490,7 @@ def get_wine_env(): def run_logos(app=None): logos_release = utils.convert_logos_release(config.current_logos_version) - wine_release, _ = get_wine_release(config.WINE_EXE) + wine_release, _ = get_wine_release(str(utils.get_wine_exe_path())) # TODO: Find a way to incorporate check_wine_version_and_branch() if 30 > logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): # noqa: E501 @@ -500,7 +502,7 @@ def run_logos(app=None): logging.critical(txt) msg.status(txt, app) else: - run_wine_proc(config.WINE_EXE, exe=config.LOGOS_EXE) + run_wine_proc(str(utils.get_wine_exe_path()), exe=config.LOGOS_EXE) run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) @@ -512,7 +514,7 @@ def run_indexing(): break run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) - run_wine_proc(config.WINE_EXE, exe=logos_indexer_exe) + run_wine_proc(str(utils.get_wine_exe_path()), exe=logos_indexer_exe) run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) From 1dbe93626aa8dc622469b7f8a226b5f972362127 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 29 Aug 2024 09:17:16 -0400 Subject: [PATCH 128/253] Code clean up --- config.py | 1 - gui_app.py | 1 - msg.py | 6 +++--- network.py | 46 +++++++++++++++++++++++----------------------- system.py | 18 +++++++++++------- tui_app.py | 51 +++++++++++++++++++++++++-------------------------- tui_screen.py | 1 - utils.py | 8 ++++++-- wine.py | 30 +++++++++++++++++------------- 9 files changed, 85 insertions(+), 77 deletions(-) diff --git a/config.py b/config.py index bab1800a..2e0d2277 100644 --- a/config.py +++ b/config.py @@ -38,7 +38,6 @@ 'SKIP_WINETRICKS': False, 'use_python_dialog': None, 'VERBOSE': False, - 'WINE_EXE': None, 'WINEBIN_CODE': None, 'WINEDEBUG': "fixme-all,err-all", 'WINEDLLOVERRIDES': '', diff --git a/gui_app.py b/gui_app.py index 189e6293..47a3b7d7 100644 --- a/gui_app.py +++ b/gui_app.py @@ -19,7 +19,6 @@ import gui import installer import network -import os import system import utils import wine diff --git a/msg.py b/msg.py index e2af0588..2f1bddaa 100644 --- a/msg.py +++ b/msg.py @@ -224,10 +224,10 @@ def cli_acknowledge_question(QUESTION_TEXT, NO_TEXT): def cli_ask_filepath(question_text): try: answer = input(f"{question_text} ") + return answer.strip('"').strip("'") except KeyboardInterrupt: print() logos_error("Cancelled with Ctrl+C") - return answer.strip('"').strip("'") def logos_continue_question(question_text, no_text, secondary, app=None): @@ -251,11 +251,11 @@ def logos_continue_question(question_text, no_text, secondary, app=None): logos_error(f"Unhandled question: {question_text}") -def logos_acknowledge_question(QUESTION_TEXT, NO_TEXT): +def logos_acknowledge_question(question_text, no_text): if config.DIALOG == 'curses': pass else: - return cli_acknowledge_question(QUESTION_TEXT, NO_TEXT) + return cli_acknowledge_question(question_text, no_text) def get_progress_str(percent): diff --git a/network.py b/network.py index 10473088..14b3f184 100644 --- a/network.py +++ b/network.py @@ -154,59 +154,59 @@ def cli_download(uri, destination): def logos_reuse_download( - SOURCEURL, - FILE, - TARGETDIR, + sourceurl, + file, + targetdir, app=None, ): - DIRS = [ + dirs = [ config.INSTALLDIR, os.getcwd(), config.MYDOWNLOADS, ] - FOUND = 1 - for i in DIRS: + found = 1 + for i in dirs: if i is not None: - logging.debug(f"Checking {i} for {FILE}.") - file_path = Path(i) / FILE + logging.debug(f"Checking {i} for {file}.") + file_path = Path(i) / file if os.path.isfile(file_path): - logging.info(f"{FILE} exists in {i}. Verifying properties.") + logging.info(f"{file} exists in {i}. Verifying properties.") if verify_downloaded_file( - SOURCEURL, + sourceurl, file_path, app=app, ): - logging.info(f"{FILE} properties match. Using it…") - msg.logos_msg(f"Copying {FILE} into {TARGETDIR}") + logging.info(f"{file} properties match. Using it…") + msg.logos_msg(f"Copying {file} into {targetdir}") try: - shutil.copy(os.path.join(i, FILE), TARGETDIR) + shutil.copy(os.path.join(i, file), targetdir) except shutil.SameFileError: pass - FOUND = 0 + found = 0 break else: logging.info(f"Incomplete file: {file_path}.") - if FOUND == 1: - file_path = os.path.join(config.MYDOWNLOADS, FILE) + if found == 1: + file_path = os.path.join(config.MYDOWNLOADS, file) if config.DIALOG == 'tk' and app: # Ensure progress bar. app.stop_indeterminate_progress() # Start download. net_get( - SOURCEURL, + sourceurl, target=file_path, app=app, ) else: - cli_download(SOURCEURL, file_path) + cli_download(sourceurl, file_path) if verify_downloaded_file( - SOURCEURL, + sourceurl, file_path, app=app, ): - msg.logos_msg(f"Copying: {FILE} into: {TARGETDIR}") + msg.logos_msg(f"Copying: {file} into: {targetdir}") try: - shutil.copy(os.path.join(config.MYDOWNLOADS, FILE), TARGETDIR) + shutil.copy(os.path.join(config.MYDOWNLOADS, file), targetdir) except shutil.SameFileError: pass else: @@ -322,10 +322,10 @@ def net_get(url, target=None, app=None, evt=None, q=None): def verify_downloaded_file(url, file_path, app=None, evt=None): if app: - msg.status(f"Verifying {file_path}…", app) if config.DIALOG == "tk": app.root.event_generate('<>') - app.status_q.put(f"Verifying {file_path}…") + msg.status(f"Verifying {file_path}…", app) + if config.DIALOG == "tk": app.root.event_generate('<>') res = False txt = f"{file_path} is the wrong size." diff --git a/system.py b/system.py index be0a055f..d7ad53ce 100644 --- a/system.py +++ b/system.py @@ -98,13 +98,13 @@ def get_dialog(): if not os.environ.get('DISPLAY'): msg.logos_error("The installer does not work unless you are running a display") # noqa: E501 - DIALOG = os.getenv('DIALOG') + dialog = os.getenv('DIALOG') # Set config.DIALOG. - if DIALOG is not None: - DIALOG = DIALOG.lower() - if DIALOG not in ['curses', 'tk']: + if dialog is not None: + dialog = dialog.lower() + if dialog not in ['curses', 'tk']: msg.logos_error("Valid values for DIALOG are 'curses' or 'tk'.") - config.DIALOG = DIALOG + config.DIALOG = dialog elif sys.__stdin__.isatty(): config.DIALOG = 'curses' else: @@ -321,8 +321,7 @@ def parse_date(version): def remove_appimagelauncher(app=None): pkg = "appimagelauncher" - cmd = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE] - cmd.append(pkg) + cmd = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE, pkg] msg.status("Removing AppImageLauncher…", app) try: logging.debug(f"Running command: {cmd}") @@ -389,6 +388,9 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) install_deps_failed = False manual_install_required = False + message = None + no_message = None + secondary = None command = [] preinstall_command = [] install_command = [] @@ -423,6 +425,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) if config.PACKAGE_MANAGER_COMMAND_INSTALL: if config.OS_NAME in ['fedora', 'arch']: message = False + no_message = False + secondary = False elif missing_packages and conflicting_packages: message = f"Your {config.OS_NAME} computer requires installing and removing some software.\nProceed?" # noqa: E501 no_message = "User refused to install and remove software via the application" # noqa: E501 diff --git a/tui_app.py b/tui_app.py index 7f1da021..f380e6b3 100644 --- a/tui_app.py +++ b/tui_app.py @@ -1,9 +1,7 @@ import logging import os -import sys import signal import threading -import time import curses from pathlib import Path from queue import Queue @@ -23,14 +21,12 @@ # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. -class TUI(): + +class TUI: def __init__(self, stdscr): self.stdscr = stdscr - #if config.current_logos_version is not None: self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" self.subtitle = f"Logos Version: {config.current_logos_version}" - #else: - # self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" self.console_message = "Starting TUI…" self.llirunning = True self.active_progress = False @@ -217,8 +213,6 @@ def display(self): self.active_screen.display() - #if (not isinstance(self.active_screen, tui_screen.TextScreen) - # and not isinstance(self.active_screen, tui_screen.TextDialog)): if self.choice_q.qsize() > 0: self.choice_processor( self.menu_window, @@ -278,8 +272,7 @@ def task_processor(self, evt=None, task=None): elif task == 'DONE': self.subtitle = f"Logos Version: {config.current_logos_version}" self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) - self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) - #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) + self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) # Force the main menu dialog self.switch_q.put(1) def choice_processor(self, stdscr, screen_id, choice): @@ -629,41 +622,45 @@ def set_tui_menu_options(self, dialog=False): def stack_menu(self, screen_id, queue, event, question, options, height=None, width=None, menu_height=8, dialog=False): if dialog: utils.append_unique(self.tui_screens, - tui_screen.MenuDialog(self, screen_id, queue, event, question, options, height, width, menu_height)) + tui_screen.MenuDialog(self, screen_id, queue, event, question, options, + height, width, menu_height)) else: utils.append_unique(self.tui_screens, - tui_screen.MenuScreen(self, screen_id, queue, event, question, options, height, width, menu_height)) + tui_screen.MenuScreen(self, screen_id, queue, event, question, options, + height, width, menu_height)) def stack_input(self, screen_id, queue, event, question, default, dialog=False): if dialog: utils.append_unique(self.tui_screens, - tui_screen.InputDialog(self, screen_id, queue, event, question, default)) + tui_screen.InputDialog(self, screen_id, queue, event, question, default)) else: utils.append_unique(self.tui_screens, - tui_screen.InputScreen(self, screen_id, queue, event, question, default)) + tui_screen.InputScreen(self, screen_id, queue, event, question, default)) def stack_password(self, screen_id, queue, event, question, default="", dialog=False): if dialog: utils.append_unique(self.tui_screens, - tui_screen.PasswordDialog(self, screen_id, queue, event, question, default)) + tui_screen.PasswordDialog(self, screen_id, queue, event, question, default)) else: utils.append_unique(self.tui_screens, - tui_screen.PasswordScreen(self, screen_id, queue, event, question, default)) + tui_screen.PasswordScreen(self, screen_id, queue, event, question, default)) def stack_confirm(self, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"], dialog=False): if dialog: yes_label = options[0] no_label = options[1] - utils.append_unique(self.tui_screens, tui_screen.ConfirmDialog(self, screen_id, queue, event, - question, no_text, secondary, - yes_label=yes_label, no_label=no_label)) + utils.append_unique(self.tui_screens, + tui_screen.ConfirmDialog(self, screen_id, queue, event, question, no_text, secondary, + yes_label=yes_label, no_label=no_label)) else: - utils.append_unique(self.tui_screens, tui_screen.ConfirmScreen(self, screen_id, queue, event, - question, no_text, secondary, options)) + utils.append_unique(self.tui_screens, + tui_screen.ConfirmScreen(self, screen_id, queue, event, question, no_text, secondary, + options)) def stack_text(self, screen_id, queue, event, text, wait=False, percent=None, dialog=False): if dialog: - utils.append_unique(self.tui_screens, tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) + utils.append_unique(self.tui_screens, + tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) else: utils.append_unique(self.tui_screens, tui_screen.TextScreen(self, screen_id, queue, event, text, wait)) @@ -679,20 +676,22 @@ def stack_tasklist(self, screen_id, queue, event, text, elements, percent, dialo def stack_buildlist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): if dialog: utils.append_unique(self.tui_screens, - tui_screen.BuildListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) + tui_screen.BuildListDialog(self, screen_id, queue, event, question, options, + height, width, list_height)) else: # TODO pass - def stack_checklist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): + def stack_checklist(self, screen_id, queue, event, question, options, + height=None, width=None, list_height=None, dialog=False): if dialog: utils.append_unique(self.tui_screens, - tui_screen.CheckListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) + tui_screen.CheckListDialog(self, screen_id, queue, event, question, options, + height, width, list_height)) else: # TODO pass - def update_tty_dimensions(self): self.window_height, self.window_width = self.stdscr.getmaxyx() diff --git a/tui_screen.py b/tui_screen.py index cae55bb3..b1b7eb71 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -2,7 +2,6 @@ import time from pathlib import Path import curses -import sys import config import installer diff --git a/utils.py b/utils.py index dd084a4b..d9c9c766 100644 --- a/utils.py +++ b/utils.py @@ -518,6 +518,8 @@ def install_premade_wine_bottle(srcdir, appdir): def compare_logos_linux_installer_version(): + status = None + message = None if ( config.LLI_CURRENT_VERSION is not None and config.LLI_LATEST_VERSION is not None @@ -553,6 +555,8 @@ def compare_logos_linux_installer_version(): def compare_recommended_appimage_version(): + status = None + message = None wine_release = [] if get_wine_exe_path() is not None: wine_release, error_message = wine.get_wine_release(get_wine_exe_path()) @@ -624,9 +628,9 @@ def is_appimage(file_path): appimage_check = elf_sig == b'ELF' and ai_sig == b'AI' appimage_type = int.from_bytes(v_sig) - return (appimage_check, appimage_type) + return appimage_check, appimage_type else: - return (False, None) + return False, None def check_appimage(filestr): diff --git a/wine.py b/wine.py index c005589a..575d952f 100644 --- a/wine.py +++ b/wine.py @@ -116,11 +116,11 @@ def check_wine_version_and_branch(release_version, test_binary): # commits in time. if config.TARGETVERSION == "10": if utils.check_logos_release_version(release_version, 30, 1): - WINE_MINIMUM = [7, 18] + wine_minimum = [7, 18] else: - WINE_MINIMUM = [9, 10] + wine_minimum = [9, 10] elif config.TARGETVERSION == "9": - WINE_MINIMUM = [7, 0] + wine_minimum = [7, 0] else: raise ValueError("TARGETVERSION not set.") @@ -152,8 +152,8 @@ def check_wine_version_and_branch(release_version, test_binary): return True, "None" elif wine_release[2] != 'staging': return False, "Needs to be Staging release" - elif wine_release[1] < WINE_MINIMUM[1]: - reason = f"{'.'.join(wine_release)} is below minimum required, {'.'.join(WINE_MINIMUM)}" # noqa: E501 + elif wine_release[1] < wine_minimum[1]: + reason = f"{'.'.join(wine_release)} is below minimum required, {'.'.join(wine_minimum)}" # noqa: E501 return False, reason elif wine_release[0] == 8: if wine_release[1] < 1: @@ -183,11 +183,11 @@ def initializeWineBottle(app=None): light_wineserver_wait() -def wine_reg_install(REG_FILE): - msg.logos_msg(f"Installing registry file: {REG_FILE}") +def wine_reg_install(reg_file): + msg.logos_msg(f"Installing registry file: {reg_file}") env = get_wine_env() result = system.run_command( - [str(utils.get_wine_exe_path()), "regedit.exe", REG_FILE], + [str(utils.get_wine_exe_path()), "regedit.exe", reg_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, @@ -195,9 +195,9 @@ def wine_reg_install(REG_FILE): capture_output=False ) if result is None or result.returncode != 0: - msg.logos_error(f"Failed to install reg file: {REG_FILE}") + msg.logos_error(f"Failed to install reg file: {reg_file}") elif result.returncode == 0: - logging.info(f"{REG_FILE} installed.") + logging.info(f"{reg_file} installed.") light_wineserver_wait() @@ -507,15 +507,19 @@ def run_logos(app=None): def run_indexing(): + logos_indexer_exe = None for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 for f in files: if f == "LogosIndexer.exe" and root.endswith("Logos/System"): logos_indexer_exe = os.path.join(root, f) break - run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) - run_wine_proc(str(utils.get_wine_exe_path()), exe=logos_indexer_exe) - run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) + if logos_indexer_exe is not None: + run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) + run_wine_proc(str(utils.get_wine_exe_path()), exe=logos_indexer_exe) + run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) + else: + logging.error("LogosIndexer.exe not found.") def end_wine_processes(): From 9794b692a3ab14829b123bc091b3634c85883de5 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 6 Sep 2024 17:20:40 +0100 Subject: [PATCH 129/253] remove extra GUI event generation --- network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network.py b/network.py index 14b3f184..14e5d1a4 100644 --- a/network.py +++ b/network.py @@ -325,8 +325,8 @@ def verify_downloaded_file(url, file_path, app=None, evt=None): if config.DIALOG == "tk": app.root.event_generate('<>') msg.status(f"Verifying {file_path}…", app) - if config.DIALOG == "tk": - app.root.event_generate('<>') + # if config.DIALOG == "tk": + # app.root.event_generate('<>') res = False txt = f"{file_path} is the wrong size." right_size = same_size(url, file_path) From b1a8f93b4ab14a1275622e79398ac95975b38fde Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 6 Sep 2024 22:21:41 -0400 Subject: [PATCH 130/253] Attempt to resolve faulty config setting Make sure to set new config if install needed --- installer.py | 21 ++++++--------------- system.py | 1 + tui_app.py | 3 +-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/installer.py b/installer.py index 0d244a88..3a73a647 100644 --- a/installer.py +++ b/installer.py @@ -177,24 +177,15 @@ def ensure_winetricks_choice(app=None): update_install_feedback("Choose winetricks binary…", app=app) logging.debug('- config.WINETRICKSBIN') - if config.WINETRICKSBIN is None: - # Check if local winetricks version available; else, download it. - config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" - - if app: + if app: + if config.WINETRICKSBIN is None: utils.send_task(app, 'WINETRICKSBIN') if config.DIALOG == 'curses': app.tricksbin_e.wait() - winetricksbin = app.tricksbin_q.get() - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) - - if not winetricksbin.startswith('Download'): - config.WINETRICKSBIN = winetricksbin + config.WINETRICKSBIN = app.tricksbin_q.get() else: - if config.DIALOG == 'curses': - app.set_winetricksbin(config.WINETRICKSBIN) + m = f"{utils.get_calling_function_name()}: --install-app is broken" + logging.critical(m) logging.debug(f"> {config.WINETRICKSBIN=}") @@ -392,7 +383,7 @@ def ensure_winetricks_executable(app=None): app=app ) - if not os.access(config.WINETRICKSBIN, os.X_OK): + if config.WINETRICKSBIN.startswith('Download') or not os.access(config.WINETRICKSBIN, os.X_OK): tricksbin = Path(config.WINETRICKSBIN) tricksbin.unlink(missing_ok=True) # The choice of System winetricks was made previously. Here we are only diff --git a/system.py b/system.py index d7ad53ce..1a810406 100644 --- a/system.py +++ b/system.py @@ -640,4 +640,5 @@ def install_winetricks( z.extract(zi, path=installdir) break os.chmod(f"{installdir}/winetricks", 0o755) + config.WINETRICKSBIN = f"{installdir}/winetricks" logging.debug("Winetricks installed.") diff --git a/tui_app.py b/tui_app.py index f380e6b3..5766555b 100644 --- a/tui_app.py +++ b/tui_app.py @@ -528,8 +528,7 @@ def set_winetricksbin(self, choice): self.tricksbin_q.put("Download") else: winetricks_options = utils.get_winetricks_options() - config.WINETRICKSBIN = winetricks_options[0] - self.tricksbin_q.put(config.WINETRICKSBIN) + self.tricksbin_q.put(winetricks_options[0]) self.menu_screen.choice = "Processing" self.tricksbin_e.set() From 21a4957ddc3acd63503bc48a3c440c9fde6ad0e5 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 17 Sep 2024 15:14:27 +0100 Subject: [PATCH 131/253] fix winetricks commands not working correctly --- installer.py | 112 +++++++++++++++++++++++++++------------------------ system.py | 17 ++++---- utils.py | 31 ++++++++++---- wine.py | 75 ++++++++++++++++++++++------------ 4 files changed, 143 insertions(+), 92 deletions(-) diff --git a/installer.py b/installer.py index 3a73a647..c51503e3 100644 --- a/installer.py +++ b/installer.py @@ -155,7 +155,7 @@ def ensure_wine_choice(app=None): app.set_wine(utils.get_wine_exe_path()) # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. - m = f"Preparing to process WINE_EXE. Currently set to: {utils.get_wine_exe_path()}." + m = f"Preparing to process WINE_EXE. Currently set to: {utils.get_wine_exe_path()}." # noqa: E501 logging.debug(m) if str(utils.get_wine_exe_path()).lower().endswith('.appimage'): config.SELECTED_APPIMAGE_FILENAME = str(utils.get_wine_exe_path()) @@ -257,11 +257,11 @@ def ensure_install_dirs(app=None): if config.INSTALLDIR is None: config.INSTALLDIR = f"{os.getenv('HOME')}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" bin_dir = Path(config.APPDIR_BINDIR) bin_dir.mkdir(parents=True, exist_ok=True) - logging.debug(f"> {bin_dir} exists: {bin_dir.is_dir()}") + logging.debug(f"> {bin_dir} exists?: {bin_dir.is_dir()}") logging.debug(f"> {config.INSTALLDIR=}") logging.debug(f"> {config.APPDIR_BINDIR=}") @@ -366,12 +366,15 @@ def ensure_wine_executables(app=None): p.symlink_to(f"./{config.APPIMAGE_LINK_SELECTION_NAME}") # Set WINESERVER_EXE. - config.WINESERVER_EXE = shutil.which('wineserver') + config.WINESERVER_EXE = f"{config.APPDIR_BINDIR}/wineserver" + # PATH is modified if wine appimage isn't found, but it's not modified + # during a restarted installation, so shutil.which doesn't find the + # executables in that case. logging.debug(f"> {config.WINESERVER_EXE=}") - logging.debug(f"> wine path: {shutil.which('wine')}") - logging.debug(f"> wine64 path: {shutil.which('wine64')}") - logging.debug(f"> wineserver path: {shutil.which('wineserver')}") + logging.debug(f"> wine path: {config.APPDIR_BINDIR}/wine") + logging.debug(f"> wine64 path: {config.APPDIR_BINDIR}/wine64") + logging.debug(f"> wineserver path: {config.APPDIR_BINDIR}/wineserver") def ensure_winetricks_executable(app=None): @@ -383,18 +386,15 @@ def ensure_winetricks_executable(app=None): app=app ) - if config.WINETRICKSBIN.startswith('Download') or not os.access(config.WINETRICKSBIN, os.X_OK): - tricksbin = Path(config.WINETRICKSBIN) - tricksbin.unlink(missing_ok=True) - # The choice of System winetricks was made previously. Here we are only - # concerned about whether the downloaded winetricks is usable. + if config.WINETRICKSBIN is None or config.WINETRICKSBIN.startswith('Download'): # noqa: E501 + config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" # default + if not os.access(config.WINETRICKSBIN, os.X_OK): + # Either previous system winetricks is no longer accessible, or the + # or the user has chosen to download it. msg.logos_msg("Downloading winetricks from the Internet…") - system.install_winetricks( - tricksbin.parent, - app=app - ) - if config.WINETRICKSBIN: - logging.debug(f"> {config.WINETRICKSBIN} is executable?: {os.access(config.WINETRICKSBIN, os.X_OK)}") # noqa: E501 + system.install_winetricks(config.APPDIR_BINDIR, app=app) + + logging.debug(f"> {config.WINETRICKSBIN} is executable?: {os.access(config.WINETRICKSBIN, os.X_OK)}") # noqa: E501 return 0 @@ -463,43 +463,34 @@ def ensure_wineprefix_init(app=None): update_install_feedback("Ensuring wineprefix is initialized…", app=app) init_file = Path(f"{config.WINEPREFIX}/system.reg") + logging.debug(f"{init_file=}") if not init_file.is_file(): + logging.debug(f"{init_file} does not exist") if config.TARGETVERSION == '9': utils.install_premade_wine_bottle( config.MYDOWNLOADS, f"{config.INSTALLDIR}/data", ) else: - if utils.get_wine_exe_path(): - wine.initializeWineBottle() + # if utils.get_wine_exe_path(): + # wine.initializeWineBottle() + logging.debug("Initializing wineprefix.") + wine.initializeWineBottle() logging.debug(f"> {init_file} exists?: {init_file.is_file()}") -def ensure_icu_data_files(app=None): - config.INSTALL_STEPS_COUNT += 1 - ensure_wineprefix_init(app=app) - config.INSTALL_STEP += 1 - status = "Ensuring ICU data files are installed…" - update_install_feedback(status, app=app) - logging.debug('- ICU data files') - - icu_license_path = f"{config.WINEPREFIX}/drive_c/windows/globalization/ICU/LICENSE-ICU.txt" # noqa: E501 - if not utils.file_exists(icu_license_path): - wine.installICUDataFiles(app=app) - logging.debug('> ICU data files installed') - - def ensure_winetricks_applied(app=None): config.INSTALL_STEPS_COUNT += 1 - ensure_icu_data_files(app=app) + ensure_wineprefix_init(app=app) config.INSTALL_STEP += 1 status = "Ensuring winetricks & other settings are applied…" update_install_feedback(status, app=app) logging.debug('- disable winemenubuilder') + logging.debug('- settings win10') logging.debug('- settings renderer=gdi') - logging.debug('- corefonts') - logging.debug('- tahoma') logging.debug('- settings fontsmooth=rgb') + logging.debug('- tahoma') + logging.debug('- corefonts') logging.debug('- d3dcompiler_47') if not config.SKIP_WINETRICKS: @@ -509,35 +500,34 @@ def ensure_winetricks_applied(app=None): workdir.mkdir(parents=True, exist_ok=True) usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") + if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - # FIXME: This command is failing. - reg_file = os.path.join( - config.WORKDIR, - 'disable-winemenubuilder.reg' - ) - with open(reg_file, 'w') as f: - f.write(r'''REGEDIT4 + reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' + reg_file.write_text(r'''REGEDIT4 [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "winemenubuilder.exe"="" ''') wine.wine_reg_install(reg_file) + if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): + # args = ["-q", "settings", "win10"] + # if not config.WINETRICKS_UNATTENDED: + # args.insert(0, "-q") + wine.winetricks_install("-q", "settings", "win10") + if not utils.grep(r'"renderer"="gdi"', usr_reg): wine.winetricks_install("-q", "settings", "renderer=gdi") + if not utils.grep(r'"FontSmoothingType"=dword:00000002', usr_reg): + wine.winetricks_install("-q", "settings", "fontsmooth=rgb") + if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 wine.installFonts() if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): wine.installD3DCompiler() - if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): - args = ["settings", "win10"] - if not config.WINETRICKS_UNATTENDED: - args.insert(0, "-q") - wine.winetricks_install(*args) - m = f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode." msg.logos_msg(m) exe_args = [ @@ -547,14 +537,32 @@ def ensure_winetricks_applied(app=None): "/t", "REG_SZ", "/d", "vista", "/f", ] - wine.run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) + wine.run_wine_proc( + str(utils.get_wine_exe_path()), + exe='reg', + exe_args=exe_args + ) logging.debug("> Done.") -def ensure_product_installed(app=None): +def ensure_icu_data_files(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_winetricks_applied(app=app) config.INSTALL_STEP += 1 + status = "Ensuring ICU data files are installed…" + update_install_feedback(status, app=app) + logging.debug('- ICU data files') + + icu_license_path = f"{config.WINEPREFIX}/drive_c/windows/globalization/ICU/LICENSE-ICU.txt" # noqa: E501 + if not utils.file_exists(icu_license_path): + wine.installICUDataFiles(app=app) + logging.debug('> ICU data files installed') + + +def ensure_product_installed(app=None): + config.INSTALL_STEPS_COUNT += 1 + ensure_icu_data_files(app=app) + config.INSTALL_STEP += 1 update_install_feedback("Ensuring product is installed…", app=app) if not utils.find_installed_product(): diff --git a/system.py b/system.py index 1a810406..0e31fa81 100644 --- a/system.py +++ b/system.py @@ -23,7 +23,7 @@ def run_command(command, retries=1, delay=0, **kwargs): env = kwargs.get("env", None) cwd = kwargs.get("cwd", None) encoding = kwargs.get("encoding", None) - input = kwargs.get("input", None) + cmdinput = kwargs.get("input", None) stdin = kwargs.get("stdin", None) stdout = kwargs.get("stdout", None) stderr = kwargs.get("stderr", None) @@ -42,13 +42,13 @@ def run_command(command, retries=1, delay=0, **kwargs): text=text, shell=shell, capture_output=capture_output, - input=input, + input=cmdinput, stdin=stdin, stdout=stdout, stderr=stderr, encoding=encoding, cwd=cwd, - env=env + env=env, ) return result except subprocess.CalledProcessError as e: @@ -145,7 +145,8 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_REMOVE = ["apt", "remove", "-y"] config.PACKAGE_MANAGER_COMMAND_QUERY = ["dpkg", "-l"] config.QUERY_PREFIX = '.i ' - config.PACKAGES = "binutils cabextract fuse3 wget winbind" + # NOTE: in 24.04 "p7zip-full" pkg is transitional toward "7zip" + config.PACKAGES = "binutils cabextract fuse3 p7zip-full wget winbind" config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "" # appimagelauncher handled separately elif shutil.which('dnf') is not None: # rhel, fedora @@ -159,6 +160,7 @@ def get_package_manager(): config.PACKAGES = ( "fuse3 fuse3-libs " # appimages "mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract " # wine # noqa: E501 + "p7zip-plugins " # winetricks ) config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "" # appimagelauncher handled separately @@ -168,7 +170,7 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pamac", "remove", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = ["pamac", "list", "-i"] config.QUERY_PREFIX = '' - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl" # noqa: E501 + config.PACKAGES = "patch wget sed grep gawk cabextract p7zip samba bc libxml2 curl" # noqa: E501 config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "" # appimagelauncher handled separately elif shutil.which('pacman') is not None: # arch, steamOS @@ -184,6 +186,7 @@ def get_package_manager(): config.PACKAGES = ( "fuse2 fuse3 " # appimages "binutils cabextract wget libwbclient " # wine + "7-zip-full " # winetricks "openjpeg2 libxcomposite libxinerama " # display "ocl-icd vulkan-icd-loader " # hardware "alsa-plugins gst-plugins-base-libs libpulse openal " # audio @@ -321,7 +324,7 @@ def parse_date(version): def remove_appimagelauncher(app=None): pkg = "appimagelauncher" - cmd = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE, pkg] + cmd = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE, pkg] # noqa: E501 msg.status("Removing AppImageLauncher…", app) try: logging.debug(f"Running command: {cmd}") @@ -502,7 +505,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) command.append('&&') command.extend(postinstall_command) if not command: # nothing to run; avoid running empty pkexec command - logging.debug(f"No dependency install required.") + logging.debug("No dependency install required.") if app: if config.DIALOG == "curses": app.installdeps_e.set() diff --git a/utils.py b/utils.py index d9c9c766..3f94e6cd 100644 --- a/utils.py +++ b/utils.py @@ -86,9 +86,13 @@ def write_config(config_file_path): try: for key, value in config_data.items(): if key == "WINE_EXE": - # We store the value of WINE_EXE as relative path if it is in the install directory. + # We store the value of WINE_EXE as relative path if it is in + # the install directory. if value is not None: - value = get_relative_path(get_config_var(value), config.INSTALLDIR) + value = get_relative_path( + get_config_var(value), + config.INSTALLDIR + ) if isinstance(value, Path): config_data[key] = str(value) with open(config_file_path, 'w') as config_file: @@ -558,8 +562,9 @@ def compare_recommended_appimage_version(): status = None message = None wine_release = [] - if get_wine_exe_path() is not None: - wine_release, error_message = wine.get_wine_release(get_wine_exe_path()) + wine_exe_path = get_wine_exe_path() + if wine_exe_path is not None: + wine_release, error_message = wine.get_wine_release(wine_exe_path) if wine_release is not None and wine_release is not False: current_version = '.'.join([str(n) for n in wine_release[:2]]) logging.debug(f"Current wine release: {current_version}") @@ -900,6 +905,7 @@ def get_relative_path(path, base_path): else: if isinstance(path, Path): path = str(path) + base_path = str(base_path) if path.startswith(base_path): return path[len(base_path):].lstrip(os.sep) else: @@ -910,10 +916,12 @@ def create_dynamic_path(path, base_path): if is_relative_path(path): if isinstance(path, str): path = Path(path) - if isinstance(path, str): + if isinstance(base_path, str): base_path = Path(base_path) + logging.debug(f"dynamic_path: {base_path / path}") return base_path / path else: + logging.debug(f"dynamic_path: {Path(path)}") return Path(path) @@ -929,10 +937,17 @@ def get_config_var(var): def get_wine_exe_path(path=None): if path is not None: path = get_relative_path(get_config_var(path), config.INSTALLDIR) - return Path(create_dynamic_path(path, config.INSTALLDIR)) + wine_exe_path = Path(create_dynamic_path(path, config.INSTALLDIR)) + logging.debug(f"{wine_exe_path=}") + return wine_exe_path else: if config.WINE_EXE is not None: - path = get_relative_path(get_config_var(config.WINE_EXE), config.INSTALLDIR) - return Path(create_dynamic_path(path, config.INSTALLDIR)) + path = get_relative_path( + get_config_var(config.WINE_EXE), + config.INSTALLDIR + ) + wine_exe_path = Path(create_dynamic_path(path, config.INSTALLDIR)) + logging.debug(f"{wine_exe_path=}") + return wine_exe_path else: return None diff --git a/wine.py b/wine.py index 575d952f..62ce3a45 100644 --- a/wine.py +++ b/wine.py @@ -34,12 +34,14 @@ def get_pids_using_file(file_path, mode=None): def wait_on(command): + env = get_wine_env() try: # Start the process in the background process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=env, text=True ) msg.logos_msg(f"Waiting on \"{' '.join(command)}\" to finish.", end='') @@ -174,25 +176,25 @@ def check_wine_version_and_branch(release_version, test_binary): def initializeWineBottle(app=None): msg.logos_msg("Initializing wine bottle...") - + wine_exe = str(utils.get_wine_exe_path().parent / 'wine64') + logging.debug(f"{wine_exe=}") # Avoid wine-mono window orig_overrides = config.WINEDLLOVERRIDES config.WINEDLLOVERRIDES = f"{config.WINEDLLOVERRIDES};mscoree=" - run_wine_proc(str(utils.get_wine_exe_path()), exe='wineboot', exe_args=['--init']) + logging.debug(f"Running: {wine_exe} wineboot --init") + run_wine_proc(wine_exe, exe='wineboot', exe_args=['--init'], init=True) config.WINEDLLOVERRIDES = orig_overrides light_wineserver_wait() + logging.debug("Wine init complete.") def wine_reg_install(reg_file): + reg_file = str(reg_file) msg.logos_msg(f"Installing registry file: {reg_file}") - env = get_wine_env() - result = system.run_command( - [str(utils.get_wine_exe_path()), "regedit.exe", reg_file], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - cwd=config.WORKDIR, - capture_output=False + result = run_wine_proc( + str(utils.get_wine_exe_path().parent / 'wine64'), + exe="regedit.exe", + exe_args=[reg_file] ) if result is None or result.returncode != 0: msg.logos_error(f"Failed to install reg file: {reg_file}") @@ -204,17 +206,20 @@ def wine_reg_install(reg_file): def install_msi(): msg.logos_msg(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.") # Execute the .MSI + wine_exe = str(utils.get_wine_exe_path().parent / 'wine64') exe_args = ["/i", f"{config.INSTALLDIR}/data/{config.LOGOS_EXECUTABLE}"] if config.PASSIVE is True: exe_args.append('/passive') - logging.info(f"Running: {utils.get_wine_exe_path()} msiexec {' '.join(exe_args)}") - run_wine_proc(str(utils.get_wine_exe_path()), exe="msiexec", exe_args=exe_args) + logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") + run_wine_proc(wine_exe, exe="msiexec", exe_args=exe_args) -def run_wine_proc(winecmd, exe=None, exe_args=list()): +def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): + logging.debug("Getting wine environment.") env = get_wine_env() - if config.WINECMD_ENCODING is None: + if not init and config.WINECMD_ENCODING is None: # Get wine system's cmd.exe encoding for proper decoding to UTF8 later. + logging.debug("Getting wine system's cmd.exe encoding.") registry_value = get_registry_value( 'HKCU\\Software\\Wine\\Fonts', 'Codepages' @@ -228,16 +233,14 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): if isinstance(winecmd, Path): winecmd = str(winecmd) logging.debug(f"run_wine_proc: {winecmd}; {exe=}; {exe_args=}") - wine_env_vars = {k: v for k, v in env.items() if k.startswith('WINE')} - logging.debug(f"wine environment: {wine_env_vars}") command = [winecmd] if exe is not None: command.append(exe) if exe_args: command.extend(exe_args) - logging.debug(f"subprocess cmd: '{' '.join(command)}'") + logging.debug(f"subprocess cmd: '{' '.join(command)}'") try: process = subprocess.Popen( command, @@ -259,14 +262,18 @@ def run_wine_proc(winecmd, exe=None, exe_args=list()): logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 else: logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 + returncode = process.wait() if returncode != 0: logging.error(f"Error running '{' '.join(command)}': {process.returncode}") # noqa: E501 + msg.logos_error(f"\"{command}\" failed with exit code: {process.returncode}") # noqa: E501 except subprocess.CalledProcessError as e: logging.error(f"Exception running '{' '.join(command)}': {e}") + return process + def run_winetricks(cmd=None): run_wine_proc(config.WINETRICKSBIN, exe=cmd) @@ -332,14 +339,23 @@ def installICUDataFiles(app=None): def get_registry_value(reg_path, name): + # NOTE: Can't use run_wine_proc here because of infinite recursion while + # trying to determine WINECMD_ENCODING. value = None env = get_wine_env() - cmd = [str(utils.get_wine_exe_path()), 'reg', 'query', reg_path, '/v', name] + + cmd = [ + str(utils.get_wine_exe_path().parent / 'wine64'), + 'reg', 'query', reg_path, '/v', name, + ] err_msg = f"Failed to get registry value: {reg_path}\\{name}" + encoding = config.WINECMD_ENCODING + if encoding is None: + encoding = 'UTF-8' try: result = system.run_command( cmd, - encoding=config.WINECMD_ENCODING, + encoding=encoding, env=env ) except subprocess.CalledProcessError as e: @@ -399,8 +415,12 @@ def switch_logging(action=None, app=None): 'add', 'HKCU\\Software\\Logos4\\Logging', '/v', 'Enabled', '/t', 'REG_DWORD', '/d', value, '/f' ] - run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) - run_wine_proc(config.WINESERVER_EXE, exe_args=['-w']) + run_wine_proc( + str(utils.get_wine_exe_path().parent / 'wine64'), + exe='reg', + exe_args=exe_args + ) + light_wineserver_wait() config.LOGS = state if app is not None: app.logging_q.put(state) @@ -464,17 +484,20 @@ def get_wine_env(): winepath = winepath.parent / 'wine64' wine_env_defaults = { 'WINE': str(winepath), - 'WINE_EXE': str(utils.get_wine_exe_path()), 'WINEDEBUG': config.WINEDEBUG, 'WINEDLLOVERRIDES': config.WINEDLLOVERRIDES, 'WINELOADER': str(winepath), 'WINEPREFIX': config.WINEPREFIX, - 'WINETRICKS_SUPER_QUIET': '', + 'WINESERVER': config.WINESERVER_EXE, + # The following seems to cause some winetricks commands to fail; e.g. + # 'winetricks settings win10' exits with ec = 1 b/c it fails to find + # %ProgramFiles%, %AppData%, etc. + # 'WINETRICKS_SUPER_QUIET': '', } for k, v in wine_env_defaults.items(): wine_env[k] = v - if config.LOG_LEVEL > logging.INFO: - wine_env['WINETRICKS_SUPER_QUIET'] = "1" + # if config.LOG_LEVEL > logging.INFO: + # wine_env['WINETRICKS_SUPER_QUIET'] = "1" # Config file takes precedence over the above variables. cfg = config.get_config_file_dict(config.CONFIG_FILE) @@ -485,6 +508,8 @@ def get_wine_env(): if key in wine_env_defaults.keys(): wine_env[key] = value + updated_env = {k: wine_env.get(k) for k in wine_env_defaults.keys()} + logging.debug(f"Wine env: {updated_env}") return wine_env From f02e0133670040337b0351de7582034f827e10ba Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 17 Sep 2024 15:35:34 +0100 Subject: [PATCH 132/253] fix release version not being honored --- gui_app.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gui_app.py b/gui_app.py index 47a3b7d7..9ffdc13e 100644 --- a/gui_app.py +++ b/gui_app.py @@ -366,7 +366,7 @@ def set_release(self, evt=None): self.gui.logos_release_version = self.gui.releasevar.get() self.gui.release_dropdown.selection_clear() if evt: # manual override - config.TARGET_RELEASE_VERSION = None + config.TARGET_RELEASE_VERSION = self.gui.logos_release_version logging.debug(f"User changed TARGET_RELEASE_VERSION to '{self.gui.logos_release_version}'") # noqa: E501 config.INSTALLDIR = None @@ -443,7 +443,12 @@ def set_wine(self, evt=None): self.start_ensure_config() else: - self.wine_q.put(utils.get_relative_path(utils.get_config_var(self.gui.wine_exe), config.INSTALLDIR)) + self.wine_q.put( + utils.get_relative_path( + utils.get_config_var(self.gui.wine_exe), + config.INSTALLDIR + ) + ) def set_winetricks(self, evt=None): self.gui.winetricksbin = self.gui.tricksvar.get() From c0d8af72da0c1460c84ed8357edd453fd2f62e75 Mon Sep 17 00:00:00 2001 From: James Blackburn Date: Sun, 22 Sep 2024 09:17:31 +0100 Subject: [PATCH 133/253] Correct ensure-venv command Corrects spelling in "scrips" -> "scripts" and a few grammatical mistakes. --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c7b88114..5d37d12f 100644 --- a/README.md +++ b/README.md @@ -13,25 +13,25 @@ This program is created and maintained by the FaithLife Community and is license The main program is a distributable executable and contains Python itself and all necessary Python packages. When running the program, it will attempt to determine your operating system and package manager. -It will then attempt to install all needed system dependencies during the install of Logos. -When the install is finished, it will place two shortcuts on your computer: one will launch Logos directly; the other will launch the Control Panel. +It will then attempt to install all needed system dependencies during the installation of Logos. +When the installation is finished, it will place two shortcuts on your computer: one will launch Logos directly; the other will launch the Control Panel. -To access the GUI version of the program, double click the executable in your file browser or on your desktop, and then follow the prompts. +To access the GUI version of the program, double-click the executable in your file browser or on your desktop, and then follow the prompts. The program can also be run from source and should be run from a Python virtual environment. See below. ## Install Guide (for users) -For an install guide with pictures and video, see the wiki's [Install Guide](https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki/Install-Guide). +For an installation guide with pictures and video, see the wiki's [Install Guide](https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki/Install-Guide). ## Installing/running from Source (for developers) You can clone the repo and install the app from source. To do so, you will need to ensure a few prerequisites: 1. Install build dependencies -1. Clone this repository -1. Build/install Python 3.12 and Tcl/Tk -1. Set up a virtual environment +2. Clone this repository +3. Build/install Python 3.12 and Tcl/Tk +4. Set up a virtual environment ### Install build dependencies @@ -95,7 +95,7 @@ up a virtual environment for running and/or building locally. **Using the script:** ``` -./scrips/ensure-venv.sh +./scripts/ensure-venv.sh ``` **Manual setup:** From ef58bdb741f95de49656080469d1d1d2db9407dc Mon Sep 17 00:00:00 2001 From: James Blackburn Date: Sun, 22 Sep 2024 10:13:23 +0100 Subject: [PATCH 134/253] Add Dockerfile for building with Python 3.12 --- Dockerfile | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d6839bb1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker.io/docker/dockerfile:1.7-labs + +FROM ubuntu:focal + +# Prevent popups during install of requirements +ENV DEBIAN_FRONTEND=noninteractive + +# LogosLinuxInstaller Requirements +RUN apt update -qq && apt install -y -qq git build-essential gdb lcov pkg-config \ + libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \ + libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \ + lzma lzma-dev python3-tk tk-dev uuid-dev zlib1g-dev && rm -rf /var/lib/apt/lists/* + +# pyenv for guaranteed py 3.12 +ENV HOME="/root" +WORKDIR ${HOME} +RUN apt update && apt install -y curl && rm -rf /var/lib/apt/lists/* +RUN curl https://pyenv.run | bash +ENV PYENV_ROOT="${HOME}/.pyenv" +ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${PATH}" + +# Ensure tkinter +ENV PYTHON_CONFIGURE_OPTS "--enable-shared" + +# install py 3.12 +ENV PYTHON_VERSION=3.12.6 +RUN pyenv install --verbose ${PYTHON_VERSION} +RUN pyenv global ${PYTHON_VERSION} + +WORKDIR /usr/src/app +ENTRYPOINT ["sh", "-c", "pip install --no-cache-dir pyinstaller -r requirements.txt && pyinstaller LogosLinuxInstaller.spec"] From 6847c98ab9ec67d48cd70de69e549476b916825e Mon Sep 17 00:00:00 2001 From: James Blackburn Date: Sun, 22 Sep 2024 10:16:03 +0100 Subject: [PATCH 135/253] Add README explanation of building with docker --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 5d37d12f..35af14cb 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,15 @@ Python 3.12.5 (env) LogosLinuxInstaller$ ./main.py --help # run the script ``` +### Building using docker + +``` +$ git clone 'https://github.com/FaithLife-Community/LogosLinuxInstaller.git' +$ cd LogosLinuxInstaller +# docker build -t logosinstaller . +# docker run --rm -v $(pwd):/usr/src/app logosinstaller +``` + ## Install guide (possibly outdated) NOTE: You can run Logos on Linux using the Steam Proton Experimental binary, which often has the latest and greatest updates to make Logos run even smoother. The script should be able to find the binary automatically, unless your Steam install is located outside of your HOME directory. From 86a2733dcbd7abaf8bfbfd7b5d79e15c606c47f7 Mon Sep 17 00:00:00 2001 From: James Blackburn Date: Tue, 1 Oct 2024 10:11:10 +0100 Subject: [PATCH 136/253] Explain where docker binary will be output --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 35af14cb..b0e28f5b 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ $ cd LogosLinuxInstaller # docker run --rm -v $(pwd):/usr/src/app logosinstaller ``` +The built binary will now be in `./dist/LogosLinuxInstaller`. + ## Install guide (possibly outdated) NOTE: You can run Logos on Linux using the Steam Proton Experimental binary, which often has the latest and greatest updates to make Logos run even smoother. The script should be able to find the binary automatically, unless your Steam install is located outside of your HOME directory. From db5b27f3cb662e0e401f92c0a8f7bccbe39c5160 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 29 Aug 2024 22:22:33 -0400 Subject: [PATCH 137/253] Partial fix for #143 --- tui_app.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tui_app.py b/tui_app.py index 5766555b..1378a2e5 100644 --- a/tui_app.py +++ b/tui_app.py @@ -395,24 +395,24 @@ def choice_processor(self, stdscr, screen_id, choice): self.password_q.put(choice) self.password_e.set() elif screen_id == 16: - if choice == "No": - self.menu_screen.choice = "Processing" - self.choice_q.put("Return to Main Menu") - else: - self.menu_screen.choice = "Processing" - self.confirm_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, - "Installing dependencies…\n", wait=True, - dialog=config.use_python_dialog)) + if choice: + if choice == "No": + self.menu_screen.choice = "Processing" + self.choice_q.put("Return to Main Menu") + else: + self.menu_screen.choice = "Processing" + self.confirm_e.set() + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, + "Installing dependencies…\n", wait=True, + dialog=config.use_python_dialog)) elif screen_id == 17: - if choice == "Yes": - self.menu_screen.choice = "Processing" - self.manualinstall_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, - "Installing dependencies…\n", wait=True, - dialog=config.use_python_dialog)) - else: - self.llirunning = False + if choice: + if choice == "Continue": + self.menu_screen.choice = "Processing" + self.manualinstall_e.set() + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, + "Installing dependencies…\n", wait=True, + dialog=config.use_python_dialog)) def switch_screen(self, dialog): if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: From 1e99647c8fbda2df32de6e5f35db998e8ede769f Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 29 Aug 2024 22:51:37 -0400 Subject: [PATCH 138/253] Switch tui_app.choice_processor to dict --- tui_app.py | 329 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 202 insertions(+), 127 deletions(-) diff --git a/tui_app.py b/tui_app.py index 1378a2e5..195cc574 100644 --- a/tui_app.py +++ b/tui_app.py @@ -276,143 +276,218 @@ def task_processor(self, evt=None, task=None): self.switch_q.put(1) def choice_processor(self, stdscr, screen_id, choice): + screen_actions = { + 0: self.main_menu_select, + 1: self.custom_appimage_select, + 2: self.product_select, + 3: self.version_select, + 4: self.release_select, + 5: self.installdir_select, + 6: self.wine_select, + 7: self.winetricksbin_select, + 8: self.waiting, + 9: self.config_update_select, + 10: self.waiting_releases, + 11: self.waiting, # Unused + 12: self.run_logos, + 13: self.waiting_finish, + 14: self.waiting_resize, + 15: self.password_prompt, + 16: self.install_dependencies_confirm, + 17: self.manual_install_confirm, + # Add more screen_id mappings as needed + } + + # Capture menu exiting before processing in the rest of the handler if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): self.switch_q.put(1) #FIXME: There is some kind of graphical glitch that activates on returning to Main Menu, # but not from all submenus. # Further, there appear to be issues with how the program exits on Ctrl+C as part of this. - elif screen_id == 0: - if choice is None or choice == "Exit": - msg.logos_warn("Exiting installation.") - self.tui_screens = [] - self.llirunning = False - elif choice.startswith("Install"): - config.INSTALL_STEPS_COUNT = 0 - config.INSTALL_STEP = 0 - utils.start_thread(installer.ensure_launcher_shortcuts, daemon_bool=True, app=self) - elif choice.startswith("Update Logos Linux Installer"): - utils.update_to_latest_lli_release() - elif choice == f"Run {config.FLPRODUCT}": - self.active_screen.running = 0 - self.active_screen.choice = "Processing" - self.screen_q.put(self.stack_text(12, self.todo_q, self.todo_e, "Logos is running…", dialog=config.use_python_dialog)) - self.choice_q.put("0") - elif choice == "Run Indexing": - wine.run_indexing() - elif choice == "Remove Library Catalog": - control.remove_library_catalog() - elif choice == "Remove All Index Files": - control.remove_all_index_files() - elif choice == "Edit Config": - control.edit_config() - elif choice == "Install Dependencies": - utils.check_dependencies() - elif choice == "Back up Data": - control.backup() - elif choice == "Restore Data": - control.restore() - elif choice == "Update to Latest AppImage": - utils.update_to_latest_recommended_appimage() - elif choice == "Set AppImage": - # TODO: Allow specifying the AppImage File - appimages = utils.find_appimage_files(utils.which_release()) - appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in - appimages] # noqa: E501 - appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) - self.menu_options = appimage_choices - question = "Which AppImage should be used?" - self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) - elif choice == "Download or Update Winetricks": - control.set_winetricks() - elif choice == "Run Winetricks": - wine.run_winetricks() - elif choice == "Install d3dcompiler": - wine.installD3DCompiler() - elif choice == "Install Fonts": - wine.installFonts() - elif choice == "Install ICU": - wine.installICUDataFiles() - elif choice.endswith("Logging"): - wine.switch_logging() - elif screen_id == 1: - #FIXME - if choice == "Input Custom AppImage": - appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") + + action = screen_actions.get(screen_id) + if action: + action(choice) + else: + pass + + def main_menu_select(self, choice): + if choice is None or choice == "Exit": + msg.logos_warn("Exiting installation.") + self.tui_screens = [] + self.llirunning = False + elif choice.startswith("Install"): + config.INSTALL_STEPS_COUNT = 0 + config.INSTALL_STEP = 0 + utils.start_thread(installer.ensure_launcher_shortcuts, True, self) + elif choice.startswith("Update Logos Linux Installer"): + utils.update_to_latest_lli_release() + elif choice == f"Run {config.FLPRODUCT}": + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + self.screen_q.put(self.stack_text(12, self.todo_q, self.todo_e, "Logos is running…", dialog=config.use_python_dialog)) + self.choice_q.put("0") + elif choice == "Run Indexing": + wine.run_indexing() + elif choice == "Remove Library Catalog": + control.remove_library_catalog() + elif choice == "Remove All Index Files": + control.remove_all_index_files() + elif choice == "Edit Config": + control.edit_config() + elif choice == "Install Dependencies": + utils.check_dependencies() + elif choice == "Back up Data": + control.backup() + elif choice == "Restore Data": + control.restore() + elif choice == "Update to Latest AppImage": + utils.update_to_latest_recommended_appimage() + elif choice == "Set AppImage": + # TODO: Allow specifying the AppImage File + appimages = utils.find_appimage_files(utils.which_release()) + appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in + appimages] # noqa: E501 + appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) + self.menu_options = appimage_choices + question = "Which AppImage should be used?" + self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) + elif choice == "Download or Update Winetricks": + control.set_winetricks() + elif choice == "Run Winetricks": + wine.run_winetricks() + elif choice == "Install d3dcompiler": + wine.installD3DCompiler() + elif choice == "Install Fonts": + wine.installFonts() + elif choice == "Install ICU": + wine.installICUDataFiles() + elif choice.endswith("Logging"): + wine.switch_logging() + + def custom_appimage_select(self, choice): + #FIXME + if choice == "Input Custom AppImage": + appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") + else: + appimage_filename = choice + config.SELECTED_APPIMAGE_FILENAME = appimage_filename + utils.set_appimage_symlink() + self.menu_screen.choice = "Processing" + self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) + self.appimage_e.set() + + def product_select(self, choice): + if choice: + if str(choice).startswith("Logos"): + config.FLPRODUCT = "Logos" + elif str(choice).startswith("Verbum"): + config.FLPRODUCT = "Verbum" + self.menu_screen.choice = "Processing" + self.product_q.put(config.FLPRODUCT) + self.product_e.set() + + def version_select(self, choice): + if choice: + if "10" in choice: + config.TARGETVERSION = "10" + elif "9" in choice: + config.TARGETVERSION = "9" + self.menu_screen.choice = "Processing" + self.version_q.put(config.TARGETVERSION) + self.version_e.set() + + def release_select(self, choice): + if choice: + config.TARGET_RELEASE_VERSION = choice + self.menu_screen.choice = "Processing" + self.release_q.put(config.TARGET_RELEASE_VERSION) + self.release_e.set() + + def installdir_select(self, choice): + if choice: + config.INSTALLDIR = choice + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + self.menu_screen.choice = "Processing" + self.installdir_q.put(config.INSTALLDIR) + self.installdir_e.set() + + + def wine_select(self, choice): + config.WINE_EXE = choice + if choice: + self.menu_screen.choice = "Processing" + self.wine_q.put(config.WINE_EXE) + self.wine_e.set() + + + def winetricksbin_select(self, choice): + winetricks_options = utils.get_winetricks_options() + if choice.startswith("Download"): + self.menu_screen.choice = "Processing" + self.tricksbin_q.put("Download") + self.tricksbin_e.set() + else: + self.menu_screen.choice = "Processing" + config.WINETRICKSBIN = winetricks_options[0] + self.tricksbin_q.put(config.WINETRICKSBIN) + self.tricksbin_e.set() + + def waiting(self, choice): + pass + + def config_update_select(self, choice): + if choice: + if choice == "Yes": + logging.info("Updating config file.") + utils.write_config(config.CONFIG_FILE) else: - appimage_filename = choice - config.SELECTED_APPIMAGE_FILENAME = appimage_filename - utils.set_appimage_symlink() + logging.info("Config file left unchanged.") self.menu_screen.choice = "Processing" - self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) - self.appimage_e.set() - elif screen_id == 2: - if choice: - self.set_product(choice) - elif screen_id == 3: - if choice: - self.set_version(choice) - elif screen_id == 4: - if choice: - self.set_release(choice) - elif screen_id == 5: - if choice: - self.set_installdir(choice) - elif screen_id == 6: - if choice: - self.set_wine(choice) - elif screen_id == 7: - if choice: - self.set_winetricksbin(choice) - elif screen_id == 8: - pass - elif screen_id == 9: - if choice: - if choice == "Yes": - logging.info("Updating config file.") - utils.write_config(config.CONFIG_FILE) - else: - logging.info("Config file left unchanged.") + self.config_q.put(True) + self.config_e.set() + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Finishing install…", dialog=config.use_python_dialog)) + + def waiting_releases(self, choice): + pass + + def run_logos(self, choice): + if choice: + self.menu_screen.choice = "Processing" + wine.run_logos(self) + self.switch_q.put(1) + + def waiting_finish(self, choice): + pass + + def waiting_resize(self, choice): + pass + + def password_prompt(self, choice): + if choice: + self.menu_screen.choice = "Processing" + self.password_q.put(choice) + self.password_e.set() + + def install_dependencies_confirm(self, choice): + if choice: + if choice == "No": self.menu_screen.choice = "Processing" - self.config_q.put(True) - self.config_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Finishing install…", dialog=config.use_python_dialog)) - elif screen_id == 10: - pass - elif screen_id == 11: - pass - elif screen_id == 12: - if choice: + self.choice_q.put("Return to Main Menu") + else: self.menu_screen.choice = "Processing" - wine.run_logos(self) - self.switch_q.put(1) - elif screen_id == 13: - pass - elif screen_id == 14: - pass - elif screen_id == 15: - if choice: + self.confirm_e.set() + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, + "Installing dependencies…\n", wait=True, + dialog=config.use_python_dialog)) + def manual_install_confirm(self, choice): + if choice: + if choice == "Continue": self.menu_screen.choice = "Processing" - self.password_q.put(choice) - self.password_e.set() - elif screen_id == 16: - if choice: - if choice == "No": - self.menu_screen.choice = "Processing" - self.choice_q.put("Return to Main Menu") - else: - self.menu_screen.choice = "Processing" - self.confirm_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, - "Installing dependencies…\n", wait=True, - dialog=config.use_python_dialog)) - elif screen_id == 17: - if choice: - if choice == "Continue": - self.menu_screen.choice = "Processing" - self.manualinstall_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, - "Installing dependencies…\n", wait=True, - dialog=config.use_python_dialog)) + self.manualinstall_e.set() + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, + "Installing dependencies…\n", wait=True, + dialog=config.use_python_dialog)) def switch_screen(self, dialog): if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: From d8e30447b91fb5edd757286bdf70e2a7641ee3a8 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 29 Aug 2024 23:14:43 -0400 Subject: [PATCH 139/253] Add TUI color theme switch --- config.py | 2 +- tui_app.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index 2e0d2277..8e85d5d6 100644 --- a/config.py +++ b/config.py @@ -7,7 +7,7 @@ # Define and set variables that are required in the config file. core_config_keys = [ "FLPRODUCT", "FLPRODUCTi", "TARGETVERSION", "TARGET_RELEASE_VERSION", - "current_logos_version", + "current_logos_version", "curses_colors", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION" diff --git a/tui_app.py b/tui_app.py index 195cc574..53b8ed0d 100644 --- a/tui_app.py +++ b/tui_app.py @@ -105,14 +105,39 @@ def set_curses_style(self): curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLUE) curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_BLUE) + curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) + curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) + + def set_curses_colors_logos(self): self.stdscr.bkgd(' ', curses.color_pair(3)) self.main_window.bkgd(' ', curses.color_pair(3)) self.menu_window.bkgd(' ', curses.color_pair(3)) + def set_curses_colors_light(self): + self.stdscr.bkgd(' ', curses.color_pair(6)) + self.main_window.bkgd(' ', curses.color_pair(6)) + self.menu_window.bkgd(' ', curses.color_pair(6)) + + def set_curses_colors_dark(self): + self.stdscr.bkgd(' ', curses.color_pair(7)) + self.main_window.bkgd(' ', curses.color_pair(7)) + self.menu_window.bkgd(' ', curses.color_pair(7)) + def init_curses(self): try: if curses.has_colors(): - self.set_curses_style() + if config.curses_colors is None or config.curses_colors == "Logos": + config.curses_colors = "Logos" + self.set_curses_style() + self.set_curses_colors_logos() + elif config.curses_colors == "Light": + config.curses_colors = "Light" + self.set_curses_style() + self.set_curses_colors_light() + elif config.curses_colors == "Dark": + config.curses_colors = "Dark" + self.set_curses_style() + self.set_curses_colors_dark() curses.curs_set(0) curses.noecho() @@ -295,7 +320,6 @@ def choice_processor(self, stdscr, screen_id, choice): 15: self.password_prompt, 16: self.install_dependencies_confirm, 17: self.manual_install_confirm, - # Add more screen_id mappings as needed } # Capture menu exiting before processing in the rest of the handler @@ -364,6 +388,20 @@ def main_menu_select(self, choice): wine.installICUDataFiles() elif choice.endswith("Logging"): wine.switch_logging() + elif choice == "Change Color Scheme": + if config.curses_colors == "Logos": + config.curses_colors = "Light" + self.set_curses_colors_light() + elif config.curses_colors == "Light": + config.curses_colors = "Dark" + self.set_curses_colors_dark() + else: + config.curses_colors = "Logos" + config.curses_colors = "Logos" + self.set_curses_colors_logos() + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + utils.write_config(config.CONFIG_FILE) def custom_appimage_select(self, choice): #FIXME @@ -687,6 +725,11 @@ def set_tui_menu_options(self, dialog=False): label = "Enable Logging" if config.LOGS == "DISABLED" else "Disable Logging" labels.append(label) + labels_options = [ + "Change Color Scheme" + ] + labels.extend(labels_options) + labels.append("Exit") options = self.which_dialog_options(labels, dialog=False) From b216599155ea4e757131f0700095ae5af9902705 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 30 Aug 2024 01:03:58 -0400 Subject: [PATCH 140/253] Fix TUI vertical resizing --- config.py | 1 + tui_app.py | 106 +++++++++++++++++++++++++++++--------------------- tui_curses.py | 4 +- tui_screen.py | 2 +- 4 files changed, 66 insertions(+), 47 deletions(-) diff --git a/config.py b/config.py index 8e85d5d6..c54d1888 100644 --- a/config.py +++ b/config.py @@ -101,6 +101,7 @@ WINETRICKS_VERSION = '20220411' WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") install_finished = False +console_log_lines = 1 current_option = 0 current_page = 0 total_pages = 0 diff --git a/tui_app.py b/tui_app.py index 53b8ed0d..0a7ef64c 100644 --- a/tui_app.py +++ b/tui_app.py @@ -80,21 +80,28 @@ def __init__(self, stdscr): self.appimage_e = threading.Event() # Window and Screen Management + self.tui_screens = [] + self.menu_options = [] self.window_height = 0 self.window_width = 0 self.update_tty_dimensions() - self.main_window_height = 9 - self.menu_window_height = 6 + config.options_per_page - self.tui_screens = [] - self.menu_options = [] + self.main_window_ratio = 0.25 + self.main_window_min = 4 + self.menu_window_ratio = 0.75 + self.menu_window_min = 3 + self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) + self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) + config.console_log_lines = max(self.main_window_height - 3, 1) + config.options_per_page = max(self.window_height - self.main_window_height - 6, 1) self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) - self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) + self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) self.resize_window = curses.newwin(2, curses.COLS, 0, 0) self.console = None self.menu_screen = None self.active_screen = None - def set_curses_style(self): + @staticmethod + def set_curses_style(): curses.start_color() curses.use_default_colors() curses.init_color(curses.COLOR_BLUE, 0, 510, 1000) # Logos Blue @@ -123,6 +130,30 @@ def set_curses_colors_dark(self): self.main_window.bkgd(' ', curses.color_pair(7)) self.menu_window.bkgd(' ', curses.color_pair(7)) + def change_color_scheme(self): + if config.curses_colors == "Logos": + config.curses_colors = "Light" + self.set_curses_colors_light() + elif config.curses_colors == "Light": + config.curses_colors = "Dark" + self.set_curses_colors_dark() + else: + config.curses_colors = "Logos" + config.curses_colors = "Logos" + self.set_curses_colors_logos() + + def clear(self): + self.stdscr.clear() + self.main_window.clear() + self.menu_window.clear() + self.resize_window.clear() + + def refresh(self): + self.main_window.noutrefresh() + self.menu_window.noutrefresh() + self.resize_window.noutrefresh() + curses.doupdate() + def init_curses(self): try: if curses.has_colors(): @@ -149,10 +180,7 @@ def init_curses(self): "Main Menu", self.set_tui_menu_options(dialog=False)) #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", # self.set_tui_menu_options(dialog=True)) - - self.main_window.noutrefresh() - self.menu_window.noutrefresh() - curses.doupdate() + self.refresh() except curses.error as e: logging.error(f"Curses error in init_curses: {e}") except Exception as e: @@ -163,9 +191,6 @@ def init_curses(self): def end_curses(self): try: self.stdscr.keypad(False) - self.stdscr.clear() - self.main_window.clear() - self.menu_window.clear() curses.nocbreak() curses.echo() except curses.error as e: @@ -180,21 +205,23 @@ def end(self, signal, frame): self.llirunning = False curses.endwin() - def resize_curses(self): - config.resizing = True - curses.endwin() - self.menu_window.clear() - self.menu_window.refresh() + def set_window_height(self): self.update_tty_dimensions() - available_height = self.window_height - self.menu_window_height - 6 - config.options_per_page = max(available_height, 3) - self.menu_window_height = 6 + config.options_per_page + self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) + self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) + config.console_log_lines = max(self.main_window_height - (self.main_window_min - 1), 1) + config.options_per_page = max(self.window_height - self.main_window_height - 6, 1) self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) - self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, 9, 0) + self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) self.resize_window = curses.newwin(2, curses.COLS, 0, 0) + + def resize_curses(self): + config.resizing = True + curses.endwin() + self.clear() + self.set_window_height() self.init_curses() - self.menu_window.refresh() - self.menu_window.clear() + self.refresh() msg.status("Resizing window.", self) config.resizing = False @@ -207,18 +234,18 @@ def signal_resize(self, signum, frame): self.choice_q.put("Return to Main Menu") else: if self.active_screen.get_screen_id == 14: - if self.window_height >= 18: + self.update_tty_dimensions() + if self.window_height > 9: self.switch_q.put(1) def draw_resize_screen(self): + self.clear() title_lines = tui_curses.wrap_text(self, "Screen too small.") self.resize_window = curses.newwin(len(title_lines), curses.COLS, 0, 0) - self.resize_window.erase() for i, line in enumerate(title_lines): if i < self.window_height: self.resize_window.addstr(i + 0, 2, line, curses.A_BOLD) - self.resize_window.noutrefresh() - curses.doupdate() + self.refresh() def display(self): signal.signal(signal.SIGWINCH, self.signal_resize) @@ -228,7 +255,7 @@ def display(self): self.active_screen = self.menu_screen while self.llirunning: - if self.window_height >= (self.main_window_height + self.menu_window_height): + if self.window_height >= 10: if not config.resizing: if isinstance(self.active_screen, tui_screen.CursesScreen): self.main_window.erase() @@ -258,9 +285,7 @@ def display(self): self.active_screen = self.tui_screens[-1] if isinstance(self.active_screen, tui_screen.CursesScreen): - self.main_window.noutrefresh() - self.menu_window.noutrefresh() - curses.doupdate() + self.refresh() else: self.draw_resize_screen() @@ -389,16 +414,8 @@ def main_menu_select(self, choice): elif choice.endswith("Logging"): wine.switch_logging() elif choice == "Change Color Scheme": - if config.curses_colors == "Logos": - config.curses_colors = "Light" - self.set_curses_colors_light() - elif config.curses_colors == "Light": - config.curses_colors = "Dark" - self.set_curses_colors_dark() - else: - config.curses_colors = "Logos" - config.curses_colors = "Logos" - self.set_curses_colors_logos() + self.change_color_scheme() + msg.status("Changing color scheme", self) self.active_screen.running = 0 self.active_screen.choice = "Processing" utils.write_config(config.CONFIG_FILE) @@ -450,7 +467,6 @@ def installdir_select(self, choice): self.installdir_q.put(config.INSTALLDIR) self.installdir_e.set() - def wine_select(self, choice): config.WINE_EXE = choice if choice: @@ -458,7 +474,6 @@ def wine_select(self, choice): self.wine_q.put(config.WINE_EXE) self.wine_e.set() - def winetricksbin_select(self, choice): winetricks_options = utils.get_winetricks_options() if choice.startswith("Download"): @@ -518,6 +533,7 @@ def install_dependencies_confirm(self, choice): self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Installing dependencies…\n", wait=True, dialog=config.use_python_dialog)) + def manual_install_confirm(self, choice): if choice: if choice == "Continue": @@ -534,7 +550,7 @@ def switch_screen(self, dialog): self.menu_screen.choice = "Processing" self.menu_screen.running = 0 if isinstance(self.active_screen, tui_screen.CursesScreen): - self.stdscr.clear() + self.clear() def get_product(self, dialog): question = "Choose which FaithLife product the script should install:" # noqa: E501 diff --git a/tui_curses.py b/tui_curses.py index 6ba48534..5e4095cb 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -56,6 +56,7 @@ def spinner(app, index, start_y=0): i = (i + 1) % len(spinner_chars) return i + #FIXME: Display flickers. def confirm(app, question_text, height=None, width=None): stdscr = app.get_menu_window() @@ -237,13 +238,14 @@ def draw(self): self.stdscr.addstr(y, x, line, curses.A_REVERSE) else: self.stdscr.addstr(y, x, line) + menu_bottom = y if type(option) is list: options_start_y += (len(option_lines)) # Display pagination information page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(self.options)}" - self.stdscr.addstr(self.app.menu_window_height - 1, 2, page_info, curses.A_BOLD) + self.stdscr.addstr(max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, curses.A_BOLD) def do_menu_up(self): if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: diff --git a/tui_screen.py b/tui_screen.py index b1b7eb71..e9618bcb 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -83,7 +83,7 @@ def display(self): tui_curses.title(self.app, self.subtitle, subtitle_start + 1) self.stdscr.addstr(3, 2, f"---Console---") - recent_messages = logging.console_log[-6:] + recent_messages = logging.console_log[-config.console_log_lines:] for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) for j, line in enumerate(message_lines): From 4a80ee5c896a87e5745787dfabb7c97105d8df91 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 18 Sep 2024 21:17:30 -0400 Subject: [PATCH 141/253] Fix TUI horizontal resizing --- tui_app.py | 34 +++++++++++++++++++++++++++------- tui_curses.py | 20 ++++++++++---------- tui_screen.py | 7 +++++-- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/tui_app.py b/tui_app.py index 0a7ef64c..fa56b033 100644 --- a/tui_app.py +++ b/tui_app.py @@ -86,7 +86,12 @@ def __init__(self, stdscr): self.window_width = 0 self.update_tty_dimensions() self.main_window_ratio = 0.25 - self.main_window_min = 4 + if logging.console_log: + min_console_height = len(tui_curses.wrap_text(self, logging.console_log[-1])) + else: + min_console_height = 2 + self.main_window_min = len(tui_curses.wrap_text(self, self.title)) + len( + tui_curses.wrap_text(self, self.subtitle)) + min_console_height self.menu_window_ratio = 0.75 self.menu_window_min = 3 self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) @@ -207,13 +212,18 @@ def end(self, signal, frame): def set_window_height(self): self.update_tty_dimensions() + curses.resizeterm(self.window_height, self.window_width) + self.main_window_min = len(tui_curses.wrap_text(self, self.title)) + len( + tui_curses.wrap_text(self, self.subtitle)) + 3 + self.menu_window_min = 3 self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) config.console_log_lines = max(self.main_window_height - (self.main_window_min - 1), 1) config.options_per_page = max(self.window_height - self.main_window_height - 6, 1) self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) - self.resize_window = curses.newwin(2, curses.COLS, 0, 0) + resize_lines = tui_curses.wrap_text(self, "Screen too small.") + self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) def resize_curses(self): config.resizing = True @@ -237,14 +247,20 @@ def signal_resize(self, signum, frame): self.update_tty_dimensions() if self.window_height > 9: self.switch_q.put(1) + elif self.window_width > 34: + self.switch_q.put(1) def draw_resize_screen(self): self.clear() - title_lines = tui_curses.wrap_text(self, "Screen too small.") - self.resize_window = curses.newwin(len(title_lines), curses.COLS, 0, 0) - for i, line in enumerate(title_lines): + if self.window_width > 10: + margin = config.margin + else: + margin = 0 + resize_lines = tui_curses.wrap_text(self, "Screen too small.") + self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) + for i, line in enumerate(resize_lines): if i < self.window_height: - self.resize_window.addstr(i + 0, 2, line, curses.A_BOLD) + self.resize_window.addnstr(i + 0, 2, line, self.window_width, curses.A_BOLD) self.refresh() def display(self): @@ -286,8 +302,12 @@ def display(self): if isinstance(self.active_screen, tui_screen.CursesScreen): self.refresh() - else: + elif self.window_width >= 10: + if self.window_width < 10: + config.margin = 1 # Avoid drawing errors on very small screens self.draw_resize_screen() + elif self.window_width < 10: + config.margin = 0 # Avoid drawing errors on very small screens def run(self): try: diff --git a/tui_curses.py b/tui_curses.py index 5e4095cb..fce1416d 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -12,10 +12,10 @@ def wrap_text(app, text): # Turn text into wrapped text, line by line, centered if "\n" in text: lines = text.splitlines() - wrapped_lines = [textwrap.fill(line, app.window_width - 4) for line in lines] + wrapped_lines = [textwrap.fill(line, app.window_width - (config.margin * 2)) for line in lines] lines = '\n'.join(wrapped_lines) else: - wrapped_text = textwrap.fill(text, app.window_width - 4) + wrapped_text = textwrap.fill(text, app.window_width - (config.margin * 2)) lines = wrapped_text.split('\n') return lines @@ -27,7 +27,7 @@ def title(app, title_text, title_start_y_adj): last_index = 0 for i, line in enumerate(title_lines): if i < app.window_height: - stdscr.addstr(i + title_start_y_adj, 2, line, curses.A_BOLD) + stdscr.addnstr(i + title_start_y_adj, 2, line, app.window_width, curses.A_BOLD) last_index = i return last_index @@ -44,7 +44,7 @@ def text_centered(app, text, start_y=0): for i, line in enumerate(text_lines): if text_start_y + i < app.window_height: x = app.window_width // 2 - text_width // 2 - stdscr.addstr(text_start_y + i, x, line) + stdscr.addnstr(text_start_y + i, x, line, app.window_width) return text_start_y, text_lines @@ -74,7 +74,7 @@ def confirm(app, question_text, height=None, width=None): elif key.lower() == 'n': return False - stdscr.addstr(y, 0, "Type Y[es] or N[o]. ") + stdscr.addnstr(y, 0, "Type Y[es] or N[o]. ", app.window_width) class CursesDialog: @@ -119,7 +119,7 @@ def draw(self): self.stdscr.refresh() def input(self): - self.stdscr.addstr(self.question_start_y + len(self.question_lines) + 2, 10, self.user_input) + self.stdscr.addnstr(self.question_start_y + len(self.question_lines) + 2, 10, self.user_input, self.app.window_width) key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.user_input)) try: @@ -162,7 +162,7 @@ def run(self): return self.user_input def input(self): - self.stdscr.addstr(self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation) + self.stdscr.addnstr(self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation, self.app.window_width) key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.obfuscation)) try: @@ -235,9 +235,9 @@ def draw(self): x = max(0, self.app.window_width // 2 - len(line) // 2) if y < self.app.menu_window_height: if index == config.current_option: - self.stdscr.addstr(y, x, line, curses.A_REVERSE) + self.stdscr.addnstr(y, x, line, self.app.window_width, curses.A_REVERSE) else: - self.stdscr.addstr(y, x, line) + self.stdscr.addnstr(y, x, line, self.app.window_width) menu_bottom = y if type(option) is list: @@ -245,7 +245,7 @@ def draw(self): # Display pagination information page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(self.options)}" - self.stdscr.addstr(max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, curses.A_BOLD) + self.stdscr.addnstr(max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) def do_menu_up(self): if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: diff --git a/tui_screen.py b/tui_screen.py index e9618bcb..61851659 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -82,13 +82,16 @@ def display(self): subtitle_start = tui_curses.title(self.app, self.title, self.title_start_y) tui_curses.title(self.app, self.subtitle, subtitle_start + 1) - self.stdscr.addstr(3, 2, f"---Console---") + console_start_y = len(tui_curses.wrap_text(self.app, self.title)) + len( + tui_curses.wrap_text(self.app, self.subtitle)) + 1 + self.stdscr.addnstr(console_start_y, 2, f"---Console---", self.app.window_width - 4) recent_messages = logging.console_log[-config.console_log_lines:] for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) for j, line in enumerate(message_lines): if 2 + j < self.app.window_height: - self.stdscr.addstr(2 + i, 2, f"{message}") + truncated = message[:self.app.window_width - 4] + self.stdscr.addnstr(console_start_y + i, 2, truncated, self.app.window_width - 4) self.stdscr.noutrefresh() curses.doupdate() From 99d4179eb5db17daa021b3bcd18a5be81e223245 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 30 Aug 2024 11:48:52 -0400 Subject: [PATCH 142/253] Add submenus to tui_app.main_menu --- tui_app.py | 115 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/tui_app.py b/tui_app.py index fa56b033..9020a9f9 100644 --- a/tui_app.py +++ b/tui_app.py @@ -182,7 +182,7 @@ def init_curses(self): self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, - "Main Menu", self.set_tui_menu_options(dialog=False)) + "Main Menu", self.set_main_menu_options(dialog=False)) #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", # self.set_tui_menu_options(dialog=True)) self.refresh() @@ -342,7 +342,8 @@ def task_processor(self, evt=None, task=None): elif task == 'DONE': self.subtitle = f"Logos Version: {config.current_logos_version}" self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) - self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) # Force the main menu dialog + self.menu_screen.set_options(self.set_main_menu_options(dialog=False)) + #self.menu_screen.set_options(self.set_main_menu_options(dialog=True)) self.switch_q.put(1) def choice_processor(self, stdscr, screen_id, choice): @@ -358,13 +359,14 @@ def choice_processor(self, stdscr, screen_id, choice): 8: self.waiting, 9: self.config_update_select, 10: self.waiting_releases, - 11: self.waiting, # Unused + 11: self.winetricks_menu_select, # Unused 12: self.run_logos, 13: self.waiting_finish, 14: self.waiting_resize, 15: self.password_prompt, 16: self.install_dependencies_confirm, 17: self.manual_install_confirm, + 18: self.utilities_menu_select } # Capture menu exiting before processing in the rest of the handler @@ -398,7 +400,35 @@ def main_menu_select(self, choice): self.choice_q.put("0") elif choice == "Run Indexing": wine.run_indexing() - elif choice == "Remove Library Catalog": + elif choice.startswith("Winetricks"): + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) + self.choice_q.put("0") + elif choice.startswith("Utilities"): + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + self.screen_q.put(self.stack_menu(18, self.todo_q, self.todo_e, "Utilities Menu", self.set_utilities_menu_options(), dialog=config.use_python_dialog)) + self.choice_q.put("0") + elif choice == "Change Color Scheme": + self.change_color_scheme() + msg.status("Changing color scheme", self) + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + utils.write_config(config.CONFIG_FILE) + + def winetricks_menu_select(self, choice): + if choice == "Download or Update Winetricks": + control.set_winetricks() + elif choice == "Run Winetricks": + wine.run_winetricks() + elif choice == "Install d3dcompiler": + wine.installD3DCompiler() + elif choice == "Install Fonts": + wine.installFonts() + + def utilities_menu_select(self, choice): + if choice == "Remove Library Catalog": control.remove_library_catalog() elif choice == "Remove All Index Files": control.remove_all_index_files() @@ -421,24 +451,10 @@ def main_menu_select(self, choice): self.menu_options = appimage_choices question = "Which AppImage should be used?" self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) - elif choice == "Download or Update Winetricks": - control.set_winetricks() - elif choice == "Run Winetricks": - wine.run_winetricks() - elif choice == "Install d3dcompiler": - wine.installD3DCompiler() - elif choice == "Install Fonts": - wine.installFonts() elif choice == "Install ICU": wine.installICUDataFiles() elif choice.endswith("Logging"): wine.switch_logging() - elif choice == "Change Color Scheme": - self.change_color_scheme() - msg.status("Changing color scheme", self) - self.active_screen.running = 0 - self.active_screen.choice = "Processing" - utils.write_config(config.CONFIG_FILE) def custom_appimage_select(self, choice): #FIXME @@ -720,7 +736,7 @@ def which_dialog_options(self, labels, dialog=False): options.append(label) return options - def set_tui_menu_options(self, dialog=False): + def set_main_menu_options(self, dialog=False): labels = [] if config.LLI_LATEST_VERSION and system.get_runmode() == 'binary': logging.debug("Checking if Logos Linux Installers needs updated.") # noqa: E501 @@ -737,30 +753,18 @@ def set_tui_menu_options(self, dialog=False): if utils.file_exists(config.LOGOS_EXE): labels_default = [ f"Run {config.FLPRODUCT}", - "Run Indexing", - "Remove Library Catalog", - "Remove All Index Files", - "Edit Config", - "Back up Data", - "Restore Data", + "Run Indexing" ] else: labels_default = ["Install Logos Bible Software"] labels.extend(labels_default) labels_support = [ - "Install Dependencies", - "Download or Update Winetricks", - "Run Winetricks", - "Install d3dcompiler", - "Install Fonts", - "Install ICU" + "Utilities →", + "Winetricks →" ] labels.extend(labels_support) - label = "Enable Logging" if config.LOGS == "DISABLED" else "Disable Logging" - labels.append(label) - labels_options = [ "Change Color Scheme" ] @@ -772,6 +776,49 @@ def set_tui_menu_options(self, dialog=False): return options + def set_winetricks_menu_options(self, dialog=False): + labels = [] + labels_support = [ + "Download or Update Winetricks", + "Run Winetricks", + "Install d3dcompiler", + "Install Fonts" + ] + labels.extend(labels_support) + + labels.append("Return to Main Menu") + + options = self.which_dialog_options(labels, dialog=False) + + return options + + def set_utilities_menu_options(self, dialog=False): + labels = [] + if utils.file_exists(config.LOGOS_EXE): + labels_catalog = [ + "Remove Library Catalog", + "Remove All Index Files", + "Install ICU" + ] + labels.extend(labels_catalog) + + labels_utilities = [ + "Install Dependencies", + "Edit Config", + "Back up Data", + "Restore Data", + ] + labels.extend(labels_utilities) + + label = "Enable Logging" if config.LOGS == "DISABLED" else "Disable Logging" + labels.append(label) + + labels.append("Return to Main Menu") + + options = self.which_dialog_options(labels, dialog=False) + + return options + def stack_menu(self, screen_id, queue, event, question, options, height=None, width=None, menu_height=8, dialog=False): if dialog: utils.append_unique(self.tui_screens, From fde05e6f73ba8d12df1cdc8b3fcb799b9830559d Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 30 Aug 2024 21:09:58 -0400 Subject: [PATCH 143/253] Add config mechanism to fix #157 --- config.py | 2 +- network.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index c54d1888..7e27fb83 100644 --- a/config.py +++ b/config.py @@ -10,7 +10,7 @@ "current_logos_version", "curses_colors", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", - "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION" + "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION", "logos_release_channel" ] for k in core_config_keys: globals()[k] = os.getenv(k) diff --git a/network.py b/network.py index 14e5d1a4..24a946c3 100644 --- a/network.py +++ b/network.py @@ -503,7 +503,10 @@ def get_logos_releases(app=None): msg.logos_msg(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. - url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 + if config.logos_release_channel is None or config.logos_release_channel == "stable": + url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 + elif config.logos_release_channel == "beta": + url = f"https://clientservices.logos.com/update/v1/feed/logos10/beta.xml" # noqa: E501 response_xml_bytes = net_get(url) # if response_xml is None and None not in [q, app]: From f1703379641b352a87b106fa6b9a1d3b50df436e Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 13 Sep 2024 00:58:00 -0400 Subject: [PATCH 144/253] Allow changing Logos release channel in TUI --- tui_app.py | 32 +++++++++++++++++++++++--------- utils.py | 17 +++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/tui_app.py b/tui_app.py index 9020a9f9..e620f1d7 100644 --- a/tui_app.py +++ b/tui_app.py @@ -25,8 +25,11 @@ class TUI: def __init__(self, stdscr): self.stdscr = stdscr - self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" - self.subtitle = f"Logos Version: {config.current_logos_version}" + #if config.current_logos_version is not None: + self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION}" + self.subtitle = f"Logos Version: {config.current_logos_version}. Channel: {config.logos_release_channel}" + #else: + # self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" self.console_message = "Starting TUI…" self.llirunning = True self.active_progress = False @@ -225,14 +228,23 @@ def set_window_height(self): resize_lines = tui_curses.wrap_text(self, "Screen too small.") self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) + def update_main_window_contents(self): + self.clear() + self.subtitle = f"Logos Version: {config.current_logos_version}. Channel: {config.logos_release_channel}" + self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) + self.menu_screen.set_options(self.set_main_menu_options(dialog=False)) + #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) + self.switch_q.put(1) + self.refresh() + def resize_curses(self): config.resizing = True curses.endwin() - self.clear() self.set_window_height() + self.clear() self.init_curses() self.refresh() - msg.status("Resizing window.", self) + msg.status("Window resized.", self) config.resizing = False def signal_resize(self, signum, frame): @@ -340,11 +352,7 @@ def task_processor(self, evt=None, task=None): elif task == 'CONFIG': utils.start_thread(self.get_config, config.use_python_dialog) elif task == 'DONE': - self.subtitle = f"Logos Version: {config.current_logos_version}" - self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) - self.menu_screen.set_options(self.set_main_menu_options(dialog=False)) - #self.menu_screen.set_options(self.set_main_menu_options(dialog=True)) - self.switch_q.put(1) + self.update_main_window_contents() def choice_processor(self, stdscr, screen_id, choice): screen_actions = { @@ -434,6 +442,11 @@ def utilities_menu_select(self, choice): control.remove_all_index_files() elif choice == "Edit Config": control.edit_config() + elif choice == "Change Release Channel": + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + utils.change_release_channel() + self.update_main_window_contents() elif choice == "Install Dependencies": utils.check_dependencies() elif choice == "Back up Data": @@ -805,6 +818,7 @@ def set_utilities_menu_options(self, dialog=False): labels_utilities = [ "Install Dependencies", "Edit Config", + "Change Release Channel", "Back up Data", "Restore Data", ] diff --git a/utils.py b/utils.py index 3f94e6cd..80167ce5 100644 --- a/utils.py +++ b/utils.py @@ -255,6 +255,23 @@ def file_exists(file_path): return False +def change_release_channel(): + if config.logos_release_channel == "stable": + config.logos_release_channel = "beta" + update_config_file( + config.CONFIG_FILE, + 'logos_release_channel', + "beta" + ) + else: + config.logos_release_channel = "stable" + update_config_file( + config.CONFIG_FILE, + 'logos_release_channel', + "stable" + ) + + def get_current_logos_version(): path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" # noqa: E501 file_paths = glob.glob(path_regex) From 5398554e865fdcf94ea2e00f0d4ee5ff10db34d0 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sun, 29 Sep 2024 20:38:23 -0400 Subject: [PATCH 145/253] Add config mechanism to fix #84 --- config.py | 3 ++- network.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index 7e27fb83..1c2da83e 100644 --- a/config.py +++ b/config.py @@ -10,7 +10,8 @@ "current_logos_version", "curses_colors", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", - "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION", "logos_release_channel" + "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION", "logos_release_channel", + "lli_release_channel" ] for k in core_config_keys: globals()[k] = os.getenv(k) diff --git a/network.py b/network.py index 24a946c3..02a6fd28 100644 --- a/network.py +++ b/network.py @@ -405,7 +405,10 @@ def get_latest_release_version_tag_name(json_data): def set_logoslinuxinstaller_latest_release_config(): - releases_url = "https://api.github.com/repos/FaithLife-Community/LogosLinuxInstaller/releases" # noqa: E501 + if config.lli_release_channel is None or config.llie_release_channel == "stable": + releases_url = "https://api.github.com/repos/FaithLife-Community/LogosLinuxInstaller/releases" # noqa: E501 + else: + releases_url = "https://api.github.com/repos/FaithLife-Community/test-builds/releases" json_data = get_latest_release_data(releases_url) logoslinuxinstaller_url = get_latest_release_url(json_data) logoslinuxinstaller_tag_name = get_latest_release_version_tag_name(json_data) # noqa: E501 From 278f28105d5ef225b29bf63460370d6e9c39a7a9 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sun, 29 Sep 2024 20:43:28 -0400 Subject: [PATCH 146/253] Allow changing LLI release channel in TUI --- network.py | 2 +- tui_app.py | 25 +++++++++++++++++-------- utils.py | 19 ++++++++++++++++++- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/network.py b/network.py index 02a6fd28..985c7601 100644 --- a/network.py +++ b/network.py @@ -405,7 +405,7 @@ def get_latest_release_version_tag_name(json_data): def set_logoslinuxinstaller_latest_release_config(): - if config.lli_release_channel is None or config.llie_release_channel == "stable": + if config.lli_release_channel is None or config.lli_release_channel == "stable": releases_url = "https://api.github.com/repos/FaithLife-Community/LogosLinuxInstaller/releases" # noqa: E501 else: releases_url = "https://api.github.com/repos/FaithLife-Community/test-builds/releases" diff --git a/tui_app.py b/tui_app.py index e620f1d7..2a965c25 100644 --- a/tui_app.py +++ b/tui_app.py @@ -26,8 +26,8 @@ class TUI: def __init__(self, stdscr): self.stdscr = stdscr #if config.current_logos_version is not None: - self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION}" - self.subtitle = f"Logos Version: {config.current_logos_version}. Channel: {config.logos_release_channel}" + self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" + self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" #else: # self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" self.console_message = "Starting TUI…" @@ -230,7 +230,8 @@ def set_window_height(self): def update_main_window_contents(self): self.clear() - self.subtitle = f"Logos Version: {config.current_logos_version}. Channel: {config.logos_release_channel}" + self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" + self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) self.menu_screen.set_options(self.set_main_menu_options(dialog=False)) #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) @@ -442,11 +443,18 @@ def utilities_menu_select(self, choice): control.remove_all_index_files() elif choice == "Edit Config": control.edit_config() - elif choice == "Change Release Channel": - self.active_screen.running = 0 - self.active_screen.choice = "Processing" - utils.change_release_channel() + self.go_to_main_menu() + elif choice == "Change Logos Release Channel": + self.reset_screen() + utils.change_logos_release_channel() + self.update_main_window_contents() + self.go_to_main_menu() + elif choice == "Change Logos on Linux Release Channel": + self.reset_screen() + utils.change_lli_release_channel() + network.set_logoslinuxinstaller_latest_release_config() self.update_main_window_contents() + self.go_to_main_menu() elif choice == "Install Dependencies": utils.check_dependencies() elif choice == "Back up Data": @@ -818,7 +826,8 @@ def set_utilities_menu_options(self, dialog=False): labels_utilities = [ "Install Dependencies", "Edit Config", - "Change Release Channel", + "Change Logos Release Channel", + "Change Logos on Linux Release Channel", "Back up Data", "Restore Data", ] diff --git a/utils.py b/utils.py index 80167ce5..d84e2e54 100644 --- a/utils.py +++ b/utils.py @@ -255,7 +255,7 @@ def file_exists(file_path): return False -def change_release_channel(): +def change_logos_release_channel(): if config.logos_release_channel == "stable": config.logos_release_channel = "beta" update_config_file( @@ -272,6 +272,23 @@ def change_release_channel(): ) +def change_lli_release_channel(): + if config.lli_release_channel == "stable": + config.logos_release_channel = "dev" + update_config_file( + config.CONFIG_FILE, + 'lli_release_channel', + "dev" + ) + else: + config.lli_release_channel = "stable" + update_config_file( + config.CONFIG_FILE, + 'lli_release_channel', + "stable" + ) + + def get_current_logos_version(): path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" # noqa: E501 file_paths = glob.glob(path_regex) From e57e38a8cf815b7df99f240ba3eb8eeb2d0c1434 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 18 Sep 2024 23:03:07 -0400 Subject: [PATCH 147/253] Add logging deduplication; also for TUI console_log --- config.py | 2 ++ msg.py | 36 +++++++++++++++++++++++++++++++----- tui_app.py | 11 ++++++----- tui_screen.py | 8 ++++---- utils.py | 2 +- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/config.py b/config.py index 1c2da83e..948ad5b8 100644 --- a/config.py +++ b/config.py @@ -102,6 +102,8 @@ WINETRICKS_VERSION = '20220411' WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") install_finished = False +console_log = [] +margin = 2 console_log_lines = 1 current_option = 0 current_page = 0 diff --git a/msg.py b/msg.py index 2f1bddaa..1a9ac2ee 100644 --- a/msg.py +++ b/msg.py @@ -13,8 +13,6 @@ from gui import ask_question from gui import show_error -logging.console_log = [] - class GzippedRotatingFileHandler(RotatingFileHandler): def doRollover(self): @@ -39,6 +37,19 @@ def doRollover(self): os.remove(last_log) +class DeduplicateFilter(logging.Filter): + def __init__(self): + super().__init__() + self.last_log = None + + def filter(self, record): + current_message = record.getMessage() + if current_message == self.last_log: + return False + self.last_log = current_message + return True + + def get_log_level_name(level): name = None levels = { @@ -81,11 +92,13 @@ def initialize_logging(stderr_log_level): ) file_h.name = "logfile" file_h.setLevel(logging.DEBUG) + file_h.addFilter(DeduplicateFilter()) # stdout_h = logging.StreamHandler(sys.stdout) # stdout_h.setLevel(stdout_log_level) stderr_h = logging.StreamHandler(sys.stderr) stderr_h.name = "terminal" stderr_h.setLevel(stderr_log_level) + stderr_h.addFilter(DeduplicateFilter()) handlers = [ file_h, # stdout_h, @@ -281,16 +294,29 @@ def progress(percent, app=None): def status(text, app=None): + def strip_timestamp(msg, timestamp_length=20): + return msg[timestamp_length:] + timestamp = time.strftime("%Y-%m-%dT%H:%M:%S") """Handles status messages for both TUI and GUI.""" if app is not None: if config.DIALOG == 'tk': app.status_q.put(text) app.root.event_generate('<>') + logging.info(f"{text}") elif config.DIALOG == 'curses': - app.status_q.put(f"{timestamp} {text}") - app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) # noqa: E501 - logging.info(f"{text}") + if len(config.console_log) > 0: + last_msg = strip_timestamp(config.console_log[-1]) + if last_msg != text: + app.status_q.put(f"{timestamp} {text}") + app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) # noqa: E501 + logging.info(f"{text}") + else: + app.status_q.put(f"{timestamp} {text}") + app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) # noqa: E501 + logging.info(f"{text}") + else: + logging.info(f"{text}") else: # Prints message to stdout regardless of log level. logos_msg(text) diff --git a/tui_app.py b/tui_app.py index 2a965c25..7c4c9694 100644 --- a/tui_app.py +++ b/tui_app.py @@ -89,8 +89,8 @@ def __init__(self, stdscr): self.window_width = 0 self.update_tty_dimensions() self.main_window_ratio = 0.25 - if logging.console_log: - min_console_height = len(tui_curses.wrap_text(self, logging.console_log[-1])) + if config.console_log: + min_console_height = len(tui_curses.wrap_text(self, config.console_log[-1])) else: min_console_height = 2 self.main_window_min = len(tui_curses.wrap_text(self, self.title)) + len( @@ -273,7 +273,7 @@ def draw_resize_screen(self): self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) for i, line in enumerate(resize_lines): if i < self.window_height: - self.resize_window.addnstr(i + 0, 2, line, self.window_width, curses.A_BOLD) + self.resize_window.addnstr(i, margin, line, self.window_width - config.margin, curses.A_BOLD) self.refresh() def display(self): @@ -284,7 +284,8 @@ def display(self): self.active_screen = self.menu_screen while self.llirunning: - if self.window_height >= 10: + if self.window_height >= 10 and self.window_width >= 35: + config.margin = 2 if not config.resizing: if isinstance(self.active_screen, tui_screen.CursesScreen): self.main_window.erase() @@ -744,7 +745,7 @@ def get_config(self, dialog): def report_waiting(self, text, dialog): #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) - logging.console_log.append(text) + config.console_log.append(text) def which_dialog_options(self, labels, dialog=False): options = [] diff --git a/tui_screen.py b/tui_screen.py index 61851659..60566c8d 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -84,14 +84,14 @@ def display(self): console_start_y = len(tui_curses.wrap_text(self.app, self.title)) + len( tui_curses.wrap_text(self.app, self.subtitle)) + 1 - self.stdscr.addnstr(console_start_y, 2, f"---Console---", self.app.window_width - 4) - recent_messages = logging.console_log[-config.console_log_lines:] + self.stdscr.addnstr(console_start_y, config.margin, f"---Console---", self.app.window_width - (config.margin * 2)) + recent_messages = config.console_log[-config.console_log_lines:] for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) for j, line in enumerate(message_lines): if 2 + j < self.app.window_height: - truncated = message[:self.app.window_width - 4] - self.stdscr.addnstr(console_start_y + i, 2, truncated, self.app.window_width - 4) + truncated = message[:self.app.window_width - (config.margin * 2)] + self.stdscr.addnstr(console_start_y + i, config.margin, truncated, self.app.window_width - (config.margin * 2)) self.stdscr.noutrefresh() curses.doupdate() diff --git a/utils.py b/utils.py index d84e2e54..0c0f7521 100644 --- a/utils.py +++ b/utils.py @@ -871,7 +871,7 @@ def get_downloaded_file_path(filename): def send_task(app, task): - logging.debug(f"{task=}") + #logging.debug(f"{task=}") app.todo_q.put(task) if config.DIALOG == 'tk': app.root.event_generate('<>') From 529d27e72bf75d45b49303b5e9303843acc6cdd7 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 13 Sep 2024 00:13:23 -0400 Subject: [PATCH 148/253] Add Winetricks Options for TUI --- installer.py | 44 ++++++-------------- tui_app.py | 113 ++++++++++++++++++++++++++++++++++++++++++++------- wine.py | 27 ++++++++++-- 3 files changed, 134 insertions(+), 50 deletions(-) diff --git a/installer.py b/installer.py index c51503e3..a3f9f955 100644 --- a/installer.py +++ b/installer.py @@ -481,16 +481,15 @@ def ensure_wineprefix_init(app=None): def ensure_winetricks_applied(app=None): config.INSTALL_STEPS_COUNT += 1 - ensure_wineprefix_init(app=app) + ensure_icu_data_files(app=app) config.INSTALL_STEP += 1 status = "Ensuring winetricks & other settings are applied…" update_install_feedback(status, app=app) logging.debug('- disable winemenubuilder') - logging.debug('- settings win10') logging.debug('- settings renderer=gdi') - logging.debug('- settings fontsmooth=rgb') - logging.debug('- tahoma') logging.debug('- corefonts') + logging.debug('- tahoma') + logging.debug('- settings fontsmooth=rgb') logging.debug('- d3dcompiler_47') if not config.SKIP_WINETRICKS: @@ -500,27 +499,19 @@ def ensure_winetricks_applied(app=None): workdir.mkdir(parents=True, exist_ok=True) usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") - if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' - reg_file.write_text(r'''REGEDIT4 + #FIXME: This command is failing. + reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') + with open(reg_file, 'w') as f: + f.write(r'''REGEDIT4 [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "winemenubuilder.exe"="" ''') wine.wine_reg_install(reg_file) - if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): - # args = ["-q", "settings", "win10"] - # if not config.WINETRICKS_UNATTENDED: - # args.insert(0, "-q") - wine.winetricks_install("-q", "settings", "win10") - if not utils.grep(r'"renderer"="gdi"', usr_reg): - wine.winetricks_install("-q", "settings", "renderer=gdi") - - if not utils.grep(r'"FontSmoothingType"=dword:00000002', usr_reg): - wine.winetricks_install("-q", "settings", "fontsmooth=rgb") + wine.set_renderer("gdi") if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 wine.installFonts() @@ -528,20 +519,11 @@ def ensure_winetricks_applied(app=None): if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): wine.installD3DCompiler() - m = f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode." - msg.logos_msg(m) - exe_args = [ - 'add', - f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 - "/v", "Version", - "/t", "REG_SZ", - "/d", "vista", "/f", - ] - wine.run_wine_proc( - str(utils.get_wine_exe_path()), - exe='reg', - exe_args=exe_args - ) + if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): + wine.set_win_version("logos", "win10") + + msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") + wine.set_win_version_indexer("vista") logging.debug("> Done.") diff --git a/tui_app.py b/tui_app.py index 7c4c9694..52a1766e 100644 --- a/tui_app.py +++ b/tui_app.py @@ -369,14 +369,17 @@ def choice_processor(self, stdscr, screen_id, choice): 8: self.waiting, 9: self.config_update_select, 10: self.waiting_releases, - 11: self.winetricks_menu_select, # Unused + 11: self.winetricks_menu_select, 12: self.run_logos, 13: self.waiting_finish, 14: self.waiting_resize, 15: self.password_prompt, 16: self.install_dependencies_confirm, 17: self.manual_install_confirm, - 18: self.utilities_menu_select + 18: self.utilities_menu_select, + 19: self.renderer_select, + 20: self.win_ver_logos_select, + 21: self.win_ver_index_select } # Capture menu exiting before processing in the rest of the handler @@ -392,6 +395,14 @@ def choice_processor(self, stdscr, screen_id, choice): else: pass + def reset_screen(self): + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + + def go_to_main_menu(self): + self.menu_screen.choice = "Processing" + self.choice_q.put("Return to Main Menu") + def main_menu_select(self, choice): if choice is None or choice == "Exit": msg.logos_warn("Exiting installation.") @@ -404,27 +415,23 @@ def main_menu_select(self, choice): elif choice.startswith("Update Logos Linux Installer"): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": - self.active_screen.running = 0 - self.active_screen.choice = "Processing" + self.reset_screen() self.screen_q.put(self.stack_text(12, self.todo_q, self.todo_e, "Logos is running…", dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice == "Run Indexing": wine.run_indexing() elif choice.startswith("Winetricks"): - self.active_screen.running = 0 - self.active_screen.choice = "Processing" + self.reset_screen() self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice.startswith("Utilities"): - self.active_screen.running = 0 - self.active_screen.choice = "Processing" + self.reset_screen() self.screen_q.put(self.stack_menu(18, self.todo_q, self.todo_e, "Utilities Menu", self.set_utilities_menu_options(), dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice == "Change Color Scheme": self.change_color_scheme() msg.status("Changing color scheme", self) - self.active_screen.running = 0 - self.active_screen.choice = "Processing" + self.reset_screen() utils.write_config(config.CONFIG_FILE) def winetricks_menu_select(self, choice): @@ -436,6 +443,27 @@ def winetricks_menu_select(self, choice): wine.installD3DCompiler() elif choice == "Install Fonts": wine.installFonts() + elif choice == "Set Renderer": + self.reset_screen() + self.screen_q.put(self.stack_menu(19, self.todo_q, self.todo_e, + "Choose Renderer", + self.set_renderer_menu_options(), + dialog=config.use_python_dialog)) + self.choice_q.put("0") + elif choice == "Set Windows Version for Logos": + self.reset_screen() + self.screen_q.put(self.stack_menu(20, self.todo_q, self.todo_e, + "Set Windows Version for Logos", + self.set_win_ver_menu_options(), + dialog=config.use_python_dialog)) + self.choice_q.put("0") + elif choice == "Set Windows Version for Indexer": + self.reset_screen() + self.screen_q.put(self.stack_menu(21, self.todo_q, self.todo_e, + "Set Windows Version for Indexer", + self.set_win_ver_menu_options(), + dialog=config.use_python_dialog)) + self.choice_q.put("0") def utilities_menu_select(self, choice): if choice == "Remove Library Catalog": @@ -550,10 +578,10 @@ def waiting(self, choice): def config_update_select(self, choice): if choice: if choice == "Yes": - logging.info("Updating config file.") + msg.status("Updating config file.", self) utils.write_config(config.CONFIG_FILE) else: - logging.info("Config file left unchanged.") + msg.status("Config file left unchanged.", self) self.menu_screen.choice = "Processing" self.config_q.put(True) self.config_e.set() @@ -583,8 +611,7 @@ def password_prompt(self, choice): def install_dependencies_confirm(self, choice): if choice: if choice == "No": - self.menu_screen.choice = "Processing" - self.choice_q.put("Return to Main Menu") + self.go_to_main_menu() else: self.menu_screen.choice = "Processing" self.confirm_e.set() @@ -592,6 +619,27 @@ def install_dependencies_confirm(self, choice): "Installing dependencies…\n", wait=True, dialog=config.use_python_dialog)) + def renderer_select(self, choice): + if choice in ["gdi", "gl", "vulkan"]: + self.reset_screen() + wine.set_renderer(choice) + msg.status(f"Changed renderer to {choice}.", self) + self.go_to_main_menu() + + def win_ver_logos_select(self, choice): + if choice in ["vista", "win7", "win8", "win10", "win11"]: + self.reset_screen() + wine.set_win_version("logos", choice) + msg.status(f"Changed Windows version for Logos to {choice}.", self) + self.go_to_main_menu() + + def win_ver_index_select(self, choice): + if choice in ["vista", "win7", "win8", "win10", "win11"]: + self.reset_screen() + wine.set_win_version("indexer", choice) + msg.status(f"Changed Windows version for Indexer to {choice}.", self) + self.go_to_main_menu() + def manual_install_confirm(self, choice): if choice: if choice == "Continue": @@ -804,7 +852,42 @@ def set_winetricks_menu_options(self, dialog=False): "Download or Update Winetricks", "Run Winetricks", "Install d3dcompiler", - "Install Fonts" + "Install Fonts", + "Set Renderer", + "Set Windows Version for Logos", + "Set Windows Version for Indexer" + ] + labels.extend(labels_support) + + labels.append("Return to Main Menu") + + options = self.which_dialog_options(labels, dialog=False) + + return options + + def set_renderer_menu_options(self, dialog=False): + labels = [] + labels_support = [ + "gdi", + "gl", + "vulkan" + ] + labels.extend(labels_support) + + labels.append("Return to Main Menu") + + options = self.which_dialog_options(labels, dialog=False) + + return options + + def set_win_ver_menu_options(self, dialog=False): + labels = [] + labels_support = [ + "vista", + "win7", + "win8", + "win10", + "win11" ] labels.extend(labels_support) diff --git a/wine.py b/wine.py index 62ce3a45..5b63d6c1 100644 --- a/wine.py +++ b/wine.py @@ -280,7 +280,7 @@ def run_winetricks(cmd=None): run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) -def winetricks_install(*args): +def run_winetricks_cmd(*args): cmd = [*args] msg.logos_msg(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") @@ -293,7 +293,7 @@ def installD3DCompiler(): cmd = ['d3dcompiler_47'] if config.WINETRICKS_UNATTENDED is None: cmd.insert(0, '-q') - winetricks_install(*cmd) + run_winetricks_cmd(*cmd) def installFonts(): @@ -304,9 +304,28 @@ def installFonts(): args = [f] if config.WINETRICKS_UNATTENDED: args.insert(0, '-q') - winetricks_install(*args) + run_winetricks_cmd(*args) - winetricks_install('-q', 'settings', 'fontsmooth=rgb') + run_winetricks_cmd('-q', 'settings', 'fontsmooth=rgb') + + +def set_renderer(renderer): + run_winetricks_cmd("-q", "settings", f"renderer={renderer}") + + +def set_win_version(exe, windows_version): + if exe == "logos": + run_winetricks_cmd('-q', 'settings', f'{windows_version}') + elif exe == "indexer": + reg = f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe" # noqa: E501 + exe_args = [ + 'add', + reg, + "/v", "Version", + "/t", "REG_SZ", + "/d", f"{windows_version}", "/f", + ] + run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) def installICUDataFiles(app=None): From 626f6c0214545fdc4a57c41b8311b86325d054dc Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Thu, 19 Sep 2024 00:46:55 -0400 Subject: [PATCH 149/253] Add some additional logging; fix menu issues --- system.py | 2 +- tui_app.py | 42 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/system.py b/system.py index 0e31fa81..b22580c5 100644 --- a/system.py +++ b/system.py @@ -505,8 +505,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) command.append('&&') command.extend(postinstall_command) if not command: # nothing to run; avoid running empty pkexec command - logging.debug("No dependency install required.") if app: + msg.status("All dependencies are met.", app) if config.DIALOG == "curses": app.installdeps_e.set() return diff --git a/tui_app.py b/tui_app.py index 52a1766e..cfae5975 100644 --- a/tui_app.py +++ b/tui_app.py @@ -3,6 +3,7 @@ import signal import threading import curses +import time from pathlib import Path from queue import Queue @@ -150,6 +151,13 @@ def change_color_scheme(self): config.curses_colors = "Logos" self.set_curses_colors_logos() + def update_windows(self): + if isinstance(self.active_screen, tui_screen.CursesScreen): + self.main_window.erase() + self.menu_window.erase() + self.stdscr.timeout(100) + self.console.display() + def clear(self): self.stdscr.clear() self.main_window.clear() @@ -287,11 +295,7 @@ def display(self): if self.window_height >= 10 and self.window_width >= 35: config.margin = 2 if not config.resizing: - if isinstance(self.active_screen, tui_screen.CursesScreen): - self.main_window.erase() - self.menu_window.erase() - self.stdscr.timeout(100) - self.console.display() + self.update_windows() self.active_screen.display() @@ -436,13 +440,20 @@ def main_menu_select(self, choice): def winetricks_menu_select(self, choice): if choice == "Download or Update Winetricks": + self.reset_screen() control.set_winetricks() + self.go_to_main_menu() elif choice == "Run Winetricks": + self.reset_screen() wine.run_winetricks() + self.go_to_main_menu() elif choice == "Install d3dcompiler": + self.reset_screen() wine.installD3DCompiler() elif choice == "Install Fonts": + self.reset_screen() wine.installFonts() + self.go_to_main_menu() elif choice == "Set Renderer": self.reset_screen() self.screen_q.put(self.stack_menu(19, self.todo_q, self.todo_e, @@ -467,10 +478,15 @@ def winetricks_menu_select(self, choice): def utilities_menu_select(self, choice): if choice == "Remove Library Catalog": + self.reset_screen() control.remove_library_catalog() + self.go_to_main_menu() elif choice == "Remove All Index Files": + self.reset_screen() control.remove_all_index_files() + self.go_to_main_menu() elif choice == "Edit Config": + self.reset_screen() control.edit_config() self.go_to_main_menu() elif choice == "Change Logos Release Channel": @@ -485,13 +501,23 @@ def utilities_menu_select(self, choice): self.update_main_window_contents() self.go_to_main_menu() elif choice == "Install Dependencies": - utils.check_dependencies() + self.reset_screen() + msg.status("Checking dependencies…", self) + self.update_windows() + utils.check_dependencies(self) + self.go_to_main_menu() elif choice == "Back up Data": + self.reset_screen() control.backup() + self.go_to_main_menu() elif choice == "Restore Data": + self.reset_screen() control.restore() + self.go_to_main_menu() elif choice == "Update to Latest AppImage": + self.reset_screen() utils.update_to_latest_recommended_appimage() + self.go_to_main_menu() elif choice == "Set AppImage": # TODO: Allow specifying the AppImage File appimages = utils.find_appimage_files(utils.which_release()) @@ -502,9 +528,13 @@ def utilities_menu_select(self, choice): question = "Which AppImage should be used?" self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) elif choice == "Install ICU": + self.reset_screen() wine.installICUDataFiles() + self.go_to_main_menu() elif choice.endswith("Logging"): + self.reset_screen() wine.switch_logging() + self.go_to_main_menu() def custom_appimage_select(self, choice): #FIXME From 318b3b025a3935e53f0cb4674a1208afbc28946a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 30 Sep 2024 06:57:31 -0400 Subject: [PATCH 150/253] Fix installer issue --- installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.py b/installer.py index a3f9f955..d371d038 100644 --- a/installer.py +++ b/installer.py @@ -481,7 +481,7 @@ def ensure_wineprefix_init(app=None): def ensure_winetricks_applied(app=None): config.INSTALL_STEPS_COUNT += 1 - ensure_icu_data_files(app=app) + ensure_wineprefix_init(app=app) config.INSTALL_STEP += 1 status = "Ensuring winetricks & other settings are applied…" update_install_feedback(status, app=app) From e5af38ca6e5c7819627ffc7914535e43d7eb4dcf Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 1 Oct 2024 16:03:29 +0100 Subject: [PATCH 151/253] fix args sent to start_thread --- tui_app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tui_app.py b/tui_app.py index cfae5975..01ef7999 100644 --- a/tui_app.py +++ b/tui_app.py @@ -415,7 +415,11 @@ def main_menu_select(self, choice): elif choice.startswith("Install"): config.INSTALL_STEPS_COUNT = 0 config.INSTALL_STEP = 0 - utils.start_thread(installer.ensure_launcher_shortcuts, True, self) + utils.start_thread( + installer.ensure_launcher_shortcuts, + daemon_bool=True, + app=self, + ) elif choice.startswith("Update Logos Linux Installer"): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": From 227b00244b23a122e71abf681b886be26d43fd33 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 1 Oct 2024 14:37:57 -0400 Subject: [PATCH 152/253] Fix TUI window and screen issues --- tui_app.py | 51 +++++++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/tui_app.py b/tui_app.py index 01ef7999..877dd028 100644 --- a/tui_app.py +++ b/tui_app.py @@ -86,9 +86,15 @@ def __init__(self, stdscr): # Window and Screen Management self.tui_screens = [] self.menu_options = [] - self.window_height = 0 - self.window_width = 0 + self.window_height = self.window_width = self.console = self.menu_screen = self.active_screen = None + self.main_window_ratio = self.main_window_ratio = self.menu_window_ratio = self.main_window_min = None + self.menu_window_min = self.main_window_height = self.menu_window_height = self.main_window = None + self.menu_window = self.resize_window = None + self.set_window_dimensions() + + def set_window_dimensions(self): self.update_tty_dimensions() + curses.resizeterm(self.window_height, self.window_width) self.main_window_ratio = 0.25 if config.console_log: min_console_height = len(tui_curses.wrap_text(self, config.console_log[-1])) @@ -100,14 +106,12 @@ def __init__(self, stdscr): self.menu_window_min = 3 self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) - config.console_log_lines = max(self.main_window_height - 3, 1) + config.console_log_lines = max(self.main_window_height - self.main_window_min, 1) config.options_per_page = max(self.window_height - self.main_window_height - 6, 1) self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) - self.resize_window = curses.newwin(2, curses.COLS, 0, 0) - self.console = None - self.menu_screen = None - self.active_screen = None + resize_lines = tui_curses.wrap_text(self, "Screen too small.") + self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) @staticmethod def set_curses_style(): @@ -221,21 +225,6 @@ def end(self, signal, frame): self.llirunning = False curses.endwin() - def set_window_height(self): - self.update_tty_dimensions() - curses.resizeterm(self.window_height, self.window_width) - self.main_window_min = len(tui_curses.wrap_text(self, self.title)) + len( - tui_curses.wrap_text(self, self.subtitle)) + 3 - self.menu_window_min = 3 - self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) - self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) - config.console_log_lines = max(self.main_window_height - (self.main_window_min - 1), 1) - config.options_per_page = max(self.window_height - self.main_window_height - 6, 1) - self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) - self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) - resize_lines = tui_curses.wrap_text(self, "Screen too small.") - self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) - def update_main_window_contents(self): self.clear() self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" @@ -249,7 +238,7 @@ def update_main_window_contents(self): def resize_curses(self): config.resizing = True curses.endwin() - self.set_window_height() + self.set_window_dimensions() self.clear() self.init_curses() self.refresh() @@ -388,16 +377,17 @@ def choice_processor(self, stdscr, screen_id, choice): # Capture menu exiting before processing in the rest of the handler if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): + self.reset_screen() self.switch_q.put(1) #FIXME: There is some kind of graphical glitch that activates on returning to Main Menu, # but not from all submenus. # Further, there appear to be issues with how the program exits on Ctrl+C as part of this. - - action = screen_actions.get(screen_id) - if action: - action(choice) else: - pass + action = screen_actions.get(screen_id) + if action: + action(choice) + else: + pass def reset_screen(self): self.active_screen.running = 0 @@ -413,6 +403,7 @@ def main_menu_select(self, choice): self.tui_screens = [] self.llirunning = False elif choice.startswith("Install"): + self.reset_screen() config.INSTALL_STEPS_COUNT = 0 config.INSTALL_STEP = 0 utils.start_thread( @@ -802,10 +793,6 @@ def set_winetricksbin(self, choice): self.tricksbin_e.set() def get_waiting(self, dialog, screen_id=8): - # FIXME: I think TUI install with existing config file fails here b/c - # the config file already defines the needed variable, so the event - # self.tricksbin_e is never triggered. - self.tricksbin_e.wait() text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) From d610603653a09285ae986d6457aea0d1bc99d1a1 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 2 Oct 2024 10:49:12 +0100 Subject: [PATCH 153/253] fix func name and args --- installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.py b/installer.py index d371d038..bd0b5be4 100644 --- a/installer.py +++ b/installer.py @@ -523,7 +523,7 @@ def ensure_winetricks_applied(app=None): wine.set_win_version("logos", "win10") msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") - wine.set_win_version_indexer("vista") + wine.set_win_version("indexer", "vista") logging.debug("> Done.") From b04b02c59075319ac5a9502a7eb06276c2035bf6 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 2 Oct 2024 11:11:57 +0100 Subject: [PATCH 154/253] use full wine exe path when running wine proc --- wine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wine.py b/wine.py index 5b63d6c1..c764b85f 100644 --- a/wine.py +++ b/wine.py @@ -325,7 +325,7 @@ def set_win_version(exe, windows_version): "/t", "REG_SZ", "/d", f"{windows_version}", "/f", ] - run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) def installICUDataFiles(app=None): From bc048b67fac5c3427eed0bad8db3204950f9ff54 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 2 Oct 2024 11:44:34 +0100 Subject: [PATCH 155/253] add logging_event to gui_app --- gui_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui_app.py b/gui_app.py index 9ffdc13e..08c55918 100644 --- a/gui_app.py +++ b/gui_app.py @@ -642,7 +642,8 @@ def __init__(self, root, *args, **kwargs): self.update_run_winetricks_button() self.logging_q = Queue() - self.root.bind('<>', self.initialize_logging_button) + self.logging_event = '<>' + self.root.bind(self.logging_event, self.initialize_logging_button) self.root.bind('<>', self.update_logging_button) self.status_q = Queue() self.status_evt = '<>' From 883b021b20ac3881d846d321406bc4dfc1aa6ead Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 2 Oct 2024 12:57:33 +0100 Subject: [PATCH 156/253] clean up app logging in GUI --- gui_app.py | 11 ++++++----- wine.py | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/gui_app.py b/gui_app.py index 08c55918..91c10b42 100644 --- a/gui_app.py +++ b/gui_app.py @@ -642,9 +642,8 @@ def __init__(self, root, *args, **kwargs): self.update_run_winetricks_button() self.logging_q = Queue() - self.logging_event = '<>' - self.root.bind(self.logging_event, self.initialize_logging_button) - self.root.bind('<>', self.update_logging_button) + self.logging_event = '<>' + self.root.bind(self.logging_event, self.update_logging_button) self.status_q = Queue() self.status_evt = '<>' self.root.bind(self.status_evt, self.update_status_text) @@ -864,8 +863,10 @@ def update_logging_button(self, evt=None): self.gui.statusvar.set('') self.gui.progress.stop() self.gui.progress.state(['disabled']) - state = self.logging_q.get() - self.gui.loggingstatevar.set(state[:-1].title()) + new_state = self.reverse_logging_state_value(self.logging_q.get()) + new_text = new_state[:-1].title() + logging.debug(f"Updating app logging button text to: {new_text}") + self.gui.loggingstatevar.set(new_text) self.gui.logging_button.state(['!disabled']) def update_app_button(self, evt=None): diff --git a/wine.py b/wine.py index c764b85f..205f9015 100644 --- a/wine.py +++ b/wine.py @@ -358,6 +358,7 @@ def installICUDataFiles(app=None): def get_registry_value(reg_path, name): + logging.debug(f"Get value for: {reg_path=}; {name=}") # NOTE: Can't use run_wine_proc here because of infinite recursion while # trying to determine WINECMD_ENCODING. value = None @@ -385,6 +386,7 @@ def get_registry_value(reg_path, name): for line in result.stdout.splitlines(): if line.strip().startswith(name): value = line.split()[-1].strip() + logging.debug(f"Registry value: {value}") break else: logging.critical(err_msg) @@ -401,14 +403,13 @@ def get_app_logging_state(app=None, init=False): state = 'ENABLED' if app is not None: app.logging_q.put(state) - if init: - app.root.event_generate('<>') - else: - app.root.event_generate('<>') + logging.debug(f"Current app logging state: {state} ({current_value})") + app.root.event_generate(app.logging_event) return state def switch_logging(action=None, app=None): + logging.debug(f"switch_logging; {action=}; {app=}") state_disabled = 'DISABLED' value_disabled = '0000' state_enabled = 'ENABLED' From 21cd6d5b3c043d48eb1f1111b68c9690790bf707 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 2 Oct 2024 13:59:11 +0100 Subject: [PATCH 157/253] update GUI threads to use utils.start_thread --- gui_app.py | 154 +++++++++++++++-------------------------------------- wine.py | 2 +- 2 files changed, 45 insertions(+), 111 deletions(-) diff --git a/gui_app.py b/gui_app.py index 91c10b42..b8879ac2 100644 --- a/gui_app.py +++ b/gui_app.py @@ -6,7 +6,6 @@ import logging from pathlib import Path from queue import Queue -from threading import Thread from tkinter import PhotoImage from tkinter import Tk @@ -93,9 +92,8 @@ def __init__(self, new_win, root, **kwargs): # Initialize variables. self.flproduct = None # config.FLPRODUCT - self.release_thread = None + self.config_thread = None self.wine_exe = None - self.wine_thread = None self.winetricksbin = None self.appimages = None # self.appimage_verified = None @@ -184,12 +182,10 @@ def start_ensure_config(self): # Ensure progress counter is reset. config.INSTALL_STEP = 1 config.INSTALL_STEPS_COUNT = 0 - self.config_thread = Thread( - target=installer.ensure_installation_config, - kwargs={'app': self}, - daemon=True + self.config_thread = utils.start_thread( + installer.ensure_installation_config, + app=self, ) - self.config_thread.start() def get_winetricks_options(self): config.WINETRICKSBIN = None # override config file b/c "Download" accounts for that # noqa: E501 @@ -348,17 +344,12 @@ def start_releases_check(self): self.release_evt, self.update_release_check_progress ) - self.release_thread = Thread( - target=network.get_logos_releases, - kwargs={'app': self}, - daemon=True, - ) # Start progress. self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set("Downloading Release list…") # Start thread. - self.release_thread.start() + utils.start_thread(network.get_logos_releases, app=self) def set_release(self, evt=None): if self.gui.releasevar.get()[0] == 'C': # ignore default text @@ -389,20 +380,16 @@ def start_find_appimage_files(self, release_version): self.appimage_evt, self.update_find_appimage_progress ) - self.appimage_thread = Thread( - target=utils.find_appimage_files, - kwargs={ - 'release_version': release_version, - 'app': self - }, - daemon=True - ) # Start progress. self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set("Finding available wine AppImages…") # Start thread. - self.appimage_thread.start() + utils.start_thread( + utils.find_appimage_files, + release_version=release_version, + app=self, + ) def start_wine_versions_check(self, release_version): if self.appimages is None: @@ -416,21 +403,17 @@ def start_wine_versions_check(self, release_version): self.wine_evt, self.update_wine_check_progress ) - self.wine_thread = Thread( - target=utils.get_wine_options, - args=[ - self.appimages, - utils.find_wine_binary_files(release_version) - ], - kwargs={'app': self}, - daemon=True - ) # Start progress. self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set("Finding available wine binaries…") # Start thread. - self.wine_thread.start() + utils.start_thread( + utils.get_wine_options, + self.appimages, + utils.find_wine_binary_files(release_version), + app=self, + ) def set_wine(self, evt=None): self.gui.wine_exe = self.gui.winevar.get() @@ -487,12 +470,7 @@ def on_cancel_released(self, evt=None): def start_install_thread(self, evt=None): self.gui.progress.config(mode='determinate') - th = Thread( - target=installer.ensure_launcher_shortcuts, - kwargs={'app': self}, - daemon=True - ) - th.start() + utils.start_thread(installer.ensure_launcher_shortcuts, app=self) def start_indeterminate_progress(self, evt=None): self.gui.progress.state(['!disabled']) @@ -674,14 +652,9 @@ def __init__(self, root, *args, **kwargs): # Start function to determine app logging state. if utils.app_is_installed(): - t = Thread( - target=wine.get_app_logging_state, - kwargs={'app': self, 'init': True}, - daemon=True, - ) - t.start() self.gui.statusvar.set('Getting current app logging status…') self.start_indeterminate_progress() + utils.start_thread(wine.get_app_logging_state, app=self) def configure_app_button(self, evt=None): if utils.find_installed_product(): @@ -700,8 +673,7 @@ def run_installer(self, evt=None): def run_logos(self, evt=None): # TODO: Add reference to App here so the status message is sent to the # GUI? See msg.status and wine.run_logos - t = Thread(target=wine.run_logos) - t.start() + utils.start_thread(wine.run_logos) def run_action_cmd(self, evt=None): self.actioncmd() @@ -720,28 +692,18 @@ def on_action_radio_clicked(self, evt=None): self.actioncmd = self.install_icu def run_indexing(self, evt=None): - t = Thread(target=wine.run_indexing) - t.start() + utils.start_thread(wine.run_indexing) def remove_library_catalog(self, evt=None): control.remove_library_catalog() def remove_indexes(self, evt=None): self.gui.statusvar.set("Removing indexes…") - t = Thread( - target=control.remove_all_index_files, - kwargs={'app': self} - ) - t.start() + utils.start_thread(control.remove_all_index_files, app=self) def install_icu(self, evt=None): self.gui.statusvar.set("Installing ICU files…") - t = Thread( - target=wine.installICUDataFiles, - kwargs={'app': self}, - daemon=True, - ) - t.start() + utils.start_thread(wine.installICUDataFiles, app=self) def run_backup(self, evt=None): # Get backup folder. @@ -759,19 +721,17 @@ def run_backup(self, evt=None): self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) # Start backup thread. - t = Thread(target=control.backup, args=[self], daemon=True) - t.start() + utils.start_thread(control.backup, app=self) def run_restore(self, evt=None): # FIXME: Allow user to choose restore source? # Start restore thread. - t = Thread(target=control.restore, args=[self], daemon=True) - t.start() + utils.start_thread(control.restore, app=self) def install_deps(self, evt=None): - t = Thread(target=utils.check_dependencies, daemon=True) + # TODO: Separate as advanced feature. self.start_indeterminate_progress() - t.start() + utils.start_thread(utils.check_dependencies) def open_file_dialog(self, filetype_name, filetype_extension): file_path = fd.askopenfilename( @@ -786,78 +746,51 @@ def open_file_dialog(self, filetype_name, filetype_extension): def update_to_latest_lli_release(self, evt=None): self.start_indeterminate_progress() self.gui.statusvar.set("Updating to latest Logos Linux Installer version…") # noqa: E501 - t = Thread( - target=utils.update_to_latest_lli_release, - kwargs={'app': self}, - daemon=True, - ) - t.start() + utils.start_thread(utils.update_to_latest_lli_release, app=self) def update_to_latest_appimage(self, evt=None): config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 self.start_indeterminate_progress() self.gui.statusvar.set("Updating to latest AppImage…") - t = Thread( - target=utils.set_appimage_symlink, - kwargs={'app': self}, - daemon=True, - ) - t.start() + utils.start_thread(utils.set_appimage_symlink, app=self) def set_appimage(self, evt=None): + # TODO: Separate as advanced feature. appimage_filename = self.open_file_dialog("AppImage", "AppImage") if not appimage_filename: return config.SELECTED_APPIMAGE_FILENAME = appimage_filename - t = Thread( - target=utils.set_appimage_symlink, - kwargs={'app': self}, - daemon=True, - ) - t.start() + utils.start_thread(utils.set_appimage_symlink, app=self) def get_winetricks(self, evt=None): + # TODO: Separate as advanced feature. self.gui.statusvar.set("Installing Winetricks…") - t1 = Thread( - target=system.install_winetricks, - args=[config.APPDIR_BINDIR], - kwargs={'app': self}, - daemon=True, + utils.start_thread( + system.install_winetricks, + config.APPDIR_BINDIR, + app=self ) - t1.start() self.update_run_winetricks_button() def launch_winetricks(self, evt=None): self.gui.statusvar.set("Launching Winetricks…") # Start winetricks in thread. - t1 = Thread(target=wine.run_winetricks) - t1.start() + utils.start_thread(wine.run_winetricks) # Start thread to clear status after delay. args = [12000, self.root.event_generate, '<>'] - t2 = Thread(target=self.root.after, args=args, daemon=True) - t2.start() + utils.start_thread(self.root.after, *args) def switch_logging(self, evt=None): - prev_state = self.gui.loggingstatevar.get() - new_state = 'Enable' if prev_state == 'Disable' else 'Disable' + desired_state = self.gui.loggingstatevar.get() + # new_state = 'Enable' if prev_state == 'Disable' else 'Disable' kwargs = { - 'action': new_state.lower(), + 'action': desired_state.lower(), 'app': self, } - self.gui.statusvar.set(f"Switching app logging to '{prev_state}d'…") - self.gui.progress.state(['!disabled']) - self.gui.progress.start() + self.gui.statusvar.set(f"Switching app logging to '{desired_state}d'…") + self.start_indeterminate_progress() self.gui.logging_button.state(['disabled']) - t = Thread(target=wine.switch_logging, kwargs=kwargs) - t.start() - - def initialize_logging_button(self, evt=None): - self.gui.statusvar.set('') - self.gui.progress.stop() - self.gui.progress.state(['disabled']) - state = self.reverse_logging_state_value(self.logging_q.get()) - self.gui.loggingstatevar.set(state[:-1].title()) - self.gui.logging_button.state(['!disabled']) + utils.start_thread(wine.switch_logging, **kwargs) def update_logging_button(self, evt=None): self.gui.statusvar.set('') @@ -874,6 +807,7 @@ def update_app_button(self, evt=None): self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.configure_app_button() self.update_run_winetricks_button() + self.gui.logging_button.state(['!disabled']) def update_latest_lli_release_button(self, evt=None): status, reason = utils.compare_logos_linux_installer_version() diff --git a/wine.py b/wine.py index 205f9015..235563d8 100644 --- a/wine.py +++ b/wine.py @@ -393,7 +393,7 @@ def get_registry_value(reg_path, name): return value -def get_app_logging_state(app=None, init=False): +def get_app_logging_state(app=None): state = 'DISABLED' current_value = get_registry_value( 'HKCU\\Software\\Logos4\\Logging', From 000a9398792ea1082cd04d276dceb16782b866f0 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 2 Oct 2024 14:10:27 +0100 Subject: [PATCH 158/253] use variable to define GUI installer widget insertion rows --- gui.py | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/gui.py b/gui.py index 13f176f6..17422889 100644 --- a/gui.py +++ b/gui.py @@ -123,27 +123,36 @@ def __init__(self, root, **kwargs): self.progress = Progressbar(self, variable=self.progressvar) # Place widgets. - self.product_label.grid(column=0, row=0, sticky='nws', pady=2) - self.product_dropdown.grid(column=1, row=0, sticky='w', pady=2) - self.version_dropdown.grid(column=2, row=0, sticky='w', pady=2) - self.release_label.grid(column=0, row=1, sticky='w', pady=2) - self.release_dropdown.grid(column=1, row=1, sticky='w', pady=2) - self.release_check_button.grid(column=2, row=1, sticky='w', pady=2) - self.wine_label.grid(column=0, row=2, sticky='w', pady=2) - self.wine_dropdown.grid(column=1, row=2, columnspan=3, sticky='we', pady=2) # noqa: E501 - self.wine_check_button.grid(column=4, row=2, sticky='e', pady=2) - self.tricks_label.grid(column=0, row=3, sticky='w', pady=2) - self.tricks_dropdown.grid(column=1, row=3, sticky='we', pady=2) - self.fonts_label.grid(column=0, row=4, sticky='nws', pady=2) - self.fonts_checkbox.grid(column=1, row=4, sticky='w', pady=2) - self.skipdeps_label.grid(column=2, row=4, sticky='nws', pady=2) - self.skipdeps_checkbox.grid(column=3, row=4, sticky='w', pady=2) - self.cancel_button.grid(column=3, row=5, sticky='e', pady=2) - self.okay_button.grid(column=4, row=5, sticky='e', pady=2) + row = 0 + self.product_label.grid(column=0, row=row, sticky='nws', pady=2) + self.product_dropdown.grid(column=1, row=row, sticky='w', pady=2) + self.version_dropdown.grid(column=2, row=row, sticky='w', pady=2) + row += 1 + self.release_label.grid(column=0, row=row, sticky='w', pady=2) + self.release_dropdown.grid(column=1, row=row, sticky='w', pady=2) + self.release_check_button.grid(column=2, row=row, sticky='w', pady=2) + row += 1 + self.wine_label.grid(column=0, row=row, sticky='w', pady=2) + self.wine_dropdown.grid(column=1, row=row, columnspan=3, sticky='we', pady=2) # noqa: E501 + self.wine_check_button.grid(column=4, row=row, sticky='e', pady=2) + row += 1 + self.tricks_label.grid(column=0, row=row, sticky='w', pady=2) + self.tricks_dropdown.grid(column=1, row=row, sticky='we', pady=2) + row += 1 + self.fonts_label.grid(column=0, row=row, sticky='nws', pady=2) + self.fonts_checkbox.grid(column=1, row=row, sticky='w', pady=2) + self.skipdeps_label.grid(column=2, row=row, sticky='nws', pady=2) + self.skipdeps_checkbox.grid(column=3, row=row, sticky='w', pady=2) + row += 1 + self.cancel_button.grid(column=3, row=row, sticky='e', pady=2) + self.okay_button.grid(column=4, row=row, sticky='e', pady=2) + row += 1 # Status area - s1.grid(column=0, row=6, columnspan=5, sticky='we') - self.status_label.grid(column=0, row=7, columnspan=5, sticky='w', pady=2) # noqa: E501 - self.progress.grid(column=0, row=8, columnspan=5, sticky='we', pady=2) + s1.grid(column=0, row=row, columnspan=5, sticky='we') + row += 1 + self.status_label.grid(column=0, row=row, columnspan=5, sticky='w', pady=2) # noqa: E501 + row += 1 + self.progress.grid(column=0, row=row, columnspan=5, sticky='we', pady=2) class ControlGui(Frame): From 05260300894582c5c0d6244de5b7d5424dac63d7 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 2 Oct 2024 14:12:12 +0100 Subject: [PATCH 159/253] pep8 fix --- gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui.py b/gui.py index 17422889..ebb7ff2f 100644 --- a/gui.py +++ b/gui.py @@ -152,7 +152,7 @@ def __init__(self, root, **kwargs): row += 1 self.status_label.grid(column=0, row=row, columnspan=5, sticky='w', pady=2) # noqa: E501 row += 1 - self.progress.grid(column=0, row=row, columnspan=5, sticky='we', pady=2) + self.progress.grid(column=0, row=row, columnspan=5, sticky='we', pady=2) # noqa: E501 class ControlGui(Frame): From 27b563c23b5cdd4ffdb0a045970cfab4a8fbffba Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 30 Aug 2024 21:27:18 -0400 Subject: [PATCH 160/253] Fill out system.run_command() defaults --- system.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/system.py b/system.py index b22580c5..39c5d002 100644 --- a/system.py +++ b/system.py @@ -27,6 +27,24 @@ def run_command(command, retries=1, delay=0, **kwargs): stdin = kwargs.get("stdin", None) stdout = kwargs.get("stdout", None) stderr = kwargs.get("stderr", None) + timeout = kwargs.get("timeout", None) + bufsize = kwargs.get("bufsize", -1) + executable = kwargs.get("executable", None) + pass_fds = kwargs.get("pass_fds", ()) + errors = kwargs.get("errors", None) + preexec_fn = kwargs.get("preexec_fn", None) + close_fds = kwargs.get("close_fds", True) + universal_newlines = kwargs.get("universal_newlines", None) + startupinfo = kwargs.get("startupinfo", None) + creationflags = kwargs.get("creationflags", 0) + restore_signals = kwargs.get("restore_signals", True) + start_new_session = kwargs.get("start_new_session", False) + user = kwargs.get("user", None) + group = kwargs.get("group", None) + extra_groups = kwargs.get("extra_groups", None) + umask = kwargs.get("umask", -1) + pipesize = kwargs.get("pipesize", -1) + process_group = kwargs.get("process_group", None) if retries < 1: retries = 1 @@ -49,6 +67,24 @@ def run_command(command, retries=1, delay=0, **kwargs): encoding=encoding, cwd=cwd, env=env, + timeout=timeout, + bufsize=bufsize, + executable=executable, + errors=errors, + pass_fds=pass_fds, + preexec_fn=preexec_fn, + close_fds=close_fds, + universal_newlines=universal_newlines, + startupinfo=startupinfo, + creationflags=creationflags, + restore_signals=restore_signals, + start_new_session=start_new_session, + user=user, + group=group, + extra_groups=extra_groups, + umask=umask, + pipesize=pipesize, + process_group=process_group ) return result except subprocess.CalledProcessError as e: From 1e33be6daac94cee185bb37b6c7011a53ff384f2 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 30 Aug 2024 21:31:47 -0400 Subject: [PATCH 161/253] Add system.popen_command() --- system.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/system.py b/system.py index 39c5d002..dbb56da0 100644 --- a/system.py +++ b/system.py @@ -13,9 +13,8 @@ import network -# TODO: Add a Popen variant to run_command to replace functions in control.py -# and wine.py -def run_command(command, retries=1, delay=0, **kwargs): +# TODO: Replace functions in control.py and wine.py with Popen command. +def run_command(command, retries=1, delay=0, wait=True, **kwargs): check = kwargs.get("check", True) text = kwargs.get("text", True) capture_output = kwargs.get("capture_output", True) @@ -86,7 +85,10 @@ def run_command(command, retries=1, delay=0, **kwargs): pipesize=pipesize, process_group=process_group ) - return result + if wait: + return result + else: + return None except subprocess.CalledProcessError as e: logging.error(f"Error occurred in run_command() while executing \"{command}\": {e}") # noqa: E501 if "lock" in str(e): @@ -102,6 +104,93 @@ def run_command(command, retries=1, delay=0, **kwargs): return None +def popen_command(command, retries=1, delay=0, wait=True, **kwargs): + shell = kwargs.get("shell", False) + env = kwargs.get("env", None) + cwd = kwargs.get("cwd", None) + stdin = kwargs.get("stdin", None) + stdout = kwargs.get("stdout", None) + stderr = kwargs.get("stderr", None) + bufsize = kwargs.get("bufsize", -1) + executable = kwargs.get("executable", None) + pass_fds = kwargs.get("pass_fds", ()) + preexec_fn = kwargs.get("preexec_fn", None) + close_fds = kwargs.get("close_fds", True) + universal_newlines = kwargs.get("universal_newlines", None) + startupinfo = kwargs.get("startupinfo", None) + creationflags = kwargs.get("creationflags", 0) + restore_signals = kwargs.get("restore_signals", True) + start_new_session = kwargs.get("start_new_session", False) + user = kwargs.get("user", None) + group = kwargs.get("group", None) + extra_groups = kwargs.get("extra_groups", None) + umask = kwargs.get("umask", -1) + pipesize = kwargs.get("pipesize", -1) + process_group = kwargs.get("process_group", None) + encoding = kwargs.get("encoding", None) + errors = kwargs.get("errors", None) + text = kwargs.get("text", None) + + if retries < 1: + retries = 1 + + if isinstance(command, str) and not shell: + command = command.split() + + for attempt in range(retries): + try: + process = subprocess.Popen( + command, + shell=shell, + env=env, + cwd=cwd, + stdin=stdin, + stdout=stdout, + stderr=stderr, + bufsize=bufsize, + executable=executable, + pass_fds=pass_fds, + preexec_fn=preexec_fn, + close_fds=close_fds, + universal_newlines=universal_newlines, + startupinfo=startupinfo, + creationflags=creationflags, + restore_signals=restore_signals, + start_new_session=start_new_session, + user=user, + group=group, + extra_groups=extra_groups, + umask=umask, + pipesize=pipesize, + process_group=process_group, + encoding=encoding, + errors=errors, + text=text + ) + + if wait: + stdout, stderr = process.communicate(timeout=kwargs.get("timeout", None)) + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, command, output=stdout, stderr=stderr) + return stdout, stderr + else: + return process + + except subprocess.CalledProcessError as e: + logging.error(f"Error occurred in popen_command() while executing \"{command}\": {e}") + if "lock" in str(e): + logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") + time.sleep(delay) + else: + raise e + except Exception as e: + logging.error(f"An unexpected error occurred when running {command}: {e}") + return None + + logging.error(f"Failed to execute after {retries} attempts: '{command}'") + return None + + def reboot(): logging.info("Rebooting system.") command = f"{config.SUPERUSER_COMMAND} reboot now" From df7505b9e5012d76d31054e31f5868bbb88f63ec Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 2 Sep 2024 17:07:51 -0400 Subject: [PATCH 162/253] Add Logos state manager - Add logos.py - Move processes from main to config to avoid conflicts - Add utils.stopwatch() - Add system.get_pid(), system.get_logos_pids() - Add wine.get_wine_user(), wine.set_logos_paths(), wine.check_wineserver(), wine.wineserver_kill(), wine.wineserver_wait() - Make wine.run_wine_proc() use system.popen_command() - tui_app menu clean up to avoid locked menus - Refactoring and PEP8 - Revert Indexer from Vista to Windows 10. Vista prevents it from running. --- config.py | 8 ++ gui_app.py | 79 +++++++++++------ installer.py | 62 ++++++++++++- logos.py | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 18 ++-- system.py | 83 +++++++++++++++--- tui_app.py | 59 ++++++++++--- utils.py | 20 ++++- wine.py | 239 +++++++++++++++------------------------------------ 9 files changed, 567 insertions(+), 236 deletions(-) create mode 100644 logos.py diff --git a/config.py b/config.py index 948ad5b8..c66f6e01 100644 --- a/config.py +++ b/config.py @@ -100,6 +100,7 @@ VERBUM_PATH = None WINETRICKS_URL = "https://raw.githubusercontent.com/Winetricks/winetricks/5904ee355e37dff4a3ab37e1573c56cffe6ce223/src/winetricks" # noqa: E501 WINETRICKS_VERSION = '20220411' +wine_user = None WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") install_finished = False console_log = [] @@ -110,6 +111,13 @@ total_pages = 0 options_per_page = 8 resizing = False +processes = {} +threads = [] +login_window_cmd = None +logos_cef_cmd = None +logos_indexing_cmd = None +logos_indexer_exe = None +check_if_indexing = None def get_config_file_dict(config_file_path): diff --git a/gui_app.py b/gui_app.py index b8879ac2..5f7c3352 100644 --- a/gui_app.py +++ b/gui_app.py @@ -4,6 +4,7 @@ # - https://github.com/thw26/LogosLinuxInstaller/blob/master/LogosLinuxInstaller.sh # noqa: E501 import logging +import threading from pathlib import Path from queue import Queue @@ -17,6 +18,7 @@ import control import gui import installer +import logos import network import system import utils @@ -335,7 +337,7 @@ def set_version(self, evt=None): def start_releases_check(self): # Disable button; clear list. - self.gui.release_check_button.state(['disabled']) + self.gui.release_check_button.logos_state(['disabled']) # self.gui.releasevar.set('') self.gui.release_dropdown['values'] = [] # Setup queue, signal, thread. @@ -446,7 +448,7 @@ def on_release_check_released(self, evt=None): self.start_releases_check() def on_wine_check_released(self, evt=None): - self.gui.wine_check_button.state(['disabled']) + self.gui.wine_check_button.logos_state(['disabled']) self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) def set_skip_fonts(self, evt=None): @@ -473,21 +475,21 @@ def start_install_thread(self, evt=None): utils.start_thread(installer.ensure_launcher_shortcuts, app=self) def start_indeterminate_progress(self, evt=None): - self.gui.progress.state(['!disabled']) + self.gui.progress.logos_state(['!disabled']) self.gui.progressvar.set(0) self.gui.progress.config(mode='indeterminate') self.gui.progress.start() def stop_indeterminate_progress(self, evt=None): self.gui.progress.stop() - self.gui.progress.state(['disabled']) + self.gui.progress.logos_state(['disabled']) self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) self.gui.statusvar.set('') def update_release_check_progress(self, evt=None): self.stop_indeterminate_progress() - self.gui.release_check_button.state(['!disabled']) + self.gui.release_check_button.logos_state(['!disabled']) if not self.releases_q.empty(): self.gui.release_dropdown['values'] = self.releases_q.get() self.gui.releasevar.set(self.gui.release_dropdown['values'][0]) @@ -510,7 +512,7 @@ def update_wine_check_progress(self, evt=None): self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) self.set_wine() self.stop_indeterminate_progress() - self.gui.wine_check_button.state(['!disabled']) + self.gui.wine_check_button.logos_state(['!disabled']) def update_file_check_progress(self, evt=None): self.gui.progress.stop() @@ -549,7 +551,7 @@ def update_install_progress(self, evt=None): text="Exit", command=self.on_cancel_released, ) - self.gui.okay_button.state(['!disabled']) + self.gui.okay_button.logos_state(['!disabled']) self.root.event_generate('<>') self.win.destroy() return 0 @@ -563,6 +565,7 @@ def __init__(self, root, *args, **kwargs): self.root.resizable(False, False) self.gui = gui.ControlGui(self.root) self.actioncmd = None + self.logos = logos.LogosManager(app=self) text = self.gui.update_lli_label.cget('text') ver = config.LLI_CURRENT_VERSION @@ -589,7 +592,7 @@ def __init__(self, root, *args, **kwargs): text=self.gui.loggingstatevar.get(), command=self.switch_logging ) - self.gui.logging_button.state(['disabled']) + self.gui.logging_button.logos_state(['disabled']) self.gui.config_button.config(command=control.edit_config) self.gui.deps_button.config(command=self.install_deps) @@ -602,12 +605,12 @@ def __init__(self, root, *args, **kwargs): command=self.update_to_latest_appimage ) if config.WINEBIN_CODE != "AppImage" and config.WINEBIN_CODE != "Recommended": # noqa: E501 - self.gui.latest_appimage_button.state(['disabled']) + self.gui.latest_appimage_button.logos_state(['disabled']) gui.ToolTip( self.gui.latest_appimage_button, "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 ) - self.gui.set_appimage_button.state(['disabled']) + self.gui.set_appimage_button.logos_state(['disabled']) gui.ToolTip( self.gui.set_appimage_button, "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 @@ -652,6 +655,12 @@ def __init__(self, root, *args, **kwargs): # Start function to determine app logging state. if utils.app_is_installed(): + t = threading.Thread( + target=self.logos.get_app_logging_state, + kwargs={'app': self, 'init': True}, + daemon=True, + ) + t.start() self.gui.statusvar.set('Getting current app logging status…') self.start_indeterminate_progress() utils.start_thread(wine.get_app_logging_state, app=self) @@ -660,7 +669,7 @@ def configure_app_button(self, evt=None): if utils.find_installed_product(): self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.gui.app_button.config(command=self.run_logos) - self.gui.get_winetricks_button.state(['!disabled']) + self.gui.get_winetricks_button.logos_state(['!disabled']) else: self.gui.app_button.config(command=self.run_installer) @@ -673,7 +682,8 @@ def run_installer(self, evt=None): def run_logos(self, evt=None): # TODO: Add reference to App here so the status message is sent to the # GUI? See msg.status and wine.run_logos - utils.start_thread(wine.run_logos) + t = threading.Thread(target=self.logos.start) + t.start() def run_action_cmd(self, evt=None): self.actioncmd() @@ -681,7 +691,7 @@ def run_action_cmd(self, evt=None): def on_action_radio_clicked(self, evt=None): logging.debug("gui_app.ControlPanel.on_action_radio_clicked START") if utils.app_is_installed(): - self.gui.actions_button.state(['!disabled']) + self.gui.actions_button.logos_state(['!disabled']) if self.gui.actionsvar.get() == 'run-indexing': self.actioncmd = self.run_indexing elif self.gui.actionsvar.get() == 'remove-library-catalog': @@ -692,7 +702,8 @@ def on_action_radio_clicked(self, evt=None): self.actioncmd = self.install_icu def run_indexing(self, evt=None): - utils.start_thread(wine.run_indexing) + t = threading.Thread(target=self.logos.index) + t.start() def remove_library_catalog(self, evt=None): control.remove_library_catalog() @@ -703,7 +714,12 @@ def remove_indexes(self, evt=None): def install_icu(self, evt=None): self.gui.statusvar.set("Installing ICU files…") - utils.start_thread(wine.installICUDataFiles, app=self) + t = threading.Thread( + target=wine.install_icu_data_files, + kwargs={'app': self}, + daemon=True, + ) + t.start() def run_backup(self, evt=None): # Get backup folder. @@ -717,7 +733,7 @@ def run_backup(self, evt=None): return # Prepare progress bar. - self.gui.progress.state(['!disabled']) + self.gui.progress.logos_state(['!disabled']) self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) # Start backup thread. @@ -789,21 +805,32 @@ def switch_logging(self, evt=None): } self.gui.statusvar.set(f"Switching app logging to '{desired_state}d'…") self.start_indeterminate_progress() - self.gui.logging_button.state(['disabled']) - utils.start_thread(wine.switch_logging, **kwargs) + self.gui.progress.logos_state(['!disabled']) + self.gui.progress.start() + self.gui.logging_button.logos_state(['disabled']) + t = threading.Thread(target=self.logos.switch_logging, kwargs=kwargs) + t.start() + + def initialize_logging_button(self, evt=None): + self.gui.statusvar.set('') + self.gui.progress.stop() + self.gui.progress.logos_state(['disabled']) + state = self.reverse_logging_state_value(self.logging_q.get()) + self.gui.loggingstatevar.set(state[:-1].title()) + self.gui.logging_button.logos_state(['!disabled']) def update_logging_button(self, evt=None): self.gui.statusvar.set('') self.gui.progress.stop() - self.gui.progress.state(['disabled']) + self.gui.progress.logos_state(['disabled']) new_state = self.reverse_logging_state_value(self.logging_q.get()) new_text = new_state[:-1].title() logging.debug(f"Updating app logging button text to: {new_text}") self.gui.loggingstatevar.set(new_text) - self.gui.logging_button.state(['!disabled']) + self.gui.logging_button.logos_state(['!disabled']) def update_app_button(self, evt=None): - self.gui.app_button.state(['!disabled']) + self.gui.app_button.logos_state(['!disabled']) self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.configure_app_button() self.update_run_winetricks_button() @@ -827,7 +854,7 @@ def update_latest_lli_release_button(self, evt=None): gui.ToolTip(self.gui.update_lli_button, msg) self.clear_status_text() self.stop_indeterminate_progress() - self.gui.update_lli_button.state([state]) + self.gui.update_lli_button.logos_state([state]) def update_latest_appimage_button(self, evt=None): status, reason = utils.compare_recommended_appimage_version() @@ -844,14 +871,14 @@ def update_latest_appimage_button(self, evt=None): gui.ToolTip(self.gui.latest_appimage_button, msg) self.clear_status_text() self.stop_indeterminate_progress() - self.gui.latest_appimage_button.state([state]) + self.gui.latest_appimage_button.logos_state([state]) def update_run_winetricks_button(self, evt=None): if utils.file_exists(config.WINETRICKSBIN): state = '!disabled' else: state = 'disabled' - self.gui.run_winetricks_button.state([state]) + self.gui.run_winetricks_button.logos_state([state]) def reverse_logging_state_value(self, state): if state == 'DISABLED': @@ -890,14 +917,14 @@ def update_status_text(self, evt=None): self.gui.statusvar.set(self.status_q.get()) def start_indeterminate_progress(self, evt=None): - self.gui.progress.state(['!disabled']) + self.gui.progress.logos_state(['!disabled']) self.gui.progressvar.set(0) self.gui.progress.config(mode='indeterminate') self.gui.progress.start() def stop_indeterminate_progress(self, evt=None): self.gui.progress.stop() - self.gui.progress.state(['disabled']) + self.gui.progress.logos_state(['disabled']) self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) diff --git a/installer.py b/installer.py index bd0b5be4..d92d90bc 100644 --- a/installer.py +++ b/installer.py @@ -537,15 +537,73 @@ def ensure_icu_data_files(app=None): icu_license_path = f"{config.WINEPREFIX}/drive_c/windows/globalization/ICU/LICENSE-ICU.txt" # noqa: E501 if not utils.file_exists(icu_license_path): - wine.installICUDataFiles(app=app) + wine.install_icu_data_files(app=app) logging.debug('> ICU data files installed') +def ensure_winetricks_applied(app=None): + config.INSTALL_STEPS_COUNT += 1 + ensure_icu_data_files(app=app) + config.INSTALL_STEP += 1 + status = "Ensuring winetricks & other settings are applied…" + update_install_feedback(status, app=app) + logging.debug('- disable winemenubuilder') + logging.debug('- settings renderer=gdi') + logging.debug('- corefonts') + logging.debug('- tahoma') + logging.debug('- settings fontsmooth=rgb') + logging.debug('- d3dcompiler_47') + + if not config.SKIP_WINETRICKS: + usr_reg = None + sys_reg = None + workdir = Path(f"{config.WORKDIR}") + workdir.mkdir(parents=True, exist_ok=True) + usr_reg = Path(f"{config.WINEPREFIX}/user.reg") + sys_reg = Path(f"{config.WINEPREFIX}/system.reg") + if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): + #FIXME: This command is failing. + reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') + with open(reg_file, 'w') as f: + f.write(r'''REGEDIT4 + +[HKEY_CURRENT_USER\Software\Wine\DllOverrides] +"winemenubuilder.exe"="" +''') + wine.wine_reg_install(reg_file) + + if not utils.grep(r'"renderer"="gdi"', usr_reg): + wine.winetricks_install("-q", "settings", "renderer=gdi") + + if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 + wine.install_fonts() + + if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): + wine.install_d3d_compiler() + + if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): + args = ["settings", "win10"] + if not config.WINETRICKS_UNATTENDED: + args.insert(0, "-q") + wine.winetricks_install(*args) + + msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Windows 10 Mode.") + exe_args = [ + 'add', + f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 + "/v", "Version", + "/t", "REG_SZ", + "/d", "win10", "/f", + ] + wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + logging.debug("> Done.") + + def ensure_product_installed(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_icu_data_files(app=app) config.INSTALL_STEP += 1 - update_install_feedback("Ensuring product is installed…", app=app) + update_install_feedback(f"Ensuring {config.FLPRODUCT} is installed…", app=app) if not utils.find_installed_product(): wine.install_msi() diff --git a/logos.py b/logos.py new file mode 100644 index 00000000..52e20686 --- /dev/null +++ b/logos.py @@ -0,0 +1,235 @@ +import time +from enum import Enum +import logging +import psutil +import threading + +import config +import main +import msg +import system +import utils +import wine + + +class State(Enum): + RUNNING = 1 + STOPPED = 2 + STARTING = 3 + STOPPING = 4 + + +class LogosManager: + def __init__(self, app=None): + self.logos_state = State.STOPPED + self.indexing_state = State.STOPPED + self.app = app + + def monitor_indexing(self): + if config.logos_indexer_exe in config.processes: + indexer = config.processes[config.logos_indexer_exe] + if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): + self.indexing_state = State.RUNNING + else: + self.indexing_state = State.STOPPED + + def monitor_logos(self): + splash = config.processes.get(config.LOGOS_EXE, []) + login_window = config.processes.get(config.login_window_cmd, []) + logos_cef = config.processes.get(config.logos_cef_cmd, []) + + splash_running = splash[0].is_running() if splash else False + login_running = login_window[0].is_running() if login_window else False + logos_cef_running = logos_cef[0].is_running() if logos_cef else False + + if self.logos_state == State.RUNNING: + if not (splash_running or login_running or logos_cef_running): + self.stop() + elif self.logos_state == State.STOPPED: + if splash and isinstance(splash[0], psutil.Process) and splash_running: + self.logos_state = State.STARTING + if (login_window and isinstance(login_window[0], psutil.Process) and login_running) or ( + logos_cef and isinstance(logos_cef[0], psutil.Process) and logos_cef_running): + self.logos_state = State.RUNNING + + def monitor(self): + if utils.file_exists(config.LOGOS_EXE): + if config.wine_user is None: + wine.get_wine_user() + if config.logos_indexer_exe is None or config.login_window_cmd is None or config.logos_cef_cmd is None: + wine.set_logos_paths() + system.get_logos_pids() + try: + self.monitor_indexing() + self.monitor_logos() + except Exception as e: + logging.debug(f"DEV: {e}") + logging.debug(f"DEV: {self.logos_state}") + + def start(self): + self.logos_state = State.STARTING + logos_release = utils.convert_logos_release(config.current_logos_version) + wine_release, _ = wine.get_wine_release(config.WINE_EXE) + + def run_logos(): + wine.run_wine_proc(config.WINE_EXE, exe=config.LOGOS_EXE) + + # TODO: Find a way to incorporate check_wine_version_and_branch() + if 30 > logos_release[0] > 9 and ( + wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): # noqa: E501 + txt = f"Can't run {config.FLPRODUCT} 10+ with Wine below 7.18." + logging.critical(txt) + msg.status(txt, self.app) + if logos_release[0] > 29 and wine_release[0] < 9 and wine_release[1] < 10: + txt = f"Can't run {config.FLPRODUCT} 30+ with Wine below 9.10." + logging.critical(txt) + msg.status(txt, self.app) + else: + wine.wineserver_kill() + msg.status(f"Running {config.FLPRODUCT}…", self.app) + thread = threading.Thread(target=run_logos) + thread.start() + self.logos_state = State.RUNNING + + def stop(self): + logging.debug(f"DEV: Stopping") + self.logos_state = State.STOPPING + if self.app: + pids = [] + for process_name in [config.LOGOS_EXE, config.login_window_cmd, config.logos_cef_cmd]: + process_list = config.processes.get(process_name) + if process_list: + pids.extend([str(process.pid) for process in process_list]) + else: + logging.debug(f"No Logos processes found for {process_name}.") + + if pids: + try: + system.run_command(['kill', '-9'] + pids) + self.logos_state = State.STOPPED + msg.status(f"Stopped Logos processes at PIDs {', '.join(pids)}.", self.app) + except Exception as e: + logging.debug("Error while stopping Logos processes: {e}.") + else: + logging.debug("No Logos processes to stop.") + self.logos_state = State.STOPPED + wine.wineserver_wait() + + def index(self): + self.indexing_state = State.STARTING + index_finished = threading.Event() + + def run_indexing(): + wine.run_wine_proc(config.WINE_EXE, exe=config.logos_indexer_exe) + + def check_if_indexing(process): + start_time = time.time() + last_time = start_time + update_send = 0 + while process.poll() is None: + update, last_time = utils.stopwatch(last_time, 3) + if update: + update_send = update_send + 1 + if update_send == 10: + total_elapsed_time = time.time() - start_time + elapsed_min = int(total_elapsed_time // 60) + elapsed_sec = int(total_elapsed_time % 60) + formatted_time = f"{elapsed_min}m {elapsed_sec}s" + msg.status(f"Indexing is running… (Elapsed Time: {formatted_time})", self.app) + update_send = 0 + index_finished.set() + + def wait_on_indexing(): + index_finished.wait() + self.indexing_state = State.STOPPED + msg.status(f"Indexing has finished.", self.app) + wine.wineserver_wait() + + wine.wineserver_kill() + msg.status(f"Indexing has begun…", self.app) + index_thread = threading.Thread(target=run_indexing) + index_thread.start() + self.indexing_state = State.RUNNING + time.sleep(1) # If we don't wait, the thread starts too quickly + # and the process won't yet be launched when we try to pull it from config.processes + process = config.processes[config.logos_indexer_exe] + check_thread = threading.Thread(target=check_if_indexing, args=(process,)) + wait_thread = threading.Thread(target=wait_on_indexing) + check_thread.start() + wait_thread.start() + main.threads.extend([index_thread, check_thread, wait_thread]) + config.processes[config.logos_indexer_exe] = index_thread + config.processes[config.check_if_indexing] = check_thread + config.processes[wait_on_indexing] = wait_thread + + def stop_indexing(self): + self.indexing_state = State.STOPPING + if self.app: + pids = [] + for process_name in [config.logos_indexer_exe]: + process_list = config.processes.get(process_name) + if process_list: + pids.extend([str(process.pid) for process in process_list]) + else: + logging.debug(f"No LogosIndexer processes found for {process_name}.") + + if pids: + try: + system.run_command(['kill', '-9'] + pids) + self.indexing_state = State.STOPPED + msg.status(f"Stopped LogosIndexer processes at PIDs {', '.join(pids)}.", self.app) + except Exception as e: + logging.debug("Error while stopping LogosIndexer processes: {e}.") + else: + logging.debug("No LogosIndexer processes to stop.") + self.indexing_state = State.STOPPED + wine.wineserver_wait() + + def get_app_logging_state(self, init=False): + state = 'DISABLED' + current_value = wine.get_registry_value( + 'HKCU\\Software\\Logos4\\Logging', + 'Enabled' + ) + if current_value == '0x1': + state = 'ENABLED' + if self.app is not None: + self.app.logging_q.put(state) + if init: + self.app.root.event_generate('<>') + else: + self.app.root.event_generate('<>') + return state + + def switch_logging(self, action=None): + state_disabled = 'DISABLED' + value_disabled = '0000' + state_enabled = 'ENABLED' + value_enabled = '0001' + if action == 'disable': + value = value_disabled + state = state_disabled + elif action == 'enable': + value = value_enabled + state = state_enabled + else: + current_state = self.get_app_logging_state() + logging.debug(f"app logging {current_state=}") + if current_state == state_enabled: + value = value_disabled + state = state_disabled + else: + value = value_enabled + state = state_enabled + + logging.info(f"Setting app logging to '{state}'.") + exe_args = [ + 'add', 'HKCU\\Software\\Logos4\\Logging', '/v', 'Enabled', + '/t', 'REG_DWORD', '/d', value, '/f' + ] + wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + wine.wineserver_wait() + config.LOGS = state + if self.app is not None: + self.app.logging_q.put(state) + self.app.root.event_generate(self.app.logging_event) diff --git a/main.py b/main.py index fa4ac33b..22bdb351 100755 --- a/main.py +++ b/main.py @@ -3,6 +3,9 @@ import config import control import curses + +import logos + try: import dialog # noqa: F401 except ImportError: @@ -20,8 +23,7 @@ import utils import wine -processes = {} -threads = [] +from config import processes, threads def get_parser(): @@ -238,8 +240,8 @@ def parse_args(args, parser): # Set ACTION function. actions = { 'install_app': installer.ensure_launcher_shortcuts, - 'run_installed_app': wine.run_logos, - 'run_indexing': wine.run_indexing, + 'run_installed_app': logos.LogosManager().start, + 'run_indexing': logos.LogosManager().index, 'remove_library_catalog': control.remove_library_catalog, 'remove_index_files': control.remove_all_index_files, 'edit_config': control.edit_config, @@ -251,10 +253,10 @@ def parse_args(args, parser): 'set_appimage': utils.set_appimage_symlink, 'get_winetricks': control.set_winetricks, 'run_winetricks': wine.run_winetricks, - 'install_d3d_compiler': wine.installD3DCompiler, - 'install_fonts': wine.installFonts, - 'install_icu': wine.installICUDataFiles, - 'toggle_app_logging': wine.switch_logging, + 'install_d3d_compiler': wine.install_d3d_compiler, + 'install_fonts': wine.install_fonts, + 'install_icu': wine.install_icu_data_files, + 'toggle_app_logging': logos.LogosManager().switch_logging, 'create_shortcuts': installer.ensure_launcher_shortcuts, 'remove_install_dir': control.remove_install_dir, } diff --git a/system.py b/system.py index dbb56da0..9f8791c0 100644 --- a/system.py +++ b/system.py @@ -8,13 +8,15 @@ import zipfile from pathlib import Path +import psutil + import config import msg import network # TODO: Replace functions in control.py and wine.py with Popen command. -def run_command(command, retries=1, delay=0, wait=True, **kwargs): +def run_command(command, retries=1, delay=0, **kwargs): check = kwargs.get("check", True) text = kwargs.get("text", True) capture_output = kwargs.get("capture_output", True) @@ -85,10 +87,7 @@ def run_command(command, retries=1, delay=0, wait=True, **kwargs): pipesize=pipesize, process_group=process_group ) - if wait: - return result - else: - return None + return result except subprocess.CalledProcessError as e: logging.error(f"Error occurred in run_command() while executing \"{command}\": {e}") # noqa: E501 if "lock" in str(e): @@ -104,7 +103,7 @@ def run_command(command, retries=1, delay=0, wait=True, **kwargs): return None -def popen_command(command, retries=1, delay=0, wait=True, **kwargs): +def popen_command(command, retries=1, delay=0, **kwargs): shell = kwargs.get("shell", False) env = kwargs.get("env", None) cwd = kwargs.get("cwd", None) @@ -167,14 +166,7 @@ def popen_command(command, retries=1, delay=0, wait=True, **kwargs): errors=errors, text=text ) - - if wait: - stdout, stderr = process.communicate(timeout=kwargs.get("timeout", None)) - if process.returncode != 0: - raise subprocess.CalledProcessError(process.returncode, command, output=stdout, stderr=stderr) - return stdout, stderr - else: - return process + return process except subprocess.CalledProcessError as e: logging.error(f"Error occurred in popen_command() while executing \"{command}\": {e}") @@ -191,6 +183,69 @@ def popen_command(command, retries=1, delay=0, wait=True, **kwargs): return None +def wait_on(command): + try: + # Start the process in the background + # TODO: Convert to use popen_command() + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + msg.logos_msg(f"Waiting on \"{' '.join(command)}\" to finish.", end='') + time.sleep(1.0) + while process.poll() is None: + msg.logos_progress() + time.sleep(0.5) + print() + + # Process has finished, check the result + stdout, stderr = process.communicate() + + if process.returncode == 0: + logging.info(f"\"{' '.join(command)}\" has ended properly.") + else: + logging.error(f"Error: {stderr}") + + except Exception as e: + logging.critical(f"{e}") + + +def get_pids(query): + results = [] + for process in psutil.process_iter(['pid', 'name', 'cmdline']): + try: + if process.info['cmdline'] is not None and query in process.info['cmdline']: + results.append(process) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + return results + + +def get_logos_pids(): + config.processes[config.LOGOS_EXE] = get_pids(config.LOGOS_EXE) + config.processes[config.login_window_cmd] = get_pids(config.login_window_cmd) + config.processes[config.logos_cef_cmd] = get_pids(config.logos_cef_cmd) + config.processes[config.logos_indexer_exe] = get_pids(config.logos_indexer_exe) + + +def get_pids_using_file(file_path, mode=None): + # Make list (set) of pids using 'directory'. + pids = set() + for proc in psutil.process_iter(['pid', 'open_files']): + try: + if mode is not None: + paths = [f.path for f in proc.open_files() if f.mode == mode] + else: + paths = [f.path for f in proc.open_files()] + if len(paths) > 0 and file_path in paths: + pids.add(proc.pid) + except psutil.AccessDenied: + pass + return pids + + def reboot(): logging.info("Rebooting system.") command = f"{config.SUPERUSER_COMMAND} reboot now" diff --git a/tui_app.py b/tui_app.py index 877dd028..7c28544d 100644 --- a/tui_app.py +++ b/tui_app.py @@ -2,6 +2,7 @@ import os import signal import threading +import time import curses import time from pathlib import Path @@ -10,6 +11,7 @@ import config import control import installer +import logos import msg import network import system @@ -20,9 +22,8 @@ console_message = "" -# TODO: Fix hitting cancel in Dialog Screens; currently crashes program. - +# TODO: Fix hitting cancel in Dialog Screens; currently crashes program. class TUI: def __init__(self, stdscr): self.stdscr = stdscr @@ -34,6 +35,7 @@ def __init__(self, stdscr): self.console_message = "Starting TUI…" self.llirunning = True self.active_progress = False + self.logos = logos.LogosManager(app=self) # Queues self.main_thread = threading.Thread() @@ -230,7 +232,7 @@ def update_main_window_contents(self): self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) - self.menu_screen.set_options(self.set_main_menu_options(dialog=False)) + self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) self.refresh() @@ -279,6 +281,8 @@ def display(self): msg.initialize_curses_logging() msg.status(self.console_message, self) self.active_screen = self.menu_screen + last_time = time.time() + self.logos.monitor() while self.llirunning: if self.window_height >= 10 and self.window_width >= 35: @@ -307,6 +311,11 @@ def display(self): else: self.active_screen = self.tui_screens[-1] + run_monitor, last_time = utils.stopwatch(last_time, 2.5) + if run_monitor: + self.logos.monitor() + self.task_processor(self, task="PID") + if isinstance(self.active_screen, tui_screen.CursesScreen): self.refresh() elif self.window_width >= 10: @@ -348,6 +357,8 @@ def task_processor(self, evt=None, task=None): utils.start_thread(self.get_config, config.use_python_dialog) elif task == 'DONE': self.update_main_window_contents() + elif task == 'PID': + self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) def choice_processor(self, stdscr, screen_id, choice): screen_actions = { @@ -415,10 +426,22 @@ def main_menu_select(self, choice): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": self.reset_screen() - self.screen_q.put(self.stack_text(12, self.todo_q, self.todo_e, "Logos is running…", dialog=config.use_python_dialog)) - self.choice_q.put("0") + self.logos.start() + self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + self.switch_q.put(1) + elif choice == f"Stop {config.FLPRODUCT}": + self.reset_screen() + self.logos.stop() + self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + self.switch_q.put(1) elif choice == "Run Indexing": - wine.run_indexing() + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + self.logos.index() + elif choice == "Remove Library Catalog": + self.active_screen.running = 0 + self.active_screen.choice = "Processing" + control.remove_library_catalog() elif choice.startswith("Winetricks"): self.reset_screen() self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) @@ -615,12 +638,6 @@ def config_update_select(self, choice): def waiting_releases(self, choice): pass - def run_logos(self, choice): - if choice: - self.menu_screen.choice = "Processing" - wine.run_logos(self) - self.switch_q.put(1) - def waiting_finish(self, choice): pass @@ -842,9 +859,23 @@ def set_main_menu_options(self, dialog=False): logging.error(f"{error_message}") if utils.file_exists(config.LOGOS_EXE): + if self.logos.logos_state == logos.State.RUNNING: + run = f"Stop {config.FLPRODUCT}" + elif self.logos.logos_state == logos.State.STOPPED: + run = f"Run {config.FLPRODUCT}" + + if self.logos.indexing_state == logos.State.RUNNING: + indexing = f"Stop Indexing" + elif self.logos.indexing_state == logos.State.STOPPED: + indexing = f"Run Indexing" labels_default = [ - f"Run {config.FLPRODUCT}", - "Run Indexing" + run, + indexing, + "Remove Library Catalog", + "Remove All Index Files", + "Edit Config", + "Back up Data", + "Restore Data", ] else: labels_default = ["Install Logos Bible Software"] diff --git a/utils.py b/utils.py index 0c0f7521..8208569c 100644 --- a/utils.py +++ b/utils.py @@ -13,6 +13,7 @@ import sys import tarfile import threading +import time import tkinter as tk from packaging import version from pathlib import Path @@ -28,8 +29,6 @@ # TODO: Move config commands to config.py -from main import threads - def get_calling_function_name(): if 'inspect' in sys.modules: @@ -901,8 +900,7 @@ def start_thread(task, *args, daemon_bool=True, **kwargs): args=args, kwargs=kwargs ) - logging.debug(f"Starting thread: {task=}; {daemon_bool=} {args=}; {kwargs=}") # noqa: E501 - threads.append(thread) + config.threads.append(thread) thread.start() return thread @@ -985,3 +983,17 @@ def get_wine_exe_path(path=None): return wine_exe_path else: return None + + +def stopwatch(start_time=None, interval=10.0): + if start_time is None: + start_time = time.time() + + current_time = time.time() + elapsed_time = current_time - start_time + + if elapsed_time >= interval: + last_log_time = current_time + return True, last_log_time + else: + return False, start_time diff --git a/wine.py b/wine.py index 235563d8..082cbf53 100644 --- a/wine.py +++ b/wine.py @@ -1,11 +1,9 @@ import logging import os -import psutil import re import shutil import signal import subprocess -import time from pathlib import Path import config @@ -17,60 +15,62 @@ from main import processes -def get_pids_using_file(file_path, mode=None): - # Make list (set) of pids using 'directory'. - pids = set() - for proc in psutil.process_iter(['pid', 'open_files']): - try: - if mode is not None: - paths = [f.path for f in proc.open_files() if f.mode == mode] - else: - paths = [f.path for f in proc.open_files()] - if len(paths) > 0 and file_path in paths: - pids.add(proc.pid) - except psutil.AccessDenied: - pass - return pids +def set_logos_paths(): + config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' + config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' + config.logos_indexing_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' + for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 + for f in files: + if f == "LogosIndexer.exe" and root.endswith("Logos/System"): + config.logos_indexer_exe = os.path.join(root, f) + break -def wait_on(command): - env = get_wine_env() - try: - # Start the process in the background - process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - text=True - ) - msg.logos_msg(f"Waiting on \"{' '.join(command)}\" to finish.", end='') - time.sleep(1.0) - while process.poll() is None: - msg.logos_progress() - time.sleep(0.5) - print() - - # Process has finished, check the result - stdout, stderr = process.communicate() - - if process.returncode == 0: - logging.info(f"\"{' '.join(command)}\" has ended properly.") - else: - logging.error(f"Error: {stderr}") +def get_wine_user(): + path = config.LOGOS_EXE + normalized_path = os.path.normpath(path) + path_parts = normalized_path.split(os.sep) + config.wine_user = path_parts[path_parts.index('users') + 1] + +def check_wineserver(): + try: + process = run_wine_proc(config.WINESERVER, exe_args=["-p"]) + return process.returncode == 0 except Exception as e: - logging.critical(f"{e}") + return False + + +def wineserver_kill(): + if check_wineserver(): + run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) + + +def wineserver_wait(): + if check_wineserver(): + run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) def light_wineserver_wait(): command = [f"{config.WINESERVER_EXE}", "-w"] - wait_on(command) + system.wait_on(command) def heavy_wineserver_wait(): utils.wait_process_using_dir(config.WINEPREFIX) - wait_on([f"{config.WINESERVER_EXE}", "-w"]) + system.wait_on([f"{config.WINESERVER_EXE}", "-w"]) + + +def end_wine_processes(): + for process_name, process in processes.items(): + if isinstance(process, subprocess.Popen): + logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") # noqa: E501 + try: + process.terminate() + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() def get_wine_release(binary): @@ -242,32 +242,35 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): logging.debug(f"subprocess cmd: '{' '.join(command)}'") try: - process = subprocess.Popen( + process = system.popen_command( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env ) - if exe is not None and isinstance(process, subprocess.Popen): - processes[exe] = process - with process.stdout: - for line in iter(process.stdout.readline, b''): - if winecmd.endswith('winetricks'): - logging.debug(line.decode('cp437').rstrip()) - else: - try: - logging.info(line.decode().rstrip()) - except UnicodeDecodeError: - if config.WINECMD_ENCODING is not None: - logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 + if process is not None: + if exe is not None and isinstance(process, subprocess.Popen): + config.processes[exe] = process + if process.poll() is None: + with process.stdout: + for line in iter(process.stdout.readline, b''): + if winecmd.endswith('winetricks'): + logging.debug(line.decode('cp437').rstrip()) else: - logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 - - returncode = process.wait() - - if returncode != 0: - logging.error(f"Error running '{' '.join(command)}': {process.returncode}") # noqa: E501 - msg.logos_error(f"\"{command}\" failed with exit code: {process.returncode}") # noqa: E501 + try: + logging.info(line.decode().rstrip()) + except UnicodeDecodeError: + if config.WINECMD_ENCODING is not None: + logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 + else: + logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 + # returncode = process.wait() + # + # if returncode != 0: + # logging.error(f"Error running '{' '.join(command)}': {process.returncode}") # noqa: E501 + return process + else: + return None except subprocess.CalledProcessError as e: logging.error(f"Exception running '{' '.join(command)}': {e}") @@ -277,7 +280,7 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): def run_winetricks(cmd=None): run_wine_proc(config.WINETRICKSBIN, exe=cmd) - run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) + wineserver_wait() def run_winetricks_cmd(*args): @@ -289,14 +292,14 @@ def run_winetricks_cmd(*args): heavy_wineserver_wait() -def installD3DCompiler(): +def install_d3d_compiler(): cmd = ['d3dcompiler_47'] if config.WINETRICKS_UNATTENDED is None: cmd.insert(0, '-q') run_winetricks_cmd(*cmd) -def installFonts(): +def install_fonts(): msg.logos_msg("Configuring fonts…") fonts = ['corefonts', 'tahoma'] if not config.SKIP_FONTS: @@ -328,7 +331,7 @@ def set_win_version(exe, windows_version): run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) -def installICUDataFiles(app=None): +def install_icu_data_files(app=None): releases_url = "https://api.github.com/repos/FaithLife-Community/icu/releases" # noqa: E501 json_data = network.get_latest_release_data(releases_url) icu_url = network.get_latest_release_url(json_data) @@ -393,60 +396,6 @@ def get_registry_value(reg_path, name): return value -def get_app_logging_state(app=None): - state = 'DISABLED' - current_value = get_registry_value( - 'HKCU\\Software\\Logos4\\Logging', - 'Enabled' - ) - if current_value == '0x1': - state = 'ENABLED' - if app is not None: - app.logging_q.put(state) - logging.debug(f"Current app logging state: {state} ({current_value})") - app.root.event_generate(app.logging_event) - return state - - -def switch_logging(action=None, app=None): - logging.debug(f"switch_logging; {action=}; {app=}") - state_disabled = 'DISABLED' - value_disabled = '0000' - state_enabled = 'ENABLED' - value_enabled = '0001' - if action == 'disable': - value = value_disabled - state = state_disabled - elif action == 'enable': - value = value_enabled - state = state_enabled - else: - current_state = get_app_logging_state() - logging.debug(f"app logging {current_state=}") - if current_state == state_enabled: - value = value_disabled - state = state_disabled - else: - value = value_enabled - state = state_enabled - - logging.info(f"Setting app logging to '{state}'.") - exe_args = [ - 'add', 'HKCU\\Software\\Logos4\\Logging', '/v', 'Enabled', - '/t', 'REG_DWORD', '/d', value, '/f' - ] - run_wine_proc( - str(utils.get_wine_exe_path().parent / 'wine64'), - exe='reg', - exe_args=exe_args - ) - light_wineserver_wait() - config.LOGS = state - if app is not None: - app.logging_q.put(state) - app.root.event_generate(app.logging_event) - - def get_mscoree_winebranch(mscoree_file): try: with mscoree_file.open('rb') as f: @@ -531,49 +480,3 @@ def get_wine_env(): updated_env = {k: wine_env.get(k) for k in wine_env_defaults.keys()} logging.debug(f"Wine env: {updated_env}") return wine_env - - -def run_logos(app=None): - logos_release = utils.convert_logos_release(config.current_logos_version) - wine_release, _ = get_wine_release(str(utils.get_wine_exe_path())) - - # TODO: Find a way to incorporate check_wine_version_and_branch() - if 30 > logos_release[0] > 9 and (wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): # noqa: E501 - txt = "Can't run Logos 10+ with Wine below 7.18." - logging.critical(txt) - msg.status(txt, app) - if logos_release[0] > 29 and wine_release[0] < 9 and wine_release[1] < 10: - txt = "Can't run Logos 30+ with Wine below 9.10." - logging.critical(txt) - msg.status(txt, app) - else: - run_wine_proc(str(utils.get_wine_exe_path()), exe=config.LOGOS_EXE) - run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) - - -def run_indexing(): - logos_indexer_exe = None - for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 - for f in files: - if f == "LogosIndexer.exe" and root.endswith("Logos/System"): - logos_indexer_exe = os.path.join(root, f) - break - - if logos_indexer_exe is not None: - run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) - run_wine_proc(str(utils.get_wine_exe_path()), exe=logos_indexer_exe) - run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) - else: - logging.error("LogosIndexer.exe not found.") - - -def end_wine_processes(): - for process_name, process in processes.items(): - if isinstance(process, subprocess.Popen): - logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") # noqa: E501 - try: - process.terminate() - process.wait(timeout=10) - except subprocess.TimeoutExpired: - process.kill() - process.wait() From 6f084de324d7ea610a4563ecdb38354e81271c69 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 21 Sep 2024 23:16:08 -0400 Subject: [PATCH 163/253] Fix #153 --- config.py | 1 + wine.py | 59 ++++++++++++++++++++++++++++--------------------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/config.py b/config.py index c66f6e01..ca864db9 100644 --- a/config.py +++ b/config.py @@ -27,6 +27,7 @@ 'DELETE_LOG': None, 'DIALOG': None, 'LOGOS_LOG': os.path.expanduser("~/.local/state/Logos_on_Linux/Logos_on_Linux.log"), # noqa: E501 + 'wine_log': os.path.expanduser("~/.local/state/Logos_on_Linux/wine.log"), # noqa: #E501 'LOGOS_EXE': None, 'LOGOS_EXECUTABLE': None, 'LOGOS_VERSION': None, diff --git a/wine.py b/wine.py index 082cbf53..324d863a 100644 --- a/wine.py +++ b/wine.py @@ -242,35 +242,36 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): logging.debug(f"subprocess cmd: '{' '.join(command)}'") try: - process = system.popen_command( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=env - ) - if process is not None: - if exe is not None and isinstance(process, subprocess.Popen): - config.processes[exe] = process - if process.poll() is None: - with process.stdout: - for line in iter(process.stdout.readline, b''): - if winecmd.endswith('winetricks'): - logging.debug(line.decode('cp437').rstrip()) - else: - try: - logging.info(line.decode().rstrip()) - except UnicodeDecodeError: - if config.WINECMD_ENCODING is not None: - logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 - else: - logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 - # returncode = process.wait() - # - # if returncode != 0: - # logging.error(f"Error running '{' '.join(command)}': {process.returncode}") # noqa: E501 - return process - else: - return None + with open(config.wine_log, 'a') as wine_log: + process = system.popen_command( + command, + stdout=wine_log, + stderr=wine_log, + env=env + ) + if process is not None: + if exe is not None and isinstance(process, subprocess.Popen): + config.processes[exe] = process + if process.poll() is None and process.stdout is not None: + with process.stdout: + for line in iter(process.stdout.readline, b''): + if winecmd.endswith('winetricks'): + logging.debug(line.decode('cp437').rstrip()) + else: + try: + logging.info(line.decode().rstrip()) + except UnicodeDecodeError: + if config.WINECMD_ENCODING is not None: + logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 + else: + logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 + # returncode = process.wait() + # + # if returncode != 0: + # logging.error(f"Error running '{' '.join(command)}': {process.returncode}") # noqa: E501 + return process + else: + return None except subprocess.CalledProcessError as e: logging.error(f"Exception running '{' '.join(command)}': {e}") From e2f6ff9b5d7f9108f15852ac30e201dcc2fec666 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sun, 22 Sep 2024 02:11:16 -0400 Subject: [PATCH 164/253] Fix numerous issues while fixing #142 - Fix #142. Switch to using tui_dialog.update_progress bar(). Progress bars are now stored as a screen class var. - Convert most uses of msg.logos_msg to msg.status, and give msg.status an end="\n" flag. - Comment out winemenubuilder, as it is not working - Remove duplicate of ensure_winetricks_applied - Add new TUI/CLI event set/wait cakks - Greatly reduce the time required to complete a system.have_lib() check - Make tui_app.display() only run logos.monitor when not displaying a python-dialog --- control.py | 18 ++++++------- installer.py | 73 ++++++++------------------------------------------- logos.py | 4 +-- msg.py | 4 +-- network.py | 17 +++++++----- system.py | 19 +++++++++----- tui_app.py | 19 +++++++++----- tui_dialog.py | 40 ++++++++++++++-------------- tui_screen.py | 11 ++++---- utils.py | 2 +- wine.py | 19 +++++++++----- 11 files changed, 97 insertions(+), 129 deletions(-) diff --git a/control.py b/control.py index 252a2d7e..42443d20 100644 --- a/control.py +++ b/control.py @@ -73,7 +73,7 @@ def backup_and_restore(mode='backup', app=None): answer = msg.cli_ask_filepath("Give backups folder path:") answer = Path(answer).expanduser().resolve() if not answer.is_dir(): - msg.logos_msg(f"Not a valid folder path: {answer}") + msg.status(f"Not a valid folder path: {answer}", app) config.BACKUPDIR = answer # Set source folders. @@ -112,7 +112,7 @@ def backup_and_restore(mode='backup', app=None): app.status_q.put(m) app.root.event_generate('<>') app.root.event_generate('<>') - msg.logos_msg(m, end='') + msg.status(m, end='') t.start() try: while t.is_alive(): @@ -187,7 +187,7 @@ def backup_and_restore(mode='backup', app=None): else: m = f"Backing up to {str(dst_dir)}" logging.info(m) - msg.logos_msg(m) + msg.status(m) if app is not None: app.status_q.put(m) app.root.event_generate('<>') @@ -251,7 +251,7 @@ def remove_all_index_files(app=None): except OSError as e: logging.error(f"Error removing {file_to_remove}: {e}") - msg.logos_msg("======= Removing all LogosBible index files done! =======") + msg.status("======= Removing all LogosBible index files done! =======") if hasattr(app, 'status_evt'): app.root.event_generate(app.status_evt) sys.exit(0) @@ -269,7 +269,7 @@ def remove_library_catalog(): def set_winetricks(): - msg.logos_msg("Preparing winetricks…") + msg.status("Preparing winetricks…") if not config.APPDIR_BINDIR: config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" # Check if local winetricks version available; else, download it @@ -307,10 +307,10 @@ def set_winetricks(): ) return 0 else: - msg.logos_msg("Installation canceled!") + msg.status("Installation canceled!") sys.exit(0) else: - msg.logos_msg("The system's winetricks is too old. Downloading an up-to-date winetricks from the Internet...") # noqa: E501 + msg.status("The system's winetricks is too old. Downloading an up-to-date winetricks from the Internet…") # noqa: E501 # download_winetricks() system.install_winetricks(config.APPDIR_BINDIR) config.WINETRICKSBIN = os.path.join( @@ -319,7 +319,7 @@ def set_winetricks(): ) return 0 else: - msg.logos_msg("Local winetricks not found. Downloading winetricks from the Internet…") # noqa: E501 + msg.status("Local winetricks not found. Downloading winetricks from the Internet…") # noqa: E501 # download_winetricks() system.install_winetricks(config.APPDIR_BINDIR) config.WINETRICKSBIN = os.path.join( @@ -331,7 +331,7 @@ def set_winetricks(): def download_winetricks(): - msg.logos_msg("Downloading winetricks…") + msg.status("Downloading winetricks…") appdir_bindir = f"{config.INSTALLDIR}/data/bin" network.logos_reuse_download( config.WINETRICKS_URL, diff --git a/installer.py b/installer.py index d92d90bc..7cc4b80a 100644 --- a/installer.py +++ b/installer.py @@ -347,7 +347,7 @@ def ensure_wine_executables(app=None): # Ensure appimage is copied to appdir_bindir. downloaded_file = utils.get_downloaded_file_path(appimage_filename) # noqa: E501 if not appimage_file.is_file(): - msg.logos_msg(f"Copying: {downloaded_file} into: {str(appdir_bindir)}") # noqa: E501 + msg.status(f"Copying: {downloaded_file} into: {str(appdir_bindir)}") # noqa: E501 shutil.copy(downloaded_file, str(appdir_bindir)) os.chmod(appimage_file, 0o755) appimage_filename = appimage_file.name @@ -391,7 +391,7 @@ def ensure_winetricks_executable(app=None): if not os.access(config.WINETRICKSBIN, os.X_OK): # Either previous system winetricks is no longer accessible, or the # or the user has chosen to download it. - msg.logos_msg("Downloading winetricks from the Internet…") + msg.status("Downloading winetricks from the Internet…") system.install_winetricks(config.APPDIR_BINDIR, app=app) logging.debug(f"> {config.WINETRICKSBIN} is executable?: {os.access(config.WINETRICKSBIN, os.X_OK)}") # noqa: E501 @@ -500,7 +500,6 @@ def ensure_winetricks_applied(app=None): usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - #FIXME: This command is failing. reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') with open(reg_file, 'w') as f: f.write(r'''REGEDIT4 @@ -522,8 +521,8 @@ def ensure_winetricks_applied(app=None): if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): wine.set_win_version("logos", "win10") - msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Vista Mode.") - wine.set_win_version("indexer", "vista") + msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Win10 Mode.") + wine.set_win_version("indexer", "win10") logging.debug("> Done.") @@ -538,65 +537,12 @@ def ensure_icu_data_files(app=None): icu_license_path = f"{config.WINEPREFIX}/drive_c/windows/globalization/ICU/LICENSE-ICU.txt" # noqa: E501 if not utils.file_exists(icu_license_path): wine.install_icu_data_files(app=app) - logging.debug('> ICU data files installed') - - -def ensure_winetricks_applied(app=None): - config.INSTALL_STEPS_COUNT += 1 - ensure_icu_data_files(app=app) - config.INSTALL_STEP += 1 - status = "Ensuring winetricks & other settings are applied…" - update_install_feedback(status, app=app) - logging.debug('- disable winemenubuilder') - logging.debug('- settings renderer=gdi') - logging.debug('- corefonts') - logging.debug('- tahoma') - logging.debug('- settings fontsmooth=rgb') - logging.debug('- d3dcompiler_47') - if not config.SKIP_WINETRICKS: - usr_reg = None - sys_reg = None - workdir = Path(f"{config.WORKDIR}") - workdir.mkdir(parents=True, exist_ok=True) - usr_reg = Path(f"{config.WINEPREFIX}/user.reg") - sys_reg = Path(f"{config.WINEPREFIX}/system.reg") - if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - #FIXME: This command is failing. - reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') - with open(reg_file, 'w') as f: - f.write(r'''REGEDIT4 - -[HKEY_CURRENT_USER\Software\Wine\DllOverrides] -"winemenubuilder.exe"="" -''') - wine.wine_reg_install(reg_file) - - if not utils.grep(r'"renderer"="gdi"', usr_reg): - wine.winetricks_install("-q", "settings", "renderer=gdi") - - if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 - wine.install_fonts() - - if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): - wine.install_d3d_compiler() + if app: + if config.DIALOG == "curses": + app.install_icu_e.wait() - if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): - args = ["settings", "win10"] - if not config.WINETRICKS_UNATTENDED: - args.insert(0, "-q") - wine.winetricks_install(*args) - - msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Windows 10 Mode.") - exe_args = [ - 'add', - f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe", # noqa: E501 - "/v", "Version", - "/t", "REG_SZ", - "/d", "win10", "/f", - ] - wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) - logging.debug("> Done.") + logging.debug('> ICU data files installed') def ensure_product_installed(app=None): @@ -607,6 +553,9 @@ def ensure_product_installed(app=None): if not utils.find_installed_product(): wine.install_msi() + if app: + if config.DIALOG == "curses": + app.install_logos_e.wait() config.LOGOS_EXE = utils.find_installed_product() config.current_logos_version = config.TARGET_RELEASE_VERSION diff --git a/logos.py b/logos.py index 52e20686..1835ba44 100644 --- a/logos.py +++ b/logos.py @@ -63,8 +63,7 @@ def monitor(self): self.monitor_indexing() self.monitor_logos() except Exception as e: - logging.debug(f"DEV: {e}") - logging.debug(f"DEV: {self.logos_state}") + pass def start(self): self.logos_state = State.STARTING @@ -92,7 +91,6 @@ def run_logos(): self.logos_state = State.RUNNING def stop(self): - logging.debug(f"DEV: Stopping") self.logos_state = State.STOPPING if self.app: pids = [] diff --git a/msg.py b/msg.py index 1a9ac2ee..819594da 100644 --- a/msg.py +++ b/msg.py @@ -293,7 +293,7 @@ def progress(percent, app=None): logos_msg(get_progress_str(percent)) # provisional -def status(text, app=None): +def status(text, app=None, end='\n'): def strip_timestamp(msg, timestamp_length=20): return msg[timestamp_length:] @@ -319,4 +319,4 @@ def strip_timestamp(msg, timestamp_length=20): logging.info(f"{text}") else: # Prints message to stdout regardless of log level. - logos_msg(text) + logos_msg(text, end=end) diff --git a/network.py b/network.py index 985c7601..26aeb39e 100644 --- a/network.py +++ b/network.py @@ -45,6 +45,7 @@ def get_size(self): def get_md5(self): if self.path is None: return + logging.debug("This may take a while…") md5 = hashlib.md5() with self.path.open('rb') as f: for chunk in iter(lambda: f.read(4096), b''): @@ -118,10 +119,9 @@ def get_md5(self): return self.md5 -def cli_download(uri, destination): +def cli_download(uri, destination, app=None): message = f"Downloading '{uri}' to '{destination}'" - logging.info(message) - msg.logos_msg(message) + msg.status(message) # Set target. if destination != destination.rstrip('/'): @@ -177,7 +177,7 @@ def logos_reuse_download( app=app, ): logging.info(f"{file} properties match. Using it…") - msg.logos_msg(f"Copying {file} into {targetdir}") + msg.status(f"Copying {file} into {targetdir}") try: shutil.copy(os.path.join(i, file), targetdir) except shutil.SameFileError: @@ -198,13 +198,13 @@ def logos_reuse_download( app=app, ) else: - cli_download(sourceurl, file_path) + cli_download(sourceurl, file_path, app=app) if verify_downloaded_file( sourceurl, file_path, app=app, ): - msg.logos_msg(f"Copying: {file} into: {targetdir}") + msg.status(f"Copying: {file} into: {targetdir}") try: shutil.copy(os.path.join(config.MYDOWNLOADS, file), targetdir) except shutil.SameFileError: @@ -352,6 +352,9 @@ def same_md5(url, file_path): if url_md5 is None: # skip MD5 check if not provided with URL res = True else: + # TODO: Figure out why this is taking a long time. + # On 20240922, I ran into an issue such that it would take + # upwards of 6.5 minutes to complete file_md5 = FileProps(file_path).get_md5() logging.debug(f"{file_md5=}") res = url_md5 == file_md5 @@ -504,7 +507,7 @@ def get_logos_releases(app=None): app.root.event_generate(app.release_evt) return downloaded_releases - msg.logos_msg(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 + msg.status(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. if config.logos_release_channel is None or config.logos_release_channel == "stable": url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 diff --git a/system.py b/system.py index 9f8791c0..15abe54c 100644 --- a/system.py +++ b/system.py @@ -193,7 +193,7 @@ def wait_on(command): stderr=subprocess.PIPE, text=True ) - msg.logos_msg(f"Waiting on \"{' '.join(command)}\" to finish.", end='') + msg.status(f"Waiting on \"{' '.join(command)}\" to finish.", end='') time.sleep(1.0) while process.poll() is None: msg.logos_progress() @@ -366,7 +366,7 @@ def get_package_manager(): config.PACKAGES = ( "fuse2 fuse3 " # appimages "binutils cabextract wget libwbclient " # wine - "7-zip-full " # winetricks + "p7zip " # winetricks "openjpeg2 libxcomposite libxinerama " # display "ocl-icd vulkan-icd-loader " # hardware "alsa-plugins gst-plugins-base-libs libpulse openal " # audio @@ -775,11 +775,18 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) def have_lib(library, ld_library_path): - roots = ['/usr/lib', '/lib'] + available_library_paths = ['/usr/lib', '/lib'] if ld_library_path is not None: - roots = [*ld_library_path.split(':'), *roots] + available_library_paths = [*ld_library_path.split(':'), *available_library_paths] + roots = [root for root in available_library_paths if not Path(root).is_symlink()] + logging.debug(f"Library Paths: {roots}") for root in roots: - libs = [lib for lib in Path(root).rglob(f"{library}*")] + libs = [] + logging.debug(f"Have lib? Checking {root}") + for lib in Path(root).rglob(f"{library}*"): + logging.debug(f"DEV: {lib}") + libs.append(lib) + break if len(libs) > 0: logging.debug(f"'{library}' found at '{libs[0]}'") return True @@ -803,7 +810,7 @@ def install_winetricks( app=None, version=config.WINETRICKS_VERSION, ): - msg.logos_msg(f"Installing winetricks v{version}…") + msg.status(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 zip_name = f"{version}.zip" network.logos_reuse_download( diff --git a/tui_app.py b/tui_app.py index 7c28544d..a8f6ebb5 100644 --- a/tui_app.py +++ b/tui_app.py @@ -84,6 +84,10 @@ def __init__(self, stdscr): self.password_e = threading.Event() self.appimage_q = Queue() self.appimage_e = threading.Event() + self.install_icu_q = Queue() + self.install_icu_e = threading.Event() + self.install_logos_q = Queue() + self.install_logos_e = threading.Event() # Window and Screen Management self.tui_screens = [] @@ -311,10 +315,11 @@ def display(self): else: self.active_screen = self.tui_screens[-1] - run_monitor, last_time = utils.stopwatch(last_time, 2.5) - if run_monitor: - self.logos.monitor() - self.task_processor(self, task="PID") + if not isinstance(self.active_screen, tui_screen.DialogScreen): + run_monitor, last_time = utils.stopwatch(last_time, 2.5) + if run_monitor: + self.logos.monitor() + self.task_processor(self, task="PID") if isinstance(self.active_screen, tui_screen.CursesScreen): self.refresh() @@ -847,14 +852,14 @@ def which_dialog_options(self, labels, dialog=False): def set_main_menu_options(self, dialog=False): labels = [] if config.LLI_LATEST_VERSION and system.get_runmode() == 'binary': - logging.debug("Checking if Logos Linux Installers needs updated.") # noqa: E501 + logging.debug("Checking if Logos Linux Installer needs updated.") # noqa: E501 status, error_message = utils.compare_logos_linux_installer_version() # noqa: E501 if status == 0: labels.append("Update Logos Linux Installer") elif status == 1: - logging.warning("Logos Linux Installer is up-to-date.") + logging.debug("Logos Linux Installer is up-to-date.") elif status == 2: - logging.warning("Logos Linux Installer is newer than the latest release.") # noqa: E501 + logging.debug("Logos Linux Installer is newer than the latest release.") # noqa: E501 else: logging.error(f"{error_message}") diff --git a/tui_dialog.py b/tui_dialog.py index 121d2d17..2f960e98 100644 --- a/tui_dialog.py +++ b/tui_dialog.py @@ -6,7 +6,7 @@ pass -def text(app, text, height=None, width=None, title=None, backtitle=None, colors=True): +def text(screen, text, height=None, width=None, title=None, backtitle=None, colors=True): dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -21,9 +21,9 @@ def text(app, text, height=None, width=None, title=None, backtitle=None, colors= dialog.infobox(text, **options) -def progress_bar(app, text, percent, height=None, width=None, title=None, backtitle=None, colors=True): - dialog = Dialog() - dialog.autowidgetsize = True +def progress_bar(screen, text, percent, height=None, width=None, title=None, backtitle=None, colors=True): + screen.dialog = Dialog() + screen.dialog.autowidgetsize = True options = {'colors': colors} if height is not None: options['height'] = height @@ -34,23 +34,21 @@ def progress_bar(app, text, percent, height=None, width=None, title=None, backti if backtitle is not None: options['backtitle'] = backtitle - dialog.gauge_start(text=text, percent=percent, **options) + screen.dialog.gauge_start(text=text, percent=percent, **options) #FIXME: Not working. See tui_screen.py#262. -def update_progress_bar(app, percent, text='', update_text=False): - dialog = Dialog() - dialog.autowidgetsize = True - dialog.gauge_update(percent, text, update_text) +def update_progress_bar(screen, percent, text='', update_text=False): + screen.dialog.autowidgetsize = True + screen.dialog.gauge_update(percent, text, update_text) -def stop_progress_bar(app): - dialog = Dialog() - dialog.autowidgetsize = True - dialog.gauge_stop() +def stop_progress_bar(screen): + screen.dialog.autowidgetsize = True + screen.dialog.gauge_stop() -def tasklist_progress_bar(app, text, percent, elements, height=None, width=None, title=None, backtitle=None, colors=None): +def tasklist_progress_bar(screen, text, percent, elements, height=None, width=None, title=None, backtitle=None, colors=None): dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -74,7 +72,7 @@ def tasklist_progress_bar(app, text, percent, elements, height=None, width=None, raise -def input(app, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): +def input(screen, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -90,7 +88,7 @@ def input(app, question_text, height=None, width=None, init="", title=None, bac return code, input -def password(app, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): +def password(screen, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -106,7 +104,7 @@ def password(app, question_text, height=None, width=None, init="", title=None, return code, password -def confirm(app, question_text, yes_label="Yes", no_label="No", +def confirm(screen, question_text, yes_label="Yes", no_label="No", height=None, width=None, title=None, backtitle=None, colors=True): dialog = Dialog() dialog.autowidgetsize = True @@ -123,7 +121,7 @@ def confirm(app, question_text, yes_label="Yes", no_label="No", return check # Returns "ok" or "cancel" -def directory_picker(app, path_dir, height=None, width=None, title=None, backtitle=None, colors=True): +def directory_picker(screen, path_dir, height=None, width=None, title=None, backtitle=None, colors=True): str_dir = str(path_dir) try: @@ -148,7 +146,7 @@ def directory_picker(app, path_dir, height=None, width=None, title=None, backtit return path -def menu(app, question_text, choices, height=None, width=None, menu_height=8, title=None, backtitle=None, colors=True): +def menu(screen, question_text, choices, height=None, width=None, menu_height=8, title=None, backtitle=None, colors=True): tag_to_description = {tag: description for tag, description in choices} dialog = Dialog(dialog="dialog") dialog.autowidgetsize = True @@ -168,7 +166,7 @@ def menu(app, question_text, choices, height=None, width=None, menu_height=8, ti return None, None, "Return to Main Menu" -def buildlist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): +def buildlist(screen, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # items is an interable of (tag, item, status) dialog = Dialog(dialog="dialog") dialog.autowidgetsize = True @@ -190,7 +188,7 @@ def buildlist(app, text, items=[], height=None, width=None, list_height=None, ti return None -def checklist(app, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): +def checklist(screen, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # items is an iterable of (tag, item, status) dialog = Dialog(dialog="dialog") dialog.autowidgetsize = True diff --git a/tui_screen.py b/tui_screen.py index 60566c8d..af1742b7 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -345,6 +345,7 @@ def __init__(self, app, screen_id, queue, event, text, wait=False, percent=None, self.backtitle = backtitle self.colors = colors self.lastpercent = 0 + self.dialog = "" def __str__(self): return f"PyDialog Text Screen" @@ -352,7 +353,6 @@ def __str__(self): def display(self): if self.running == 0: if self.wait: - self.running = 1 if config.INSTALL_STEPS_COUNT > 0: self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) else: @@ -362,23 +362,24 @@ def display(self): self.lastpercent = self.percent else: tui_dialog.text(self, self.text) + self.running = 1 elif self.running == 1: if self.wait: if config.INSTALL_STEPS_COUNT > 0: self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) else: self.percent = 0 - # tui_dialog.update_progress_bar(self, self.percent, self.text, True) + if self.lastpercent != self.percent: - tui_dialog.progress_bar(self, self.text, self.percent) + self.lastpercent = self.percent + tui_dialog.update_progress_bar(self, self.percent, self.text, True) + #tui_dialog.progress_bar(self, self.text, self.percent) if self.percent == 100: tui_dialog.stop_progress_bar(self) self.running = 2 self.wait = False - time.sleep(0.1) - def get_text(self): return self.text diff --git a/utils.py b/utils.py index 8208569c..eb5df598 100644 --- a/utils.py +++ b/utils.py @@ -547,7 +547,7 @@ def get_latest_folder(folder_path): def install_premade_wine_bottle(srcdir, appdir): - msg.logos_msg(f"Extracting: '{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 + msg.status(f"Extracting: '{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 shutil.unpack_archive( f"{srcdir}/{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}", appdir diff --git a/wine.py b/wine.py index 324d863a..a004d14e 100644 --- a/wine.py +++ b/wine.py @@ -175,7 +175,7 @@ def check_wine_version_and_branch(release_version, test_binary): def initializeWineBottle(app=None): - msg.logos_msg("Initializing wine bottle...") + msg.status("Initializing wine bottle…") wine_exe = str(utils.get_wine_exe_path().parent / 'wine64') logging.debug(f"{wine_exe=}") # Avoid wine-mono window @@ -190,7 +190,7 @@ def initializeWineBottle(app=None): def wine_reg_install(reg_file): reg_file = str(reg_file) - msg.logos_msg(f"Installing registry file: {reg_file}") + msg.status(f"Installing registry file: {reg_file}") result = run_wine_proc( str(utils.get_wine_exe_path().parent / 'wine64'), exe="regedit.exe", @@ -203,8 +203,8 @@ def wine_reg_install(reg_file): light_wineserver_wait() -def install_msi(): - msg.logos_msg(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.") +def install_msi(app=None): + msg.status(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.", app) # Execute the .MSI wine_exe = str(utils.get_wine_exe_path().parent / 'wine64') exe_args = ["/i", f"{config.INSTALLDIR}/data/{config.LOGOS_EXECUTABLE}"] @@ -212,6 +212,9 @@ def install_msi(): exe_args.append('/passive') logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") run_wine_proc(wine_exe, exe="msiexec", exe_args=exe_args) + if app: + if config.DIALOG == "curses": + app.install_logos_e.set() def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): @@ -286,7 +289,7 @@ def run_winetricks(cmd=None): def run_winetricks_cmd(*args): cmd = [*args] - msg.logos_msg(f"Running winetricks \"{args[-1]}\"") + msg.status(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") run_wine_proc(config.WINETRICKSBIN, exe_args=cmd) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") @@ -301,7 +304,7 @@ def install_d3d_compiler(): def install_fonts(): - msg.logos_msg("Configuring fonts…") + msg.status("Configuring fonts…") fonts = ['corefonts', 'tahoma'] if not config.SKIP_FONTS: for f in fonts: @@ -360,6 +363,10 @@ def install_icu_data_files(app=None): app.status_q.put("ICU files copied.") app.root.event_generate(app.status_evt) + if app: + if config.DIALOG == "curses": + app.install_icu_e.set() + def get_registry_value(reg_path, name): logging.debug(f"Get value for: {reg_path=}; {name=}") From 27205f2027d78f0b240362de49f69a2308dd4fa3 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 30 Sep 2024 12:02:53 -0400 Subject: [PATCH 165/253] Begin adding process groups. - Fix utils.grep FileNotFound --- installer.py | 14 ++++++++++++-- utils.py | 15 ++++++++------- wine.py | 45 +++++++++++++++++++++++++++++---------------- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/installer.py b/installer.py index 7cc4b80a..903b9a8b 100644 --- a/installer.py +++ b/installer.py @@ -475,7 +475,10 @@ def ensure_wineprefix_init(app=None): # if utils.get_wine_exe_path(): # wine.initializeWineBottle() logging.debug("Initializing wineprefix.") - wine.initializeWineBottle() + process = wine.initializeWineBottle() + os.waitpid(-process.pid, 0) + wine.light_wineserver_wait() + logging.debug("Wine init complete.") logging.debug(f"> {init_file} exists?: {init_file.is_file()}") @@ -503,15 +506,21 @@ def ensure_winetricks_applied(app=None): reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') with open(reg_file, 'w') as f: f.write(r'''REGEDIT4 - + [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "winemenubuilder.exe"="" ''') wine.wine_reg_install(reg_file) + msg.status("Setting Renderer to GDI…", app) if not utils.grep(r'"renderer"="gdi"', usr_reg): wine.set_renderer("gdi") + msg.status("Setting Font Smooting to RGB…", app) + if not utils.grep(r'"FontSmoothingType"=dword:00000002', usr_reg): + wine.install_font_smoothing() + + msg.status("Installing fonts…", app) if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 wine.installFonts() @@ -523,6 +532,7 @@ def ensure_winetricks_applied(app=None): msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Win10 Mode.") wine.set_win_version("indexer", "win10") + wine.light_wineserver_wait() logging.debug("> Done.") diff --git a/utils.py b/utils.py index eb5df598..71eead97 100644 --- a/utils.py +++ b/utils.py @@ -882,13 +882,14 @@ def grep(regexp, filepath): fp = Path(filepath) found = False ct = 0 - with fp.open() as f: - for line in f: - ct += 1 - text = line.rstrip() - if re.search(regexp, text): - logging.debug(f"{filepath}:{ct}:{text}") - found = True + if fp.exists(): + with fp.open() as f: + for line in f: + ct += 1 + text = line.rstrip() + if re.search(regexp, text): + logging.debug(f"{filepath}:{ct}:{text}") + found = True return found diff --git a/wine.py b/wine.py index a004d14e..d454f0c5 100644 --- a/wine.py +++ b/wine.py @@ -36,6 +36,7 @@ def get_wine_user(): def check_wineserver(): try: process = run_wine_proc(config.WINESERVER, exe_args=["-p"]) + process.wait() return process.returncode == 0 except Exception as e: return False @@ -43,12 +44,14 @@ def check_wineserver(): def wineserver_kill(): if check_wineserver(): - run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) + process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) + process.wait() def wineserver_wait(): if check_wineserver(): - run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) + process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) + process.wait() def light_wineserver_wait(): @@ -69,8 +72,8 @@ def end_wine_processes(): process.terminate() process.wait(timeout=10) except subprocess.TimeoutExpired: - process.kill() - process.wait() + os.killpg(process.pid, signal.SIGTERM) + os.waitpid(-process.pid, 0) def get_wine_release(binary): @@ -182,23 +185,22 @@ def initializeWineBottle(app=None): orig_overrides = config.WINEDLLOVERRIDES config.WINEDLLOVERRIDES = f"{config.WINEDLLOVERRIDES};mscoree=" logging.debug(f"Running: {wine_exe} wineboot --init") - run_wine_proc(wine_exe, exe='wineboot', exe_args=['--init'], init=True) + process = run_wine_proc(wine_exe, exe='wineboot', exe_args=['--init'], init=True) config.WINEDLLOVERRIDES = orig_overrides - light_wineserver_wait() - logging.debug("Wine init complete.") + return process def wine_reg_install(reg_file): reg_file = str(reg_file) msg.status(f"Installing registry file: {reg_file}") - result = run_wine_proc( + process = run_wine_proc( str(utils.get_wine_exe_path().parent / 'wine64'), exe="regedit.exe", exe_args=[reg_file] ) - if result is None or result.returncode != 0: + if process is None or process.returncode != 0: msg.logos_error(f"Failed to install reg file: {reg_file}") - elif result.returncode == 0: + elif process.returncode == 0: logging.info(f"{reg_file} installed.") light_wineserver_wait() @@ -211,7 +213,8 @@ def install_msi(app=None): if config.PASSIVE is True: exe_args.append('/passive') logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") - run_wine_proc(wine_exe, exe="msiexec", exe_args=exe_args) + process = run_wine_proc(wine_exe, exe="msiexec", exe_args=exe_args) + process.wait() if app: if config.DIALOG == "curses": app.install_logos_e.set() @@ -250,7 +253,8 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): command, stdout=wine_log, stderr=wine_log, - env=env + env=env, + start_new_session=True ) if process is not None: if exe is not None and isinstance(process, subprocess.Popen): @@ -283,7 +287,8 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): def run_winetricks(cmd=None): - run_wine_proc(config.WINETRICKSBIN, exe=cmd) + process = run_wine_proc(config.WINETRICKSBIN, exe=cmd) + os.waitpid(-process.pid, 0) wineserver_wait() @@ -291,7 +296,8 @@ def run_winetricks_cmd(*args): cmd = [*args] msg.status(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") - run_wine_proc(config.WINETRICKSBIN, exe_args=cmd) + process = run_wine_proc(config.WINETRICKSBIN, exe_args=cmd) + os.waitpid(-process.pid, 0) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") heavy_wineserver_wait() @@ -313,7 +319,13 @@ def install_fonts(): args.insert(0, '-q') run_winetricks_cmd(*args) - run_winetricks_cmd('-q', 'settings', 'fontsmooth=rgb') + +def install_font_smoothing(): + msg.status("Setting font smoothing…") + args = ['settings', 'fontsmooth=rgb'] + if config.WINETRICKS_UNATTENDED: + args.insert(0, '-q') + run_winetricks_cmd(*args) def set_renderer(renderer): @@ -332,7 +344,8 @@ def set_win_version(exe, windows_version): "/t", "REG_SZ", "/d", f"{windows_version}", "/f", ] - run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) + process = run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) + os.waitpid(-process.pid, 0) def install_icu_data_files(app=None): From 2a30ddb046bc319c216a8e8b349c76f2908e747e Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Wed, 2 Oct 2024 21:50:07 -0400 Subject: [PATCH 166/253] Fix issues following rebase on #163 - Encapsulate TUI addnstr in try/except to avoid resize errors --- installer.py | 32 +++++++++++++++----------------- tui_app.py | 21 ++++++++++++--------- tui_curses.py | 5 ++++- tui_screen.py | 11 +++++++++-- wine.py | 20 +++++++++++--------- 5 files changed, 51 insertions(+), 38 deletions(-) diff --git a/installer.py b/installer.py index 903b9a8b..d45aae3d 100644 --- a/installer.py +++ b/installer.py @@ -476,7 +476,7 @@ def ensure_wineprefix_init(app=None): # wine.initializeWineBottle() logging.debug("Initializing wineprefix.") process = wine.initializeWineBottle() - os.waitpid(-process.pid, 0) + wine.wait_pid(process) wine.light_wineserver_wait() logging.debug("Wine init complete.") logging.debug(f"> {init_file} exists?: {init_file.is_file()}") @@ -502,15 +502,15 @@ def ensure_winetricks_applied(app=None): workdir.mkdir(parents=True, exist_ok=True) usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") - if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - reg_file = os.path.join(config.WORKDIR, 'disable-winemenubuilder.reg') - with open(reg_file, 'w') as f: - f.write(r'''REGEDIT4 - -[HKEY_CURRENT_USER\Software\Wine\DllOverrides] -"winemenubuilder.exe"="" -''') - wine.wine_reg_install(reg_file) + #FIXME: This is failing (20241002). +# if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): +# reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' +# reg_file.write_text(r'''REGEDIT4 +# +# [HKEY_CURRENT_USER\Software\Wine\DllOverrides] +# "winemenubuilder.exe"="" +# ''') +# wine.wine_reg_install(reg_file) msg.status("Setting Renderer to GDI…", app) if not utils.grep(r'"renderer"="gdi"', usr_reg): @@ -522,15 +522,15 @@ def ensure_winetricks_applied(app=None): msg.status("Installing fonts…", app) if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 - wine.installFonts() + wine.install_fonts() if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): - wine.installD3DCompiler() + wine.install_d3d_compiler() if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): wine.set_win_version("logos", "win10") - msg.logos_msg(f"Setting {config.FLPRODUCT}Bible Indexing to Win10 Mode.") + msg.logos_msg(f"Setting {config.FLPRODUCT} Bible Indexing to Win10 Mode.") wine.set_win_version("indexer", "win10") wine.light_wineserver_wait() logging.debug("> Done.") @@ -562,10 +562,8 @@ def ensure_product_installed(app=None): update_install_feedback(f"Ensuring {config.FLPRODUCT} is installed…", app=app) if not utils.find_installed_product(): - wine.install_msi() - if app: - if config.DIALOG == "curses": - app.install_logos_e.wait() + process = wine.install_msi() + wine.wait_pid(process) config.LOGOS_EXE = utils.find_installed_product() config.current_logos_version = config.TARGET_RELEASE_VERSION diff --git a/tui_app.py b/tui_app.py index a8f6ebb5..488e224e 100644 --- a/tui_app.py +++ b/tui_app.py @@ -203,7 +203,7 @@ def init_curses(self): self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, - "Main Menu", self.set_main_menu_options(dialog=False)) + "Main Menu", self.set_tui_menu_options(dialog=False)) #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", # self.set_tui_menu_options(dialog=True)) self.refresh() @@ -241,9 +241,17 @@ def update_main_window_contents(self): self.switch_q.put(1) self.refresh() + #ERR: On a sudden resize, the Curses menu is not properly resized, + # and we are not currently dynamically passing the menu options based + # on the current screen, but rather always passing the tui menu options. + # To replicate, open Terminator, run LLI full screen, then his Ctrl+A. + # The menu should survive, but the size does not resize to the new screen, + # even though the resize signal is sent. See tui_curses, line #251 and + # tui_screen, line #98. def resize_curses(self): config.resizing = True curses.endwin() + self.update_tty_dimensions() self.set_window_dimensions() self.clear() self.init_curses() @@ -379,7 +387,7 @@ def choice_processor(self, stdscr, screen_id, choice): 9: self.config_update_select, 10: self.waiting_releases, 11: self.winetricks_menu_select, - 12: self.run_logos, + 12: self.logos.start, 13: self.waiting_finish, 14: self.waiting_resize, 15: self.password_prompt, @@ -849,7 +857,7 @@ def which_dialog_options(self, labels, dialog=False): options.append(label) return options - def set_main_menu_options(self, dialog=False): + def set_tui_menu_options(self, dialog=False): labels = [] if config.LLI_LATEST_VERSION and system.get_runmode() == 'binary': logging.debug("Checking if Logos Linux Installer needs updated.") # noqa: E501 @@ -875,12 +883,7 @@ def set_main_menu_options(self, dialog=False): indexing = f"Run Indexing" labels_default = [ run, - indexing, - "Remove Library Catalog", - "Remove All Index Files", - "Edit Config", - "Back up Data", - "Restore Data", + indexing ] else: labels_default = ["Install Logos Bible Software"] diff --git a/tui_curses.py b/tui_curses.py index fce1416d..e2285258 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -245,7 +245,10 @@ def draw(self): # Display pagination information page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(self.options)}" - self.stdscr.addnstr(max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) + try: + self.stdscr.addnstr(max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) + except curses.error: + signal.signal(signal.SIGWINCH, self.app.signal_resize) def do_menu_up(self): if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: diff --git a/tui_screen.py b/tui_screen.py index af1742b7..19f66d56 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -1,5 +1,6 @@ import logging import time +import signal from pathlib import Path import curses @@ -84,14 +85,20 @@ def display(self): console_start_y = len(tui_curses.wrap_text(self.app, self.title)) + len( tui_curses.wrap_text(self.app, self.subtitle)) + 1 - self.stdscr.addnstr(console_start_y, config.margin, f"---Console---", self.app.window_width - (config.margin * 2)) + try: + self.stdscr.addnstr(console_start_y, config.margin, f"---Console---", self.app.window_width - (config.margin * 2)) + except curses.error: + signal.signal(signal.SIGWINCH, self.app.signal_resize) recent_messages = config.console_log[-config.console_log_lines:] for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) for j, line in enumerate(message_lines): if 2 + j < self.app.window_height: truncated = message[:self.app.window_width - (config.margin * 2)] - self.stdscr.addnstr(console_start_y + i, config.margin, truncated, self.app.window_width - (config.margin * 2)) + try: + self.stdscr.addnstr(console_start_y + i, config.margin, truncated, self.app.window_width - (config.margin * 2)) + except curses.error: + signal.signal(signal.SIGWINCH, self.app.signal_resize) self.stdscr.noutrefresh() curses.doupdate() diff --git a/wine.py b/wine.py index d454f0c5..a2f80ef4 100644 --- a/wine.py +++ b/wine.py @@ -48,6 +48,8 @@ def wineserver_kill(): process.wait() +#TODO: Review these three commands. The top is the newest and should be preserved. +# Can the other two be refactored out? def wineserver_wait(): if check_wineserver(): process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) @@ -73,7 +75,7 @@ def end_wine_processes(): process.wait(timeout=10) except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGTERM) - os.waitpid(-process.pid, 0) + wait_pid(process) def get_wine_release(binary): @@ -214,10 +216,11 @@ def install_msi(app=None): exe_args.append('/passive') logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") process = run_wine_proc(wine_exe, exe="msiexec", exe_args=exe_args) - process.wait() - if app: - if config.DIALOG == "curses": - app.install_logos_e.set() + return process + + +def wait_pid(process): + os.waitpid(-process.pid, 0) def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): @@ -288,16 +291,15 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): def run_winetricks(cmd=None): process = run_wine_proc(config.WINETRICKSBIN, exe=cmd) - os.waitpid(-process.pid, 0) + wait_pid(process) wineserver_wait() - def run_winetricks_cmd(*args): cmd = [*args] msg.status(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") process = run_wine_proc(config.WINETRICKSBIN, exe_args=cmd) - os.waitpid(-process.pid, 0) + wait_pid(process) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") heavy_wineserver_wait() @@ -345,7 +347,7 @@ def set_win_version(exe, windows_version): "/d", f"{windows_version}", "/f", ] process = run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) - os.waitpid(-process.pid, 0) + wait_pid(process) def install_icu_data_files(app=None): From 234e5e17861e0fa88b2090aecd455a24aadc144d Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 15:32:07 +0100 Subject: [PATCH 167/253] fix widget.state method names --- gui_app.py | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/gui_app.py b/gui_app.py index 5f7c3352..51efc00c 100644 --- a/gui_app.py +++ b/gui_app.py @@ -337,7 +337,7 @@ def set_version(self, evt=None): def start_releases_check(self): # Disable button; clear list. - self.gui.release_check_button.logos_state(['disabled']) + self.gui.release_check_button.state(['disabled']) # self.gui.releasevar.set('') self.gui.release_dropdown['values'] = [] # Setup queue, signal, thread. @@ -448,7 +448,7 @@ def on_release_check_released(self, evt=None): self.start_releases_check() def on_wine_check_released(self, evt=None): - self.gui.wine_check_button.logos_state(['disabled']) + self.gui.wine_check_button.state(['disabled']) self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) def set_skip_fonts(self, evt=None): @@ -475,21 +475,21 @@ def start_install_thread(self, evt=None): utils.start_thread(installer.ensure_launcher_shortcuts, app=self) def start_indeterminate_progress(self, evt=None): - self.gui.progress.logos_state(['!disabled']) + self.gui.progress.state(['!disabled']) self.gui.progressvar.set(0) self.gui.progress.config(mode='indeterminate') self.gui.progress.start() def stop_indeterminate_progress(self, evt=None): self.gui.progress.stop() - self.gui.progress.logos_state(['disabled']) + self.gui.progress.state(['disabled']) self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) self.gui.statusvar.set('') def update_release_check_progress(self, evt=None): self.stop_indeterminate_progress() - self.gui.release_check_button.logos_state(['!disabled']) + self.gui.release_check_button.state(['!disabled']) if not self.releases_q.empty(): self.gui.release_dropdown['values'] = self.releases_q.get() self.gui.releasevar.set(self.gui.release_dropdown['values'][0]) @@ -512,7 +512,7 @@ def update_wine_check_progress(self, evt=None): self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) self.set_wine() self.stop_indeterminate_progress() - self.gui.wine_check_button.logos_state(['!disabled']) + self.gui.wine_check_button.state(['!disabled']) def update_file_check_progress(self, evt=None): self.gui.progress.stop() @@ -551,7 +551,7 @@ def update_install_progress(self, evt=None): text="Exit", command=self.on_cancel_released, ) - self.gui.okay_button.logos_state(['!disabled']) + self.gui.okay_button.state(['!disabled']) self.root.event_generate('<>') self.win.destroy() return 0 @@ -592,7 +592,7 @@ def __init__(self, root, *args, **kwargs): text=self.gui.loggingstatevar.get(), command=self.switch_logging ) - self.gui.logging_button.logos_state(['disabled']) + self.gui.logging_button.state(['disabled']) self.gui.config_button.config(command=control.edit_config) self.gui.deps_button.config(command=self.install_deps) @@ -605,12 +605,12 @@ def __init__(self, root, *args, **kwargs): command=self.update_to_latest_appimage ) if config.WINEBIN_CODE != "AppImage" and config.WINEBIN_CODE != "Recommended": # noqa: E501 - self.gui.latest_appimage_button.logos_state(['disabled']) + self.gui.latest_appimage_button.state(['disabled']) gui.ToolTip( self.gui.latest_appimage_button, "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 ) - self.gui.set_appimage_button.logos_state(['disabled']) + self.gui.set_appimage_button.state(['disabled']) gui.ToolTip( self.gui.set_appimage_button, "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 @@ -669,7 +669,7 @@ def configure_app_button(self, evt=None): if utils.find_installed_product(): self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.gui.app_button.config(command=self.run_logos) - self.gui.get_winetricks_button.logos_state(['!disabled']) + self.gui.get_winetricks_button.state(['!disabled']) else: self.gui.app_button.config(command=self.run_installer) @@ -691,7 +691,7 @@ def run_action_cmd(self, evt=None): def on_action_radio_clicked(self, evt=None): logging.debug("gui_app.ControlPanel.on_action_radio_clicked START") if utils.app_is_installed(): - self.gui.actions_button.logos_state(['!disabled']) + self.gui.actions_button.state(['!disabled']) if self.gui.actionsvar.get() == 'run-indexing': self.actioncmd = self.run_indexing elif self.gui.actionsvar.get() == 'remove-library-catalog': @@ -733,7 +733,7 @@ def run_backup(self, evt=None): return # Prepare progress bar. - self.gui.progress.logos_state(['!disabled']) + self.gui.progress.state(['!disabled']) self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) # Start backup thread. @@ -805,32 +805,32 @@ def switch_logging(self, evt=None): } self.gui.statusvar.set(f"Switching app logging to '{desired_state}d'…") self.start_indeterminate_progress() - self.gui.progress.logos_state(['!disabled']) + self.gui.progress.state(['!disabled']) self.gui.progress.start() - self.gui.logging_button.logos_state(['disabled']) + self.gui.logging_button.state(['disabled']) t = threading.Thread(target=self.logos.switch_logging, kwargs=kwargs) t.start() def initialize_logging_button(self, evt=None): self.gui.statusvar.set('') self.gui.progress.stop() - self.gui.progress.logos_state(['disabled']) + self.gui.progress.state(['disabled']) state = self.reverse_logging_state_value(self.logging_q.get()) self.gui.loggingstatevar.set(state[:-1].title()) - self.gui.logging_button.logos_state(['!disabled']) + self.gui.logging_button.state(['!disabled']) def update_logging_button(self, evt=None): self.gui.statusvar.set('') self.gui.progress.stop() - self.gui.progress.logos_state(['disabled']) + self.gui.progress.state(['disabled']) new_state = self.reverse_logging_state_value(self.logging_q.get()) new_text = new_state[:-1].title() logging.debug(f"Updating app logging button text to: {new_text}") self.gui.loggingstatevar.set(new_text) - self.gui.logging_button.logos_state(['!disabled']) + self.gui.logging_button.state(['!disabled']) def update_app_button(self, evt=None): - self.gui.app_button.logos_state(['!disabled']) + self.gui.app_button.state(['!disabled']) self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.configure_app_button() self.update_run_winetricks_button() @@ -854,7 +854,7 @@ def update_latest_lli_release_button(self, evt=None): gui.ToolTip(self.gui.update_lli_button, msg) self.clear_status_text() self.stop_indeterminate_progress() - self.gui.update_lli_button.logos_state([state]) + self.gui.update_lli_button.state([state]) def update_latest_appimage_button(self, evt=None): status, reason = utils.compare_recommended_appimage_version() @@ -871,14 +871,14 @@ def update_latest_appimage_button(self, evt=None): gui.ToolTip(self.gui.latest_appimage_button, msg) self.clear_status_text() self.stop_indeterminate_progress() - self.gui.latest_appimage_button.logos_state([state]) + self.gui.latest_appimage_button.state([state]) def update_run_winetricks_button(self, evt=None): if utils.file_exists(config.WINETRICKSBIN): state = '!disabled' else: state = 'disabled' - self.gui.run_winetricks_button.logos_state([state]) + self.gui.run_winetricks_button.state([state]) def reverse_logging_state_value(self, state): if state == 'DISABLED': @@ -917,14 +917,14 @@ def update_status_text(self, evt=None): self.gui.statusvar.set(self.status_q.get()) def start_indeterminate_progress(self, evt=None): - self.gui.progress.logos_state(['!disabled']) + self.gui.progress.state(['!disabled']) self.gui.progressvar.set(0) self.gui.progress.config(mode='indeterminate') self.gui.progress.start() def stop_indeterminate_progress(self, evt=None): self.gui.progress.stop() - self.gui.progress.logos_state(['disabled']) + self.gui.progress.state(['disabled']) self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) From 9f0286a8fab05c21f6d5b0eed12bd131878fc1e2 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 17:11:10 +0100 Subject: [PATCH 168/253] update installer status messaging --- installer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/installer.py b/installer.py index d45aae3d..d0b6a016 100644 --- a/installer.py +++ b/installer.py @@ -512,25 +512,27 @@ def ensure_winetricks_applied(app=None): # ''') # wine.wine_reg_install(reg_file) - msg.status("Setting Renderer to GDI…", app) if not utils.grep(r'"renderer"="gdi"', usr_reg): + msg.status("Setting Renderer to GDI…", app) wine.set_renderer("gdi") - msg.status("Setting Font Smooting to RGB…", app) if not utils.grep(r'"FontSmoothingType"=dword:00000002', usr_reg): + msg.status("Setting Font Smooting to RGB…", app) wine.install_font_smoothing() - msg.status("Installing fonts…", app) if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 + msg.status("Installing fonts…", app) wine.install_fonts() if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): + msg.status("Installing D3D…", app) wine.install_d3d_compiler() if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): + msg.status(f"Setting {config.FLPRODUCT} to Win10 Mode…", app) wine.set_win_version("logos", "win10") - msg.logos_msg(f"Setting {config.FLPRODUCT} Bible Indexing to Win10 Mode.") + msg.logos_msg(f"Setting {config.FLPRODUCT} Bible Indexing to Win10 Mode…") # noqa: E501 wine.set_win_version("indexer", "win10") wine.light_wineserver_wait() logging.debug("> Done.") From 0028347e00b1d0e654f64c1729924fad66f9e7fc Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 17:20:27 +0100 Subject: [PATCH 169/253] fix disabling of winemenubuilder --- installer.py | 19 ++++++++++--------- wine.py | 5 ++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/installer.py b/installer.py index d0b6a016..3e0e41a9 100644 --- a/installer.py +++ b/installer.py @@ -502,15 +502,16 @@ def ensure_winetricks_applied(app=None): workdir.mkdir(parents=True, exist_ok=True) usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") - #FIXME: This is failing (20241002). -# if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): -# reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' -# reg_file.write_text(r'''REGEDIT4 -# -# [HKEY_CURRENT_USER\Software\Wine\DllOverrides] -# "winemenubuilder.exe"="" -# ''') -# wine.wine_reg_install(reg_file) + # FIXME: This is failing (20241002). + if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): + msg.status("Disabling winemenubuilder…", app) + reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' + reg_file.write_text(r'''REGEDIT4 + +[HKEY_CURRENT_USER\Software\Wine\DllOverrides] +"winemenubuilder.exe"="" +''') + wine.wine_reg_install(reg_file) if not utils.grep(r'"renderer"="gdi"', usr_reg): msg.status("Setting Renderer to GDI…", app) diff --git a/wine.py b/wine.py index a2f80ef4..9559160f 100644 --- a/wine.py +++ b/wine.py @@ -200,8 +200,11 @@ def wine_reg_install(reg_file): exe="regedit.exe", exe_args=[reg_file] ) + process.wait() if process is None or process.returncode != 0: - msg.logos_error(f"Failed to install reg file: {reg_file}") + failed = "Failed to install reg file" + logging.debug(f"{failed}. {process=}") + msg.logos_error(f"{failed}: {reg_file}") elif process.returncode == 0: logging.info(f"{reg_file} installed.") light_wineserver_wait() From beafadd99265b2dece990e24bce0546f07d9ce78 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 17:21:32 +0100 Subject: [PATCH 170/253] remove comment about failing winemenubuilder step --- installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.py b/installer.py index 3e0e41a9..8cc3cd7f 100644 --- a/installer.py +++ b/installer.py @@ -502,7 +502,7 @@ def ensure_winetricks_applied(app=None): workdir.mkdir(parents=True, exist_ok=True) usr_reg = Path(f"{config.WINEPREFIX}/user.reg") sys_reg = Path(f"{config.WINEPREFIX}/system.reg") - # FIXME: This is failing (20241002). + if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): msg.status("Disabling winemenubuilder…", app) reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' From f107acb09ab32817c191b19c6f0da5cb5b4ad0fe Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 18:15:29 +0100 Subject: [PATCH 171/253] fix Run Logos by sending unique status event to ControlPanel --- gui_app.py | 2 +- msg.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui_app.py b/gui_app.py index 51efc00c..771ac8be 100644 --- a/gui_app.py +++ b/gui_app.py @@ -626,7 +626,7 @@ def __init__(self, root, *args, **kwargs): self.logging_event = '<>' self.root.bind(self.logging_event, self.update_logging_button) self.status_q = Queue() - self.status_evt = '<>' + self.status_evt = '<>' self.root.bind(self.status_evt, self.update_status_text) self.root.bind('<>', self.clear_status_text) self.progress_q = Queue() diff --git a/msg.py b/msg.py index 819594da..2c231eec 100644 --- a/msg.py +++ b/msg.py @@ -302,7 +302,7 @@ def strip_timestamp(msg, timestamp_length=20): if app is not None: if config.DIALOG == 'tk': app.status_q.put(text) - app.root.event_generate('<>') + app.root.event_generate(app.status_evt) logging.info(f"{text}") elif config.DIALOG == 'curses': if len(config.console_log) > 0: From de6b1c72dc81a5afa24c21eaed25939d8b24bbe5 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 18:39:46 +0100 Subject: [PATCH 172/253] use utils.start_thread function in gui_app --- gui_app.py | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/gui_app.py b/gui_app.py index 771ac8be..c0dd2181 100644 --- a/gui_app.py +++ b/gui_app.py @@ -4,7 +4,6 @@ # - https://github.com/thw26/LogosLinuxInstaller/blob/master/LogosLinuxInstaller.sh # noqa: E501 import logging -import threading from pathlib import Path from queue import Queue @@ -655,15 +654,9 @@ def __init__(self, root, *args, **kwargs): # Start function to determine app logging state. if utils.app_is_installed(): - t = threading.Thread( - target=self.logos.get_app_logging_state, - kwargs={'app': self, 'init': True}, - daemon=True, - ) - t.start() self.gui.statusvar.set('Getting current app logging status…') self.start_indeterminate_progress() - utils.start_thread(wine.get_app_logging_state, app=self) + utils.start_thread(self.logos.get_app_logging_state) def configure_app_button(self, evt=None): if utils.find_installed_product(): @@ -680,10 +673,7 @@ def run_installer(self, evt=None): self.root.icon = config.LOGOS_ICON_URL def run_logos(self, evt=None): - # TODO: Add reference to App here so the status message is sent to the - # GUI? See msg.status and wine.run_logos - t = threading.Thread(target=self.logos.start) - t.start() + utils.start_thread(self.logos.start) def run_action_cmd(self, evt=None): self.actioncmd() @@ -702,8 +692,7 @@ def on_action_radio_clicked(self, evt=None): self.actioncmd = self.install_icu def run_indexing(self, evt=None): - t = threading.Thread(target=self.logos.index) - t.start() + utils.start_thread(self.logos.index) def remove_library_catalog(self, evt=None): control.remove_library_catalog() @@ -714,12 +703,7 @@ def remove_indexes(self, evt=None): def install_icu(self, evt=None): self.gui.statusvar.set("Installing ICU files…") - t = threading.Thread( - target=wine.install_icu_data_files, - kwargs={'app': self}, - daemon=True, - ) - t.start() + utils.start_thread(wine.install_icu_data_files, app=self) def run_backup(self, evt=None): # Get backup folder. @@ -798,18 +782,15 @@ def launch_winetricks(self, evt=None): def switch_logging(self, evt=None): desired_state = self.gui.loggingstatevar.get() - # new_state = 'Enable' if prev_state == 'Disable' else 'Disable' - kwargs = { - 'action': desired_state.lower(), - 'app': self, - } self.gui.statusvar.set(f"Switching app logging to '{desired_state}d'…") self.start_indeterminate_progress() self.gui.progress.state(['!disabled']) self.gui.progress.start() self.gui.logging_button.state(['disabled']) - t = threading.Thread(target=self.logos.switch_logging, kwargs=kwargs) - t.start() + utils.start_thread( + self.logos.switch_logging, + action=desired_state.lower() + ) def initialize_logging_button(self, evt=None): self.gui.statusvar.set('') From aad833859f46882d9ac489b6bed2293a546f84e6 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 18:58:13 +0100 Subject: [PATCH 173/253] use utils.start_thread function in various modules --- control.py | 18 ++++-------------- logos.py | 20 +++++++++++--------- network.py | 4 +--- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/control.py b/control.py index 42443d20..581463b3 100644 --- a/control.py +++ b/control.py @@ -102,18 +102,13 @@ def backup_and_restore(mode='backup', app=None): # Get source transfer size. q = queue.Queue() - t = threading.Thread( - target=utils.get_folder_group_size, - args=[src_dirs, q], - daemon=True - ) m = "Calculating backup size" if app is not None: app.status_q.put(m) app.root.event_generate('<>') - app.root.event_generate('<>') + app.root.event_generate(app.status_evt) msg.status(m, end='') - t.start() + t = utils.start_thread(utils.get_folder_group_size, src_dirs, q) try: while t.is_alive(): msg.logos_progress() @@ -177,11 +172,6 @@ def backup_and_restore(mode='backup', app=None): msg.logos_error(f"Backup already exists: {dst_dir}") # Run file transfer. - t = threading.Thread( - target=copy_data, - args=(src_dirs, dst_dir), - daemon=True - ) if mode == 'restore': m = f"Restoring backup from {str(source_dir_base)}" else: @@ -190,9 +180,9 @@ def backup_and_restore(mode='backup', app=None): msg.status(m) if app is not None: app.status_q.put(m) - app.root.event_generate('<>') + app.root.event_generate(app.status_evt) dst_dir_size = utils.get_path_size(dst_dir) - t.start() + t = utils.start_thread(copy_data, src_dirs, dst_dir) try: while t.is_alive(): progress = utils.get_copy_progress( diff --git a/logos.py b/logos.py index 1835ba44..0d075a5d 100644 --- a/logos.py +++ b/logos.py @@ -85,9 +85,8 @@ def run_logos(): msg.status(txt, self.app) else: wine.wineserver_kill() - msg.status(f"Running {config.FLPRODUCT}…", self.app) - thread = threading.Thread(target=run_logos) - thread.start() + msg.status(f"Running {config.FLPRODUCT}…", app=app) + utils.start_thread(run_logos, daemon=False) self.logos_state = State.RUNNING def stop(self): @@ -145,16 +144,19 @@ def wait_on_indexing(): wine.wineserver_kill() msg.status(f"Indexing has begun…", self.app) - index_thread = threading.Thread(target=run_indexing) - index_thread.start() + # index_thread = threading.Thread(target=run_indexing) + # index_thread.start() + index_thread = utils.start_thread(run_indexing, daemon=False) self.indexing_state = State.RUNNING time.sleep(1) # If we don't wait, the thread starts too quickly # and the process won't yet be launched when we try to pull it from config.processes process = config.processes[config.logos_indexer_exe] - check_thread = threading.Thread(target=check_if_indexing, args=(process,)) - wait_thread = threading.Thread(target=wait_on_indexing) - check_thread.start() - wait_thread.start() + # check_thread = threading.Thread(target=check_if_indexing, args=(process,)) + check_thread = utils.start_thread(check_if_indexing, process) + # wait_thread = threading.Thread(target=wait_on_indexing) + wait_thread = utils.start_thread(wait_on_indexing) + # check_thread.start() + # wait_thread.start() main.threads.extend([index_thread, check_thread, wait_thread]) config.processes[config.logos_indexer_exe] = index_thread config.processes[config.check_if_indexing] = check_thread diff --git a/network.py b/network.py index 26aeb39e..d5faa70f 100644 --- a/network.py +++ b/network.py @@ -138,10 +138,8 @@ def cli_download(uri, destination, app=None): # Download from uri in thread while showing progress bar. cli_queue = queue.Queue() - args = [uri] kwargs = {'q': cli_queue, 'target': target} - t = threading.Thread(target=net_get, args=args, kwargs=kwargs, daemon=True) - t.start() + t = utils.start_thread(net_get, uri, **kwargs) try: while t.is_alive(): if cli_queue.empty(): From 978da45445315d956e82fdf4fa1afb7b332a7e58 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 19:03:50 +0100 Subject: [PATCH 174/253] pep8 cleanup --- control.py | 1 - installer.py | 5 ++++- logos.py | 21 ++++++++++++++++----- network.py | 9 ++++----- wine.py | 26 ++++++++++++++++++-------- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/control.py b/control.py index 581463b3..7d3bad98 100644 --- a/control.py +++ b/control.py @@ -9,7 +9,6 @@ import shutil import subprocess import sys -import threading import time from datetime import datetime from pathlib import Path diff --git a/installer.py b/installer.py index 8cc3cd7f..732a3ba6 100644 --- a/installer.py +++ b/installer.py @@ -562,7 +562,10 @@ def ensure_product_installed(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_icu_data_files(app=app) config.INSTALL_STEP += 1 - update_install_feedback(f"Ensuring {config.FLPRODUCT} is installed…", app=app) + update_install_feedback( + f"Ensuring {config.FLPRODUCT} is installed…", + app=app + ) if not utils.find_installed_product(): process = wine.install_msi() diff --git a/logos.py b/logos.py index 0d075a5d..f97e7227 100644 --- a/logos.py +++ b/logos.py @@ -67,11 +67,14 @@ def monitor(self): def start(self): self.logos_state = State.STARTING - logos_release = utils.convert_logos_release(config.current_logos_version) - wine_release, _ = wine.get_wine_release(config.WINE_EXE) + logos_release = utils.convert_logos_release(config.current_logos_version) # noqa: E501 + wine_release, _ = wine.get_wine_release(str(utils.get_wine_exe_path())) def run_logos(): - wine.run_wine_proc(config.WINE_EXE, exe=config.LOGOS_EXE) + wine.run_wine_proc( + str(utils.get_wine_exe_path()), + exe=config.LOGOS_EXE + ) # TODO: Find a way to incorporate check_wine_version_and_branch() if 30 > logos_release[0] > 9 and ( @@ -117,7 +120,10 @@ def index(self): index_finished = threading.Event() def run_indexing(): - wine.run_wine_proc(config.WINE_EXE, exe=config.logos_indexer_exe) + wine.run_wine_proc( + str(utils.get_wine_exe_path()), + exe=config.logos_indexer_exe + ) def check_if_indexing(process): start_time = time.time() @@ -227,7 +233,12 @@ def switch_logging(self, action=None): 'add', 'HKCU\\Software\\Logos4\\Logging', '/v', 'Enabled', '/t', 'REG_DWORD', '/d', value, '/f' ] - wine.run_wine_proc(config.WINE_EXE, exe='reg', exe_args=exe_args) + process = wine.run_wine_proc( + str(utils.get_wine_exe_path()), + exe='reg', + exe_args=exe_args + ) + process.wait() wine.wineserver_wait() config.LOGS = state if self.app is not None: diff --git a/network.py b/network.py index d5faa70f..79d4f498 100644 --- a/network.py +++ b/network.py @@ -6,7 +6,6 @@ import requests import shutil import sys -import threading from base64 import b64encode from datetime import datetime, timedelta from pathlib import Path @@ -406,10 +405,10 @@ def get_latest_release_version_tag_name(json_data): def set_logoslinuxinstaller_latest_release_config(): - if config.lli_release_channel is None or config.lli_release_channel == "stable": + if config.lli_release_channel is None or config.lli_release_channel == "stable": # noqa: E501 releases_url = "https://api.github.com/repos/FaithLife-Community/LogosLinuxInstaller/releases" # noqa: E501 else: - releases_url = "https://api.github.com/repos/FaithLife-Community/test-builds/releases" + releases_url = "https://api.github.com/repos/FaithLife-Community/test-builds/releases" # noqa: E501 json_data = get_latest_release_data(releases_url) logoslinuxinstaller_url = get_latest_release_url(json_data) logoslinuxinstaller_tag_name = get_latest_release_version_tag_name(json_data) # noqa: E501 @@ -507,10 +506,10 @@ def get_logos_releases(app=None): msg.status(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. - if config.logos_release_channel is None or config.logos_release_channel == "stable": + if config.logos_release_channel is None or config.logos_release_channel == "stable": # noqa: E501 url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 elif config.logos_release_channel == "beta": - url = f"https://clientservices.logos.com/update/v1/feed/logos10/beta.xml" # noqa: E501 + url = "https://clientservices.logos.com/update/v1/feed/logos10/beta.xml" # noqa: E501 response_xml_bytes = net_get(url) # if response_xml is None and None not in [q, app]: diff --git a/wine.py b/wine.py index 9559160f..bfcf6828 100644 --- a/wine.py +++ b/wine.py @@ -16,9 +16,9 @@ def set_logos_paths(): - config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' - config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' - config.logos_indexing_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' + config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 + config.logos_indexing_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 for f in files: if f == "LogosIndexer.exe" and root.endswith("Logos/System"): @@ -38,7 +38,7 @@ def check_wineserver(): process = run_wine_proc(config.WINESERVER, exe_args=["-p"]) process.wait() return process.returncode == 0 - except Exception as e: + except Exception: return False @@ -48,8 +48,8 @@ def wineserver_kill(): process.wait() -#TODO: Review these three commands. The top is the newest and should be preserved. -# Can the other two be refactored out? +# TODO: Review these three commands. The top is the newest and should be +# preserved. Can the other two be refactored out? def wineserver_wait(): if check_wineserver(): process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) @@ -187,7 +187,12 @@ def initializeWineBottle(app=None): orig_overrides = config.WINEDLLOVERRIDES config.WINEDLLOVERRIDES = f"{config.WINEDLLOVERRIDES};mscoree=" logging.debug(f"Running: {wine_exe} wineboot --init") - process = run_wine_proc(wine_exe, exe='wineboot', exe_args=['--init'], init=True) + process = run_wine_proc( + wine_exe, + exe='wineboot', + exe_args=['--init'], + init=True + ) config.WINEDLLOVERRIDES = orig_overrides return process @@ -297,6 +302,7 @@ def run_winetricks(cmd=None): wait_pid(process) wineserver_wait() + def run_winetricks_cmd(*args): cmd = [*args] msg.status(f"Running winetricks \"{args[-1]}\"") @@ -349,7 +355,11 @@ def set_win_version(exe, windows_version): "/t", "REG_SZ", "/d", f"{windows_version}", "/f", ] - process = run_wine_proc(str(utils.get_wine_exe_path()), exe='reg', exe_args=exe_args) + process = run_wine_proc( + str(utils.get_wine_exe_path()), + exe='reg', + exe_args=exe_args + ) wait_pid(process) From c0103904d3baa3c0882b5e4ee2e9dd252be7927c Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 3 Oct 2024 19:04:11 +0100 Subject: [PATCH 175/253] don't sent status msg to GUI when Logos runs --- logos.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/logos.py b/logos.py index f97e7227..9ddaa68d 100644 --- a/logos.py +++ b/logos.py @@ -88,6 +88,10 @@ def run_logos(): msg.status(txt, self.app) else: wine.wineserver_kill() + app = self.app + if config.DIALOG == 'tk': + # Don't send "Running" message to GUI b/c it never clears. + app = None msg.status(f"Running {config.FLPRODUCT}…", app=app) utils.start_thread(run_logos, daemon=False) self.logos_state = State.RUNNING From f64ff356295fdd90c6844c4ea84f6d6e53f5df86 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 4 Oct 2024 17:26:49 +0100 Subject: [PATCH 176/253] remove logging for LLI version comparison; add TODO item --- utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/utils.py b/utils.py index 71eead97..a571d2d6 100644 --- a/utils.py +++ b/utils.py @@ -555,13 +555,15 @@ def install_premade_wine_bottle(srcdir, appdir): def compare_logos_linux_installer_version(): + # TODO: Save this as a config variable and only run it once in + # network.check_for_updates(). status = None message = None if ( config.LLI_CURRENT_VERSION is not None and config.LLI_LATEST_VERSION is not None ): - logging.debug(f"{config.LLI_CURRENT_VERSION=}; {config.LLI_LATEST_VERSION=}") # noqa: E501 + # logging.debug(f"{config.LLI_CURRENT_VERSION=}; {config.LLI_LATEST_VERSION=}") # noqa: E501 if ( version.parse(config.LLI_CURRENT_VERSION) < version.parse(config.LLI_LATEST_VERSION) @@ -587,7 +589,7 @@ def compare_logos_linux_installer_version(): status = False message = "config.LLI_CURRENT_VERSION or config.LLI_LATEST_VERSION is not set." # noqa: E501 - logging.debug(f"{status=}; {message=}") + # logging.debug(f"{status=}; {message=}") return status, message From 2070982148cb7f42e82ef00ca412e62560fadfc6 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 4 Oct 2024 17:44:16 +0100 Subject: [PATCH 177/253] fix bad arg name --- logos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logos.py b/logos.py index 9ddaa68d..b1cce890 100644 --- a/logos.py +++ b/logos.py @@ -93,7 +93,7 @@ def run_logos(): # Don't send "Running" message to GUI b/c it never clears. app = None msg.status(f"Running {config.FLPRODUCT}…", app=app) - utils.start_thread(run_logos, daemon=False) + utils.start_thread(run_logos, daemon_bool=False) self.logos_state = State.RUNNING def stop(self): From 9c3f5ff858c462784421f4d5cea8c7f5ba2958a8 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 4 Oct 2024 17:59:35 +0100 Subject: [PATCH 178/253] further reduction of logging for LLI version comparison --- tui_app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tui_app.py b/tui_app.py index 488e224e..97d38247 100644 --- a/tui_app.py +++ b/tui_app.py @@ -860,14 +860,16 @@ def which_dialog_options(self, labels, dialog=False): def set_tui_menu_options(self, dialog=False): labels = [] if config.LLI_LATEST_VERSION and system.get_runmode() == 'binary': - logging.debug("Checking if Logos Linux Installer needs updated.") # noqa: E501 + # logging.debug("Checking if Logos Linux Installer needs updated.") # noqa: E501 status, error_message = utils.compare_logos_linux_installer_version() # noqa: E501 if status == 0: labels.append("Update Logos Linux Installer") elif status == 1: - logging.debug("Logos Linux Installer is up-to-date.") + # logging.debug("Logos Linux Installer is up-to-date.") + pass elif status == 2: - logging.debug("Logos Linux Installer is newer than the latest release.") # noqa: E501 + # logging.debug("Logos Linux Installer is newer than the latest release.") # noqa: E501 + pass else: logging.error(f"{error_message}") From 2296b7b9c81e122280044233c7a9d16038b678b5 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 5 Oct 2024 19:37:24 -0700 Subject: [PATCH 179/253] fix: ensure app is set before attempting to operate on it --- installer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/installer.py b/installer.py index 732a3ba6..283f3b62 100644 --- a/installer.py +++ b/installer.py @@ -35,7 +35,7 @@ def ensure_product_choice(app=None): m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) else: - if config.DIALOG == 'curses': + if config.DIALOG == 'curses' and app: app.set_product(config.FLPRODUCT) if config.FLPRODUCT == 'Logos': @@ -66,7 +66,7 @@ def ensure_version_choice(app=None): m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) else: - if config.DIALOG == 'curses': + if config.DIALOG == 'curses' and app: app.set_version(config.TARGETVERSION) logging.debug(f"> {config.TARGETVERSION=}") @@ -90,7 +90,7 @@ def ensure_release_choice(app=None): m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) else: - if config.DIALOG == 'curses': + if config.DIALOG == 'curses' and app: app.set_release(config.TARGET_RELEASE_VERSION) logging.debug(f"> {config.TARGET_RELEASE_VERSION=}") @@ -121,7 +121,7 @@ def ensure_install_dir_choice(app=None): m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) else: - if config.DIALOG == 'curses': + if config.DIALOG == 'curses' and app: app.set_installdir(config.INSTALLDIR) logging.debug(f"> {config.INSTALLDIR=}") @@ -151,7 +151,7 @@ def ensure_wine_choice(app=None): m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) else: - if config.DIALOG == 'curses': + if config.DIALOG == 'curses' and app: app.set_wine(utils.get_wine_exe_path()) # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. From 113eed97cca1f6a97ff47fc548277e6d4559cdcf Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 27 Aug 2024 23:57:35 -0400 Subject: [PATCH 180/253] Fix #128. Support the Campaign to Save the CLI --- cli.py | 52 ++++++++++++++++ installer.py | 168 +++++++++++++++++++++++++++++++++------------------ main.py | 8 ++- msg.py | 10 +-- network.py | 6 +- system.py | 2 + tui_app.py | 15 ++--- utils.py | 6 +- 8 files changed, 190 insertions(+), 77 deletions(-) create mode 100644 cli.py diff --git a/cli.py b/cli.py new file mode 100644 index 00000000..832e6b9c --- /dev/null +++ b/cli.py @@ -0,0 +1,52 @@ +import logging +import threading +import queue + +import config +import installer +import msg +import utils + + +class CLI: + def __init__(self): + self.running = True + self.choice_q = queue.Queue() + self.input_q = queue.Queue() + self.event = threading.Event() + + def stop(self): + self.running = False + + def run(self): + config.DIALOG = "cli" + + self.thread = utils.start_thread(installer.ensure_launcher_shortcuts, daemon_bool=True, app=self) + + while self.running: + self.user_input_processor() + + msg.logos_msg("Exiting CLI installer.") + + + def user_input_processor(self): + prompt = None + question = None + options = None + choice = None + if self.input_q.qsize() > 0: + prompt = self.input_q.get() + if prompt is not None and isinstance(prompt, tuple): + question = prompt[0] + options = prompt[1] + if question is not None and options is not None: + choice = input(f"{question}: {options}: ") + if choice is not None and choice.lower() == 'exit': + self.running = False + if choice is not None: + self.choice_q.put(choice) + self.event.set() + + +def command_line_interface(): + CLI().run() diff --git a/installer.py b/installer.py index 283f3b62..ecab6ffe 100644 --- a/installer.py +++ b/installer.py @@ -15,8 +15,6 @@ # To replicate, start a TUI install, return/cancel on second step # Then launch a new install -# TODO: Reimplement `--install-app`? - def ensure_product_choice(app=None): config.INSTALL_STEPS_COUNT += 1 @@ -27,13 +25,16 @@ def ensure_product_choice(app=None): if not config.FLPRODUCT: if app: - utils.send_task(app, 'FLPRODUCT') - if config.DIALOG == 'curses': - app.product_e.wait() - config.FLPRODUCT = app.product_q.get() - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) + if config.DIALOG == 'cli': + app.input_q.put(("Choose which FaithLife product the script should install: ", ["Logos", "Verbum", "Exit"])) + app.event.wait() + app.event.clear() + config.FLPRODUCT = app.choice_q.get() + else: + utils.send_task(app, 'FLPRODUCT') + if config.DIALOG == 'curses': + app.product_e.wait() + config.FLPRODUCT = app.product_q.get() else: if config.DIALOG == 'curses' and app: app.set_product(config.FLPRODUCT) @@ -58,13 +59,16 @@ def ensure_version_choice(app=None): logging.debug('- config.TARGETVERSION') if not config.TARGETVERSION: if app: - utils.send_task(app, 'TARGETVERSION') - if config.DIALOG == 'curses': - app.version_e.wait() - config.TARGETVERSION = app.version_q.get() - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) + if config.DIALOG == 'cli': + app.input_q.put((f"Which version of {config.FLPRODUCT} should the script install?: ", ["10", "9", "Exit"])) + app.event.wait() + app.event.clear() + config.TARGETVERSION = app.choice_q.get() + else: + utils.send_task(app, 'TARGETVERSION') + if config.DIALOG == 'curses': + app.version_e.wait() + config.TARGETVERSION = app.version_q.get() else: if config.DIALOG == 'curses' and app: app.set_version(config.TARGETVERSION) @@ -81,14 +85,19 @@ def ensure_release_choice(app=None): if not config.TARGET_RELEASE_VERSION: if app: - utils.send_task(app, 'TARGET_RELEASE_VERSION') - if config.DIALOG == 'curses': - app.release_e.wait() - config.TARGET_RELEASE_VERSION = app.release_q.get() - logging.debug(f"{config.TARGET_RELEASE_VERSION=}") - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) + if config.DIALOG == 'cli': + utils.start_thread(network.get_logos_releases, daemon_bool=True, app=app) + app.event.wait() + app.event.clear() + app.event.wait() # Wait for user input queue to receive input + app.event.clear() + config.TARGET_RELEASE_VERSION = app.choice_q.get() + else: + utils.send_task(app, 'TARGET_RELEASE_VERSION') + if config.DIALOG == 'curses': + app.release_e.wait() + config.TARGET_RELEASE_VERSION = app.release_q.get() + logging.debug(f"{config.TARGET_RELEASE_VERSION=}") else: if config.DIALOG == 'curses' and app: app.set_release(config.TARGET_RELEASE_VERSION) @@ -109,17 +118,20 @@ def ensure_install_dir_choice(app=None): default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 if not config.INSTALLDIR: if app: - if config.DIALOG == 'tk': + if config.DIALOG == 'cli': + default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 + question = f"Where should {config.FLPRODUCT} files be installed to?: " # noqa: E501 + app.input_q.put((question, [default, "Type your own custom path", "Exit"])) + app.event.wait() + app.event.clear() + config.INSTALLDIR = app.choice_q.get() + elif config.DIALOG == 'tk': config.INSTALLDIR = default - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" elif config.DIALOG == 'curses': utils.send_task(app, 'INSTALLDIR') app.installdir_e.wait() config.INSTALLDIR = app.installdir_q.get() - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" else: if config.DIALOG == 'curses' and app: app.set_installdir(config.INSTALLDIR) @@ -143,13 +155,23 @@ def ensure_wine_choice(app=None): if utils.get_wine_exe_path() is None: network.set_recommended_appimage_config() if app: - utils.send_task(app, 'WINE_EXE') - if config.DIALOG == 'curses': - app.wine_e.wait() - config.WINE_EXE = app.wine_q.get() - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) + if config.DIALOG == 'cli': + options = utils.get_wine_options( + utils.find_appimage_files(config.TARGET_RELEASE_VERSION), + utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) + ) + if config.DIALOG == 'cli': + app.input_q.put(( + f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?: ", options)) + app.event.set() + app.event.wait() + app.event.clear() + config.WINE_EXE = utils.get_relative_path(utils.get_config_var(app.choice_q.get()), config.INSTALLDIR) + else: + utils.send_task(app, 'WINE_EXE') + if config.DIALOG == 'curses': + app.wine_e.wait() + config.WINE_EXE = app.wines_q.get() else: if config.DIALOG == 'curses' and app: app.set_wine(utils.get_wine_exe_path()) @@ -177,12 +199,28 @@ def ensure_winetricks_choice(app=None): update_install_feedback("Choose winetricks binary…", app=app) logging.debug('- config.WINETRICKSBIN') - if app: - if config.WINETRICKSBIN is None: - utils.send_task(app, 'WINETRICKSBIN') - if config.DIALOG == 'curses': - app.tricksbin_e.wait() - config.WINETRICKSBIN = app.tricksbin_q.get() + if config.WINETRICKSBIN is None: + # Check if local winetricks version available; else, download it. + config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" + + winetricks_options = utils.get_winetricks_options() + + if app: + if config.DIALOG == 'cli': + app.input_q.put((f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux.", winetricks_options)) + app.event.wait() + app.event.clear() + winetricksbin = app.choice_q.get() + else: + utils.send_task(app, 'WINETRICKSBIN') + if config.DIALOG == 'curses': + app.tricksbin_e.wait() + winetricksbin = app.tricksbin_q.get() + + if not winetricksbin.startswith('Download'): + config.WINETRICKSBIN = winetricksbin + else: + config.WINETRICKSBIN = winetricks_options[0] else: m = f"{utils.get_calling_function_name()}: --install-app is broken" logging.critical(m) @@ -241,7 +279,10 @@ def ensure_installation_config(app=None): logging.debug(f"> {config.LOGOS64_URL=}") if app: - utils.send_task(app, 'INSTALL') + if config.DIALOG == 'cli': + msg.logos_msg("Install is running…") + else: + utils.send_task(app, 'INSTALL') def ensure_install_dirs(app=None): @@ -274,10 +315,10 @@ def ensure_install_dirs(app=None): logging.debug(f"> {config.WINEPREFIX=}") if app: - utils.send_task(app, 'INSTALLING') - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) + if config.DIALOG == 'cli': + pass + else: + utils.send_task(app, 'INSTALLING') def ensure_sys_deps(app=None): @@ -608,18 +649,24 @@ def ensure_config_file(app=None): if different: if app: - utils.send_task(app, 'CONFIG') - if config.DIALOG == 'curses': - app.config_e.wait() - elif msg.logos_acknowledge_question( - f"Update config file at {config.CONFIG_FILE}?", - "The existing config file was not overwritten." - ): - logging.info("Updating config file.") - utils.write_config(config.CONFIG_FILE) + if config.DIALOG == 'cli': + if msg.logos_acknowledge_question( + f"Update config file at {config.CONFIG_FILE}?", + "The existing config file was not overwritten.", + "" + ): + logging.info("Updating config file.") + utils.write_config(config.CONFIG_FILE) + else: + utils.send_task(app, 'CONFIG') + if config.DIALOG == 'curses': + app.config_e.wait() if app: - utils.send_task(app, 'DONE') + if config.DIALOG == 'cli': + msg.logos_msg("Install has finished.") + else: + utils.send_task(app, 'DONE') logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 @@ -697,12 +744,15 @@ def ensure_launcher_shortcuts(app=None): fpath = Path.home() / '.local' / 'share' / 'applications' / f logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") else: - logging.debug("Running from source. Skipping launcher creation.") update_install_feedback( "Running from source. Skipping launcher creation.", app=app ) + if app: + if config.DIALOG == 'cli': + app.stop() + def update_install_feedback(text, app=None): percent = get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) diff --git a/main.py b/main.py index 22bdb351..1fa92f69 100755 --- a/main.py +++ b/main.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 import argparse + +import cli import config import control import curses @@ -239,7 +241,7 @@ def parse_args(args, parser): # Set ACTION function. actions = { - 'install_app': installer.ensure_launcher_shortcuts, + 'install_app': run_cli, 'run_installed_app': logos.LogosManager().start, 'run_indexing': logos.LogosManager().index, 'remove_library_catalog': control.remove_library_catalog, @@ -285,6 +287,10 @@ def parse_args(args, parser): logging.debug(f"{config.ACTION=}") +def run_cli(): + cli.command_line_interface() + + def run_control_panel(): logging.info(f"Using DIALOG: {config.DIALOG}") if config.DIALOG is None or config.DIALOG == 'tk': diff --git a/msg.py b/msg.py index 2c231eec..634f90d1 100644 --- a/msg.py +++ b/msg.py @@ -226,9 +226,9 @@ def gui_continue_question(question_text, no_text, secondary): logos_error(no_text) -def cli_acknowledge_question(QUESTION_TEXT, NO_TEXT): - if not cli_question(QUESTION_TEXT): - logos_msg(NO_TEXT) +def cli_acknowledge_question(question_text, no_text, secondary): + if not cli_question(question_text, secondary): + logos_msg(no_text) return False else: return True @@ -264,11 +264,11 @@ def logos_continue_question(question_text, no_text, secondary, app=None): logos_error(f"Unhandled question: {question_text}") -def logos_acknowledge_question(question_text, no_text): +def logos_acknowledge_question(question_text, no_text, secondary): if config.DIALOG == 'curses': pass else: - return cli_acknowledge_question(question_text, no_text) + return cli_acknowledge_question(question_text, no_text, secondary) def get_progress_str(percent): diff --git a/network.py b/network.py index 79d4f498..2d5c7532 100644 --- a/network.py +++ b/network.py @@ -546,11 +546,15 @@ def get_logos_releases(app=None): filtered_releases = releases if app: - app.releases_q.put(filtered_releases) if config.DIALOG == 'tk': + app.releases_q.put(filtered_releases) app.root.event_generate(app.release_evt) elif config.DIALOG == 'curses': + app.releases_q.put(filtered_releases) app.releases_e.set() + elif config.DIALOG == 'cli': + app.input_q.put((f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?: ", filtered_releases)) + app.event.set() return filtered_releases diff --git a/system.py b/system.py index 15abe54c..5d0591b6 100644 --- a/system.py +++ b/system.py @@ -738,6 +738,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) app.manualinstall_e.wait() if not install_deps_failed and not manual_install_required: + if config.DIALOG == 'cli': + command_str = command_str.replace("pkexec", "sudo") try: logging.debug(f"Attempting to run this command: {command_str}") run_command(command_str, shell=True) diff --git a/tui_app.py b/tui_app.py index 97d38247..c4e92749 100644 --- a/tui_app.py +++ b/tui_app.py @@ -68,7 +68,7 @@ def __init__(self, stdscr): self.installdeps_e = threading.Event() self.installdir_q = Queue() self.installdir_e = threading.Event() - self.wine_q = Queue() + self.wines_q = Queue() self.wine_e = threading.Event() self.tricksbin_q = Queue() self.tricksbin_e = threading.Event() @@ -362,7 +362,7 @@ def task_processor(self, evt=None, task=None): utils.start_thread(self.get_wine, config.use_python_dialog) elif task == 'WINETRICKSBIN': utils.start_thread(self.get_winetricksbin, config.use_python_dialog) - elif task == 'INSTALLING': + elif task == 'INSTALL' or task == 'INSTALLING': utils.start_thread(self.get_waiting, config.use_python_dialog) elif task == 'INSTALLING_PW': utils.start_thread(self.get_waiting, config.use_python_dialog, screen_id=15) @@ -754,10 +754,7 @@ def get_release(self, dialog): utils.start_thread(network.get_logos_releases, daemon_bool=True, app=self) self.releases_e.wait() - if config.TARGETVERSION == '10': - labels = self.releases_q.get() - elif config.TARGETVERSION == '9': - labels = self.releases_q.get() + labels = self.releases_q.get() if labels is None: msg.logos_error("Failed to fetch TARGET_RELEASE_VERSION.") @@ -787,7 +784,7 @@ def set_installdir(self, choice): def get_wine(self, dialog): self.installdir_e.wait() - self.screen_q.put(self.stack_text(10, self.wine_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog)) + self.screen_q.put(self.stack_text(10, self.wines_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog)) question = f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?" # noqa: E501 labels = utils.get_wine_options( utils.find_appimage_files(config.TARGET_RELEASE_VERSION), @@ -798,10 +795,10 @@ def get_wine(self, dialog): max_length += len(str(len(labels))) + 10 options = self.which_dialog_options(labels, dialog) self.menu_options = options - self.screen_q.put(self.stack_menu(6, self.wine_q, self.wine_e, question, options, width=max_length, dialog=dialog)) + self.screen_q.put(self.stack_menu(6, self.wines_q, self.wine_e, question, options, width=max_length, dialog=dialog)) def set_wine(self, choice): - self.wine_q.put(utils.get_relative_path(utils.get_config_var(choice), config.INSTALLDIR)) + self.wines_q.put(utils.get_relative_path(utils.get_config_var(choice), config.INSTALLDIR)) self.menu_screen.choice = "Processing" self.wine_e.set() diff --git a/utils.py b/utils.py index a571d2d6..d6f375f8 100644 --- a/utils.py +++ b/utils.py @@ -425,8 +425,10 @@ def get_wine_options(appimages, binaries, app=None) -> Union[List[List[str]], Li logging.debug(f"{wine_binary_options=}") if app: - app.wines_q.put(wine_binary_options) - app.root.event_generate(app.wine_evt) + if config.DIALOG != "cli": + app.wines_q.put(wine_binary_options) + if config.DIALOG == 'tk': + app.root.event_generate(app.wine_evt) return wine_binary_options From d1b804fbd310f5107a3b6362b0ef8e0b643f1073 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 05:59:47 +0100 Subject: [PATCH 181/253] add FIXME --- control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/control.py b/control.py index 7d3bad98..effb6b24 100644 --- a/control.py +++ b/control.py @@ -211,6 +211,7 @@ def copy_data(src_dirs, dst_dir): def remove_install_dir(): folder = Path(config.INSTALLDIR) + # FIXME: msg.cli_question needs additional arg if ( folder.is_dir() and msg.cli_question(f"Delete \"{folder}\" and all its contents?") From 68d2271be5fd6d92495c874d82b5ff6461bfd7fb Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 06:00:51 +0100 Subject: [PATCH 182/253] fix typo blocking selection of wine binary --- tui_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tui_app.py b/tui_app.py index c4e92749..9be36b0e 100644 --- a/tui_app.py +++ b/tui_app.py @@ -618,7 +618,7 @@ def wine_select(self, choice): config.WINE_EXE = choice if choice: self.menu_screen.choice = "Processing" - self.wine_q.put(config.WINE_EXE) + self.wines_q.put(config.WINE_EXE) self.wine_e.set() def winetricksbin_select(self, choice): From 23b347f254aac27860efccc0016a8c4ff1023d06 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 07:21:03 +0100 Subject: [PATCH 183/253] add "choose default with Enter" feature --- cli.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index 832e6b9c..8e3830dd 100644 --- a/cli.py +++ b/cli.py @@ -28,7 +28,6 @@ def run(self): msg.logos_msg("Exiting CLI installer.") - def user_input_processor(self): prompt = None question = None @@ -40,7 +39,13 @@ def user_input_processor(self): question = prompt[0] options = prompt[1] if question is not None and options is not None: - choice = input(f"{question}: {options}: ") + # Convert options list to string. + default = options[0] + options[0] = f"{options[0]} [default]" + optstr = ', '.join(options) + choice = input(f"{question}: {optstr}: ") + if len(choice) == 0: + choice = default if choice is not None and choice.lower() == 'exit': self.running = False if choice is not None: From 8c79e29c2c2528ac2245a98f7fcf3a84617af25c Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 07:22:22 +0100 Subject: [PATCH 184/253] fix crazy slow MD5 sum calculation --- network.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/network.py b/network.py index 2d5c7532..43a00220 100644 --- a/network.py +++ b/network.py @@ -44,10 +44,9 @@ def get_size(self): def get_md5(self): if self.path is None: return - logging.debug("This may take a while…") md5 = hashlib.md5() with self.path.open('rb') as f: - for chunk in iter(lambda: f.read(4096), b''): + for chunk in iter(lambda: f.read(524288), b''): md5.update(chunk) self.md5 = b64encode(md5.digest()).decode('utf-8') logging.debug(f"{str(self.path)} MD5: {self.md5}") @@ -349,9 +348,6 @@ def same_md5(url, file_path): if url_md5 is None: # skip MD5 check if not provided with URL res = True else: - # TODO: Figure out why this is taking a long time. - # On 20240922, I ran into an issue such that it would take - # upwards of 6.5 minutes to complete file_md5 = FileProps(file_path).get_md5() logging.debug(f"{file_md5=}") res = url_md5 == file_md5 From 82d75e528fe845da37134462e0b2bdf4f2e94d76 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 14:52:40 +0100 Subject: [PATCH 185/253] use separate events for input_q and choice_q --- cli.py | 75 +++++++++++++++++++++++++++++----------------------- installer.py | 44 +++++++++++++++++------------- main.py | 6 +---- network.py | 2 +- 4 files changed, 70 insertions(+), 57 deletions(-) diff --git a/cli.py b/cli.py index 8e3830dd..8311b7fb 100644 --- a/cli.py +++ b/cli.py @@ -1,56 +1,65 @@ -import logging -import threading +# import logging import queue +import threading import config import installer -import msg +import logos +# import msg import utils class CLI: def __init__(self): + config.DIALOG = "cli" self.running = True self.choice_q = queue.Queue() self.input_q = queue.Queue() - self.event = threading.Event() + self.input_event = threading.Event() + self.choice_event = threading.Event() + self.logos = logos.LogosManager(app=self) def stop(self): self.running = False - def run(self): - config.DIALOG = "cli" + def install_app(self): + self.thread = utils.start_thread( + installer.ensure_launcher_shortcuts, + app=self + ) + self.user_input_processor() - self.thread = utils.start_thread(installer.ensure_launcher_shortcuts, daemon_bool=True, app=self) + def run_installed_app(self): + self.thread = utils.start_thread(self.logos.start, app=self) + def user_input_processor(self, evt=None): while self.running: - self.user_input_processor() - - msg.logos_msg("Exiting CLI installer.") - - def user_input_processor(self): - prompt = None - question = None - options = None - choice = None - if self.input_q.qsize() > 0: + prompt = None + question = None + options = None + choice = None + # Wait for next input queue item. + self.input_event.wait() + self.input_event.clear() prompt = self.input_q.get() - if prompt is not None and isinstance(prompt, tuple): - question = prompt[0] - options = prompt[1] - if question is not None and options is not None: - # Convert options list to string. - default = options[0] - options[0] = f"{options[0]} [default]" - optstr = ', '.join(options) - choice = input(f"{question}: {optstr}: ") - if len(choice) == 0: - choice = default - if choice is not None and choice.lower() == 'exit': - self.running = False - if choice is not None: - self.choice_q.put(choice) - self.event.set() + if prompt is None: + return + if prompt is not None and isinstance(prompt, tuple): + question = prompt[0] + options = prompt[1] + if question is not None and options is not None: + # Convert options list to string. + default = options[0] + options[0] = f"{options[0]} [default]" + optstr = ', '.join(options) + choice = input(f"{question}: {optstr}: ") + if len(choice) == 0: + choice = default + if choice is not None and choice.lower() == 'exit': + self.running = False + if choice is not None: + self.choice_q.put(choice) + self.choice_event.set() def command_line_interface(): diff --git a/installer.py b/installer.py index ecab6ffe..6056dba8 100644 --- a/installer.py +++ b/installer.py @@ -27,8 +27,9 @@ def ensure_product_choice(app=None): if app: if config.DIALOG == 'cli': app.input_q.put(("Choose which FaithLife product the script should install: ", ["Logos", "Verbum", "Exit"])) - app.event.wait() - app.event.clear() + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() config.FLPRODUCT = app.choice_q.get() else: utils.send_task(app, 'FLPRODUCT') @@ -61,8 +62,9 @@ def ensure_version_choice(app=None): if app: if config.DIALOG == 'cli': app.input_q.put((f"Which version of {config.FLPRODUCT} should the script install?: ", ["10", "9", "Exit"])) - app.event.wait() - app.event.clear() + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() config.TARGETVERSION = app.choice_q.get() else: utils.send_task(app, 'TARGETVERSION') @@ -87,10 +89,9 @@ def ensure_release_choice(app=None): if app: if config.DIALOG == 'cli': utils.start_thread(network.get_logos_releases, daemon_bool=True, app=app) - app.event.wait() - app.event.clear() - app.event.wait() # Wait for user input queue to receive input - app.event.clear() + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() config.TARGET_RELEASE_VERSION = app.choice_q.get() else: utils.send_task(app, 'TARGET_RELEASE_VERSION') @@ -122,8 +123,9 @@ def ensure_install_dir_choice(app=None): default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 question = f"Where should {config.FLPRODUCT} files be installed to?: " # noqa: E501 app.input_q.put((question, [default, "Type your own custom path", "Exit"])) - app.event.wait() - app.event.clear() + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() config.INSTALLDIR = app.choice_q.get() elif config.DIALOG == 'tk': config.INSTALLDIR = default @@ -163,9 +165,10 @@ def ensure_wine_choice(app=None): if config.DIALOG == 'cli': app.input_q.put(( f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?: ", options)) - app.event.set() - app.event.wait() - app.event.clear() + app.input_event.set() + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() config.WINE_EXE = utils.get_relative_path(utils.get_config_var(app.choice_q.get()), config.INSTALLDIR) else: utils.send_task(app, 'WINE_EXE') @@ -208,8 +211,9 @@ def ensure_winetricks_choice(app=None): if app: if config.DIALOG == 'cli': app.input_q.put((f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux.", winetricks_options)) - app.event.wait() - app.event.clear() + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() winetricksbin = app.choice_q.get() else: utils.send_task(app, 'WINETRICKSBIN') @@ -221,9 +225,9 @@ def ensure_winetricks_choice(app=None): config.WINETRICKSBIN = winetricksbin else: config.WINETRICKSBIN = winetricks_options[0] - else: - m = f"{utils.get_calling_function_name()}: --install-app is broken" - logging.critical(m) + # else: + # m = f"{utils.get_calling_function_name()}: --install-app is broken" + # logging.critical(m) logging.debug(f"> {config.WINETRICKSBIN=}") @@ -751,6 +755,10 @@ def ensure_launcher_shortcuts(app=None): if app: if config.DIALOG == 'cli': + # Signal CLI.user_input_processor to stop. + app.input_q.put(None) + app.input_event.set() + # Signal CLI itself to stop. app.stop() diff --git a/main.py b/main.py index 1fa92f69..175cadd6 100755 --- a/main.py +++ b/main.py @@ -241,7 +241,7 @@ def parse_args(args, parser): # Set ACTION function. actions = { - 'install_app': run_cli, + 'install_app': cli.CLI().install_app, 'run_installed_app': logos.LogosManager().start, 'run_indexing': logos.LogosManager().index, 'remove_library_catalog': control.remove_library_catalog, @@ -287,10 +287,6 @@ def parse_args(args, parser): logging.debug(f"{config.ACTION=}") -def run_cli(): - cli.command_line_interface() - - def run_control_panel(): logging.info(f"Using DIALOG: {config.DIALOG}") if config.DIALOG is None or config.DIALOG == 'tk': diff --git a/network.py b/network.py index 43a00220..1053840b 100644 --- a/network.py +++ b/network.py @@ -550,7 +550,7 @@ def get_logos_releases(app=None): app.releases_e.set() elif config.DIALOG == 'cli': app.input_q.put((f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?: ", filtered_releases)) - app.event.set() + app.input_event.set() return filtered_releases From f00f71139a101d1ae0ce658c6c02061f084de4d4 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 15:08:14 +0100 Subject: [PATCH 186/253] initial work for --run-installed-app --- cli.py | 2 +- main.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cli.py b/cli.py index 8311b7fb..84ca3f09 100644 --- a/cli.py +++ b/cli.py @@ -30,7 +30,7 @@ def install_app(self): self.user_input_processor() def run_installed_app(self): - self.thread = utils.start_thread(self.logos.start, app=self) + self.logos.start() def user_input_processor(self, evt=None): while self.running: diff --git a/main.py b/main.py index 175cadd6..22197a0b 100755 --- a/main.py +++ b/main.py @@ -242,7 +242,7 @@ def parse_args(args, parser): # Set ACTION function. actions = { 'install_app': cli.CLI().install_app, - 'run_installed_app': logos.LogosManager().start, + 'run_installed_app': cli.CLI().run_installed_app, 'run_indexing': logos.LogosManager().index, 'remove_library_catalog': control.remove_library_catalog, 'remove_index_files': control.remove_all_index_files, @@ -413,6 +413,7 @@ def main(): 'remove_library_catalog', 'restore', 'run_indexing', + 'run_installed_app', 'run_logos', 'switch_logging', ] @@ -423,7 +424,7 @@ def main(): config.ACTION() elif utils.app_is_installed(): # Run the desired Logos action. - logging.info(f"Running function: {config.ACTION.__name__}") + logging.info(f"Running function for installed app: {config.ACTION.__name__}") # noqa: E501 config.ACTION() # defaults to run_control_panel() else: logging.info("Starting Control Panel") From b5e93b4679212595e50e885a163996f19c1eea10 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 15:08:41 +0100 Subject: [PATCH 187/253] additional TODOs, etc. --- main.py | 2 ++ utils.py | 1 + 2 files changed, 3 insertions(+) diff --git a/main.py b/main.py index 22197a0b..6663d85a 100755 --- a/main.py +++ b/main.py @@ -54,6 +54,7 @@ def get_parser(): help='skip font installations', ) cfg.add_argument( + # TODO: Make this a hidden option? '-W', '--skip-winetricks', action='store_true', help='skip winetricks installations. For development purposes only!!!', ) @@ -181,6 +182,7 @@ def get_parser(): help='[re-]create app shortcuts', ) cmd.add_argument( + # TODO: Make this a hidden option? '--remove-install-dir', action='store_true', help='delete the current installation folder', ) diff --git a/utils.py b/utils.py index d6f375f8..ce795108 100644 --- a/utils.py +++ b/utils.py @@ -243,6 +243,7 @@ def check_dependencies(app=None): if app: if config.DIALOG == "tk": + # FIXME: This should get moved to gui_app. app.root.event_generate('<>') From d1dab09a79eee68c34bbb79438077c77014d43b6 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 15:10:15 +0100 Subject: [PATCH 188/253] minor CLI fixes --- msg.py | 2 +- system.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/msg.py b/msg.py index 634f90d1..2d2517e4 100644 --- a/msg.py +++ b/msg.py @@ -246,7 +246,7 @@ def cli_ask_filepath(question_text): def logos_continue_question(question_text, no_text, secondary, app=None): if config.DIALOG == 'tk': gui_continue_question(question_text, no_text, secondary) - elif app is None: + elif config.DIALOG == 'cli': cli_continue_question(question_text, no_text, secondary) elif config.DIALOG == 'curses': app.screen_q.put( diff --git a/system.py b/system.py index 5d0591b6..0d006db0 100644 --- a/system.py +++ b/system.py @@ -282,8 +282,8 @@ def get_dialog(): # Set config.DIALOG. if dialog is not None: dialog = dialog.lower() - if dialog not in ['curses', 'tk']: - msg.logos_error("Valid values for DIALOG are 'curses' or 'tk'.") + if dialog not in ['cli', 'curses', 'tk']: + msg.logos_error("Valid values for DIALOG are 'cli', 'curses' or 'tk'.") # noqa: E501 config.DIALOG = dialog elif sys.__stdin__.isatty(): config.DIALOG = 'curses' @@ -780,8 +780,9 @@ def have_lib(library, ld_library_path): available_library_paths = ['/usr/lib', '/lib'] if ld_library_path is not None: available_library_paths = [*ld_library_path.split(':'), *available_library_paths] - roots = [root for root in available_library_paths if not Path(root).is_symlink()] - logging.debug(f"Library Paths: {roots}") + + roots = [root for root in available_library_paths if not Path(root).is_symlink()] + logging.debug(f"Library Paths: {roots}") for root in roots: libs = [] logging.debug(f"Have lib? Checking {root}") @@ -796,7 +797,7 @@ def have_lib(library, ld_library_path): def check_libs(libraries, app=None): - ld_library_path = os.environ.get('LD_LIBRARY_PATH', '') + ld_library_path = os.environ.get('LD_LIBRARY_PATH') for library in libraries: have_lib_result = have_lib(library, ld_library_path) if have_lib_result: From 22cbaca0b9e7af6b0b240ae7ebdb653493bf7951 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 19:24:01 +0100 Subject: [PATCH 189/253] fix incorrect setting of config.DIALOG as 'cli' when using other DIALOGs --- main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 6663d85a..91bcd37e 100755 --- a/main.py +++ b/main.py @@ -243,8 +243,8 @@ def parse_args(args, parser): # Set ACTION function. actions = { - 'install_app': cli.CLI().install_app, - 'run_installed_app': cli.CLI().run_installed_app, + 'install_app': install_app, + 'run_installed_app': run_installed_app, 'run_indexing': logos.LogosManager().index, 'remove_library_catalog': control.remove_library_catalog, 'remove_index_files': control.remove_all_index_files, @@ -289,6 +289,17 @@ def parse_args(args, parser): logging.debug(f"{config.ACTION=}") +# NOTE: install_app() and run_installed_app() have to be explicit functions to +# avoid instantiating cli.CLI() when CLI is not being run. Otherwise, +# config.DIALOG is incorrectly set as 'cli'. +def install_app(): + cli.CLI().install_app() + + +def run_installed_app(): + cli.CLI().run_installed_app() + + def run_control_panel(): logging.info(f"Using DIALOG: {config.DIALOG}") if config.DIALOG is None or config.DIALOG == 'tk': From 29eed5763e06abbde5b6c4827c0bab49befa04e9 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 10:06:59 +0100 Subject: [PATCH 190/253] add TODO --- wine.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wine.py b/wine.py index bfcf6828..bb9ffa4c 100644 --- a/wine.py +++ b/wine.py @@ -19,6 +19,9 @@ def set_logos_paths(): config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 config.logos_indexing_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 + # TODO: Can't this just be set based on WINEPREFIX and USER vars without + # having to walk through the WINEPREFIX tree? Or maybe this is to account + # for a non-typical installation location? for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 for f in files: if f == "LogosIndexer.exe" and root.endswith("Logos/System"): From bd7e048cf5163897fd55668944170823e43d006b Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 10:53:48 +0100 Subject: [PATCH 191/253] use single func wine.wineserver_wait; log if procs still using WINEPREFIX --- installer.py | 6 ++-- system.py | 82 ++++++++++++++++++++++++++-------------------------- utils.py | 49 ++++++++++++++++++++----------- wine.py | 24 ++++++++++----- 4 files changed, 93 insertions(+), 68 deletions(-) diff --git a/installer.py b/installer.py index 6056dba8..43097753 100644 --- a/installer.py +++ b/installer.py @@ -522,7 +522,8 @@ def ensure_wineprefix_init(app=None): logging.debug("Initializing wineprefix.") process = wine.initializeWineBottle() wine.wait_pid(process) - wine.light_wineserver_wait() + # wine.light_wineserver_wait() + wine.wineserver_wait() logging.debug("Wine init complete.") logging.debug(f"> {init_file} exists?: {init_file.is_file()}") @@ -580,7 +581,8 @@ def ensure_winetricks_applied(app=None): msg.logos_msg(f"Setting {config.FLPRODUCT} Bible Indexing to Win10 Mode…") # noqa: E501 wine.set_win_version("indexer", "win10") - wine.light_wineserver_wait() + # wine.light_wineserver_wait() + wine.wineserver_wait() logging.debug("> Done.") diff --git a/system.py b/system.py index 0d006db0..41cebf7f 100644 --- a/system.py +++ b/system.py @@ -183,33 +183,33 @@ def popen_command(command, retries=1, delay=0, **kwargs): return None -def wait_on(command): - try: - # Start the process in the background - # TODO: Convert to use popen_command() - process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - msg.status(f"Waiting on \"{' '.join(command)}\" to finish.", end='') - time.sleep(1.0) - while process.poll() is None: - msg.logos_progress() - time.sleep(0.5) - print() - - # Process has finished, check the result - stdout, stderr = process.communicate() - - if process.returncode == 0: - logging.info(f"\"{' '.join(command)}\" has ended properly.") - else: - logging.error(f"Error: {stderr}") - - except Exception as e: - logging.critical(f"{e}") +# def wait_on(command): +# try: +# # Start the process in the background +# # TODO: Convert to use popen_command() +# process = subprocess.Popen( +# command, +# stdout=subprocess.PIPE, +# stderr=subprocess.PIPE, +# text=True +# ) +# msg.status(f"Waiting on \"{' '.join(command)}\" to finish.", end='') +# time.sleep(1.0) +# while process.poll() is None: +# msg.logos_progress() +# time.sleep(0.5) +# print() + +# # Process has finished, check the result +# stdout, stderr = process.communicate() + +# if process.returncode == 0: +# logging.info(f"\"{' '.join(command)}\" has ended properly.") +# else: +# logging.error(f"Error: {stderr}") + +# except Exception as e: +# logging.critical(f"{e}") def get_pids(query): @@ -230,20 +230,20 @@ def get_logos_pids(): config.processes[config.logos_indexer_exe] = get_pids(config.logos_indexer_exe) -def get_pids_using_file(file_path, mode=None): - # Make list (set) of pids using 'directory'. - pids = set() - for proc in psutil.process_iter(['pid', 'open_files']): - try: - if mode is not None: - paths = [f.path for f in proc.open_files() if f.mode == mode] - else: - paths = [f.path for f in proc.open_files()] - if len(paths) > 0 and file_path in paths: - pids.add(proc.pid) - except psutil.AccessDenied: - pass - return pids +# def get_pids_using_file(file_path, mode=None): +# # Make list (set) of pids using 'directory'. +# pids = set() +# for proc in psutil.process_iter(['pid', 'open_files']): +# try: +# if mode is not None: +# paths = [f.path for f in proc.open_files() if f.mode == mode] +# else: +# paths = [f.path for f in proc.open_files()] +# if len(paths) > 0 and file_path in paths: +# pids.add(proc.pid) +# except psutil.AccessDenied: +# pass +# return pids def reboot(): diff --git a/utils.py b/utils.py index ce795108..9580c38e 100644 --- a/utils.py +++ b/utils.py @@ -449,31 +449,46 @@ def get_winetricks_options(): return winetricks_options -def get_pids_using_file(file_path, mode=None): - pids = set() - for proc in psutil.process_iter(['pid', 'open_files']): +def get_procs_using_file(file_path, mode=None): + procs = set() + for proc in psutil.process_iter(['pid', 'open_files', 'name']): try: if mode is not None: paths = [f.path for f in proc.open_files() if f.mode == mode] else: paths = [f.path for f in proc.open_files()] if len(paths) > 0 and file_path in paths: - pids.add(proc.pid) + procs.add(proc.pid) except psutil.AccessDenied: pass - return pids - - -def wait_process_using_dir(directory): - logging.info(f"* Starting wait_process_using_dir for {directory}…") - - # Get pids and wait for them to finish. - pids = get_pids_using_file(directory) - for pid in pids: - logging.info(f"wait_process_using_dir PID: {pid}") - psutil.wait(pid) - - logging.info("* End of wait_process_using_dir.") + return procs + + +# def get_pids_using_file(file_path, mode=None): +# pids = set() +# for proc in psutil.process_iter(['pid', 'open_files']): +# try: +# if mode is not None: +# paths = [f.path for f in proc.open_files() if f.mode == mode] +# else: +# paths = [f.path for f in proc.open_files()] +# if len(paths) > 0 and file_path in paths: +# pids.add(proc.pid) +# except psutil.AccessDenied: +# pass +# return pids + + +# def wait_process_using_dir(directory): +# logging.info(f"* Starting wait_process_using_dir for {directory}…") + +# # Get pids and wait for them to finish. +# pids = get_pids_using_file(directory) +# for pid in pids: +# logging.info(f"wait_process_using_dir PID: {pid}") +# psutil.wait(pid) + +# logging.info("* End of wait_process_using_dir.") def write_progress_bar(percent, screen_width=80): diff --git a/wine.py b/wine.py index bb9ffa4c..0fb8232c 100644 --- a/wine.py +++ b/wine.py @@ -59,14 +59,15 @@ def wineserver_wait(): process.wait() -def light_wineserver_wait(): - command = [f"{config.WINESERVER_EXE}", "-w"] - system.wait_on(command) +# def light_wineserver_wait(): +# command = [f"{config.WINESERVER_EXE}", "-w"] +# system.wait_on(command) -def heavy_wineserver_wait(): - utils.wait_process_using_dir(config.WINEPREFIX) - system.wait_on([f"{config.WINESERVER_EXE}", "-w"]) +# def heavy_wineserver_wait(): +# utils.wait_process_using_dir(config.WINEPREFIX) +# # system.wait_on([f"{config.WINESERVER_EXE}", "-w"]) +# wineserver_wait() def end_wine_processes(): @@ -215,7 +216,8 @@ def wine_reg_install(reg_file): msg.logos_error(f"{failed}: {reg_file}") elif process.returncode == 0: logging.info(f"{reg_file} installed.") - light_wineserver_wait() + # light_wineserver_wait() + wineserver_wait() def install_msi(app=None): @@ -313,7 +315,13 @@ def run_winetricks_cmd(*args): process = run_wine_proc(config.WINETRICKSBIN, exe_args=cmd) wait_pid(process) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") - heavy_wineserver_wait() + # heavy_wineserver_wait() + wineserver_wait() + logging.debug(f"procs using {config.WINEPREFIX}:") + for proc in utils.get_procs_using_file(config.WINEPREFIX): + logging.debug(f"{proc=}") + else: + logging.debug('') def install_d3d_compiler(): From 21bd78e76efb3287862a0e1de085cf40b2b29e07 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 11:12:22 +0100 Subject: [PATCH 192/253] only kill wine procs in close func if exiting from Control Panel --- main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 91bcd37e..1baa3ed3 100755 --- a/main.py +++ b/main.py @@ -448,10 +448,12 @@ def close(): logging.debug("Closing Logos on Linux.") for thread in threads: thread.join() - if len(processes) > 0: + # Only kill wine processes if closing the Control Panel. Otherwise, some + # CLI commands get killed as soon as they're started. + if config.ACTION.__name__ == 'run_control_panel' and len(processes) > 0: wine.end_wine_processes() else: - logging.debug("No processes found.") + logging.debug("No extra processes found.") logging.debug("Closing Logos on Linux finished.") From bbfa927fa7baa10f0b5f71096c7d3c8dea13f825 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 13:00:41 +0100 Subject: [PATCH 193/253] fix --set-appimage CLI subcommand --- cli.py | 15 +++++++-- gui_app.py | 3 +- installer.py | 6 ++-- main.py | 20 ++++-------- utils.py | 88 +++++++++++++++++++++++++--------------------------- 5 files changed, 66 insertions(+), 66 deletions(-) diff --git a/cli.py b/cli.py index 84ca3f09..ac9c9f20 100644 --- a/cli.py +++ b/cli.py @@ -32,6 +32,9 @@ def install_app(self): def run_installed_app(self): self.logos.start() + def set_appimage(self): + utils.set_appimage_symlink(app=self) + def user_input_processor(self, evt=None): while self.running: prompt = None @@ -62,5 +65,13 @@ def user_input_processor(self, evt=None): self.choice_event.set() -def command_line_interface(): - CLI().run() +def install_app(): + CLI().install_app() + + +def run_installed_app(): + CLI().run_installed_app() + + +def set_appimage(): + CLI().set_appimage() diff --git a/gui_app.py b/gui_app.py index c0dd2181..536a5bf4 100644 --- a/gui_app.py +++ b/gui_app.py @@ -759,7 +759,8 @@ def set_appimage(self, evt=None): appimage_filename = self.open_file_dialog("AppImage", "AppImage") if not appimage_filename: return - config.SELECTED_APPIMAGE_FILENAME = appimage_filename + # config.SELECTED_APPIMAGE_FILENAME = appimage_filename + config.APPIMAGE_FILE_PATH = appimage_filename utils.start_thread(utils.set_appimage_symlink, app=self) def get_winetricks(self, evt=None): diff --git a/installer.py b/installer.py index 43097753..ba35e2b7 100644 --- a/installer.py +++ b/installer.py @@ -162,10 +162,8 @@ def ensure_wine_choice(app=None): utils.find_appimage_files(config.TARGET_RELEASE_VERSION), utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) ) - if config.DIALOG == 'cli': - app.input_q.put(( - f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?: ", options)) - app.input_event.set() + app.input_q.put(( + f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?: ", options)) app.input_event.set() app.choice_event.wait() app.choice_event.clear() diff --git a/main.py b/main.py index 1baa3ed3..b1260611 100755 --- a/main.py +++ b/main.py @@ -213,6 +213,9 @@ def parse_args(args, parser): if args.delete_log: config.DELETE_LOG = True + if args.set_appimage: + config.APPIMAGE_FILE_PATH = args.set_appimage[0] + if args.skip_fonts: config.SKIP_FONTS = True @@ -243,8 +246,8 @@ def parse_args(args, parser): # Set ACTION function. actions = { - 'install_app': install_app, - 'run_installed_app': run_installed_app, + 'install_app': cli.install_app, + 'run_installed_app': cli.run_installed_app, 'run_indexing': logos.LogosManager().index, 'remove_library_catalog': control.remove_library_catalog, 'remove_index_files': control.remove_all_index_files, @@ -254,7 +257,7 @@ def parse_args(args, parser): 'restore': control.restore, 'update_self': utils.update_to_latest_lli_release, 'update_latest_appimage': utils.update_to_latest_recommended_appimage, - 'set_appimage': utils.set_appimage_symlink, + 'set_appimage': cli.set_appimage, 'get_winetricks': control.set_winetricks, 'run_winetricks': wine.run_winetricks, 'install_d3d_compiler': wine.install_d3d_compiler, @@ -289,17 +292,6 @@ def parse_args(args, parser): logging.debug(f"{config.ACTION=}") -# NOTE: install_app() and run_installed_app() have to be explicit functions to -# avoid instantiating cli.CLI() when CLI is not being run. Otherwise, -# config.DIALOG is incorrectly set as 'cli'. -def install_app(): - cli.CLI().install_app() - - -def run_installed_app(): - cli.CLI().run_installed_app() - - def run_control_panel(): logging.info(f"Using DIALOG: {config.DIALOG}") if config.DIALOG is None or config.DIALOG == 'tk': diff --git a/utils.py b/utils.py index 9580c38e..60bec075 100644 --- a/utils.py +++ b/utils.py @@ -24,7 +24,9 @@ import network import system if system.have_dep("dialog"): - import tui_dialog + import tui_dialog as tui +else: + import tui_curses as tui import wine # TODO: Move config commands to config.py @@ -688,6 +690,7 @@ def is_appimage(file_path): return appimage_check, appimage_type else: + logging.error(f"File does not exist: {expanded_path}") return False, None @@ -791,27 +794,31 @@ def set_appimage_symlink(app=None): # if config.APPIMAGE_FILE_PATH is None: # config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 - # logging.debug(f"{config.APPIMAGE_FILE_PATH=}") - # if config.APPIMAGE_FILE_PATH == config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: # noqa: E501 - # get_recommended_appimage() - # selected_appimage_file_path = Path(config.APPDIR_BINDIR) / config.APPIMAGE_FILE_PATH # noqa: E501 - # else: - # selected_appimage_file_path = Path(config.APPIMAGE_FILE_PATH) - selected_appimage_file_path = Path(config.SELECTED_APPIMAGE_FILENAME) - - if not check_appimage(selected_appimage_file_path): - logging.warning(f"Cannot use {selected_appimage_file_path}.") - return - - copy_message = ( - f"Should the program copy {selected_appimage_file_path} to the" - f" {config.APPDIR_BINDIR} directory?" - ) - - # Determine if user wants their AppImage in the Logos on Linux bin dir. - if selected_appimage_file_path.exists(): - confirm = False + logging.debug(f"{config.APPIMAGE_FILE_PATH=}") + logging.debug(f"{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME=}") + appimage_file_path = Path(config.APPIMAGE_FILE_PATH) + appdir_bindir = Path(config.APPDIR_BINDIR) + appimage_symlink_path = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME + if appimage_file_path.name == config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: # noqa: E501 + # Default case. + network.get_recommended_appimage() + selected_appimage_file_path = Path(config.APPDIR_BINDIR) / appimage_file_path.name # noqa: E501 + bindir_appimage = selected_appimage_file_path / config.APPDIR_BINDIR + if not bindir_appimage.exists(): + logging.info(f"Copying {selected_appimage_file_path} to {config.APPDIR_BINDIR}.") # noqa: E501 + shutil.copy(selected_appimage_file_path, f"{config.APPDIR_BINDIR}") else: + selected_appimage_file_path = appimage_file_path + # Verify user-selected AppImage. + if not check_appimage(selected_appimage_file_path): + msg.logos_error(f"Cannot use {selected_appimage_file_path}.") + + # Determine if user wants their AppImage in the Logos on Linux bin dir. + copy_message = ( + f"Should the program copy {selected_appimage_file_path} to the" + f" {config.APPDIR_BINDIR} directory?" + ) + # FIXME: What if user cancels the confirmation dialog? if config.DIALOG == "tk": # TODO: With the GUI this runs in a thread. It's not clear if the # messagebox will work correctly. It may need to be triggered from @@ -820,34 +827,25 @@ def set_appimage_symlink(app=None): tk_root.withdraw() confirm = tk.messagebox.askquestion("Confirmation", copy_message) tk_root.destroy() - else: - confirm = tui_dialog.confirm("Confirmation", copy_message) - # FIXME: What if user cancels the confirmation dialog? + elif config.DIALOG in ['curses', 'dialog']: + confirm = tui.confirm("Confirmation", copy_message) + elif config.DIALOG == 'cli': + confirm = msg.logos_acknowledge_question(copy_message, '', '') + + # Copy AppImage if confirmed. + if confirm is True or confirm == 'yes': + logging.info(f"Copying {selected_appimage_file_path} to {config.APPDIR_BINDIR}.") # noqa: E501 + dest = appdir_bindir / selected_appimage_file_path.name + if not dest.exists(): + shutil.copy(selected_appimage_file_path, f"{config.APPDIR_BINDIR}") # noqa: E501 + selected_appimage_file_path = dest - appimage_symlink_path = Path(f"{config.APPDIR_BINDIR}/{config.APPIMAGE_LINK_SELECTION_NAME}") # noqa: E501 delete_symlink(appimage_symlink_path) - - # FIXME: confirm is always False b/c appimage_filepath always exists b/c - # it's copied in place via logos_reuse_download function above in - # get_recommended_appimage. - appimage_filename = selected_appimage_file_path.name - if confirm is True or confirm == 'yes': - logging.info(f"Copying {selected_appimage_file_path} to {config.APPDIR_BINDIR}.") # noqa: E501 - shutil.copy(selected_appimage_file_path, f"{config.APPDIR_BINDIR}") - os.symlink(selected_appimage_file_path, appimage_symlink_path) - config.SELECTED_APPIMAGE_FILENAME = f"{appimage_filename}" - # If not, use the selected AppImage's full path for link creation. - elif confirm is False or confirm == 'no': - logging.debug(f"{selected_appimage_file_path} already exists in {config.APPDIR_BINDIR}. No need to copy.") # noqa: E501 - os.symlink(selected_appimage_file_path, appimage_symlink_path) - logging.debug("AppImage symlink updated.") - config.SELECTED_APPIMAGE_FILENAME = f"{selected_appimage_file_path}" - logging.debug("Updated config with new AppImage path.") - else: - logging.error("Error getting user confirmation.") + os.symlink(selected_appimage_file_path, appimage_symlink_path) + config.SELECTED_APPIMAGE_FILENAME = f"{selected_appimage_file_path.name}" # noqa: E501 write_config(config.CONFIG_FILE) - if app: + if config.DIALOG == 'tk': app.root.event_generate("<>") From f4029368c953644c6b2e2d64f3bd2d5356f18f87 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 13:41:05 +0100 Subject: [PATCH 194/253] update list of subcommands that assume Logos is already installed --- main.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index b1260611..924139d3 100755 --- a/main.py +++ b/main.py @@ -246,26 +246,26 @@ def parse_args(args, parser): # Set ACTION function. actions = { - 'install_app': cli.install_app, - 'run_installed_app': cli.run_installed_app, - 'run_indexing': logos.LogosManager().index, - 'remove_library_catalog': control.remove_library_catalog, - 'remove_index_files': control.remove_all_index_files, - 'edit_config': control.edit_config, - 'install_dependencies': utils.check_dependencies, 'backup': control.backup, - 'restore': control.restore, - 'update_self': utils.update_to_latest_lli_release, - 'update_latest_appimage': utils.update_to_latest_recommended_appimage, - 'set_appimage': cli.set_appimage, + 'create_shortcuts': installer.ensure_launcher_shortcuts, + 'edit_config': control.edit_config, 'get_winetricks': control.set_winetricks, - 'run_winetricks': wine.run_winetricks, + 'install_app': cli.install_app, 'install_d3d_compiler': wine.install_d3d_compiler, + 'install_dependencies': utils.check_dependencies, 'install_fonts': wine.install_fonts, 'install_icu': wine.install_icu_data_files, - 'toggle_app_logging': logos.LogosManager().switch_logging, - 'create_shortcuts': installer.ensure_launcher_shortcuts, + 'remove_index_files': control.remove_all_index_files, 'remove_install_dir': control.remove_install_dir, + 'remove_library_catalog': control.remove_library_catalog, + 'restore': control.restore, + 'run_indexing': logos.LogosManager().index, + 'run_installed_app': cli.run_installed_app, + 'run_winetricks': wine.run_winetricks, + 'set_appimage': cli.set_appimage, + 'toggle_app_logging': logos.LogosManager().switch_logging, + 'update_self': utils.update_to_latest_lli_release, + 'update_latest_appimage': utils.update_to_latest_recommended_appimage, } config.ACTION = None @@ -414,13 +414,17 @@ def main(): install_required = [ 'backup', 'create_shortcuts', - 'remove_all_index_files', + 'install_d3d_compiler', + 'install_fonts', + 'install_icu', + 'remove_index_files', 'remove_library_catalog', 'restore', 'run_indexing', 'run_installed_app', - 'run_logos', - 'switch_logging', + 'run_winetricks', + 'set_appimage', + 'toggle_app_logging', ] if config.ACTION == "disabled": msg.logos_error("That option is disabled.", "info") From 31e2934b9cf2d6a6ed8b3a5a217ee4f6d98bff36 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 14:32:56 +0100 Subject: [PATCH 195/253] convert all actions to cli app methods --- cli.py | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- main.py | 36 ++++++++-------- 2 files changed, 141 insertions(+), 20 deletions(-) diff --git a/cli.py b/cli.py index ac9c9f20..e4a0f145 100644 --- a/cli.py +++ b/cli.py @@ -3,9 +3,11 @@ import threading import config +import control import installer import logos # import msg +import wine import utils @@ -19,8 +21,14 @@ def __init__(self): self.choice_event = threading.Event() self.logos = logos.LogosManager(app=self) - def stop(self): - self.running = False + def backup(self): + control.backup() + + def edit_config(self): + control.edit_config() + + def get_winetricks(self): + control.set_winetricks() def install_app(self): self.thread = utils.start_thread( @@ -29,12 +37,54 @@ def install_app(self): ) self.user_input_processor() + def install_d3d_compiler(self): + wine.install_d3d_compiler() + + def install_dependencies(self): + utils.check_dependencies() + + def install_fonts(self): + wine.install_fonts() + + def install_icu(self): + wine.install_icu_data_files() + + def remove_index_files(self): + control.remove_all_index_files() + + def remove_install_dir(self): + control.remove_install_dir() + + def remove_library_catalog(self): + control.remove_library_catalog() + + def restore(self): + control.restore() + + def run_indexing(self): + self.logos.index() + def run_installed_app(self): self.logos.start() + def run_winetricks(self): + wine.run_winetricks() + def set_appimage(self): utils.set_appimage_symlink(app=self) + def stop(self): + self.running = False + + def toggle_app_logging(self): + self.logos.switch_logging() + + def update_latest_appimage(self): + utils.update_to_latest_recommended_appimage() + + def update_self(self): + utils.update_to_latest_lli_release() + def user_input_processor(self, evt=None): while self.running: prompt = None @@ -65,13 +115,84 @@ def user_input_processor(self, evt=None): self.choice_event.set() +# NOTE: These subcommands are outside the CLI class so that the class can be +# instantiated at the moment the subcommand is run. This lets any CLI-specific +# code get executed along with the subcommand. +def backup(): + CLI().backup() + + +def create_shortcuts(): + CLI().install_app() + + +def edit_config(): + CLI().edit_config() + + +def get_winetricks(): + CLI().get_winetricks() + + def install_app(): CLI().install_app() +def install_d3d_compiler(): + CLI().install_d3d_compiler() + + +def install_dependencies(): + CLI().install_dependencies() + + +def install_fonts(): + CLI().install_fonts() + + +def install_icu(): + CLI().install_icu() + + +def remove_index_files(): + CLI().remove_index_files() + + +def remove_install_dir(): + CLI().remove_install_dir() + + +def remove_library_catalog(): + CLI().remove_library_catalog() + + +def restore(): + CLI().restore() + + +def run_indexing(): + CLI().run_indexing() + + def run_installed_app(): CLI().run_installed_app() +def run_winetricks(): + CLI().run_winetricks() + + def set_appimage(): CLI().set_appimage() + + +def toggle_app_logging(): + CLI().toggle_app_logging() + + +def update_latest_appimage(): + CLI().update_latest_appimage() + + +def update_self(): + CLI().update_self() diff --git a/main.py b/main.py index 924139d3..66cf8dd4 100755 --- a/main.py +++ b/main.py @@ -246,26 +246,26 @@ def parse_args(args, parser): # Set ACTION function. actions = { - 'backup': control.backup, - 'create_shortcuts': installer.ensure_launcher_shortcuts, - 'edit_config': control.edit_config, - 'get_winetricks': control.set_winetricks, + 'backup': cli.backup, + 'create_shortcuts': cli.create_shortcuts, + 'edit_config': cli.edit_config, + 'get_winetricks': cli.get_winetricks, 'install_app': cli.install_app, - 'install_d3d_compiler': wine.install_d3d_compiler, - 'install_dependencies': utils.check_dependencies, - 'install_fonts': wine.install_fonts, - 'install_icu': wine.install_icu_data_files, - 'remove_index_files': control.remove_all_index_files, - 'remove_install_dir': control.remove_install_dir, - 'remove_library_catalog': control.remove_library_catalog, - 'restore': control.restore, - 'run_indexing': logos.LogosManager().index, + 'install_d3d_compiler': cli.install_d3d_compiler, + 'install_dependencies': cli.install_dependencies, + 'install_fonts': cli.install_fonts, + 'install_icu': cli.install_icu, + 'remove_index_files': cli.remove_index_files, + 'remove_install_dir': cli.remove_install_dir, + 'remove_library_catalog': cli.remove_library_catalog, + 'restore': cli.restore, + 'run_indexing': cli.run_indexing, 'run_installed_app': cli.run_installed_app, - 'run_winetricks': wine.run_winetricks, + 'run_winetricks': cli.run_winetricks, 'set_appimage': cli.set_appimage, - 'toggle_app_logging': logos.LogosManager().switch_logging, - 'update_self': utils.update_to_latest_lli_release, - 'update_latest_appimage': utils.update_to_latest_recommended_appimage, + 'toggle_app_logging': cli.toggle_app_logging, + 'update_self': cli.update_self, + 'update_latest_appimage': cli.update_latest_appimage, } config.ACTION = None @@ -433,7 +433,7 @@ def main(): config.ACTION() elif utils.app_is_installed(): # Run the desired Logos action. - logging.info(f"Running function for installed app: {config.ACTION.__name__}") # noqa: E501 + logging.info(f"Running function for {config.FLPRODUCT}: {config.ACTION.__name__}") # noqa: E501 config.ACTION() # defaults to run_control_panel() else: logging.info("Starting Control Panel") From cdc6c277bb17e3a3d22d0f14eca900dd5c60ef47 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 14:59:02 +0100 Subject: [PATCH 196/253] add TODO --- network.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/network.py b/network.py index 1053840b..2e6ee2f6 100644 --- a/network.py +++ b/network.py @@ -138,6 +138,8 @@ def cli_download(uri, destination, app=None): cli_queue = queue.Queue() kwargs = {'q': cli_queue, 'target': target} t = utils.start_thread(net_get, uri, **kwargs) + # TODO: This results in high CPU usage while showing the progress bar. + # The solution will be to rework the wait on the cli_queue. try: while t.is_alive(): if cli_queue.empty(): From a9a00aa2e460221c333efbc508165c03cc73b1e0 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 15:23:44 +0100 Subject: [PATCH 197/253] add TODO --- cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli.py b/cli.py index e4a0f145..1cc19c25 100644 --- a/cli.py +++ b/cli.py @@ -123,6 +123,9 @@ def backup(): def create_shortcuts(): + # TODO: This takes surprisingly long because it walks through all the + # installer steps to confirm everything up to the shortcuts. Can this be + # shortcutted? CLI().install_app() From 3d5c64ee9ed50a389a1fbdf6b53d9661e71a1621 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 30 Aug 2024 23:04:28 -0400 Subject: [PATCH 198/253] Utilize rules-based function --- wine.py | 98 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/wine.py b/wine.py index 0fb8232c..cd34c52b 100644 --- a/wine.py +++ b/wine.py @@ -122,22 +122,68 @@ def get_wine_release(binary): return False, f"Error: {e}" -def check_wine_version_and_branch(release_version, test_binary): +def check_wine_rules(wine_release, release_version): # Does not check for Staging. Will not implement: expecting merging of # commits in time. if config.TARGETVERSION == "10": if utils.check_logos_release_version(release_version, 30, 1): - wine_minimum = [7, 18] + required_wine_minimum = [7, 18] else: - wine_minimum = [9, 10] + required_wine_minimum = [9, 10] elif config.TARGETVERSION == "9": - wine_minimum = [7, 0] + required_wine_minimum = [7, 0] else: raise ValueError("TARGETVERSION not set.") - # Check if the binary is executable. If so, check if TESTBINARY's version - # is ≥ WINE_MINIMUM, or if it is Proton or a link to a Proton binary, else - # remove. + rules = [ + { + "major": 7, + "proton": True, # Proton release tend to use the x.0 release, but can include changes found in devel/staging # noqa: E501 + "minor_bad": [], # exceptions to minimum + "allowed_releases": ["staging"] + }, + { + "major": 8, + "proton": False, + "minor_bad": [0], + "allowed_releases": ["staging"], + "devel_allowed": 16, # devel permissible at this point + }, + { + "major": 9, + "proton": False, + "minor_bad": [], + "allowed_releases": ["devel", "staging"], + }, + ] + + major_min, minor_min = required_wine_minimum + major, minor, release_type = wine_release + for rule in rules: + if major == rule["major"]: + # Verify release is allowed + if release_type not in rule["allowed_releases"]: + if minor >= rule.get("devel_allowed", float('inf')): + if release_type != "staging": + return False, (f"Wine release needs to be devel or staging. " + f"Current release: {release_type}.") + else: + return False, (f"Wine release needs to be {rule["allowed_releases"]}. " + f"Current release: {release_type}.") + # Verify version is allowed + if minor in rule.get("minor_bad", []): + return False, f"Wine version {major}.{minor} will not work." + if major < major_min: + return False, (f"Wine version {major}.{minor} is " + f"below minimum required ({major_min}.{minor_min}).") + elif major == major_min and minor < minor_min: + if not rule["proton"]: + return False, (f"Wine version {major}.{minor} is " + f"below minimum required ({major_min}.{minor_min}).") + return True, "None" # Whether the release is allowed and the error message + + +def check_wine_version_and_branch(release_version, test_binary): if not os.path.exists(test_binary): reason = "Binary does not exist." return False, reason @@ -146,40 +192,18 @@ def check_wine_version_and_branch(release_version, test_binary): reason = "Binary is not executable." return False, reason - wine_release = [] wine_release, error_message = get_wine_release(test_binary) - if wine_release is not False and error_message is not None: - if wine_release[2] == 'stable': - return False, "Can't use Stable release" - elif wine_release[0] < 7: - return False, "Version is < 7.0" - elif wine_release[0] == 7: - if ( - "Proton" in test_binary - or ("Proton" in os.path.realpath(test_binary) if os.path.islink(test_binary) else False) # noqa: E501 - ): - if wine_release[1] == 0: - return True, "None" - elif wine_release[2] != 'staging': - return False, "Needs to be Staging release" - elif wine_release[1] < wine_minimum[1]: - reason = f"{'.'.join(wine_release)} is below minimum required, {'.'.join(wine_minimum)}" # noqa: E501 - return False, reason - elif wine_release[0] == 8: - if wine_release[1] < 1: - return False, "Version is 8.0" - elif wine_release[1] < 16: - if wine_release[2] != 'staging': - return False, "Version < 8.16 needs to be Staging release" - elif wine_release[0] == 9: - if wine_release[1] < 10: - return False, "Version < 9.10" - elif wine_release[0] > 9: - pass - else: + if wine_release is False and error_message is not None: return False, error_message + result, message = check_wine_rules(wine_release, release_version) + if not result: + return result, message + + if wine_release[0] > 9: + pass + return True, "None" From 3839c9c53a5ffef012852852693ad2ee3eadc740 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 20 Sep 2024 06:27:53 +0100 Subject: [PATCH 199/253] add debug logging; rearrage to log result --- wine.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/wine.py b/wine.py index cd34c52b..5aa29292 100644 --- a/wine.py +++ b/wine.py @@ -125,6 +125,7 @@ def get_wine_release(binary): def check_wine_rules(wine_release, release_version): # Does not check for Staging. Will not implement: expecting merging of # commits in time. + logging.debug(f"Checking {wine_release} for {release_version}.") if config.TARGETVERSION == "10": if utils.check_logos_release_version(release_version, 30, 1): required_wine_minimum = [7, 18] @@ -133,7 +134,7 @@ def check_wine_rules(wine_release, release_version): elif config.TARGETVERSION == "9": required_wine_minimum = [7, 0] else: - raise ValueError("TARGETVERSION not set.") + raise ValueError(f"Invalid TARGETVERSION: {config.TARGETVERSION} ({type(config.TARGETVERSION)})") rules = [ { @@ -159,28 +160,35 @@ def check_wine_rules(wine_release, release_version): major_min, minor_min = required_wine_minimum major, minor, release_type = wine_release + result = True, "None" # Whether the release is allowed and the error message for rule in rules: if major == rule["major"]: # Verify release is allowed if release_type not in rule["allowed_releases"]: if minor >= rule.get("devel_allowed", float('inf')): if release_type != "staging": - return False, (f"Wine release needs to be devel or staging. " + result = False, (f"Wine release needs to be devel or staging. " f"Current release: {release_type}.") + break else: - return False, (f"Wine release needs to be {rule["allowed_releases"]}. " + result = False, (f"Wine release needs to be {rule["allowed_releases"]}. " f"Current release: {release_type}.") + break # Verify version is allowed if minor in rule.get("minor_bad", []): - return False, f"Wine version {major}.{minor} will not work." + result = False, f"Wine version {major}.{minor} will not work." + break if major < major_min: - return False, (f"Wine version {major}.{minor} is " + result = False, (f"Wine version {major}.{minor} is " f"below minimum required ({major_min}.{minor_min}).") + break elif major == major_min and minor < minor_min: if not rule["proton"]: - return False, (f"Wine version {major}.{minor} is " + result = False, (f"Wine version {major}.{minor} is " f"below minimum required ({major_min}.{minor_min}).") - return True, "None" # Whether the release is allowed and the error message + break + logging.debug(f"Result: {result}") + return result def check_wine_version_and_branch(release_version, test_binary): From 91dfadb09455752c19f8e5061c162a7fd89e5557 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 21 Sep 2024 09:04:20 -0400 Subject: [PATCH 200/253] Fix var check --- wine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wine.py b/wine.py index 5aa29292..a93e0dda 100644 --- a/wine.py +++ b/wine.py @@ -166,7 +166,7 @@ def check_wine_rules(wine_release, release_version): # Verify release is allowed if release_type not in rule["allowed_releases"]: if minor >= rule.get("devel_allowed", float('inf')): - if release_type != "staging": + if release_type not in ["staging", "devel"]: result = False, (f"Wine release needs to be devel or staging. " f"Current release: {release_type}.") break From a0e01c2543353eaaa483460f3d8ea70415149390 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 27 Aug 2024 15:37:14 -0400 Subject: [PATCH 201/253] Fix #127 --- config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index ca864db9..edd1b47d 100644 --- a/config.py +++ b/config.py @@ -26,8 +26,8 @@ 'DEBUG': False, 'DELETE_LOG': None, 'DIALOG': None, - 'LOGOS_LOG': os.path.expanduser("~/.local/state/Logos_on_Linux/Logos_on_Linux.log"), # noqa: E501 - 'wine_log': os.path.expanduser("~/.local/state/Logos_on_Linux/wine.log"), # noqa: #E501 + 'LOGOS_LOG': os.path.expanduser("~/.local/state/FaithLife-Community/Logos_on_Linux.log"), # noqa: E501 + 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 'LOGOS_EXE': None, 'LOGOS_EXECUTABLE': None, 'LOGOS_VERSION': None, @@ -56,7 +56,7 @@ APPIMAGE_FILE_PATH = None authenticated = False BADPACKAGES = None -DEFAULT_CONFIG_PATH = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.json") # noqa: E501 +DEFAULT_CONFIG_PATH = os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json") # noqa: E501 GUI = None INSTALL_STEP = 0 INSTALL_STEPS_COUNT = 0 From e86b5fe48670beed91657abb7531bcbac163a71c Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 27 Sep 2024 20:05:09 -0400 Subject: [PATCH 202/253] Fix #181 --- control.py | 6 +++--- msg.py | 2 +- utils.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/control.py b/control.py index effb6b24..da25406f 100644 --- a/control.py +++ b/control.py @@ -66,7 +66,7 @@ def backup_and_restore(mode='backup', app=None): pass # user confirms in GUI else: verb = 'Use' if mode == 'backup' else 'Restore backup from' - if not msg.cli_question(f"{verb} existing backups folder \"{config.BACKUPDIR}\"?"): # noqa: E501 + if not msg.cli_question(f"{verb} existing backups folder \"{config.BACKUPDIR}\"?", ""): # noqa: E501 answer = None while answer is None or (mode == 'restore' and not answer.is_dir()): # noqa: E501 answer = msg.cli_ask_filepath("Give backups folder path:") @@ -82,7 +82,7 @@ def backup_and_restore(mode='backup', app=None): else: # Offer to restore the most recent backup. config.RESTOREDIR = utils.get_latest_folder(config.BACKUPDIR) - if not msg.cli_question(f"Restore most-recent backup?: {config.RESTOREDIR}"): # noqa: E501 + if not msg.cli_question(f"Restore most-recent backup?: {config.RESTOREDIR}", ""): # noqa: E501 config.RESTOREDIR = msg.cli_ask_filepath("Path to backup set that you want to restore:") # noqa: E501 config.RESTOREDIR = Path(config.RESTOREDIR).expanduser().resolve() source_dir_base = config.RESTOREDIR @@ -214,7 +214,7 @@ def remove_install_dir(): # FIXME: msg.cli_question needs additional arg if ( folder.is_dir() - and msg.cli_question(f"Delete \"{folder}\" and all its contents?") + and msg.cli_question(f"Delete \"{folder}\" and all its contents?", "") ): shutil.rmtree(folder) logging.warning(f"Deleted folder and all its contents: {folder}") diff --git a/msg.py b/msg.py index 2d2517e4..e4ec7444 100644 --- a/msg.py +++ b/msg.py @@ -199,7 +199,7 @@ def logos_error(message, secondary=None, detail=None, app=None, parent=None): sys.exit(1) -def cli_question(question_text, secondary): +def cli_question(question_text, secondary=""): while True: try: cli_msg(secondary) diff --git a/utils.py b/utils.py index 60bec075..aeb484a2 100644 --- a/utils.py +++ b/utils.py @@ -143,7 +143,7 @@ def remove_pid_file(): elif config.DIALOG == "curses": confirm = tui_dialog.confirm("Confirmation", message) else: - confirm = msg.cli_question(message) + confirm = msg.cli_question(message, "") if confirm: os.kill(int(pid), signal.SIGKILL) From 6df9e23efad84fa9e91fe278e893da2529dd745c Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Fri, 27 Sep 2024 20:13:53 -0400 Subject: [PATCH 203/253] Modularize main() --- main.py | 61 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index 66cf8dd4..4f9db72a 100755 --- a/main.py +++ b/main.py @@ -316,7 +316,7 @@ def run_control_panel(): raise e -def main(): +def set_config(): parser = get_parser() cli_args = parser.parse_args() # parsing early lets 'help' run immediately @@ -354,6 +354,8 @@ def main(): if config.LOG_LEVEL != current_log_level: msg.update_log_level(config.LOG_LEVEL) + +def set_dialog(): # Set DIALOG and GUI variables. if config.DIALOG is None: system.get_dialog() @@ -374,28 +376,8 @@ def main(): config.use_python_dialog = False logging.debug(f"Use Python Dialog?: {config.use_python_dialog}") - # Log persistent config. - utils.log_current_persistent_config() - - # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that - # can be run in conjunction with other actions, so it gets special - # treatment here once config is set. - if config.DELETE_LOG and os.path.isfile(config.LOGOS_LOG): - control.delete_log_file_contents() - - # Run desired action (requested function, defaulting to installer) - # Run safety checks. - # FIXME: Fix utils.die_if_running() for GUI; as it is, it breaks GUI - # self-update when updating LLI as it asks for a confirmation in the CLI. - # Disabled until it can be fixed. Avoid running multiple instances of the - # program. - # utils.die_if_running() - utils.die_if_root() - - # Print terminal banner - logging.info(f"{config.LLI_TITLE}, {config.LLI_CURRENT_VERSION} by {config.LLI_AUTHOR}.") # noqa: E501 - logging.debug(f"Installer log file: {config.LOGOS_LOG}") +def check_incompatibilities(): # Check for AppImageLauncher if shutil.which('AppImageLauncher'): question_text = "Remove AppImageLauncher? A reboot will be required." @@ -408,8 +390,9 @@ def main(): msg.logos_continue_question(question_text, no_text, secondary) system.remove_appimagelauncher() - network.check_for_updates() +def run(): + # Run desired action (requested function, defaulting to installer) # Check if app is installed. install_required = [ 'backup', @@ -440,6 +423,38 @@ def main(): run_control_panel() +def main(): + set_config() + set_dialog() + + # Log persistent config. + utils.log_current_persistent_config() + + # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that + # can be run in conjunction with other actions, so it gets special + # treatment here once config is set. + if config.DELETE_LOG and os.path.isfile(config.LOGOS_LOG): + control.delete_log_file_contents() + + # Run safety checks. + # FIXME: Fix utils.die_if_running() for GUI; as it is, it breaks GUI + # self-update when updating LLI as it asks for a confirmation in the CLI. + # Disabled until it can be fixed. Avoid running multiple instances of the + # program. + # utils.die_if_running() + utils.die_if_root() + + # Print terminal banner + logging.info(f"{config.LLI_TITLE}, {config.LLI_CURRENT_VERSION} by {config.LLI_AUTHOR}.") # noqa: E501 + logging.debug(f"Installer log file: {config.LOGOS_LOG}") + + check_incompatibilities() + + network.check_for_updates() + + run() + + def close(): logging.debug("Closing Logos on Linux.") for thread in threads: From 35982279abdaa5af5a60de4f074b0cd95d03cac4 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 7 Oct 2024 13:57:34 -0400 Subject: [PATCH 204/253] Fix TODO --- control.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/control.py b/control.py index da25406f..da34736f 100644 --- a/control.py +++ b/control.py @@ -211,10 +211,9 @@ def copy_data(src_dirs, dst_dir): def remove_install_dir(): folder = Path(config.INSTALLDIR) - # FIXME: msg.cli_question needs additional arg if ( folder.is_dir() - and msg.cli_question(f"Delete \"{folder}\" and all its contents?", "") + and msg.cli_question(f"Delete \"{folder}\" and all its contents?", "", "") ): shutil.rmtree(folder) logging.warning(f"Deleted folder and all its contents: {folder}") From 3c409e5756259693560328ace4c0c41fdae14cb9 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Sat, 28 Sep 2024 11:19:09 -0400 Subject: [PATCH 205/253] Fix #16 --- system.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/system.py b/system.py index 41cebf7f..66fb92c7 100644 --- a/system.py +++ b/system.py @@ -344,6 +344,15 @@ def get_package_manager(): ) config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages config.BADPACKAGES = "" # appimagelauncher handled separately + elif shutil.which('zypper') is not None: # manjaro + config.PACKAGE_MANAGER_COMMAND_INSTALL = ["zypper", "--non-interactive", "install"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["zypper", "download"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_REMOVE = ["zypper", "--non-interactive", "remove"] # noqa: E501 + config.PACKAGE_MANAGER_COMMAND_QUERY = ["zypper", "se", "-si"] + config.QUERY_PREFIX = 'i | ' + config.PACKAGES = "fuse patch wget sed grep gawk cabextract 7zip samba curl" # noqa: E501 + config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages + config.BADPACKAGES = "" # appimagelauncher handled separately elif shutil.which('pamac') is not None: # manjaro config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pamac", "install", "--no-upgrade", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pamac", "install", "--no-upgrade", "--download-only", "--no-confirm"] # noqa: E501 From 5012da294c48e912fe3a64a43abe7229464530c7 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 7 Oct 2024 18:42:54 -0400 Subject: [PATCH 206/253] Fix #188 --- config.py | 2 +- logos.py | 18 ++++++++++++++---- wine.py | 14 -------------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/config.py b/config.py index edd1b47d..afc0a33a 100644 --- a/config.py +++ b/config.py @@ -116,7 +116,7 @@ threads = [] login_window_cmd = None logos_cef_cmd = None -logos_indexing_cmd = None +logos_indexer_cmd = None logos_indexer_exe = None check_if_indexing = None diff --git a/logos.py b/logos.py index b1cce890..0600ac86 100644 --- a/logos.py +++ b/logos.py @@ -1,3 +1,4 @@ +import os import time from enum import Enum import logging @@ -24,10 +25,21 @@ def __init__(self, app=None): self.logos_state = State.STOPPED self.indexing_state = State.STOPPED self.app = app + if config.wine_user is None: + wine.get_wine_user() + if config.logos_indexer_cmd is None or config.login_window_cmd is None or config.logos_cef_cmd is None: + config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 + config.logos_indexer_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 + for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 + for f in files: + if f == "LogosIndexer.exe" and root.endswith("Logos/System"): + config.logos_indexer_exe = os.path.join(root, f) + break def monitor_indexing(self): - if config.logos_indexer_exe in config.processes: - indexer = config.processes[config.logos_indexer_exe] + if config.logos_indexer_cmd in config.processes: + indexer = config.processes[config.logos_indexer_cmd] if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): self.indexing_state = State.RUNNING else: @@ -56,8 +68,6 @@ def monitor(self): if utils.file_exists(config.LOGOS_EXE): if config.wine_user is None: wine.get_wine_user() - if config.logos_indexer_exe is None or config.login_window_cmd is None or config.logos_cef_cmd is None: - wine.set_logos_paths() system.get_logos_pids() try: self.monitor_indexing() diff --git a/wine.py b/wine.py index a93e0dda..591ad518 100644 --- a/wine.py +++ b/wine.py @@ -15,20 +15,6 @@ from main import processes -def set_logos_paths(): - config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 - config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 - config.logos_indexing_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 - # TODO: Can't this just be set based on WINEPREFIX and USER vars without - # having to walk through the WINEPREFIX tree? Or maybe this is to account - # for a non-typical installation location? - for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 - for f in files: - if f == "LogosIndexer.exe" and root.endswith("Logos/System"): - config.logos_indexer_exe = os.path.join(root, f) - break - - def get_wine_user(): path = config.LOGOS_EXE normalized_path = os.path.normpath(path) From ca4490b88e995b29b41f38ca5d7989927c94ed11 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 8 Oct 2024 11:50:18 +0100 Subject: [PATCH 207/253] fix setting of config.WINE_EXE in GUI --- installer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/installer.py b/installer.py index ba35e2b7..02e17ade 100644 --- a/installer.py +++ b/installer.py @@ -172,7 +172,12 @@ def ensure_wine_choice(app=None): utils.send_task(app, 'WINE_EXE') if config.DIALOG == 'curses': app.wine_e.wait() - config.WINE_EXE = app.wines_q.get() + config.WINE_EXE = app.wines_q.get() + # GUI uses app.wines_q for list of available, then app.wine_q + # for the user's choice of specific binary. + elif config.DIALOG == 'tk': + config.WINE_EXE = app.wine_q.get() + else: if config.DIALOG == 'curses' and app: app.set_wine(utils.get_wine_exe_path()) From 6ada54224a6dec1792c19ca0a9befceba8f2b438 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 8 Oct 2024 08:23:20 -0400 Subject: [PATCH 208/253] Add calls to set_logos_paths - Set logos paths in CLI/TUI --- cli.py | 4 +++- gui_app.py | 1 + installer.py | 2 ++ logos.py | 15 +-------------- tui_app.py | 3 ++- utils.py | 2 ++ wine.py | 13 +++++++++++++ 7 files changed, 24 insertions(+), 16 deletions(-) diff --git a/cli.py b/cli.py index 1cc19c25..a10bac36 100644 --- a/cli.py +++ b/cli.py @@ -19,7 +19,9 @@ def __init__(self): self.input_q = queue.Queue() self.input_event = threading.Event() self.choice_event = threading.Event() - self.logos = logos.LogosManager(app=self) + if utils.find_installed_product(): + wine.set_logos_paths() + self.logos = logos.LogosManager(app=self) def backup(self): control.backup() diff --git a/gui_app.py b/gui_app.py index 536a5bf4..0f5a7fde 100644 --- a/gui_app.py +++ b/gui_app.py @@ -660,6 +660,7 @@ def __init__(self, root, *args, **kwargs): def configure_app_button(self, evt=None): if utils.find_installed_product(): + wine.set_logos_paths() self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.gui.app_button.config(command=self.run_logos) self.gui.get_winetricks_button.state(['!disabled']) diff --git a/installer.py b/installer.py index 02e17ade..042179d3 100644 --- a/installer.py +++ b/installer.py @@ -623,6 +623,8 @@ def ensure_product_installed(app=None): config.LOGOS_EXE = utils.find_installed_product() config.current_logos_version = config.TARGET_RELEASE_VERSION + wine.set_logos_paths() + # Clean up temp files, etc. utils.clean_all() diff --git a/logos.py b/logos.py index 0600ac86..4c8c1474 100644 --- a/logos.py +++ b/logos.py @@ -25,17 +25,6 @@ def __init__(self, app=None): self.logos_state = State.STOPPED self.indexing_state = State.STOPPED self.app = app - if config.wine_user is None: - wine.get_wine_user() - if config.logos_indexer_cmd is None or config.login_window_cmd is None or config.logos_cef_cmd is None: - config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 - config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 - config.logos_indexer_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 - for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 - for f in files: - if f == "LogosIndexer.exe" and root.endswith("Logos/System"): - config.logos_indexer_exe = os.path.join(root, f) - break def monitor_indexing(self): if config.logos_indexer_cmd in config.processes: @@ -65,9 +54,7 @@ def monitor_logos(self): self.logos_state = State.RUNNING def monitor(self): - if utils.file_exists(config.LOGOS_EXE): - if config.wine_user is None: - wine.get_wine_user() + if utils.find_installed_product(): system.get_logos_pids() try: self.monitor_indexing() diff --git a/tui_app.py b/tui_app.py index 9be36b0e..0838be68 100644 --- a/tui_app.py +++ b/tui_app.py @@ -870,7 +870,8 @@ def set_tui_menu_options(self, dialog=False): else: logging.error(f"{error_message}") - if utils.file_exists(config.LOGOS_EXE): + if utils.find_installed_product(): + wine.set_logos_paths() if self.logos.logos_state == logos.State.RUNNING: run = f"Stop {config.FLPRODUCT}" elif self.logos.logos_state == logos.State.STOPPED: diff --git a/utils.py b/utils.py index aeb484a2..867ddfc6 100644 --- a/utils.py +++ b/utils.py @@ -70,6 +70,8 @@ def set_runtime_config(): config.WINESERVER_EXE = str(bin_dir / 'wineserver') if config.FLPRODUCT and config.WINEPREFIX and not config.LOGOS_EXE: config.LOGOS_EXE = find_installed_product() + if find_installed_product(): + wine.set_logos_paths() def log_current_persistent_config(): diff --git a/wine.py b/wine.py index 591ad518..7e7b1fb4 100644 --- a/wine.py +++ b/wine.py @@ -22,6 +22,19 @@ def get_wine_user(): config.wine_user = path_parts[path_parts.index('users') + 1] +def set_logos_paths(): + if config.wine_user is None: + get_wine_user() + if config.logos_indexer_cmd is None or config.login_window_cmd is None or config.logos_cef_cmd is None: + config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 + config.logos_indexer_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 + for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 + for f in files: + if f == "LogosIndexer.exe" and root.endswith("Logos/System"): + config.logos_indexer_exe = os.path.join(root, f) + break + def check_wineserver(): try: process = run_wine_proc(config.WINESERVER, exe_args=["-p"]) From 22dfdbf0b072b3048683905cc8c4df2f6e640add Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 8 Oct 2024 08:14:16 -0400 Subject: [PATCH 209/253] Create tui_curses.write_line() --- tui_app.py | 2 +- tui_curses.py | 27 ++++++++++++++++----------- tui_screen.py | 10 ++-------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/tui_app.py b/tui_app.py index 0838be68..032755fe 100644 --- a/tui_app.py +++ b/tui_app.py @@ -284,7 +284,7 @@ def draw_resize_screen(self): self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) for i, line in enumerate(resize_lines): if i < self.window_height: - self.resize_window.addnstr(i, margin, line, self.window_width - config.margin, curses.A_BOLD) + tui_curses.write_line(self, self.resize_window, i, margin, line, self.window_width - config.margin, curses.A_BOLD) self.refresh() def display(self): diff --git a/tui_curses.py b/tui_curses.py index e2285258..510e6b05 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -20,6 +20,13 @@ def wrap_text(app, text): return lines +def write_line(app, stdscr, start_y, start_x, text, char_limit, attributes=curses.A_NORMAL): + try: + stdscr.addnstr(start_y, start_x, text, char_limit, attributes) + except curses.error: + signal.signal(signal.SIGWINCH, app.signal_resize) + + def title(app, title_text, title_start_y_adj): stdscr = app.get_main_window() title_lines = wrap_text(app, title_text) @@ -27,7 +34,7 @@ def title(app, title_text, title_start_y_adj): last_index = 0 for i, line in enumerate(title_lines): if i < app.window_height: - stdscr.addnstr(i + title_start_y_adj, 2, line, app.window_width, curses.A_BOLD) + write_line(app, stdscr, i + title_start_y_adj, 2, line, app.window_width, curses.A_BOLD) last_index = i return last_index @@ -44,7 +51,7 @@ def text_centered(app, text, start_y=0): for i, line in enumerate(text_lines): if text_start_y + i < app.window_height: x = app.window_width // 2 - text_width // 2 - stdscr.addnstr(text_start_y + i, x, line, app.window_width) + write_line(app, stdscr, text_start_y + i, x, line, app.window_width, curses.A_BOLD) return text_start_y, text_lines @@ -74,7 +81,7 @@ def confirm(app, question_text, height=None, width=None): elif key.lower() == 'n': return False - stdscr.addnstr(y, 0, "Type Y[es] or N[o]. ", app.window_width) + write_line(app, stdscr, y, 0, "Type Y[es] or N[o]. ", app.window_width, curses.A_BOLD) class CursesDialog: @@ -119,7 +126,7 @@ def draw(self): self.stdscr.refresh() def input(self): - self.stdscr.addnstr(self.question_start_y + len(self.question_lines) + 2, 10, self.user_input, self.app.window_width) + write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.user_input, self.app.window_width) key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.user_input)) try: @@ -162,7 +169,8 @@ def run(self): return self.user_input def input(self): - self.stdscr.addnstr(self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation, self.app.window_width) + write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation, + self.app.window_width) key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.obfuscation)) try: @@ -235,9 +243,9 @@ def draw(self): x = max(0, self.app.window_width // 2 - len(line) // 2) if y < self.app.menu_window_height: if index == config.current_option: - self.stdscr.addnstr(y, x, line, self.app.window_width, curses.A_REVERSE) + write_line(self.app, self.stdscr, y, x, line, self.app.window_width, curses.A_REVERSE) else: - self.stdscr.addnstr(y, x, line, self.app.window_width) + write_line(self.app, self.stdscr, y, x, line, self.app.window_width) menu_bottom = y if type(option) is list: @@ -245,10 +253,7 @@ def draw(self): # Display pagination information page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(self.options)}" - try: - self.stdscr.addnstr(max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) - except curses.error: - signal.signal(signal.SIGWINCH, self.app.signal_resize) + write_line(self.app, self.stdscr, max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) def do_menu_up(self): if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: diff --git a/tui_screen.py b/tui_screen.py index 19f66d56..1eecef84 100644 --- a/tui_screen.py +++ b/tui_screen.py @@ -85,20 +85,14 @@ def display(self): console_start_y = len(tui_curses.wrap_text(self.app, self.title)) + len( tui_curses.wrap_text(self.app, self.subtitle)) + 1 - try: - self.stdscr.addnstr(console_start_y, config.margin, f"---Console---", self.app.window_width - (config.margin * 2)) - except curses.error: - signal.signal(signal.SIGWINCH, self.app.signal_resize) + tui_curses.write_line(self.app, self.stdscr, console_start_y, config.margin, f"---Console---", self.app.window_width - (config.margin * 2)) recent_messages = config.console_log[-config.console_log_lines:] for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) for j, line in enumerate(message_lines): if 2 + j < self.app.window_height: truncated = message[:self.app.window_width - (config.margin * 2)] - try: - self.stdscr.addnstr(console_start_y + i, config.margin, truncated, self.app.window_width - (config.margin * 2)) - except curses.error: - signal.signal(signal.SIGWINCH, self.app.signal_resize) + tui_curses.write_line(self.app, self.stdscr, console_start_y + i, config.margin, truncated, self.app.window_width - (config.margin * 2)) self.stdscr.noutrefresh() curses.doupdate() From 5c1345ffd70251d31744fbfc6f14321afb911f21 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 8 Oct 2024 09:20:00 -0400 Subject: [PATCH 210/253] Fix setting config.DIALOG=cli --- cli.py | 4 +--- gui_app.py | 2 +- logos.py | 2 +- main.py | 13 +++++++++---- tui_app.py | 3 +-- utils.py | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/cli.py b/cli.py index a10bac36..1cc19c25 100644 --- a/cli.py +++ b/cli.py @@ -19,9 +19,7 @@ def __init__(self): self.input_q = queue.Queue() self.input_event = threading.Event() self.choice_event = threading.Event() - if utils.find_installed_product(): - wine.set_logos_paths() - self.logos = logos.LogosManager(app=self) + self.logos = logos.LogosManager(app=self) def backup(self): control.backup() diff --git a/gui_app.py b/gui_app.py index 0f5a7fde..546a4bc4 100644 --- a/gui_app.py +++ b/gui_app.py @@ -659,7 +659,7 @@ def __init__(self, root, *args, **kwargs): utils.start_thread(self.logos.get_app_logging_state) def configure_app_button(self, evt=None): - if utils.find_installed_product(): + if utils.app_is_installed(): wine.set_logos_paths() self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.gui.app_button.config(command=self.run_logos) diff --git a/logos.py b/logos.py index 4c8c1474..2408068c 100644 --- a/logos.py +++ b/logos.py @@ -54,7 +54,7 @@ def monitor_logos(self): self.logos_state = State.RUNNING def monitor(self): - if utils.find_installed_product(): + if utils.app_is_installed(): system.get_logos_pids() try: self.monitor_indexing() diff --git a/main.py b/main.py index 4f9db72a..3cf561c3 100755 --- a/main.py +++ b/main.py @@ -414,15 +414,20 @@ def run(): elif config.ACTION.__name__ not in install_required: logging.info(f"Running function: {config.ACTION.__name__}") config.ACTION() - elif utils.app_is_installed(): - # Run the desired Logos action. - logging.info(f"Running function for {config.FLPRODUCT}: {config.ACTION.__name__}") # noqa: E501 - config.ACTION() # defaults to run_control_panel() + elif config.ACTION.__name__ in install_required: + if utils.app_is_installed(): + config.DIALOG = "cli" + # Run the desired Logos action. + logging.info(f"Running function for {config.FLPRODUCT}: {config.ACTION.__name__}") # noqa: E501 + config.ACTION() # defaults to run_control_panel() + else: + msg.logos_error("App not installed…") else: logging.info("Starting Control Panel") run_control_panel() + def main(): set_config() set_dialog() diff --git a/tui_app.py b/tui_app.py index 032755fe..8587e3de 100644 --- a/tui_app.py +++ b/tui_app.py @@ -870,8 +870,7 @@ def set_tui_menu_options(self, dialog=False): else: logging.error(f"{error_message}") - if utils.find_installed_product(): - wine.set_logos_paths() + if utils.app_is_installed(): if self.logos.logos_state == logos.State.RUNNING: run = f"Stop {config.FLPRODUCT}" elif self.logos.logos_state == logos.State.STOPPED: diff --git a/utils.py b/utils.py index 867ddfc6..3608d88a 100644 --- a/utils.py +++ b/utils.py @@ -70,7 +70,7 @@ def set_runtime_config(): config.WINESERVER_EXE = str(bin_dir / 'wineserver') if config.FLPRODUCT and config.WINEPREFIX and not config.LOGOS_EXE: config.LOGOS_EXE = find_installed_product() - if find_installed_product(): + if app_is_installed(): wine.set_logos_paths() From af90dfcfa20b5e287c5ef2c3eec7258bf5398624 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 8 Oct 2024 17:53:09 +0100 Subject: [PATCH 211/253] only wait on non-daemon threads in close func --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 3cf561c3..41689906 100755 --- a/main.py +++ b/main.py @@ -463,7 +463,8 @@ def main(): def close(): logging.debug("Closing Logos on Linux.") for thread in threads: - thread.join() + if not thread.daemon: + thread.join() # Only kill wine processes if closing the Control Panel. Otherwise, some # CLI commands get killed as soon as they're started. if config.ACTION.__name__ == 'run_control_panel' and len(processes) > 0: From 907ddb7881ce1132dec74c47d5331b7f0e370918 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 8 Oct 2024 17:59:24 +0100 Subject: [PATCH 212/253] rework main.run to set config.DIALOG and logos_paths before UIs are started --- cli.py | 1 - gui_app.py | 2 +- main.py | 37 ++++++++++++++++++++----------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/cli.py b/cli.py index 1cc19c25..05db1c8b 100644 --- a/cli.py +++ b/cli.py @@ -13,7 +13,6 @@ class CLI: def __init__(self): - config.DIALOG = "cli" self.running = True self.choice_q = queue.Queue() self.input_q = queue.Queue() diff --git a/gui_app.py b/gui_app.py index 546a4bc4..943bc5a0 100644 --- a/gui_app.py +++ b/gui_app.py @@ -660,7 +660,7 @@ def __init__(self, root, *args, **kwargs): def configure_app_button(self, evt=None): if utils.app_is_installed(): - wine.set_logos_paths() + # wine.set_logos_paths() self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") self.gui.app_button.config(command=self.run_logos) self.gui.get_winetricks_button.state(['!disabled']) diff --git a/main.py b/main.py index 41689906..4a013356 100755 --- a/main.py +++ b/main.py @@ -392,8 +392,18 @@ def check_incompatibilities(): def run(): - # Run desired action (requested function, defaulting to installer) - # Check if app is installed. + # Run desired action (requested function, defaults to control_panel) + if config.ACTION == "disabled": + msg.logos_error("That option is disabled.", "info") + if config.ACTION.__name__ == 'run_control_panel': + # if utils.app_is_installed(): + # wine.set_logos_paths() + config.ACTION() # run control_panel right away + return + + # Only control_panel ACTION uses TUI/GUI interface; all others are CLI. + config.DIALOG = 'cli' + install_required = [ 'backup', 'create_shortcuts', @@ -409,23 +419,16 @@ def run(): 'set_appimage', 'toggle_app_logging', ] - if config.ACTION == "disabled": - msg.logos_error("That option is disabled.", "info") - elif config.ACTION.__name__ not in install_required: + if config.ACTION.__name__ not in install_required: logging.info(f"Running function: {config.ACTION.__name__}") config.ACTION() - elif config.ACTION.__name__ in install_required: - if utils.app_is_installed(): - config.DIALOG = "cli" - # Run the desired Logos action. - logging.info(f"Running function for {config.FLPRODUCT}: {config.ACTION.__name__}") # noqa: E501 - config.ACTION() # defaults to run_control_panel() - else: - msg.logos_error("App not installed…") - else: - logging.info("Starting Control Panel") - run_control_panel() - + elif utils.app_is_installed(): # install_required; checking for app + # wine.set_logos_paths() + # Run the desired Logos action. + logging.info(f"Running function for {config.FLPRODUCT}: {config.ACTION.__name__}") # noqa: E501 + config.ACTION() + else: # install_required, but app not installed + msg.logos_error("App not installed…") def main(): From 1b40c3c83e6b760d6db3e39329c9bfaad20a50b1 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 8 Oct 2024 18:02:21 +0100 Subject: [PATCH 213/253] add explanatory comment in close func --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index 4a013356..53ee486c 100755 --- a/main.py +++ b/main.py @@ -466,6 +466,7 @@ def main(): def close(): logging.debug("Closing Logos on Linux.") for thread in threads: + # Only wait on non-daemon threads. if not thread.daemon: thread.join() # Only kill wine processes if closing the Control Panel. Otherwise, some From 4b5c3fbeaaef07a335e53efc0c5f90b0c484b190 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Tue, 8 Oct 2024 18:49:20 +0100 Subject: [PATCH 214/253] fix too-many params issue --- control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control.py b/control.py index da34736f..aed626a6 100644 --- a/control.py +++ b/control.py @@ -213,7 +213,7 @@ def remove_install_dir(): folder = Path(config.INSTALLDIR) if ( folder.is_dir() - and msg.cli_question(f"Delete \"{folder}\" and all its contents?", "", "") + and msg.cli_question(f"Delete \"{folder}\" and all its contents?") ): shutil.rmtree(folder) logging.warning(f"Deleted folder and all its contents: {folder}") From 07844af13875fc1b1d4db46fe57c7a7dd2e9d7cd Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 9 Oct 2024 05:46:25 +0100 Subject: [PATCH 215/253] fix wait for indexing process to start --- logos.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/logos.py b/logos.py index 2408068c..181578b3 100644 --- a/logos.py +++ b/logos.py @@ -146,24 +146,25 @@ def check_if_indexing(process): def wait_on_indexing(): index_finished.wait() self.indexing_state = State.STOPPED - msg.status(f"Indexing has finished.", self.app) + msg.status("Indexing has finished.", self.app) wine.wineserver_wait() wine.wineserver_kill() - msg.status(f"Indexing has begun…", self.app) - # index_thread = threading.Thread(target=run_indexing) - # index_thread.start() - index_thread = utils.start_thread(run_indexing, daemon=False) + msg.status("Indexing has begun…", self.app) + index_thread = utils.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING - time.sleep(1) # If we don't wait, the thread starts too quickly - # and the process won't yet be launched when we try to pull it from config.processes + # If we don't wait the process won't yet be launched when we try to + # pull it from config.processes. + while config.processes.get(config.logos_indexer_exe) is None: + time.sleep(0.1) + logging.debug(f"{config.processes=}") process = config.processes[config.logos_indexer_exe] - # check_thread = threading.Thread(target=check_if_indexing, args=(process,)) - check_thread = utils.start_thread(check_if_indexing, process) - # wait_thread = threading.Thread(target=wait_on_indexing) - wait_thread = utils.start_thread(wait_on_indexing) - # check_thread.start() - # wait_thread.start() + check_thread = utils.start_thread( + check_if_indexing, + process, + daemon_bool=False + ) + wait_thread = utils.start_thread(wait_on_indexing, daemon_bool=False) main.threads.extend([index_thread, check_thread, wait_thread]) config.processes[config.logos_indexer_exe] = index_thread config.processes[config.check_if_indexing] = check_thread From 883fb491708146338c0790353fb72ce893e2f7ee Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 9 Oct 2024 06:36:46 +0100 Subject: [PATCH 216/253] fix backup and restore functions --- cli.py | 4 ++-- control.py | 47 +++++++++++++++++++---------------------------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/cli.py b/cli.py index 05db1c8b..7cfcff87 100644 --- a/cli.py +++ b/cli.py @@ -21,7 +21,7 @@ def __init__(self): self.logos = logos.LogosManager(app=self) def backup(self): - control.backup() + control.backup(app=self) def edit_config(self): control.edit_config() @@ -58,7 +58,7 @@ def remove_library_catalog(self): control.remove_library_catalog() def restore(self): - control.restore() + control.restore(app=self) def run_indexing(self): self.logos.index() diff --git a/control.py b/control.py index aed626a6..bbdb760b 100644 --- a/control.py +++ b/control.py @@ -92,7 +92,7 @@ def backup_and_restore(mode='backup', app=None): logging.debug(f"{src_dirs=}") if not src_dirs: m = "No files to backup" - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.status_q.put(m) app.root.event_generate('<>') app.root.event_generate('<>') @@ -102,7 +102,7 @@ def backup_and_restore(mode='backup', app=None): # Get source transfer size. q = queue.Queue() m = "Calculating backup size" - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.status_q.put(m) app.root.event_generate('<>') app.root.event_generate(app.status_evt) @@ -117,14 +117,14 @@ def backup_and_restore(mode='backup', app=None): print() msg.logos_error("Cancelled with Ctrl+C.") t.join() - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.root.event_generate('<>') app.root.event_generate('<>') src_size = q.get() if src_size == 0: m = f"Nothing to {mode}!" logging.warning(m) - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.status_q.put(m) app.root.event_generate('<>') return @@ -140,36 +140,27 @@ def backup_and_restore(mode='backup', app=None): else: timestamp = datetime.today().strftime('%Y%m%dT%H%M%S') current_backup_name = f"{config.FLPRODUCT}{config.TARGETVERSION}-{timestamp}" # noqa: E501 - dst_dir = Path(config.BACKUPDIR) / current_backup_name - dst_dir.mkdir(exist_ok=True, parents=True) + backup_dir = Path(config.BACKUPDIR) + backup_dir.mkdir(exist_ok=True, parents=True) + dst_dir = backup_dir / current_backup_name + logging.debug(f"Backup directory path: {dst_dir}") + + # Check for existing backup. + try: + dst_dir.mkdir() + except FileExistsError: + msg.logos_error(f"Backup already exists: {dst_dir}") # Verify disk space. - if ( - not utils.enough_disk_space(dst_dir, src_size) - and not Path(dst_dir / 'Data').is_dir() - ): + if not utils.enough_disk_space(dst_dir, src_size): m = f"Not enough free disk space for {mode}." - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.status_q.put(m) app.root.event_generate('<>') return else: msg.logos_error(m) - # Verify destination. - if config.BACKUPDIR is None: - config.BACKUPDIR = Path().home() / 'Logos_on_Linux_backups' - backup_dir = Path(config.BACKUPDIR) - backup_dir.mkdir(exist_ok=True, parents=True) - if not utils.enough_disk_space(backup_dir, src_size): - msg.logos_error("Not enough free disk space for backup.") - - # Run backup. - try: - dst_dir.mkdir() - except FileExistsError: - msg.logos_error(f"Backup already exists: {dst_dir}") - # Run file transfer. if mode == 'restore': m = f"Restoring backup from {str(source_dir_base)}" @@ -177,7 +168,7 @@ def backup_and_restore(mode='backup', app=None): m = f"Backing up to {str(dst_dir)}" logging.info(m) msg.status(m) - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.status_q.put(m) app.root.event_generate(app.status_evt) dst_dir_size = utils.get_path_size(dst_dir) @@ -190,7 +181,7 @@ def backup_and_restore(mode='backup', app=None): dest_size_init=dst_dir_size ) utils.write_progress_bar(progress) - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.progress_q.put(progress) app.root.event_generate('<>') time.sleep(0.5) @@ -199,7 +190,7 @@ def backup_and_restore(mode='backup', app=None): print() msg.logos_error("Cancelled with Ctrl+C.") t.join() - if app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: app.root.event_generate('<>') logging.info(f"Finished. {src_size} bytes copied to {str(dst_dir)}") From da78a447fae66edb404d8c7cc80685b4495bed10 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 9 Oct 2024 11:38:14 +0100 Subject: [PATCH 217/253] ignore all terminal output in TUI --- msg.py | 3 ++- tui_app.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/msg.py b/msg.py index e4ec7444..04e711ac 100644 --- a/msg.py +++ b/msg.py @@ -114,10 +114,11 @@ def initialize_logging(stderr_log_level): ) -def initialize_curses_logging(): +def initialize_tui_logging(): current_logger = logging.getLogger() for h in current_logger.handlers: if h.name == 'terminal': + current_logger.removeHandler(h) break diff --git a/tui_app.py b/tui_app.py index 8587e3de..c0d7e3dc 100644 --- a/tui_app.py +++ b/tui_app.py @@ -290,7 +290,7 @@ def draw_resize_screen(self): def display(self): signal.signal(signal.SIGWINCH, self.signal_resize) signal.signal(signal.SIGINT, self.end) - msg.initialize_curses_logging() + msg.initialize_tui_logging() msg.status(self.console_message, self) self.active_screen = self.menu_screen last_time = time.time() From 360bba51ab7812194e93fb85793123de267dc8b7 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 9 Oct 2024 15:34:00 +0100 Subject: [PATCH 218/253] add timestamp function; add timestamp to wine.log entries --- control.py | 2 +- msg.py | 4 ++-- utils.py | 5 +++++ wine.py | 5 ++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/control.py b/control.py index bbdb760b..8cf6bfeb 100644 --- a/control.py +++ b/control.py @@ -138,7 +138,7 @@ def backup_and_restore(mode='backup', app=None): if dst.is_dir(): shutil.rmtree(dst) else: - timestamp = datetime.today().strftime('%Y%m%dT%H%M%S') + timestamp = utils.get_timestamp().replace('-', '') current_backup_name = f"{config.FLPRODUCT}{config.TARGETVERSION}-{timestamp}" # noqa: E501 backup_dir = Path(config.BACKUPDIR) backup_dir.mkdir(exist_ok=True, parents=True) diff --git a/msg.py b/msg.py index 04e711ac..e493cff0 100644 --- a/msg.py +++ b/msg.py @@ -5,13 +5,13 @@ import signal import shutil import sys -import time from pathlib import Path import config from gui import ask_question from gui import show_error +from utils import get_timestamp class GzippedRotatingFileHandler(RotatingFileHandler): @@ -298,7 +298,7 @@ def status(text, app=None, end='\n'): def strip_timestamp(msg, timestamp_length=20): return msg[timestamp_length:] - timestamp = time.strftime("%Y-%m-%dT%H:%M:%S") + timestamp = get_timestamp() """Handles status messages for both TUI and GUI.""" if app is not None: if config.DIALOG == 'tk': diff --git a/utils.py b/utils.py index 3608d88a..d9629615 100644 --- a/utils.py +++ b/utils.py @@ -15,6 +15,7 @@ import threading import time import tkinter as tk +from datetime import datetime from packaging import version from pathlib import Path from typing import List, Union @@ -1018,3 +1019,7 @@ def stopwatch(start_time=None, interval=10.0): return True, last_log_time else: return False, start_time + + +def get_timestamp(): + return datetime.today().strftime('%Y-%m-%dT%H%M%S') diff --git a/wine.py b/wine.py index 7e7b1fb4..af875037 100644 --- a/wine.py +++ b/wine.py @@ -293,7 +293,10 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): if exe_args: command.extend(exe_args) - logging.debug(f"subprocess cmd: '{' '.join(command)}'") + cmd = f"subprocess cmd: '{' '.join(command)}'" + with open(config.wine_log, 'a') as wine_log: + print(f"{utils.get_timestamp()}: {cmd}", file=wine_log) + logging.debug(cmd) try: with open(config.wine_log, 'a') as wine_log: process = system.popen_command( From 68e082e8b08587db490938d807675fdedce6f4ca Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 9 Oct 2024 16:21:52 +0100 Subject: [PATCH 219/253] update logos.LogosManager.monitor_logs to account for all States --- config.py | 6 ++--- logos.py | 68 ++++++++++++++++++++++++++++++++++++------------------ system.py | 18 +++++++-------- tui_app.py | 8 +++---- wine.py | 54 +++++++++++++++++++++++++++++-------------- 5 files changed, 98 insertions(+), 56 deletions(-) diff --git a/config.py b/config.py index afc0a33a..03c83734 100644 --- a/config.py +++ b/config.py @@ -10,8 +10,8 @@ "current_logos_version", "curses_colors", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", - "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION", "logos_release_channel", - "lli_release_channel" + "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION", + "logos_release_channel", "lli_release_channel", ] for k in core_config_keys: globals()[k] = os.getenv(k) @@ -114,7 +114,7 @@ resizing = False processes = {} threads = [] -login_window_cmd = None +logos_login_cmd = None logos_cef_cmd = None logos_indexer_cmd = None logos_indexer_exe = None diff --git a/logos.py b/logos.py index 181578b3..99621caf 100644 --- a/logos.py +++ b/logos.py @@ -1,4 +1,3 @@ -import os import time from enum import Enum import logging @@ -28,29 +27,37 @@ def __init__(self, app=None): def monitor_indexing(self): if config.logos_indexer_cmd in config.processes: - indexer = config.processes[config.logos_indexer_cmd] - if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): + indexer = config.processes.get(config.logos_indexer_cmd) + if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): # noqa: E501 self.indexing_state = State.RUNNING else: self.indexing_state = State.STOPPED def monitor_logos(self): splash = config.processes.get(config.LOGOS_EXE, []) - login_window = config.processes.get(config.login_window_cmd, []) - logos_cef = config.processes.get(config.logos_cef_cmd, []) + login = config.processes.get(config.logos_login_cmd, []) + cef = config.processes.get(config.logos_cef_cmd, []) splash_running = splash[0].is_running() if splash else False - login_running = login_window[0].is_running() if login_window else False - logos_cef_running = logos_cef[0].is_running() if logos_cef else False + login_running = login[0].is_running() if login else False + cef_running = cef[0].is_running() if cef else False + # logging.debug(f"{self.logos_state=}") + # logging.debug(f"{splash_running=}; {login_running=}; {cef_running=}") - if self.logos_state == State.RUNNING: - if not (splash_running or login_running or logos_cef_running): + if self.logos_state == State.STARTING: + if login_running or cef_running: + self.logos_state = State.RUNNING + elif self.logos_state == State.RUNNING: + if not any((splash_running, login_running, cef_running)): self.stop() + elif self.logos_state == State.STOPPING: + pass elif self.logos_state == State.STOPPED: - if splash and isinstance(splash[0], psutil.Process) and splash_running: + if splash_running: self.logos_state = State.STARTING - if (login_window and isinstance(login_window[0], psutil.Process) and login_running) or ( - logos_cef and isinstance(logos_cef[0], psutil.Process) and logos_cef_running): + if login_running: + self.logos_state = State.RUNNING + if cef_running: self.logos_state = State.RUNNING def monitor(self): @@ -60,7 +67,8 @@ def monitor(self): self.monitor_indexing() self.monitor_logos() except Exception as e: - pass + # pass + logging.error(e) def start(self): self.logos_state = State.STARTING @@ -79,7 +87,7 @@ def run_logos(): txt = f"Can't run {config.FLPRODUCT} 10+ with Wine below 7.18." logging.critical(txt) msg.status(txt, self.app) - if logos_release[0] > 29 and wine_release[0] < 9 and wine_release[1] < 10: + if logos_release[0] > 29 and wine_release[0] < 9 and wine_release[1] < 10: # noqa: E501 txt = f"Can't run {config.FLPRODUCT} 30+ with Wine below 9.10." logging.critical(txt) msg.status(txt, self.app) @@ -91,26 +99,40 @@ def run_logos(): app = None msg.status(f"Running {config.FLPRODUCT}…", app=app) utils.start_thread(run_logos, daemon_bool=False) - self.logos_state = State.RUNNING + # NOTE: The following code would keep the CLI open while running + # Logos, but since wine logging is sent directly to wine.log, + # there's no terminal output to see. A user can see that output by: + # tail -f ~/.local/state/FaithLife-Community/wine.log + # if config.DIALOG == 'cli': + # run_logos() + # self.monitor() + # while config.processes.get(config.LOGOS_EXE) is None: + # time.sleep(0.1) + # while self.logos_state != State.STOPPED: + # time.sleep(0.1) + # self.monitor() + # else: + # utils.start_thread(run_logos, daemon_bool=False) def stop(self): + logging.debug("Stopping LogosManager.") self.logos_state = State.STOPPING if self.app: pids = [] - for process_name in [config.LOGOS_EXE, config.login_window_cmd, config.logos_cef_cmd]: + for process_name in [config.LOGOS_EXE, config.logos_login_cmd, config.logos_cef_cmd]: # noqa: E501 process_list = config.processes.get(process_name) if process_list: pids.extend([str(process.pid) for process in process_list]) else: - logging.debug(f"No Logos processes found for {process_name}.") + logging.debug(f"No Logos processes found for {process_name}.") # noqa: E501 if pids: try: system.run_command(['kill', '-9'] + pids) self.logos_state = State.STOPPED - msg.status(f"Stopped Logos processes at PIDs {', '.join(pids)}.", self.app) + msg.status(f"Stopped Logos processes at PIDs {', '.join(pids)}.", self.app) # noqa: E501 except Exception as e: - logging.debug("Error while stopping Logos processes: {e}.") + logging.debug(f"Error while stopping Logos processes: {e}.") # noqa: E501 else: logging.debug("No Logos processes to stop.") self.logos_state = State.STOPPED @@ -139,7 +161,7 @@ def check_if_indexing(process): elapsed_min = int(total_elapsed_time // 60) elapsed_sec = int(total_elapsed_time % 60) formatted_time = f"{elapsed_min}m {elapsed_sec}s" - msg.status(f"Indexing is running… (Elapsed Time: {formatted_time})", self.app) + msg.status(f"Indexing is running… (Elapsed Time: {formatted_time})", self.app) # noqa: E501 update_send = 0 index_finished.set() @@ -179,15 +201,15 @@ def stop_indexing(self): if process_list: pids.extend([str(process.pid) for process in process_list]) else: - logging.debug(f"No LogosIndexer processes found for {process_name}.") + logging.debug(f"No LogosIndexer processes found for {process_name}.") # noqa: E501 if pids: try: system.run_command(['kill', '-9'] + pids) self.indexing_state = State.STOPPED - msg.status(f"Stopped LogosIndexer processes at PIDs {', '.join(pids)}.", self.app) + msg.status(f"Stopped LogosIndexer processes at PIDs {', '.join(pids)}.", self.app) # noqa: E501 except Exception as e: - logging.debug("Error while stopping LogosIndexer processes: {e}.") + logging.debug(f"Error while stopping LogosIndexer processes: {e}.") # noqa: E501 else: logging.debug("No LogosIndexer processes to stop.") self.indexing_state = State.STOPPED diff --git a/system.py b/system.py index 66fb92c7..c7878e5e 100644 --- a/system.py +++ b/system.py @@ -169,14 +169,14 @@ def popen_command(command, retries=1, delay=0, **kwargs): return process except subprocess.CalledProcessError as e: - logging.error(f"Error occurred in popen_command() while executing \"{command}\": {e}") + logging.error(f"Error occurred in popen_command() while executing \"{command}\": {e}") # noqa: E501 if "lock" in str(e): - logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") + logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") # noqa: E501 time.sleep(delay) else: raise e except Exception as e: - logging.error(f"An unexpected error occurred when running {command}: {e}") + logging.error(f"An unexpected error occurred when running {command}: {e}") # noqa: E501 return None logging.error(f"Failed to execute after {retries} attempts: '{command}'") @@ -216,18 +216,18 @@ def get_pids(query): results = [] for process in psutil.process_iter(['pid', 'name', 'cmdline']): try: - if process.info['cmdline'] is not None and query in process.info['cmdline']: + if process.info['cmdline'] is not None and query in process.info['cmdline']: # noqa: E501 results.append(process) - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): # noqa: E501 pass return results def get_logos_pids(): config.processes[config.LOGOS_EXE] = get_pids(config.LOGOS_EXE) - config.processes[config.login_window_cmd] = get_pids(config.login_window_cmd) + config.processes[config.logos_login_cmd] = get_pids(config.logos_login_cmd) config.processes[config.logos_cef_cmd] = get_pids(config.logos_cef_cmd) - config.processes[config.logos_indexer_exe] = get_pids(config.logos_indexer_exe) + config.processes[config.logos_indexer_exe] = get_pids(config.logos_indexer_exe) # noqa: E501 # def get_pids_using_file(file_path, mode=None): @@ -788,9 +788,9 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) def have_lib(library, ld_library_path): available_library_paths = ['/usr/lib', '/lib'] if ld_library_path is not None: - available_library_paths = [*ld_library_path.split(':'), *available_library_paths] + available_library_paths = [*ld_library_path.split(':'), *available_library_paths] # noqa: E501 - roots = [root for root in available_library_paths if not Path(root).is_symlink()] + roots = [root for root in available_library_paths if not Path(root).is_symlink()] # noqa: E501 logging.debug(f"Library Paths: {roots}") for root in roots: libs = [] diff --git a/tui_app.py b/tui_app.py index c0d7e3dc..7967a298 100644 --- a/tui_app.py +++ b/tui_app.py @@ -871,15 +871,15 @@ def set_tui_menu_options(self, dialog=False): logging.error(f"{error_message}") if utils.app_is_installed(): - if self.logos.logos_state == logos.State.RUNNING: + if self.logos.logos_state in [logos.State.STARTING, logos.State.RUNNING]: # noqa: E501 run = f"Stop {config.FLPRODUCT}" - elif self.logos.logos_state == logos.State.STOPPED: + elif self.logos.logos_state in [logos.State.STOPPING, logos.State.STOPPED]: # noqa: E501 run = f"Run {config.FLPRODUCT}" if self.logos.indexing_state == logos.State.RUNNING: - indexing = f"Stop Indexing" + indexing = "Stop Indexing" elif self.logos.indexing_state == logos.State.STOPPED: - indexing = f"Run Indexing" + indexing = "Run Indexing" labels_default = [ run, indexing diff --git a/wine.py b/wine.py index af875037..f217af6a 100644 --- a/wine.py +++ b/wine.py @@ -25,15 +25,17 @@ def get_wine_user(): def set_logos_paths(): if config.wine_user is None: get_wine_user() - if config.logos_indexer_cmd is None or config.login_window_cmd is None or config.logos_cef_cmd is None: - config.login_window_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + logos_cmds = [ + config.logos_cef_cmd, + config.logos_indexer_cmd, + config.logos_login_cmd, + ] + if None in logos_cmds: config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 config.logos_indexer_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 - for root, dirs, files in os.walk(os.path.join(config.WINEPREFIX, "drive_c")): # noqa: E501 - for f in files: - if f == "LogosIndexer.exe" and root.endswith("Logos/System"): - config.logos_indexer_exe = os.path.join(root, f) - break + config.logos_login_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + config.logos_indexer_exe = str(Path(utils.find_installed_product()).parent / 'System' / 'LogosIndexer.exe') # noqa: E501 + def check_wineserver(): try: @@ -133,7 +135,7 @@ def check_wine_rules(wine_release, release_version): elif config.TARGETVERSION == "9": required_wine_minimum = [7, 0] else: - raise ValueError(f"Invalid TARGETVERSION: {config.TARGETVERSION} ({type(config.TARGETVERSION)})") + raise ValueError(f"Invalid TARGETVERSION: {config.TARGETVERSION} ({type(config.TARGETVERSION)})") # noqa: E501 rules = [ { @@ -159,32 +161,50 @@ def check_wine_rules(wine_release, release_version): major_min, minor_min = required_wine_minimum major, minor, release_type = wine_release - result = True, "None" # Whether the release is allowed and the error message + result = True, "None" # Whether the release is allowed; error message for rule in rules: if major == rule["major"]: # Verify release is allowed if release_type not in rule["allowed_releases"]: if minor >= rule.get("devel_allowed", float('inf')): if release_type not in ["staging", "devel"]: - result = False, (f"Wine release needs to be devel or staging. " - f"Current release: {release_type}.") + result = ( + False, + ( + f"Wine release needs to be devel or staging. " + f"Current release: {release_type}." + ) + ) break else: - result = False, (f"Wine release needs to be {rule["allowed_releases"]}. " - f"Current release: {release_type}.") + result = ( + False, + ( + f"Wine release needs to be {rule['allowed_releases']}. " # noqa: E501 + f"Current release: {release_type}." + ) + ) break # Verify version is allowed if minor in rule.get("minor_bad", []): result = False, f"Wine version {major}.{minor} will not work." break if major < major_min: - result = False, (f"Wine version {major}.{minor} is " - f"below minimum required ({major_min}.{minor_min}).") + result = ( + False, + ( + f"Wine version {major}.{minor} is " + f"below minimum required ({major_min}.{minor_min}).") + ) break elif major == major_min and minor < minor_min: if not rule["proton"]: - result = False, (f"Wine version {major}.{minor} is " - f"below minimum required ({major_min}.{minor_min}).") + result = ( + False, + ( + f"Wine version {major}.{minor} is " + f"below minimum required ({major_min}.{minor_min}).") # noqa: E501 + ) break logging.debug(f"Result: {result}") return result From f289f04827307ce19cd3e232edde9834d45b94bc Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 9 Oct 2024 18:14:39 +0100 Subject: [PATCH 220/253] add warning window for UIs --- gui.py | 8 ++++++-- msg.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/gui.py b/gui.py index ebb7ff2f..68c207bd 100644 --- a/gui.py +++ b/gui.py @@ -364,14 +364,18 @@ def draw_prompt(self): store_button.pack(pady=20) -def show_error(message, title="Fatal Error", detail=None, app=None, parent=None): # noqa: E501 +def show_error(message, fatal=True, detail=None, app=None, parent=None): # noqa: E501 + title = "Error" + if fatal: + title = "Fatal Error" + kwargs = {'message': message} if parent and hasattr(app, parent): kwargs['parent'] = app.__dict__.get(parent) if detail: kwargs['detail'] = detail messagebox.showerror(title, **kwargs) - if hasattr(app, 'root'): + if fatal and hasattr(app, 'root'): app.root.destroy() diff --git a/msg.py b/msg.py index e493cff0..c869d6a3 100644 --- a/msg.py +++ b/msg.py @@ -163,11 +163,9 @@ def logos_warn(message): logos_msg(message) -# TODO: I think detail is doing the same thing as secondary. -def logos_error(message, secondary=None, detail=None, app=None, parent=None): +def ui_message(message, secondary=None, detail=None, app=None, parent=None, fatal=False): # noqa: E501 if detail is None: detail = '' - logging.critical(message) WIKI_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki" # noqa: E501 TELEGRAM_LINK = "https://t.me/linux_logos" MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" @@ -177,6 +175,7 @@ def logos_error(message, secondary=None, detail=None, app=None, parent=None): message, detail=f"{detail}\n\n{help_message}", app=app, + fatal=fatal, parent=parent ) elif config.DIALOG == 'curses': @@ -188,6 +187,33 @@ def logos_error(message, secondary=None, detail=None, app=None, parent=None): else: logos_msg(message) + +# TODO: I think detail is doing the same thing as secondary. +def logos_error(message, secondary=None, detail=None, app=None, parent=None): + # if detail is None: + # detail = '' + # WIKI_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki" # noqa: E501 + # TELEGRAM_LINK = "https://t.me/linux_logos" + # MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" + # help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 + # if config.DIALOG == 'tk': + # show_error( + # message, + # detail=f"{detail}\n\n{help_message}", + # app=app, + # parent=parent + # ) + # elif config.DIALOG == 'curses': + # if secondary != "info": + # status(message) + # status(help_message) + # else: + # logos_msg(message) + # else: + # logos_msg(message) + ui_message(message, secondary=secondary, detail=detail, app=app, parent=parent, fatal=True) # noqa: E501 + + logging.critical(message) if secondary is None or secondary == "": try: os.remove("/tmp/LogosLinuxInstaller.pid") @@ -200,6 +226,11 @@ def logos_error(message, secondary=None, detail=None, app=None, parent=None): sys.exit(1) +def logos_warning(message, secondary=None, detail=None, app=None, parent=None): + ui_message(message, secondary=secondary, detail=detail, app=app, parent=parent) # noqa: E501 + logging.error(message) + + def cli_question(question_text, secondary=""): while True: try: From 421aea31783455707dbf28d3882d3d52e068d1e3 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 9 Oct 2024 18:32:25 +0100 Subject: [PATCH 221/253] fix backup and restore for GUI --- control.py | 60 +++++++++++++++++++++--------------------------------- gui_app.py | 11 +++++----- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/control.py b/control.py index 8cf6bfeb..852e3520 100644 --- a/control.py +++ b/control.py @@ -72,41 +72,41 @@ def backup_and_restore(mode='backup', app=None): answer = msg.cli_ask_filepath("Give backups folder path:") answer = Path(answer).expanduser().resolve() if not answer.is_dir(): - msg.status(f"Not a valid folder path: {answer}", app) + msg.status(f"Not a valid folder path: {answer}", app=app) config.BACKUPDIR = answer # Set source folders. + backup_dir = Path(config.BACKUPDIR) + try: + backup_dir.mkdir(exist_ok=True, parents=True) + except PermissionError: + verb = 'access' + if mode == 'backup': + verb = 'create' + msg.logos_warning(f"Can't {verb} folder: {backup_dir}") + return + if mode == 'restore': + config.RESTOREDIR = utils.get_latest_folder(config.BACKUPDIR) + config.RESTOREDIR = Path(config.RESTOREDIR).expanduser().resolve() if config.DIALOG == 'tk': pass else: # Offer to restore the most recent backup. - config.RESTOREDIR = utils.get_latest_folder(config.BACKUPDIR) if not msg.cli_question(f"Restore most-recent backup?: {config.RESTOREDIR}", ""): # noqa: E501 config.RESTOREDIR = msg.cli_ask_filepath("Path to backup set that you want to restore:") # noqa: E501 - config.RESTOREDIR = Path(config.RESTOREDIR).expanduser().resolve() source_dir_base = config.RESTOREDIR else: source_dir_base = Path(config.LOGOS_EXE).parent src_dirs = [source_dir_base / d for d in data_dirs if Path(source_dir_base / d).is_dir()] # noqa: E501 logging.debug(f"{src_dirs=}") if not src_dirs: - m = "No files to backup" - if config.DIALOG in ['curses', 'dialog', 'tk']: - app.status_q.put(m) - app.root.event_generate('<>') - app.root.event_generate('<>') - logging.warning(m) + msg.logos_warning(f"No files to {mode}", app=app) return # Get source transfer size. q = queue.Queue() - m = "Calculating backup size" - if config.DIALOG in ['curses', 'dialog', 'tk']: - app.status_q.put(m) - app.root.event_generate('<>') - app.root.event_generate(app.status_evt) - msg.status(m, end='') + msg.status("Calculating backup size", app=app) t = utils.start_thread(utils.get_folder_group_size, src_dirs, q) try: while t.is_alive(): @@ -115,18 +115,14 @@ def backup_and_restore(mode='backup', app=None): print() except KeyboardInterrupt: print() - msg.logos_error("Cancelled with Ctrl+C.") + msg.logos_error("Cancelled with Ctrl+C.", app=app) t.join() if config.DIALOG in ['curses', 'dialog', 'tk']: app.root.event_generate('<>') app.root.event_generate('<>') src_size = q.get() if src_size == 0: - m = f"Nothing to {mode}!" - logging.warning(m) - if config.DIALOG in ['curses', 'dialog', 'tk']: - app.status_q.put(m) - app.root.event_generate('<>') + msg.logos_warning(f"Nothing to {mode}!", app=app) return # Set destination folder. @@ -137,11 +133,9 @@ def backup_and_restore(mode='backup', app=None): dst = Path(dst_dir) / d if dst.is_dir(): shutil.rmtree(dst) - else: + else: # backup mode timestamp = utils.get_timestamp().replace('-', '') current_backup_name = f"{config.FLPRODUCT}{config.TARGETVERSION}-{timestamp}" # noqa: E501 - backup_dir = Path(config.BACKUPDIR) - backup_dir.mkdir(exist_ok=True, parents=True) dst_dir = backup_dir / current_backup_name logging.debug(f"Backup directory path: {dst_dir}") @@ -153,24 +147,16 @@ def backup_and_restore(mode='backup', app=None): # Verify disk space. if not utils.enough_disk_space(dst_dir, src_size): - m = f"Not enough free disk space for {mode}." - if config.DIALOG in ['curses', 'dialog', 'tk']: - app.status_q.put(m) - app.root.event_generate('<>') - return - else: - msg.logos_error(m) + dst_dir.rmdir() + msg.logos_warning(f"Not enough free disk space for {mode}.", app=app) + return # Run file transfer. if mode == 'restore': m = f"Restoring backup from {str(source_dir_base)}" else: m = f"Backing up to {str(dst_dir)}" - logging.info(m) - msg.status(m) - if config.DIALOG in ['curses', 'dialog', 'tk']: - app.status_q.put(m) - app.root.event_generate(app.status_evt) + msg.status(m, app=app) dst_dir_size = utils.get_path_size(dst_dir) t = utils.start_thread(copy_data, src_dirs, dst_dir) try: @@ -184,7 +170,7 @@ def backup_and_restore(mode='backup', app=None): if config.DIALOG in ['curses', 'dialog', 'tk']: app.progress_q.put(progress) app.root.event_generate('<>') - time.sleep(0.5) + time.sleep(0.1) print() except KeyboardInterrupt: print() diff --git a/gui_app.py b/gui_app.py index 943bc5a0..effa2c7c 100644 --- a/gui_app.py +++ b/gui_app.py @@ -883,10 +883,6 @@ def update_download_progress(self, evt=None): self.gui.progressvar.set(int(d)) def update_progress(self, evt=None): - if self.config_thread.is_alive(): - # Don't update config progress. - self.gui.progressvar.set(0) - return progress = self.progress_q.get() if not type(progress) is int: return @@ -897,7 +893,12 @@ def update_progress(self, evt=None): self.gui.progressvar.set(progress) def update_status_text(self, evt=None): - self.gui.statusvar.set(self.status_q.get()) + if evt: + self.gui.statusvar.set(self.status_q.get()) + self.root.after(3000, self.update_status_text) + else: # clear status text if called manually and no progress shown + if self.gui.progressvar.get() == 0: + self.gui.statusvar.set('') def start_indeterminate_progress(self, evt=None): self.gui.progress.state(['!disabled']) From b395d1f7479efbc968a957592ba5ce24e4e21871 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 15:42:10 +0100 Subject: [PATCH 222/253] use standalone function for create_launcher_shortcuts --- cli.py | 11 ++-- config.py | 3 +- installer.py | 143 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 100 insertions(+), 57 deletions(-) diff --git a/cli.py b/cli.py index 7cfcff87..1ce81aa3 100644 --- a/cli.py +++ b/cli.py @@ -1,12 +1,9 @@ -# import logging import queue import threading -import config import control import installer import logos -# import msg import wine import utils @@ -23,6 +20,9 @@ def __init__(self): def backup(self): control.backup(app=self) + def create_shortcuts(self): + installer.create_launcher_shortcuts() + def edit_config(self): control.edit_config() @@ -122,10 +122,7 @@ def backup(): def create_shortcuts(): - # TODO: This takes surprisingly long because it walks through all the - # installer steps to confirm everything up to the shortcuts. Can this be - # shortcutted? - CLI().install_app() + CLI().create_shortcuts() def edit_config(): diff --git a/config.py b/config.py index 03c83734..23e73ff7 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,7 @@ # Define and set variables that are required in the config file. core_config_keys = [ - "FLPRODUCT", "FLPRODUCTi", "TARGETVERSION", "TARGET_RELEASE_VERSION", + "FLPRODUCT", "TARGETVERSION", "TARGET_RELEASE_VERSION", "current_logos_version", "curses_colors", "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", @@ -57,6 +57,7 @@ authenticated = False BADPACKAGES = None DEFAULT_CONFIG_PATH = os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json") # noqa: E501 +FLPRODUCTi = None GUI = None INSTALL_STEP = 0 INSTALL_STEPS_COUNT = 0 diff --git a/installer.py b/installer.py index 042179d3..455988b0 100644 --- a/installer.py +++ b/installer.py @@ -40,11 +40,10 @@ def ensure_product_choice(app=None): if config.DIALOG == 'curses' and app: app.set_product(config.FLPRODUCT) + config.FLPRODUCTi = get_flproducti_name(config.FLPRODUCT) if config.FLPRODUCT == 'Logos': - config.FLPRODUCTi = 'logos4' config.VERBUM_PATH = "/" elif config.FLPRODUCT == 'Verbum': - config.FLPRODUCTi = 'verbum' config.VERBUM_PATH = "/Verbum/" logging.debug(f"> {config.FLPRODUCT=}") @@ -271,7 +270,8 @@ def ensure_installation_config(app=None): # Set icon variables. app_dir = Path(__file__).parent - logos_icon_url = app_dir / 'img' / f"{config.FLPRODUCTi}-128-icon.png" + flproducti = get_flproducti_name(config.FLPRODUCT) + logos_icon_url = app_dir / 'img' / f"{flproducti}-128-icon.png" config.LOGOS_ICON_URL = str(logos_icon_url) config.LOGOS_ICON_FILENAME = logos_icon_url.name config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{config.TARGETVERSION}{config.VERBUM_PATH}Installer/{config.TARGET_RELEASE_VERSION}/{config.FLPRODUCT}-x64.msi" # noqa: E501 @@ -715,58 +715,19 @@ def ensure_launcher_shortcuts(app=None): runmode = system.get_runmode() if runmode == 'binary': update_install_feedback("Creating launcher shortcuts…", app=app) - - app_dir = Path(config.INSTALLDIR) / 'data' - logos_icon_path = app_dir / config.LOGOS_ICON_FILENAME # noqa: E501 - if not logos_icon_path.is_file(): - app_dir.mkdir(exist_ok=True) - shutil.copy(config.LOGOS_ICON_URL, logos_icon_path) - else: - logging.info(f"Icon found at {logos_icon_path}.") - - desktop_files = [ - ( - f"{config.FLPRODUCT}Bible.desktop", - f"""[Desktop Entry] -Name={config.FLPRODUCT}Bible -Comment=A Bible Study Library with Built-In Tools -Exec={config.INSTALLDIR}/LogosLinuxInstaller --run-installed-app -Icon={str(logos_icon_path)} -Terminal=false -Type=Application -Categories=Education; -""" - ), - ( - f"{config.FLPRODUCT}Bible-ControlPanel.desktop", - f"""[Desktop Entry] -Name={config.FLPRODUCT}Bible Control Panel -Comment=Perform various tasks for {config.FLPRODUCT} app -Exec={config.INSTALLDIR}/LogosLinuxInstaller -Icon={str(logos_icon_path)} -Terminal=false -Type=Application -Categories=Education; -""" - ), - ] - for f, c in desktop_files: - create_desktop_file(f, c) - fpath = Path.home() / '.local' / 'share' / 'applications' / f - logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") + create_launcher_shortcuts() else: update_install_feedback( "Running from source. Skipping launcher creation.", app=app ) - if app: - if config.DIALOG == 'cli': - # Signal CLI.user_input_processor to stop. - app.input_q.put(None) - app.input_event.set() - # Signal CLI itself to stop. - app.stop() + if config.DIALOG == 'cli': + # Signal CLI.user_input_processor to stop. + app.input_q.put(None) + app.input_event.set() + # Signal CLI itself to stop. + app.stop() def update_install_feedback(text, app=None): @@ -780,6 +741,14 @@ def get_progress_pct(current, total): return round(current * 100 / total) +def get_flproducti_name(product_name): + lname = product_name.lower() + if lname == 'logos': + return 'logos4' + elif lname == 'verbum': + return lname + + def create_desktop_file(name, contents): launcher_path = Path(f"~/.local/share/applications/{name}").expanduser() if launcher_path.is_file(): @@ -790,3 +759,79 @@ def create_desktop_file(name, contents): with launcher_path.open('w') as f: f.write(contents) os.chmod(launcher_path, 0o755) + + +def create_launcher_shortcuts(): + # Set variables for use in launcher files. + flproduct = config.FLPRODUCT + installdir = Path(config.INSTALLDIR) + m = "Can't create launchers" + if flproduct is None: + reason = "because the FaithLife product is not defined." + msg.logos_warning(f"{m} {reason}") # noqa: E501 + return + flproducti = get_flproducti_name(flproduct) + src_dir = Path(__file__).parent + logos_icon_src = src_dir / 'img' / f"{flproducti}-128-icon.png" + + if installdir is None: + reason = "because the installation folder is not defined." + msg.logos_warning(f"{m} {reason}") + return + if not installdir.is_dir(): + reason = "because the installation folder does not exist." + msg.logos_warning(f"{m} {reason}") + return + app_dir = Path(installdir) / 'data' + logos_icon_path = app_dir / logos_icon_src.name + + if system.get_runmode() == 'binary': + lli_executable = f"{installdir}/LogosLinuxInstaller" + else: + script = Path(sys.argv[0]).expanduser().resolve() + # Find python in virtual environment. + py_bin = next(script.parent.glob('*/bin/python')) + if not py_bin.is_file(): + msg.logos_warning("Could not locate python binary in virtual environment.") # noqa: E501 + return + lli_executable = f"{py_bin} {script}" + + if not logos_icon_path.is_file(): + app_dir.mkdir(exist_ok=True) + shutil.copy(logos_icon_src, logos_icon_path) + else: + logging.info(f"Icon found at {logos_icon_path}.") + + # Set launcher file names and content. + desktop_files = [ + ( + f"{flproduct}Bible.desktop", + f"""[Desktop Entry] +Name={flproduct}Bible +Comment=A Bible Study Library with Built-In Tools +Exec={lli_executable} --run-installed-app +Icon={logos_icon_path} +Terminal=false +Type=Application +Categories=Education; +""" + ), + ( + f"{flproduct}Bible-ControlPanel.desktop", + f"""[Desktop Entry] +Name={flproduct}Bible Control Panel +Comment=Perform various tasks for {flproduct} app +Exec={lli_executable} +Icon={logos_icon_path} +Terminal=false +Type=Application +Categories=Education; +""" + ), + ] + + # Create the files. + for file_name, content in desktop_files: + create_desktop_file(file_name, content) + fpath = Path.home() / '.local' / 'share' / 'applications' / file_name + logging.debug(f"> File exists?: {fpath}: {fpath.is_file()}") From 1936d35028833047abacc4cc603096babb918609 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 16:00:22 +0100 Subject: [PATCH 223/253] add functions for ensure_config_file --- installer.py | 77 ++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/installer.py b/installer.py index 455988b0..4f17fd81 100644 --- a/installer.py +++ b/installer.py @@ -639,45 +639,27 @@ def ensure_config_file(app=None): if not Path(config.CONFIG_FILE).is_file(): logging.info(f"No config file at {config.CONFIG_FILE}") - parent = Path.home() / ".config" / "Logos_on_Linux" - parent.mkdir(exist_ok=True, parents=True) - if parent.is_dir(): - utils.write_config(config.CONFIG_FILE) - logging.info(f"A config file was created at {config.CONFIG_FILE}.") - else: - msg.logos_warn(f"{str(parent)} does not exist. Failed to create config file.") # noqa: E501 + create_config_file() else: logging.info(f"Config file exists at {config.CONFIG_FILE}.") - # Compare existing config file contents with installer config. - logging.info("Comparing its contents with current config.") - current_config_file_dict = config.get_config_file_dict(config.CONFIG_FILE) # noqa: E501 - different = False - - for key in config.core_config_keys: - if current_config_file_dict.get(key) != config.__dict__.get(key): - different = True - break - - if different: - if app: - if config.DIALOG == 'cli': - if msg.logos_acknowledge_question( - f"Update config file at {config.CONFIG_FILE}?", - "The existing config file was not overwritten.", - "" - ): - logging.info("Updating config file.") - utils.write_config(config.CONFIG_FILE) - else: - utils.send_task(app, 'CONFIG') - if config.DIALOG == 'curses': - app.config_e.wait() + if config_has_changed(): + if config.DIALOG == 'cli': + if msg.logos_acknowledge_question( + f"Update config file at {config.CONFIG_FILE}?", + "The existing config file was not overwritten.", + "" + ): + logging.info("Updating config file.") + utils.write_config(config.CONFIG_FILE) + else: + utils.send_task(app, 'CONFIG') + if config.DIALOG == 'curses': + app.config_e.wait() - if app: - if config.DIALOG == 'cli': - msg.logos_msg("Install has finished.") - else: - utils.send_task(app, 'DONE') + if config.DIALOG == 'cli': + msg.logos_msg("Install has finished.") + else: + utils.send_task(app, 'DONE') logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 @@ -749,6 +731,29 @@ def get_flproducti_name(product_name): return lname +def create_config_file(): + config_dir = Path(config.DEFAULT_CONFIG_PATH).parent + config_dir.mkdir(exist_ok=True, parents=True) + if config_dir.is_dir(): + utils.write_config(config.CONFIG_FILE) + logging.info(f"A config file was created at {config.CONFIG_FILE}.") + else: + msg.logos_warn(f"{config_dir} does not exist. Failed to create config file.") # noqa: E501 + + +def config_has_changed(): + # Compare existing config file contents with installer config. + logging.info("Comparing its contents with current config.") + current_config_file_dict = config.get_config_file_dict(config.CONFIG_FILE) + changed = False + + for key in config.core_config_keys: + if current_config_file_dict.get(key) != config.__dict__.get(key): + changed = True + break + return changed + + def create_desktop_file(name, contents): launcher_path = Path(f"~/.local/share/applications/{name}").expanduser() if launcher_path.is_file(): From b75b9bd96c0691fe3803906b62a3ee8bf6d1fec9 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 16:27:22 +0100 Subject: [PATCH 224/253] standardize use of wine.wait_pid(process) over process.wait() --- logos.py | 6 +++--- wine.py | 12 +++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/logos.py b/logos.py index 99621caf..422ce29e 100644 --- a/logos.py +++ b/logos.py @@ -223,7 +223,7 @@ def get_app_logging_state(self, init=False): ) if current_value == '0x1': state = 'ENABLED' - if self.app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: self.app.logging_q.put(state) if init: self.app.root.event_generate('<>') @@ -262,9 +262,9 @@ def switch_logging(self, action=None): exe='reg', exe_args=exe_args ) - process.wait() + wine.wait_pid(process) wine.wineserver_wait() config.LOGS = state - if self.app is not None: + if config.DIALOG in ['curses', 'dialog', 'tk']: self.app.logging_q.put(state) self.app.root.event_generate(self.app.logging_event) diff --git a/wine.py b/wine.py index f217af6a..8f1599dd 100644 --- a/wine.py +++ b/wine.py @@ -40,7 +40,7 @@ def set_logos_paths(): def check_wineserver(): try: process = run_wine_proc(config.WINESERVER, exe_args=["-p"]) - process.wait() + wait_pid(process) return process.returncode == 0 except Exception: return False @@ -49,7 +49,7 @@ def check_wineserver(): def wineserver_kill(): if check_wineserver(): process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) - process.wait() + wait_pid(process) # TODO: Review these three commands. The top is the newest and should be @@ -57,7 +57,7 @@ def wineserver_kill(): def wineserver_wait(): if check_wineserver(): process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) - process.wait() + wait_pid(process) # def light_wineserver_wait(): @@ -260,6 +260,8 @@ def wine_reg_install(reg_file): exe="regedit.exe", exe_args=[reg_file] ) + # NOTE: For some reason wait_pid results in the reg install failing. + # wait_pid(process) process.wait() if process is None or process.returncode != 0: failed = "Failed to install reg file" @@ -342,10 +344,6 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 else: logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 - # returncode = process.wait() - # - # if returncode != 0: - # logging.error(f"Error running '{' '.join(command)}': {process.returncode}") # noqa: E501 return process else: return None From 1e58af40661506490d99aeb471401524df228a9c Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 16:36:54 +0100 Subject: [PATCH 225/253] add function wine.disable_winemenubuilder --- installer.py | 10 +++------- wine.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/installer.py b/installer.py index 4f17fd81..39324bec 100644 --- a/installer.py +++ b/installer.py @@ -554,13 +554,7 @@ def ensure_winetricks_applied(app=None): if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): msg.status("Disabling winemenubuilder…", app) - reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' - reg_file.write_text(r'''REGEDIT4 - -[HKEY_CURRENT_USER\Software\Wine\DllOverrides] -"winemenubuilder.exe"="" -''') - wine.wine_reg_install(reg_file) + wine.disable_winemenubuilder() if not utils.grep(r'"renderer"="gdi"', usr_reg): msg.status("Setting Renderer to GDI…", app) @@ -582,6 +576,8 @@ def ensure_winetricks_applied(app=None): msg.status(f"Setting {config.FLPRODUCT} to Win10 Mode…", app) wine.set_win_version("logos", "win10") + # NOTE: Can't use utils.grep check here because the string + # "Version"="win10" might appear elsewhere in the registry. msg.logos_msg(f"Setting {config.FLPRODUCT} Bible Indexing to Win10 Mode…") # noqa: E501 wine.set_win_version("indexer", "win10") # wine.light_wineserver_wait() diff --git a/wine.py b/wine.py index 8f1599dd..b9245420 100644 --- a/wine.py +++ b/wine.py @@ -273,6 +273,16 @@ def wine_reg_install(reg_file): wineserver_wait() +def disable_winemenubuilder(): + reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' + reg_file.write_text(r'''REGEDIT4 + +[HKEY_CURRENT_USER\Software\Wine\DllOverrides] +"winemenubuilder.exe"="" +''') + wine_reg_install(reg_file) + + def install_msi(app=None): msg.status(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.", app) # Execute the .MSI From aeab8e73fe3d0c4d3a12b30754e7b98920097f1f Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 17:39:12 +0100 Subject: [PATCH 226/253] add symlink to appimage winetricks --- installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.py b/installer.py index 39324bec..4de72009 100644 --- a/installer.py +++ b/installer.py @@ -408,7 +408,7 @@ def ensure_wine_executables(app=None): appimage_link.symlink_to(f"./{appimage_filename}") # Ensure wine executables symlinks. - for name in ["wine", "wine64", "wineserver"]: + for name in ["wine", "wine64", "wineserver", "winetricks"]: p = appdir_bindir / name p.unlink(missing_ok=True) p.symlink_to(f"./{config.APPIMAGE_LINK_SELECTION_NAME}") From 140baf4d0edb020d60fa6f572941d691ec7a6fcb Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 17:39:51 +0100 Subject: [PATCH 227/253] add --winetricks subcommand --- cli.py | 8 ++++++++ installer.py | 3 +-- main.py | 8 ++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index 1ce81aa3..b2f07c03 100644 --- a/cli.py +++ b/cli.py @@ -84,6 +84,10 @@ def update_latest_appimage(self): def update_self(self): utils.update_to_latest_lli_release() + def winetricks(self): + import config + wine.run_winetricks_cmd(*config.winetricks_args) + def user_input_processor(self, evt=None): while self.running: prompt = None @@ -195,3 +199,7 @@ def update_latest_appimage(): def update_self(): CLI().update_self() + + +def winetricks(): + CLI().winetricks() diff --git a/installer.py b/installer.py index 4de72009..7f39d875 100644 --- a/installer.py +++ b/installer.py @@ -423,6 +423,7 @@ def ensure_wine_executables(app=None): logging.debug(f"> wine path: {config.APPDIR_BINDIR}/wine") logging.debug(f"> wine64 path: {config.APPDIR_BINDIR}/wine64") logging.debug(f"> wineserver path: {config.APPDIR_BINDIR}/wineserver") + logging.debug(f"> winetricks path: {config.APPDIR_BINDIR}/winetricks") def ensure_winetricks_executable(app=None): @@ -520,8 +521,6 @@ def ensure_wineprefix_init(app=None): f"{config.INSTALLDIR}/data", ) else: - # if utils.get_wine_exe_path(): - # wine.initializeWineBottle() logging.debug("Initializing wineprefix.") process = wine.initializeWineBottle() wine.wait_pid(process) diff --git a/main.py b/main.py index 53ee486c..6e1a7828 100755 --- a/main.py +++ b/main.py @@ -196,6 +196,10 @@ def get_parser(): # help='check resources' help=argparse.SUPPRESS, ) + cmd.add_argument( + '--winetricks', nargs='+', + help="run winetricks command", + ) return parser @@ -266,6 +270,7 @@ def parse_args(args, parser): 'toggle_app_logging': cli.toggle_app_logging, 'update_self': cli.update_self, 'update_latest_appimage': cli.update_latest_appimage, + 'winetricks': cli.winetricks, } config.ACTION = None @@ -285,6 +290,8 @@ def parse_args(args, parser): if not utils.check_appimage(config.APPIMAGE_FILE_PATH): e = f"{config.APPIMAGE_FILE_PATH} is not an AppImage." raise argparse.ArgumentTypeError(e) + if arg == 'winetricks': + config.winetricks_args = getattr(args, 'winetricks') config.ACTION = action break if config.ACTION is None: @@ -418,6 +425,7 @@ def run(): 'run_winetricks', 'set_appimage', 'toggle_app_logging', + 'winetricks', ] if config.ACTION.__name__ not in install_required: logging.info(f"Running function: {config.ACTION.__name__}") From 1c4bd5784398bde149aca0d3d73d77dc5d09f9c3 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 17:57:45 +0100 Subject: [PATCH 228/253] add function create_wine_appimage_symlinks --- installer.py | 67 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/installer.py b/installer.py index 7f39d875..3115358a 100644 --- a/installer.py +++ b/installer.py @@ -385,33 +385,8 @@ def ensure_wine_executables(app=None): # Add APPDIR_BINDIR to PATH. if not os.access(utils.get_wine_exe_path(), os.X_OK): - appdir_bindir = Path(config.APPDIR_BINDIR) - os.environ['PATH'] = f"{config.APPDIR_BINDIR}:{os.getenv('PATH')}" - # Ensure AppImage symlink. - appimage_link = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME - appimage_file = Path(config.SELECTED_APPIMAGE_FILENAME) - appimage_filename = Path(config.SELECTED_APPIMAGE_FILENAME).name - if config.WINEBIN_CODE in ['AppImage', 'Recommended']: - # Ensure appimage is copied to appdir_bindir. - downloaded_file = utils.get_downloaded_file_path(appimage_filename) # noqa: E501 - if not appimage_file.is_file(): - msg.status(f"Copying: {downloaded_file} into: {str(appdir_bindir)}") # noqa: E501 - shutil.copy(downloaded_file, str(appdir_bindir)) - os.chmod(appimage_file, 0o755) - appimage_filename = appimage_file.name - elif config.WINEBIN_CODE in ["System", "Proton", "PlayOnLinux", "Custom"]: # noqa: E501 - appimage_filename = "none.AppImage" - else: - msg.logos_error(f"WINEBIN_CODE error. WINEBIN_CODE is {config.WINEBIN_CODE}. Installation canceled!") # noqa: E501 - - appimage_link.unlink(missing_ok=True) # remove & replace - appimage_link.symlink_to(f"./{appimage_filename}") - - # Ensure wine executables symlinks. - for name in ["wine", "wine64", "wineserver", "winetricks"]: - p = appdir_bindir / name - p.unlink(missing_ok=True) - p.symlink_to(f"./{config.APPIMAGE_LINK_SELECTION_NAME}") + msg.status("Creating wine appimage symlinks…", app=app) + create_wine_appimage_symlinks(app=app) # Set WINESERVER_EXE. config.WINESERVER_EXE = f"{config.APPDIR_BINDIR}/wineserver" @@ -440,7 +415,7 @@ def ensure_winetricks_executable(app=None): if not os.access(config.WINETRICKSBIN, os.X_OK): # Either previous system winetricks is no longer accessible, or the # or the user has chosen to download it. - msg.status("Downloading winetricks from the Internet…") + msg.status("Downloading winetricks from the Internet…", app=app) system.install_winetricks(config.APPDIR_BINDIR, app=app) logging.debug(f"> {config.WINETRICKSBIN} is executable?: {os.access(config.WINETRICKSBIN, os.X_OK)}") # noqa: E501 @@ -718,6 +693,42 @@ def get_progress_pct(current, total): return round(current * 100 / total) +def create_wine_appimage_symlinks(app=None): + appdir_bindir = Path(config.APPDIR_BINDIR) + os.environ['PATH'] = f"{config.APPDIR_BINDIR}:{os.getenv('PATH')}" + # Ensure AppImage symlink. + appimage_link = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME + appimage_file = Path(config.SELECTED_APPIMAGE_FILENAME) + appimage_filename = Path(config.SELECTED_APPIMAGE_FILENAME).name + if config.WINEBIN_CODE in ['AppImage', 'Recommended']: + # Ensure appimage is copied to appdir_bindir. + downloaded_file = utils.get_downloaded_file_path(appimage_filename) + if not appimage_file.is_file(): + msg.status( + f"Copying: {downloaded_file} into: {appdir_bindir}", + app=app + ) + shutil.copy(downloaded_file, str(appdir_bindir)) + os.chmod(appimage_file, 0o755) + appimage_filename = appimage_file.name + elif config.WINEBIN_CODE in ["System", "Proton", "PlayOnLinux", "Custom"]: + appimage_filename = "none.AppImage" + else: + msg.logos_error( + f"WINEBIN_CODE error. WINEBIN_CODE is {config.WINEBIN_CODE}. Installation canceled!", # noqa: E501 + app=app + ) + + appimage_link.unlink(missing_ok=True) # remove & replace + appimage_link.symlink_to(f"./{appimage_filename}") + + # Ensure wine executables symlinks. + for name in ["wine", "wine64", "wineserver", "winetricks"]: + p = appdir_bindir / name + p.unlink(missing_ok=True) + p.symlink_to(f"./{config.APPIMAGE_LINK_SELECTION_NAME}") + + def get_flproducti_name(product_name): lname = product_name.lower() if lname == 'logos': From 2503662662d754c09dc9a63f42c7a9602e142350 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 18:14:47 +0100 Subject: [PATCH 229/253] remove 'if app:' checks; rely on config.DIALOG instead --- installer.py | 224 +++++++++++++++++++++++++++------------------------ 1 file changed, 120 insertions(+), 104 deletions(-) diff --git a/installer.py b/installer.py index 3115358a..0ee9008b 100644 --- a/installer.py +++ b/installer.py @@ -24,18 +24,22 @@ def ensure_product_choice(app=None): logging.debug('- config.VERBUM_PATH') if not config.FLPRODUCT: - if app: - if config.DIALOG == 'cli': - app.input_q.put(("Choose which FaithLife product the script should install: ", ["Logos", "Verbum", "Exit"])) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.FLPRODUCT = app.choice_q.get() - else: - utils.send_task(app, 'FLPRODUCT') - if config.DIALOG == 'curses': - app.product_e.wait() - config.FLPRODUCT = app.product_q.get() + if config.DIALOG == 'cli': + app.input_q.put( + ( + "Choose which FaithLife product the script should install: ", # noqa: E501 + ["Logos", "Verbum", "Exit"] + ) + ) + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() + config.FLPRODUCT = app.choice_q.get() + else: + utils.send_task(app, 'FLPRODUCT') + if config.DIALOG == 'curses': + app.product_e.wait() + config.FLPRODUCT = app.product_q.get() else: if config.DIALOG == 'curses' and app: app.set_product(config.FLPRODUCT) @@ -58,18 +62,22 @@ def ensure_version_choice(app=None): update_install_feedback("Choose version…", app=app) logging.debug('- config.TARGETVERSION') if not config.TARGETVERSION: - if app: - if config.DIALOG == 'cli': - app.input_q.put((f"Which version of {config.FLPRODUCT} should the script install?: ", ["10", "9", "Exit"])) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.TARGETVERSION = app.choice_q.get() - else: - utils.send_task(app, 'TARGETVERSION') - if config.DIALOG == 'curses': - app.version_e.wait() - config.TARGETVERSION = app.version_q.get() + if config.DIALOG == 'cli': + app.input_q.put( + ( + f"Which version of {config.FLPRODUCT} should the script install?: ", # noqa: E501 + ["10", "9", "Exit"] + ) + ) + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() + config.TARGETVERSION = app.choice_q.get() + else: + utils.send_task(app, 'TARGETVERSION') + if config.DIALOG == 'curses': + app.version_e.wait() + config.TARGETVERSION = app.version_q.get() else: if config.DIALOG == 'curses' and app: app.set_version(config.TARGETVERSION) @@ -85,19 +93,22 @@ def ensure_release_choice(app=None): logging.debug('- config.TARGET_RELEASE_VERSION') if not config.TARGET_RELEASE_VERSION: - if app: - if config.DIALOG == 'cli': - utils.start_thread(network.get_logos_releases, daemon_bool=True, app=app) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.TARGET_RELEASE_VERSION = app.choice_q.get() - else: - utils.send_task(app, 'TARGET_RELEASE_VERSION') - if config.DIALOG == 'curses': - app.release_e.wait() - config.TARGET_RELEASE_VERSION = app.release_q.get() - logging.debug(f"{config.TARGET_RELEASE_VERSION=}") + if config.DIALOG == 'cli': + utils.start_thread( + network.get_logos_releases, + daemon_bool=True, + app=app + ) + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() + config.TARGET_RELEASE_VERSION = app.choice_q.get() + else: + utils.send_task(app, 'TARGET_RELEASE_VERSION') + if config.DIALOG == 'curses': + app.release_e.wait() + config.TARGET_RELEASE_VERSION = app.release_q.get() + logging.debug(f"{config.TARGET_RELEASE_VERSION=}") else: if config.DIALOG == 'curses' and app: app.set_release(config.TARGET_RELEASE_VERSION) @@ -117,22 +128,26 @@ def ensure_install_dir_choice(app=None): default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 if not config.INSTALLDIR: - if app: - if config.DIALOG == 'cli': - default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - question = f"Where should {config.FLPRODUCT} files be installed to?: " # noqa: E501 - app.input_q.put((question, [default, "Type your own custom path", "Exit"])) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.INSTALLDIR = app.choice_q.get() - elif config.DIALOG == 'tk': - config.INSTALLDIR = default - elif config.DIALOG == 'curses': - utils.send_task(app, 'INSTALLDIR') - app.installdir_e.wait() - config.INSTALLDIR = app.installdir_q.get() - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" + if config.DIALOG == 'cli': + default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 + question = f"Where should {config.FLPRODUCT} files be installed to?: " # noqa: E501 + app.input_q.put( + ( + question, + [default, "Type your own custom path", "Exit"] + ) + ) + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() + config.INSTALLDIR = app.choice_q.get() + elif config.DIALOG == 'tk': + config.INSTALLDIR = default + elif config.DIALOG == 'curses': + utils.send_task(app, 'INSTALLDIR') + app.installdir_e.wait() + config.INSTALLDIR = app.installdir_q.get() + config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" else: if config.DIALOG == 'curses' and app: app.set_installdir(config.INSTALLDIR) @@ -155,27 +170,33 @@ def ensure_wine_choice(app=None): if utils.get_wine_exe_path() is None: network.set_recommended_appimage_config() - if app: - if config.DIALOG == 'cli': - options = utils.get_wine_options( - utils.find_appimage_files(config.TARGET_RELEASE_VERSION), - utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) + if config.DIALOG == 'cli': + options = utils.get_wine_options( + utils.find_appimage_files(config.TARGET_RELEASE_VERSION), + utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) + ) + app.input_q.put( + ( + f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?: ", # noqa: E501 + options ) - app.input_q.put(( - f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?: ", options)) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.WINE_EXE = utils.get_relative_path(utils.get_config_var(app.choice_q.get()), config.INSTALLDIR) - else: - utils.send_task(app, 'WINE_EXE') - if config.DIALOG == 'curses': - app.wine_e.wait() - config.WINE_EXE = app.wines_q.get() - # GUI uses app.wines_q for list of available, then app.wine_q - # for the user's choice of specific binary. - elif config.DIALOG == 'tk': - config.WINE_EXE = app.wine_q.get() + ) + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() + config.WINE_EXE = utils.get_relative_path( + utils.get_config_var(app.choice_q.get()), + config.INSTALLDIR + ) + else: + utils.send_task(app, 'WINE_EXE') + if config.DIALOG == 'curses': + app.wine_e.wait() + config.WINE_EXE = app.wines_q.get() + # GUI uses app.wines_q for list of available, then app.wine_q + # for the user's choice of specific binary. + elif config.DIALOG == 'tk': + config.WINE_EXE = app.wine_q.get() else: if config.DIALOG == 'curses' and app: @@ -210,26 +231,27 @@ def ensure_winetricks_choice(app=None): winetricks_options = utils.get_winetricks_options() - if app: - if config.DIALOG == 'cli': - app.input_q.put((f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux.", winetricks_options)) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - winetricksbin = app.choice_q.get() - else: - utils.send_task(app, 'WINETRICKSBIN') - if config.DIALOG == 'curses': - app.tricksbin_e.wait() - winetricksbin = app.tricksbin_q.get() + if config.DIALOG == 'cli': + app.input_q.put( + ( + f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux.", # noqa: E501 + winetricks_options + ) + ) + app.input_event.set() + app.choice_event.wait() + app.choice_event.clear() + winetricksbin = app.choice_q.get() + else: + utils.send_task(app, 'WINETRICKSBIN') + if config.DIALOG == 'curses': + app.tricksbin_e.wait() + winetricksbin = app.tricksbin_q.get() if not winetricksbin.startswith('Download'): config.WINETRICKSBIN = winetricksbin else: config.WINETRICKSBIN = winetricks_options[0] - # else: - # m = f"{utils.get_calling_function_name()}: --install-app is broken" - # logging.critical(m) logging.debug(f"> {config.WINETRICKSBIN=}") @@ -285,11 +307,10 @@ def ensure_installation_config(app=None): logging.debug(f"> {config.LOGOS64_MSI=}") logging.debug(f"> {config.LOGOS64_URL=}") - if app: - if config.DIALOG == 'cli': - msg.logos_msg("Install is running…") - else: - utils.send_task(app, 'INSTALL') + if config.DIALOG in ['curses', 'dialog', 'tk']: + utils.send_task(app, 'INSTALL') + else: + msg.logos_msg("Install is running…") def ensure_install_dirs(app=None): @@ -321,11 +342,8 @@ def ensure_install_dirs(app=None): logging.debug(f"> {wine_dir} exists: {wine_dir.is_dir()}") logging.debug(f"> {config.WINEPREFIX=}") - if app: - if config.DIALOG == 'cli': - pass - else: - utils.send_task(app, 'INSTALLING') + if config.DIALOG in ['curses', 'dialog', 'tk']: + utils.send_task(app, 'INSTALLING') def ensure_sys_deps(app=None): @@ -336,9 +354,8 @@ def ensure_sys_deps(app=None): if not config.SKIP_DEPENDENCIES: utils.check_dependencies(app) - if app: - if config.DIALOG == "curses": - app.installdeps_e.wait() + if config.DIALOG == "curses": + app.installdeps_e.wait() logging.debug("> Done.") else: logging.debug("> Skipped.") @@ -571,9 +588,8 @@ def ensure_icu_data_files(app=None): if not utils.file_exists(icu_license_path): wine.install_icu_data_files(app=app) - if app: - if config.DIALOG == "curses": - app.install_icu_e.wait() + if config.DIALOG == "curses": + app.install_icu_e.wait() logging.debug('> ICU data files installed') From 5186e01a5a4f44a41e398fa3ee2e4d7282472996 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Thu, 10 Oct 2024 18:27:45 +0100 Subject: [PATCH 230/253] remove fixed TODO --- installer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/installer.py b/installer.py index 0ee9008b..5d4d8615 100644 --- a/installer.py +++ b/installer.py @@ -11,10 +11,6 @@ import utils import wine -# TODO: Fix install progress if user returns to main menu? -# To replicate, start a TUI install, return/cancel on second step -# Then launch a new install - def ensure_product_choice(app=None): config.INSTALL_STEPS_COUNT += 1 From f225a48068b763e64bacf3b61aa7adcbbad0f4ab Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 11 Oct 2024 07:58:33 +0100 Subject: [PATCH 231/253] use wine.check_wine_rules in LogosManager.start() --- gui_app.py | 1 - logos.py | 18 +++++++----------- wine.py | 2 +- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/gui_app.py b/gui_app.py index effa2c7c..a235e882 100644 --- a/gui_app.py +++ b/gui_app.py @@ -730,7 +730,6 @@ def run_restore(self, evt=None): utils.start_thread(control.restore, app=self) def install_deps(self, evt=None): - # TODO: Separate as advanced feature. self.start_indeterminate_progress() utils.start_thread(utils.check_dependencies) diff --git a/logos.py b/logos.py index 422ce29e..45647c94 100644 --- a/logos.py +++ b/logos.py @@ -72,7 +72,6 @@ def monitor(self): def start(self): self.logos_state = State.STARTING - logos_release = utils.convert_logos_release(config.current_logos_version) # noqa: E501 wine_release, _ = wine.get_wine_release(str(utils.get_wine_exe_path())) def run_logos(): @@ -81,16 +80,13 @@ def run_logos(): exe=config.LOGOS_EXE ) - # TODO: Find a way to incorporate check_wine_version_and_branch() - if 30 > logos_release[0] > 9 and ( - wine_release[0] < 7 or (wine_release[0] == 7 and wine_release[1] < 18)): # noqa: E501 - txt = f"Can't run {config.FLPRODUCT} 10+ with Wine below 7.18." - logging.critical(txt) - msg.status(txt, self.app) - if logos_release[0] > 29 and wine_release[0] < 9 and wine_release[1] < 10: # noqa: E501 - txt = f"Can't run {config.FLPRODUCT} 30+ with Wine below 9.10." - logging.critical(txt) - msg.status(txt, self.app) + # Ensure wine version is compatible with Logos release version. + good_wine, reason = wine.check_wine_rules( + wine_release, + config.current_logos_version + ) + if not good_wine: + msg.logos_error(reason, app=self) else: wine.wineserver_kill() app = self.app diff --git a/wine.py b/wine.py index b9245420..f3eb46a9 100644 --- a/wine.py +++ b/wine.py @@ -12,7 +12,7 @@ import system import utils -from main import processes +from config import processes def get_wine_user(): From 9772c28b8a7f0374e900c858cbb646baf52731f9 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 11 Oct 2024 17:01:27 +0100 Subject: [PATCH 232/253] move get_timestamp to config module --- config.py | 12 ++++++++++++ control.py | 2 +- msg.py | 3 +-- wine.py | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/config.py b/config.py index 23e73ff7..7a72db80 100644 --- a/config.py +++ b/config.py @@ -2,6 +2,7 @@ import logging import os import tempfile +from datetime import datetime # Define and set variables that are required in the config file. @@ -119,6 +120,13 @@ logos_cef_cmd = None logos_indexer_cmd = None logos_indexer_exe = None +logos_linux_installer_status = None +logos_linux_installer_status_info = { + 0: "yes", + 1: "uptodate", + 2: "no", + None: "config.LLI_CURRENT_VERSION or config.LLI_LATEST_VERSION is not set.", # noqa: E501 +} check_if_indexing = None @@ -183,3 +191,7 @@ def get_env_config(): if val is not None: logging.info(f"Setting '{var}' to '{val}'") globals()[var] = val + + +def get_timestamp(): + return datetime.today().strftime('%Y-%m-%dT%H%M%S') diff --git a/control.py b/control.py index 852e3520..2c0a079d 100644 --- a/control.py +++ b/control.py @@ -134,7 +134,7 @@ def backup_and_restore(mode='backup', app=None): if dst.is_dir(): shutil.rmtree(dst) else: # backup mode - timestamp = utils.get_timestamp().replace('-', '') + timestamp = config.get_timestamp().replace('-', '') current_backup_name = f"{config.FLPRODUCT}{config.TARGETVERSION}-{timestamp}" # noqa: E501 dst_dir = backup_dir / current_backup_name logging.debug(f"Backup directory path: {dst_dir}") diff --git a/msg.py b/msg.py index c869d6a3..66c0af61 100644 --- a/msg.py +++ b/msg.py @@ -11,7 +11,6 @@ import config from gui import ask_question from gui import show_error -from utils import get_timestamp class GzippedRotatingFileHandler(RotatingFileHandler): @@ -329,7 +328,7 @@ def status(text, app=None, end='\n'): def strip_timestamp(msg, timestamp_length=20): return msg[timestamp_length:] - timestamp = get_timestamp() + timestamp = config.get_timestamp() """Handles status messages for both TUI and GUI.""" if app is not None: if config.DIALOG == 'tk': diff --git a/wine.py b/wine.py index f3eb46a9..9c75b1fa 100644 --- a/wine.py +++ b/wine.py @@ -327,7 +327,7 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): cmd = f"subprocess cmd: '{' '.join(command)}'" with open(config.wine_log, 'a') as wine_log: - print(f"{utils.get_timestamp()}: {cmd}", file=wine_log) + print(f"{config.get_timestamp()}: {cmd}", file=wine_log) logging.debug(cmd) try: with open(config.wine_log, 'a') as wine_log: From 4e092ae012ffd184bf862472566c085ca4096f5d Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 11 Oct 2024 17:04:22 +0100 Subject: [PATCH 233/253] cleanup some TODOs --- main.py | 5 ----- network.py | 27 +++++++++++++++------------ system.py | 39 +++++++++++++++++++-------------------- utils.py | 9 ++++----- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/main.py b/main.py index 6e1a7828..6a3008ed 100755 --- a/main.py +++ b/main.py @@ -6,14 +6,11 @@ import control import curses -import logos - try: import dialog # noqa: F401 except ImportError: pass import gui_app -import installer import logging import msg import network @@ -54,7 +51,6 @@ def get_parser(): help='skip font installations', ) cfg.add_argument( - # TODO: Make this a hidden option? '-W', '--skip-winetricks', action='store_true', help='skip winetricks installations. For development purposes only!!!', ) @@ -182,7 +178,6 @@ def get_parser(): help='[re-]create app shortcuts', ) cmd.add_argument( - # TODO: Make this a hidden option? '--remove-install-dir', action='store_true', help='delete the current installation folder', ) diff --git a/network.py b/network.py index 2e6ee2f6..037df241 100644 --- a/network.py +++ b/network.py @@ -138,10 +138,9 @@ def cli_download(uri, destination, app=None): cli_queue = queue.Queue() kwargs = {'q': cli_queue, 'target': target} t = utils.start_thread(net_get, uri, **kwargs) - # TODO: This results in high CPU usage while showing the progress bar. - # The solution will be to rework the wait on the cli_queue. try: while t.is_alive(): + sleep(0.1) if cli_queue.empty(): continue utils.write_progress_bar(cli_queue.get()) @@ -389,17 +388,17 @@ def get_latest_release_data(releases_url): def get_latest_release_url(json_data): release_url = None if json_data: - release_url = json_data[0].get('assets')[0].get('browser_download_url') # noqa: E501 + release_url = json_data[0].get('assets')[0].get('browser_download_url') logging.info(f"Release URL: {release_url}") return release_url -def get_latest_release_version_tag_name(json_data): - release_tag_name = None +def get_tag_name(json_data): + tag_name = None if json_data: - release_tag_name = json_data[0].get('tag_name') # noqa: E501 - logging.info(f"Release URL Tag Name: {release_tag_name}") - return release_tag_name + tag_name = json_data[0].get('tag_name') + logging.info(f"Release URL Tag Name: {tag_name}") + return tag_name def set_logoslinuxinstaller_latest_release_config(): @@ -409,7 +408,6 @@ def set_logoslinuxinstaller_latest_release_config(): releases_url = "https://api.github.com/repos/FaithLife-Community/test-builds/releases" # noqa: E501 json_data = get_latest_release_data(releases_url) logoslinuxinstaller_url = get_latest_release_url(json_data) - logoslinuxinstaller_tag_name = get_latest_release_version_tag_name(json_data) # noqa: E501 if logoslinuxinstaller_url is None: logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 return @@ -417,8 +415,8 @@ def set_logoslinuxinstaller_latest_release_config(): config.LOGOS_LATEST_VERSION_FILENAME = os.path.basename(logoslinuxinstaller_url) # noqa: #501 # Getting version relies on the the tag_name field in the JSON data. This # is already parsed down to vX.X.X. Therefore we must strip the v. - config.LLI_LATEST_VERSION = logoslinuxinstaller_tag_name.lstrip('v') - logging.info(f"{config.LLI_LATEST_VERSION}") + config.LLI_LATEST_VERSION = get_tag_name(json_data).lstrip('v') + logging.info(f"{config.LLI_LATEST_VERSION=}") def set_recommended_appimage_config(): @@ -551,7 +549,12 @@ def get_logos_releases(app=None): app.releases_q.put(filtered_releases) app.releases_e.set() elif config.DIALOG == 'cli': - app.input_q.put((f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?: ", filtered_releases)) + app.input_q.put( + ( + f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?: ", # noqa: E501 + filtered_releases + ) + ) app.input_event.set() return filtered_releases diff --git a/system.py b/system.py index c7878e5e..783e5993 100644 --- a/system.py +++ b/system.py @@ -292,7 +292,9 @@ def get_dialog(): def get_os(): - config.OS_NAME = distro.id() # FIXME: Not working. Returns "Linux". + # FIXME: Not working? Returns "Linux" on some systems? On Ubuntu 24.04 it + # correctly returns "ubuntu". + config.OS_NAME = distro.id() logging.info(f"OS name: {config.OS_NAME}") config.OS_RELEASE = distro.version() logging.info(f"OS release: {config.OS_RELEASE}") @@ -641,9 +643,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) logging.error(m) else: msg.logos_continue_question(message, no_message, secondary, app) - if app: - if config.DIALOG == "curses": - app.confirm_e.wait() + if config.DIALOG == "curses": + app.confirm_e.wait() # TODO: Need to send continue question to user based on DIALOG. # All we do above is create a message that we never send. @@ -716,23 +717,21 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) "Please run the following command in a terminal, then restart " f"LogosLinuxInstaller:\n{sudo_command}\n" ) - if app: - if config.DIALOG == "tk": - if hasattr(app, 'root'): - detail += "\nThe command has been copied to the clipboard." # noqa: E501 - app.root.clipboard_clear() - app.root.clipboard_append(sudo_command) - app.root.update() - msg.logos_error( - message, - detail=detail, - app=app, - parent='installer_win' - ) - install_deps_failed = True - else: + if config.DIALOG == "tk": + if hasattr(app, 'root'): + detail += "\nThe command has been copied to the clipboard." # noqa: E501 + app.root.clipboard_clear() + app.root.clipboard_append(sudo_command) + app.root.update() + msg.logos_error( + message, + detail=detail, + app=app, + parent='installer_win' + ) + elif config.DIALOG == 'cli': msg.logos_error(message + "\n" + detail) - install_deps_failed = True + install_deps_failed = True if manual_install_required and app and config.DIALOG == "curses": app.screen_q.put( diff --git a/utils.py b/utils.py index d9629615..15f8d46e 100644 --- a/utils.py +++ b/utils.py @@ -144,7 +144,7 @@ def remove_pid_file(): confirm = tk.messagebox.askquestion("Confirmation", message) tk_root.destroy() elif config.DIALOG == "curses": - confirm = tui_dialog.confirm("Confirmation", message) + confirm = tui.confirm("Confirmation", message) else: confirm = msg.cli_question(message, "") @@ -246,10 +246,9 @@ def check_dependencies(app=None): else: logging.error(f"TARGETVERSION not found: {config.TARGETVERSION}.") - if app: - if config.DIALOG == "tk": - # FIXME: This should get moved to gui_app. - app.root.event_generate('<>') + if config.DIALOG == "tk": + # FIXME: This should get moved to gui_app. + app.root.event_generate('<>') def file_exists(file_path): From bc761cfee7aa51abfde5d80cbbcdd6ce77017e06 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 11 Oct 2024 17:05:26 +0100 Subject: [PATCH 234/253] only run compare_logos_linux_installer_version once to set config var --- gui_app.py | 7 +++---- network.py | 2 ++ tui_app.py | 4 ++-- utils.py | 53 ++++++++++++++++++++++------------------------------- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/gui_app.py b/gui_app.py index a235e882..0a35a1ba 100644 --- a/gui_app.py +++ b/gui_app.py @@ -819,17 +819,16 @@ def update_app_button(self, evt=None): self.gui.logging_button.state(['!disabled']) def update_latest_lli_release_button(self, evt=None): - status, reason = utils.compare_logos_linux_installer_version() msg = None if system.get_runmode() != 'binary': state = 'disabled' msg = "This button is disabled. Can't run self-update from script." - elif status == 0: + elif config.logos_linux_installer_status == 0: state = '!disabled' - elif status == 1: + elif config.logos_linux_installer_status == 1: state = 'disabled' msg = "This button is disabled. Logos Linux Installer is up-to-date." # noqa: E501 - elif status == 2: + elif config.logos_linux_installer_status == 2: state = 'disabled' msg = "This button is disabled. Logos Linux Installer is newer than the latest release." # noqa: E501 if msg: diff --git a/network.py b/network.py index 037df241..e3215a9f 100644 --- a/network.py +++ b/network.py @@ -9,6 +9,7 @@ from base64 import b64encode from datetime import datetime, timedelta from pathlib import Path +from time import sleep from urllib.parse import urlparse from xml.etree import ElementTree as ET @@ -466,6 +467,7 @@ def check_for_updates(): logging.debug("Running self-update.") set_logoslinuxinstaller_latest_release_config() + utils.compare_logos_linux_installer_version() set_recommended_appimage_config() config.LAST_UPDATED = now.isoformat() diff --git a/tui_app.py b/tui_app.py index 7967a298..ab8163f3 100644 --- a/tui_app.py +++ b/tui_app.py @@ -857,8 +857,8 @@ def which_dialog_options(self, labels, dialog=False): def set_tui_menu_options(self, dialog=False): labels = [] if config.LLI_LATEST_VERSION and system.get_runmode() == 'binary': - # logging.debug("Checking if Logos Linux Installer needs updated.") # noqa: E501 - status, error_message = utils.compare_logos_linux_installer_version() # noqa: E501 + status = config.logos_linux_installer_status + error_message = config.logos_linux_installer_status_info.get(status) # noqa: E501 if status == 0: labels.append("Update Logos Linux Installer") elif status == 1: diff --git a/utils.py b/utils.py index 15f8d46e..a8366bf0 100644 --- a/utils.py +++ b/utils.py @@ -576,42 +576,37 @@ def install_premade_wine_bottle(srcdir, appdir): ) -def compare_logos_linux_installer_version(): - # TODO: Save this as a config variable and only run it once in - # network.check_for_updates(). +def compare_logos_linux_installer_version( + current=config.LLI_CURRENT_VERSION, + latest=config.LLI_LATEST_VERSION, +): + # NOTE: The above params evaluate the variables when the module is + # imported. The following re-evaluates when the function is called. + if latest is None: + latest = config.LLI_LATEST_VERSION + + # Check if status has already been evaluated. + if config.logos_linux_installer_status is not None: + status = config.logos_linux_installer_status + message = config.logos_linux_installer_status_info.get(status) + return status, message + status = None message = None - if ( - config.LLI_CURRENT_VERSION is not None - and config.LLI_LATEST_VERSION is not None - ): - # logging.debug(f"{config.LLI_CURRENT_VERSION=}; {config.LLI_LATEST_VERSION=}") # noqa: E501 - if ( - version.parse(config.LLI_CURRENT_VERSION) - < version.parse(config.LLI_LATEST_VERSION) - ): + if current is not None and latest is not None: + if version.parse(current) < version.parse(latest): # Current release is older than recommended. status = 0 - message = "yes" - elif ( - version.parse(config.LLI_CURRENT_VERSION) - == version.parse(config.LLI_LATEST_VERSION) - ): + elif version.parse(current) == version.parse(latest): # Current release is latest. status = 1 - message = "uptodate" - elif ( - version.parse(config.LLI_CURRENT_VERSION) - > version.parse(config.LLI_LATEST_VERSION) - ): + elif version.parse(current) > version.parse(latest): # Installed version is custom. status = 2 - message = "no" - else: - status = False - message = "config.LLI_CURRENT_VERSION or config.LLI_LATEST_VERSION is not set." # noqa: E501 - # logging.debug(f"{status=}; {message=}") + config.logos_linux_installer_status = status + message = config.logos_linux_installer_status_info.get(status) + logging.debug(f"LLI self-update check: {status=}; {message=}") return status, message @@ -1018,7 +1013,3 @@ def stopwatch(start_time=None, interval=10.0): return True, last_log_time else: return False, start_time - - -def get_timestamp(): - return datetime.today().strftime('%Y-%m-%dT%H%M%S') From 814b4636ae727c040d5dded9eeeb7cec386d5ff0 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 11 Oct 2024 17:07:14 +0100 Subject: [PATCH 235/253] clear one more TODO --- wine.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/wine.py b/wine.py index 9c75b1fa..8cf5ab82 100644 --- a/wine.py +++ b/wine.py @@ -52,8 +52,6 @@ def wineserver_kill(): wait_pid(process) -# TODO: Review these three commands. The top is the newest and should be -# preserved. Can the other two be refactored out? def wineserver_wait(): if check_wineserver(): process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) From 604c5e716122074ece47745295db2bce2b08e652 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 14 Oct 2024 09:44:14 -0400 Subject: [PATCH 236/253] Fix TUI Backup and Restore --- control.py | 63 +++++++++++++++++++++++----------- system.py | 4 +-- tui_app.py | 94 ++++++++++++++++++++++++++++++++++++++++++++------- tui_curses.py | 1 - 4 files changed, 127 insertions(+), 35 deletions(-) diff --git a/control.py b/control.py index 2c0a079d..c61cc920 100644 --- a/control.py +++ b/control.py @@ -48,6 +48,9 @@ def backup_and_restore(mode='backup', app=None): if config.BACKUPDIR is None: if config.DIALOG == 'tk': pass # config.BACKUPDIR is already set in GUI + elif config.DIALOG == 'curses': + app.todo_e.wait() # Wait for TUI to resolve config.BACKUPDIR + app.todo_e.clear() else: try: config.BACKUPDIR = input("New or existing folder to store backups in: ") # noqa: E501 @@ -62,14 +65,14 @@ def backup_and_restore(mode='backup', app=None): ) # Confirm BACKUPDIR. - if config.DIALOG == 'tk': - pass # user confirms in GUI + if config.DIALOG == 'tk' or config.DIALOG == 'curses': + pass # user confirms in GUI or TUI else: verb = 'Use' if mode == 'backup' else 'Restore backup from' if not msg.cli_question(f"{verb} existing backups folder \"{config.BACKUPDIR}\"?", ""): # noqa: E501 answer = None while answer is None or (mode == 'restore' and not answer.is_dir()): # noqa: E501 - answer = msg.cli_ask_filepath("Give backups folder path:") + answer = msg.cli_ask_filepath("Please provide a backups folder path:") answer = Path(answer).expanduser().resolve() if not answer.is_dir(): msg.status(f"Not a valid folder path: {answer}", app=app) @@ -91,6 +94,19 @@ def backup_and_restore(mode='backup', app=None): config.RESTOREDIR = Path(config.RESTOREDIR).expanduser().resolve() if config.DIALOG == 'tk': pass + elif config.DIALOG == 'curses': + app.screen_q.put(app.stack_confirm(24, app.todo_q, app.todo_e, + f"Restore most-recent backup?: {config.RESTOREDIR}", "", "", + dialog=config.use_python_dialog)) + app.todo_e.wait() # Wait for TUI to confirm RESTOREDIR + app.todo_e.clear() + if app.tmp == "No": + question = "Please choose a different restore folder path:" + app.screen_q.put(app.stack_input(25, app.todo_q, app.todo_e, question, f"{config.RESTOREDIR}", + dialog=config.use_python_dialog)) + app.todo_e.wait() + app.todo_e.clear() + config.RESTOREDIR = Path(app.tmp).expanduser().resolve() else: # Offer to restore the most recent backup. if not msg.cli_question(f"Restore most-recent backup?: {config.RESTOREDIR}", ""): # noqa: E501 @@ -104,9 +120,15 @@ def backup_and_restore(mode='backup', app=None): msg.logos_warning(f"No files to {mode}", app=app) return + if config.DIALOG == 'curses': + if mode == 'backup': + app.screen_q.put(app.stack_text(8, app.todo_q, app.todo_e, "Backing up data…", wait=True)) + else: + app.screen_q.put(app.stack_text(8, app.todo_q, app.todo_e, "Restoring data…", wait=True)) + # Get source transfer size. q = queue.Queue() - msg.status("Calculating backup size", app=app) + msg.status("Calculating backup size…", app=app) t = utils.start_thread(utils.get_folder_group_size, src_dirs, q) try: while t.is_alive(): @@ -117,7 +139,7 @@ def backup_and_restore(mode='backup', app=None): print() msg.logos_error("Cancelled with Ctrl+C.", app=app) t.join() - if config.DIALOG in ['curses', 'dialog', 'tk']: + if config.DIALOG == 'tk': app.root.event_generate('<>') app.root.event_generate('<>') src_size = q.get() @@ -137,13 +159,13 @@ def backup_and_restore(mode='backup', app=None): timestamp = config.get_timestamp().replace('-', '') current_backup_name = f"{config.FLPRODUCT}{config.TARGETVERSION}-{timestamp}" # noqa: E501 dst_dir = backup_dir / current_backup_name - logging.debug(f"Backup directory path: {dst_dir}") + logging.debug(f"Backup directory path: \"{dst_dir}\".") # Check for existing backup. try: dst_dir.mkdir() except FileExistsError: - msg.logos_error(f"Backup already exists: {dst_dir}") + msg.logos_error(f"Backup already exists: {dst_dir}.") # Verify disk space. if not utils.enough_disk_space(dst_dir, src_size): @@ -153,30 +175,33 @@ def backup_and_restore(mode='backup', app=None): # Run file transfer. if mode == 'restore': - m = f"Restoring backup from {str(source_dir_base)}" + m = f"Restoring backup from {str(source_dir_base)}…" else: - m = f"Backing up to {str(dst_dir)}" + m = f"Backing up to {str(dst_dir)}…" msg.status(m, app=app) + msg.status("Calculating destination directory size", app=app) dst_dir_size = utils.get_path_size(dst_dir) + msg.status("Starting backup…", app=app) t = utils.start_thread(copy_data, src_dirs, dst_dir) try: while t.is_alive(): - progress = utils.get_copy_progress( - dst_dir, - src_size, - dest_size_init=dst_dir_size - ) - utils.write_progress_bar(progress) - if config.DIALOG in ['curses', 'dialog', 'tk']: - app.progress_q.put(progress) - app.root.event_generate('<>') + logging.debug("DEV: Still copying…") + # progress = utils.get_copy_progress( + # dst_dir, + # src_size, + # dest_size_init=dst_dir_size + # ) + # utils.write_progress_bar(progress) + # if config.DIALOG == 'tk': + # app.progress_q.put(progress) + # app.root.event_generate('<>') time.sleep(0.1) print() except KeyboardInterrupt: print() msg.logos_error("Cancelled with Ctrl+C.") t.join() - if config.DIALOG in ['curses', 'dialog', 'tk']: + if config.DIALOG == 'tk': app.root.event_generate('<>') logging.info(f"Finished. {src_size} bytes copied to {str(dst_dir)}") diff --git a/system.py b/system.py index 783e5993..859abd58 100644 --- a/system.py +++ b/system.py @@ -737,8 +737,8 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) app.screen_q.put( app.stack_confirm( 17, - app.todo_q, - app.todo_e, + app.manualinstall_q, + app.manualinstall_e, f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\nLogosLinuxInstaller:\n{sudo_command}\n", # noqa: E501 "User cancelled dependency installation.", # noqa: E501 message, diff --git a/tui_app.py b/tui_app.py index ab8163f3..12ddb1d8 100644 --- a/tui_app.py +++ b/tui_app.py @@ -36,6 +36,7 @@ def __init__(self, stdscr): self.llirunning = True self.active_progress = False self.logos = logos.LogosManager(app=self) + self.tmp = "" # Queues self.main_thread = threading.Thread() @@ -396,7 +397,11 @@ def choice_processor(self, stdscr, screen_id, choice): 18: self.utilities_menu_select, 19: self.renderer_select, 20: self.win_ver_logos_select, - 21: self.win_ver_index_select + 21: self.win_ver_index_select, + 22: self.verify_backup_path, + 23: self.use_backup_path, + 24: self.confirm_restore_dir, + 25: self.choose_restore_dir } # Capture menu exiting before processing in the rest of the handler @@ -457,11 +462,13 @@ def main_menu_select(self, choice): control.remove_library_catalog() elif choice.startswith("Winetricks"): self.reset_screen() - self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) + self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", + self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice.startswith("Utilities"): self.reset_screen() - self.screen_q.put(self.stack_menu(18, self.todo_q, self.todo_e, "Utilities Menu", self.set_utilities_menu_options(), dialog=config.use_python_dialog)) + self.screen_q.put(self.stack_menu(18, self.todo_q, self.todo_e, "Utilities Menu", + self.set_utilities_menu_options(), dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice == "Change Color Scheme": self.change_color_scheme() @@ -537,14 +544,14 @@ def utilities_menu_select(self, choice): self.update_windows() utils.check_dependencies(self) self.go_to_main_menu() - elif choice == "Back up Data": + elif choice == "Back Up Data": self.reset_screen() - control.backup() - self.go_to_main_menu() + self.get_backup_path(mode="backup") + utils.start_thread(self.do_backup) elif choice == "Restore Data": self.reset_screen() - control.restore() - self.go_to_main_menu() + self.get_backup_path(mode="restore") + utils.start_thread(self.do_backup) elif choice == "Update to Latest AppImage": self.reset_screen() utils.update_to_latest_recommended_appimage() @@ -839,6 +846,62 @@ def get_config(self, dialog): # f"Please provide your password to provide escalation privileges.") # self.screen_q.put(self.stack_password(15, self.password_q, self.password_e, question, dialog=dialog)) + def get_backup_path(self, mode): + self.tmp = mode + if config.BACKUPDIR is None or not Path(config.BACKUPDIR).is_dir(): + if config.BACKUPDIR is None: + question = "Please provide a backups folder path:" + else: + question = f"Current backups folder path \"{config.BACKUPDIR}\" is invalid. Please provide a new one:" + self.screen_q.put(self.stack_input(22, self.todo_q, self.todo_e, question, + os.path.expanduser("~/Backups"), dialog=config.use_python_dialog)) + else: + verb = 'Use' if mode == 'backup' else 'Restore backup from' + question = f"{verb} backup from existing backups folder \"{config.BACKUPDIR}\"?" + self.screen_q.put(self.stack_confirm(23, self.todo_q, self.todo_e, question, "", + "", dialog=config.use_python_dialog)) + + def verify_backup_path(self, choice): + if choice: + if not Path(choice).is_dir(): + msg.status(f"Not a valid folder path: {choice}. Try again.", app=self) + question = "Please provide a different backups folder path:" + self.screen_q.put(self.stack_input(22, self.todo_q, self.todo_e, question, + os.path.expanduser("~/Backups"), dialog=config.use_python_dialog)) + else: + config.BACKUPDIR = choice + self.todo_e.set() + + def use_backup_path(self, choice): + if choice == "No": + question = "Please provide a new backups folder path:" + self.screen_q.put(self.stack_input(22, self.todo_q, self.todo_e, question, + os.path.expanduser(f"{config.BACKUPDIR}"), dialog=config.use_python_dialog)) + else: + self.todo_e.set() + + def confirm_restore_dir(self, choice): + if choice: + if choice == "Yes": + self.tmp = "Yes" + else: + self.tmp = "No" + self.todo_e.set() + + def choose_restore_dir(self, choice): + if choice: + self.tmp = choice + self.todo_e.set() + + def do_backup(self): + self.todo_e.wait() + self.todo_e.clear() + if self.tmp == 'backup': + control.backup(self) + else: + control.restore(self) + self.go_to_main_menu() + def report_waiting(self, text, dialog): #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) config.console_log.append(text) @@ -968,14 +1031,19 @@ def set_utilities_menu_options(self, dialog=False): labels_utilities = [ "Install Dependencies", - "Edit Config", - "Change Logos Release Channel", - "Change Logos on Linux Release Channel", - "Back up Data", - "Restore Data", + "Edit Config" ] labels.extend(labels_utilities) + if utils.file_exists(config.LOGOS_EXE): + labels_utils_installed = [ + "Change Logos Release Channel", + "Change Logos on Linux Release Channel", + "Back Up Data", + "Restore Data" + ] + labels.extend(labels_utils_installed) + label = "Enable Logging" if config.LOGS == "DISABLED" else "Disable Logging" labels.append(label) diff --git a/tui_curses.py b/tui_curses.py index 510e6b05..08e8b0f5 100644 --- a/tui_curses.py +++ b/tui_curses.py @@ -149,7 +149,6 @@ def run(self): else: if self.user_input is None or self.user_input == "": self.user_input = self.default_text - logging.debug(f"Selected Path: {self.user_input}") return self.user_input From 22dcfde67cd9fce1b8d89d2159de79cbe1c8d6fe Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 14 Oct 2024 10:34:02 -0400 Subject: [PATCH 237/253] Add DEV counter --- control.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/control.py b/control.py index c61cc920..ea72d663 100644 --- a/control.py +++ b/control.py @@ -184,8 +184,10 @@ def backup_and_restore(mode='backup', app=None): msg.status("Starting backup…", app=app) t = utils.start_thread(copy_data, src_dirs, dst_dir) try: + counter = 0 while t.is_alive(): - logging.debug("DEV: Still copying…") + logging.debug(f"DEV: Still copying… {counter}") + counter = counter + 1 # progress = utils.get_copy_progress( # dst_dir, # src_size, @@ -195,7 +197,7 @@ def backup_and_restore(mode='backup', app=None): # if config.DIALOG == 'tk': # app.progress_q.put(progress) # app.root.event_generate('<>') - time.sleep(0.1) + time.sleep(1) print() except KeyboardInterrupt: print() From e6e4aa9bbb6cdad135cd3a39c3caaaf240b51a30 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 14 Oct 2024 17:22:35 +0100 Subject: [PATCH 238/253] hide backup/restore functionality in GUI --- gui.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gui.py b/gui.py index 68c207bd..dd6f8005 100644 --- a/gui.py +++ b/gui.py @@ -277,10 +277,10 @@ def __init__(self, root, *args, **kwargs): row += 1 self.deps_label.grid(column=0, row=row, sticky='w', pady=2) self.deps_button.grid(column=1, row=row, sticky='w', pady=2) - row += 1 - self.backups_label.grid(column=0, row=row, sticky='w', pady=2) - self.backup_button.grid(column=1, row=row, sticky='w', pady=2) - self.restore_button.grid(column=2, row=row, sticky='w', pady=2) + # row += 1 + # self.backups_label.grid(column=0, row=row, sticky='w', pady=2) + # self.backup_button.grid(column=1, row=row, sticky='w', pady=2) + # self.restore_button.grid(column=2, row=row, sticky='w', pady=2) row += 1 self.update_lli_label.grid(column=0, row=row, sticky='w', pady=2) self.update_lli_button.grid(column=1, row=row, sticky='w', pady=2) From 0f64fd37317b3f0b87844c678e02fc368818273d Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 14 Oct 2024 18:17:14 +0100 Subject: [PATCH 239/253] hide backup/restore functionality in TUI --- tui_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tui_app.py b/tui_app.py index 12ddb1d8..dafae16b 100644 --- a/tui_app.py +++ b/tui_app.py @@ -1039,8 +1039,8 @@ def set_utilities_menu_options(self, dialog=False): labels_utils_installed = [ "Change Logos Release Channel", "Change Logos on Linux Release Channel", - "Back Up Data", - "Restore Data" + # "Back Up Data", + # "Restore Data" ] labels.extend(labels_utils_installed) From 532a86117ca2b5c570c6822ba2375ebe4b88460d Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 14 Oct 2024 13:53:59 -0400 Subject: [PATCH 240/253] Update version and changelog --- CHANGELOG.md | 18 ++++++++++++++++++ config.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abddaf05..ba89a57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +- 4.0.0-beta.1 + - Migrate .config and logs from `~/.config/Logos_on_Linux` and `~/.local/state/Logos_on_Linux` to `~/.config/FaithLife-Community` and `~/.local/state/FaithLife-Community` + - Add Logos State Manager [T. H. Wright, N. Marti] + - Numerous bug fixes [N. Marti, ctrlaltf24] + - Make config.WINE_EXE dynamic [T. H. Wright] + - Add Docker Build File [jimbob88] + - Fix numerous TUI issues [T. H. Wright] + - Fix #16 [T. H. Wright] + - Fix #84 [T. H. Wright] + - Fix #106 [T. H. Wright] + - Fix #127 [T. H. Wright] + - Fix #128 [T. H. Wright] + - Fix #142 [T. H. Wright] + - Fix #143 [T. H. Wright] + - Fix #153 [T. H. Wright] + - Fix #157 [T. H. Wright] + - Fix #181 [T. H. Wright] + - Fix #188 [T. H. Wright] - 4.0.0-alpha.14 - Fix install routine [N. Marti, T. H. Wright] - Fix #144, #154, #156 diff --git a/config.py b/config.py index 7a72db80..6b308117 100644 --- a/config.py +++ b/config.py @@ -65,7 +65,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-alpha.14" +LLI_CURRENT_VERSION = "4.0.0-beta.1" LLI_LATEST_VERSION = None LLI_TITLE = "Logos Linux Installer" LOG_LEVEL = logging.WARNING From 1fe47b5208b2d5c0bca5c2b3081fb5d46a14fe52 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Fri, 18 Oct 2024 14:46:41 +0100 Subject: [PATCH 241/253] only wait on ICU install in curses if actually installing the ICU files --- installer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer.py b/installer.py index 5d4d8615..a5223978 100644 --- a/installer.py +++ b/installer.py @@ -584,8 +584,8 @@ def ensure_icu_data_files(app=None): if not utils.file_exists(icu_license_path): wine.install_icu_data_files(app=app) - if config.DIALOG == "curses": - app.install_icu_e.wait() + if config.DIALOG == "curses": + app.install_icu_e.wait() logging.debug('> ICU data files installed') From dd2826e69a5efe7482da7d449adc489921798458 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Wed, 16 Oct 2024 13:05:34 +0100 Subject: [PATCH 242/253] convert to pyproject --- .github/workflows/build-branch.yml | 11 +++--- .gitignore | 1 + Dockerfile | 2 +- ...LinuxInstaller.spec => logos_on_linux.spec | 8 ++-- logos_on_linux/__init__.py | 0 cli.py => logos_on_linux/cli.py | 10 ++--- config.py => logos_on_linux/config.py | 0 control.py => logos_on_linux/control.py | 16 +++----- gui.py => logos_on_linux/gui.py | 4 +- gui_app.py => logos_on_linux/gui_app.py | 18 ++++----- .../img}/logos4-128-icon.png | Bin {img => logos_on_linux/img}/step_01.png | Bin {img => logos_on_linux/img}/step_02.png | Bin {img => logos_on_linux/img}/step_03.png | Bin {img => logos_on_linux/img}/step_04.png | Bin {img => logos_on_linux/img}/step_05.png | Bin {img => logos_on_linux/img}/step_06.png | Bin {img => logos_on_linux/img}/step_07.png | Bin {img => logos_on_linux/img}/step_08.png | Bin {img => logos_on_linux/img}/step_09.png | Bin {img => logos_on_linux/img}/step_10.png | Bin {img => logos_on_linux/img}/step_11.png | Bin {img => logos_on_linux/img}/step_12.png | Bin {img => logos_on_linux/img}/step_13.png | Bin {img => logos_on_linux/img}/step_14.png | Bin {img => logos_on_linux/img}/step_15.png | Bin {img => logos_on_linux/img}/step_16.png | Bin {img => logos_on_linux/img}/step_17.png | Bin {img => logos_on_linux/img}/step_18.png | Bin {img => logos_on_linux/img}/step_19.png | Bin {img => logos_on_linux/img}/step_20.png | Bin {img => logos_on_linux/img}/step_21.png | Bin {img => logos_on_linux/img}/step_22.png | Bin .../img}/verbum-128-icon.png | Bin installer.py => logos_on_linux/installer.py | 12 +++--- logos.py => logos_on_linux/logos.py | 12 +++--- main.py => logos_on_linux/main.py | 25 ++++++------ msg.py => logos_on_linux/msg.py | 6 +-- network.py => logos_on_linux/network.py | 6 +-- system.py => logos_on_linux/system.py | 10 ++--- tui_app.py => logos_on_linux/tui_app.py | 23 ++++++----- tui_curses.py => logos_on_linux/tui_curses.py | 7 ++-- tui_dialog.py => logos_on_linux/tui_dialog.py | 1 + tui_screen.py => logos_on_linux/tui_screen.py | 15 ++++--- utils.py => logos_on_linux/utils.py | 15 ++++--- wine.py => logos_on_linux/wine.py | 12 +++--- pyproject.toml | 37 ++++++++++++++++++ requirements.txt | 10 ----- scripts/build-binary.sh | 2 +- scripts/run_app.py | 12 ++++++ 50 files changed, 153 insertions(+), 122 deletions(-) rename LogosLinuxInstaller.spec => logos_on_linux.spec (80%) create mode 100644 logos_on_linux/__init__.py rename cli.py => logos_on_linux/cli.py (97%) rename config.py => logos_on_linux/config.py (100%) rename control.py => logos_on_linux/control.py (98%) rename gui.py => logos_on_linux/gui.py (99%) rename gui_app.py => logos_on_linux/gui_app.py (99%) rename {img => logos_on_linux/img}/logos4-128-icon.png (100%) rename {img => logos_on_linux/img}/step_01.png (100%) rename {img => logos_on_linux/img}/step_02.png (100%) rename {img => logos_on_linux/img}/step_03.png (100%) rename {img => logos_on_linux/img}/step_04.png (100%) rename {img => logos_on_linux/img}/step_05.png (100%) rename {img => logos_on_linux/img}/step_06.png (100%) rename {img => logos_on_linux/img}/step_07.png (100%) rename {img => logos_on_linux/img}/step_08.png (100%) rename {img => logos_on_linux/img}/step_09.png (100%) rename {img => logos_on_linux/img}/step_10.png (100%) rename {img => logos_on_linux/img}/step_11.png (100%) rename {img => logos_on_linux/img}/step_12.png (100%) rename {img => logos_on_linux/img}/step_13.png (100%) rename {img => logos_on_linux/img}/step_14.png (100%) rename {img => logos_on_linux/img}/step_15.png (100%) rename {img => logos_on_linux/img}/step_16.png (100%) rename {img => logos_on_linux/img}/step_17.png (100%) rename {img => logos_on_linux/img}/step_18.png (100%) rename {img => logos_on_linux/img}/step_19.png (100%) rename {img => logos_on_linux/img}/step_20.png (100%) rename {img => logos_on_linux/img}/step_21.png (100%) rename {img => logos_on_linux/img}/step_22.png (100%) rename {img => logos_on_linux/img}/verbum-128-icon.png (100%) rename installer.py => logos_on_linux/installer.py (99%) rename logos.py => logos_on_linux/logos.py (98%) rename main.py => logos_on_linux/main.py (98%) rename msg.py => logos_on_linux/msg.py (99%) rename network.py => logos_on_linux/network.py (99%) rename system.py => logos_on_linux/system.py (99%) rename tui_app.py => logos_on_linux/tui_app.py (99%) rename tui_curses.py => logos_on_linux/tui_curses.py (99%) rename tui_dialog.py => logos_on_linux/tui_dialog.py (99%) rename tui_screen.py => logos_on_linux/tui_screen.py (99%) rename utils.py => logos_on_linux/utils.py (99%) rename wine.py => logos_on_linux/wine.py (99%) create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100755 scripts/run_app.py diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index e215b6bc..dad654a0 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -47,18 +47,17 @@ jobs: run: | # apt-get install python3-tk pip install --upgrade pip - pip install -r requirements.txt - pip install pyinstaller + pip install .[build] - name: Build with pyinstaller id: pyinstaller run: | - pyinstaller LogosLinuxInstaller.spec --clean - echo "bin_name=LogosLinuxInstaller" >> $GITHUB_OUTPUT + pyinstaller logos_on_linux.spec --clean + echo "bin_name=logos-on-linux" >> $GITHUB_OUTPUT - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: LogosLinuxInstaller - path: dist/LogosLinuxInstaller + name: logos-on-linux + path: dist/logos-on-linux compression-level: 0 diff --git a/.gitignore b/.gitignore index d0a38f15..6c1324ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ env/ venv/ .venv/ .idea/ +*.egg-info \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d6839bb1..ce814b8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ RUN pyenv install --verbose ${PYTHON_VERSION} RUN pyenv global ${PYTHON_VERSION} WORKDIR /usr/src/app -ENTRYPOINT ["sh", "-c", "pip install --no-cache-dir pyinstaller -r requirements.txt && pyinstaller LogosLinuxInstaller.spec"] +ENTRYPOINT ["sh", "-c", "pip install --no-cache-dir .[build] && pyinstaller logos_on_linux.spec"] diff --git a/LogosLinuxInstaller.spec b/logos_on_linux.spec similarity index 80% rename from LogosLinuxInstaller.spec rename to logos_on_linux.spec index f1d889b1..50a0baba 100644 --- a/LogosLinuxInstaller.spec +++ b/logos_on_linux.spec @@ -2,12 +2,12 @@ a = Analysis( - ['main.py'], + ['scripts/run_app.py'], pathex=[], #binaries=[('/usr/bin/tclsh8.6', '.')], binaries=[], - datas=[('img/*-128-icon.png', 'img')], - hiddenimports=[], + datas=[('logos_on_linux/img/*-128-icon.png', 'img')], + hiddenimports=['logos_on_linux'], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -22,7 +22,7 @@ exe = EXE( a.binaries, a.datas, [], - name='LogosLinuxInstaller', + name='logos-on-linux', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/logos_on_linux/__init__.py b/logos_on_linux/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cli.py b/logos_on_linux/cli.py similarity index 97% rename from cli.py rename to logos_on_linux/cli.py index b2f07c03..af17cc20 100644 --- a/cli.py +++ b/logos_on_linux/cli.py @@ -1,11 +1,11 @@ import queue import threading -import control -import installer -import logos -import wine -import utils +from . import control +from . import installer +from . import logos +from . import wine +from . import utils class CLI: diff --git a/config.py b/logos_on_linux/config.py similarity index 100% rename from config.py rename to logos_on_linux/config.py diff --git a/control.py b/logos_on_linux/control.py similarity index 98% rename from control.py rename to logos_on_linux/control.py index ea72d663..ce7bdd1a 100644 --- a/control.py +++ b/logos_on_linux/control.py @@ -10,18 +10,14 @@ import subprocess import sys import time -from datetime import datetime from pathlib import Path -import config -# import installer -import msg -import network -import system -import tui_curses -# import tui_app -import utils -# import wine +from . import config +from . import msg +from . import network +from . import system +from . import tui_curses +from . import utils def edit_config(): diff --git a/gui.py b/logos_on_linux/gui.py similarity index 99% rename from gui.py rename to logos_on_linux/gui.py index dd6f8005..aed45ec8 100644 --- a/gui.py +++ b/logos_on_linux/gui.py @@ -14,8 +14,8 @@ from tkinter.ttk import Radiobutton from tkinter.ttk import Separator -import config -import utils +from . import config +from . import utils class InstallerGui(Frame): diff --git a/gui_app.py b/logos_on_linux/gui_app.py similarity index 99% rename from gui_app.py rename to logos_on_linux/gui_app.py index 0a35a1ba..1165786b 100644 --- a/gui_app.py +++ b/logos_on_linux/gui_app.py @@ -13,15 +13,15 @@ from tkinter import filedialog as fd from tkinter.ttk import Style -import config -import control -import gui -import installer -import logos -import network -import system -import utils -import wine +from . import config +from . import control +from . import gui +from . import installer +from . import logos +from . import network +from . import system +from . import utils +from . import wine class Root(Tk): diff --git a/img/logos4-128-icon.png b/logos_on_linux/img/logos4-128-icon.png similarity index 100% rename from img/logos4-128-icon.png rename to logos_on_linux/img/logos4-128-icon.png diff --git a/img/step_01.png b/logos_on_linux/img/step_01.png similarity index 100% rename from img/step_01.png rename to logos_on_linux/img/step_01.png diff --git a/img/step_02.png b/logos_on_linux/img/step_02.png similarity index 100% rename from img/step_02.png rename to logos_on_linux/img/step_02.png diff --git a/img/step_03.png b/logos_on_linux/img/step_03.png similarity index 100% rename from img/step_03.png rename to logos_on_linux/img/step_03.png diff --git a/img/step_04.png b/logos_on_linux/img/step_04.png similarity index 100% rename from img/step_04.png rename to logos_on_linux/img/step_04.png diff --git a/img/step_05.png b/logos_on_linux/img/step_05.png similarity index 100% rename from img/step_05.png rename to logos_on_linux/img/step_05.png diff --git a/img/step_06.png b/logos_on_linux/img/step_06.png similarity index 100% rename from img/step_06.png rename to logos_on_linux/img/step_06.png diff --git a/img/step_07.png b/logos_on_linux/img/step_07.png similarity index 100% rename from img/step_07.png rename to logos_on_linux/img/step_07.png diff --git a/img/step_08.png b/logos_on_linux/img/step_08.png similarity index 100% rename from img/step_08.png rename to logos_on_linux/img/step_08.png diff --git a/img/step_09.png b/logos_on_linux/img/step_09.png similarity index 100% rename from img/step_09.png rename to logos_on_linux/img/step_09.png diff --git a/img/step_10.png b/logos_on_linux/img/step_10.png similarity index 100% rename from img/step_10.png rename to logos_on_linux/img/step_10.png diff --git a/img/step_11.png b/logos_on_linux/img/step_11.png similarity index 100% rename from img/step_11.png rename to logos_on_linux/img/step_11.png diff --git a/img/step_12.png b/logos_on_linux/img/step_12.png similarity index 100% rename from img/step_12.png rename to logos_on_linux/img/step_12.png diff --git a/img/step_13.png b/logos_on_linux/img/step_13.png similarity index 100% rename from img/step_13.png rename to logos_on_linux/img/step_13.png diff --git a/img/step_14.png b/logos_on_linux/img/step_14.png similarity index 100% rename from img/step_14.png rename to logos_on_linux/img/step_14.png diff --git a/img/step_15.png b/logos_on_linux/img/step_15.png similarity index 100% rename from img/step_15.png rename to logos_on_linux/img/step_15.png diff --git a/img/step_16.png b/logos_on_linux/img/step_16.png similarity index 100% rename from img/step_16.png rename to logos_on_linux/img/step_16.png diff --git a/img/step_17.png b/logos_on_linux/img/step_17.png similarity index 100% rename from img/step_17.png rename to logos_on_linux/img/step_17.png diff --git a/img/step_18.png b/logos_on_linux/img/step_18.png similarity index 100% rename from img/step_18.png rename to logos_on_linux/img/step_18.png diff --git a/img/step_19.png b/logos_on_linux/img/step_19.png similarity index 100% rename from img/step_19.png rename to logos_on_linux/img/step_19.png diff --git a/img/step_20.png b/logos_on_linux/img/step_20.png similarity index 100% rename from img/step_20.png rename to logos_on_linux/img/step_20.png diff --git a/img/step_21.png b/logos_on_linux/img/step_21.png similarity index 100% rename from img/step_21.png rename to logos_on_linux/img/step_21.png diff --git a/img/step_22.png b/logos_on_linux/img/step_22.png similarity index 100% rename from img/step_22.png rename to logos_on_linux/img/step_22.png diff --git a/img/verbum-128-icon.png b/logos_on_linux/img/verbum-128-icon.png similarity index 100% rename from img/verbum-128-icon.png rename to logos_on_linux/img/verbum-128-icon.png diff --git a/installer.py b/logos_on_linux/installer.py similarity index 99% rename from installer.py rename to logos_on_linux/installer.py index a5223978..c303ec81 100644 --- a/installer.py +++ b/logos_on_linux/installer.py @@ -4,12 +4,12 @@ import sys from pathlib import Path -import config -import msg -import network -import system -import utils -import wine +from . import config +from . import msg +from . import network +from . import system +from . import utils +from . import wine def ensure_product_choice(app=None): diff --git a/logos.py b/logos_on_linux/logos.py similarity index 98% rename from logos.py rename to logos_on_linux/logos.py index 45647c94..4e5189ba 100644 --- a/logos.py +++ b/logos_on_linux/logos.py @@ -4,12 +4,12 @@ import psutil import threading -import config -import main -import msg -import system -import utils -import wine +from . import config +from . import main +from . import msg +from . import system +from . import utils +from . import wine class State(Enum): diff --git a/main.py b/logos_on_linux/main.py similarity index 98% rename from main.py rename to logos_on_linux/main.py index 6a3008ed..c7ce6e28 100755 --- a/main.py +++ b/logos_on_linux/main.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 import argparse - -import cli -import config -import control import curses - try: import dialog # noqa: F401 except ImportError: pass -import gui_app import logging -import msg -import network import os import shutil import sys -import system -import tui_app -import utils -import wine -from config import processes, threads +from . import cli +from . import config +from . import control +from . import gui_app +from . import msg +from . import network +from . import system +from . import tui_app +from . import utils +from . import wine + +from .config import processes, threads def get_parser(): diff --git a/msg.py b/logos_on_linux/msg.py similarity index 99% rename from msg.py rename to logos_on_linux/msg.py index 66c0af61..f9d5b506 100644 --- a/msg.py +++ b/logos_on_linux/msg.py @@ -8,9 +8,9 @@ from pathlib import Path -import config -from gui import ask_question -from gui import show_error +from . import config +from .gui import ask_question +from .gui import show_error class GzippedRotatingFileHandler(RotatingFileHandler): diff --git a/network.py b/logos_on_linux/network.py similarity index 99% rename from network.py rename to logos_on_linux/network.py index e3215a9f..6ea931eb 100644 --- a/network.py +++ b/logos_on_linux/network.py @@ -13,9 +13,9 @@ from urllib.parse import urlparse from xml.etree import ElementTree as ET -import config -import msg -import utils +from . import config +from . import msg +from . import utils class Props(): diff --git a/system.py b/logos_on_linux/system.py similarity index 99% rename from system.py rename to logos_on_linux/system.py index 859abd58..474264c4 100644 --- a/system.py +++ b/logos_on_linux/system.py @@ -1,6 +1,7 @@ -import logging import distro +import logging import os +import psutil import shutil import subprocess import sys @@ -8,11 +9,10 @@ import zipfile from pathlib import Path -import psutil -import config -import msg -import network +from . import config +from . import msg +from . import network # TODO: Replace functions in control.py and wine.py with Popen command. diff --git a/tui_app.py b/logos_on_linux/tui_app.py similarity index 99% rename from tui_app.py rename to logos_on_linux/tui_app.py index dafae16b..12df17d1 100644 --- a/tui_app.py +++ b/logos_on_linux/tui_app.py @@ -4,21 +4,20 @@ import threading import time import curses -import time from pathlib import Path from queue import Queue -import config -import control -import installer -import logos -import msg -import network -import system -import tui_curses -import tui_screen -import utils -import wine +from . import config +from . import control +from . import installer +from . import logos +from . import msg +from . import network +from . import system +from . import tui_curses +from . import tui_screen +from . import utils +from . import wine console_message = "" diff --git a/tui_curses.py b/logos_on_linux/tui_curses.py similarity index 99% rename from tui_curses.py rename to logos_on_linux/tui_curses.py index 08e8b0f5..26fdf00e 100644 --- a/tui_curses.py +++ b/logos_on_linux/tui_curses.py @@ -1,11 +1,10 @@ import curses -import logging import signal import textwrap -import config -import msg -import utils +from . import config +from . import msg +from . import utils def wrap_text(app, text): diff --git a/tui_dialog.py b/logos_on_linux/tui_dialog.py similarity index 99% rename from tui_dialog.py rename to logos_on_linux/tui_dialog.py index 2f960e98..44a838dc 100644 --- a/tui_dialog.py +++ b/logos_on_linux/tui_dialog.py @@ -6,6 +6,7 @@ pass + def text(screen, text, height=None, width=None, title=None, backtitle=None, colors=True): dialog = Dialog() dialog.autowidgetsize = True diff --git a/tui_screen.py b/logos_on_linux/tui_screen.py similarity index 99% rename from tui_screen.py rename to logos_on_linux/tui_screen.py index 1eecef84..b9b3b1a7 100644 --- a/tui_screen.py +++ b/logos_on_linux/tui_screen.py @@ -1,16 +1,15 @@ +import curses import logging import time -import signal from pathlib import Path -import curses -import config -import installer -import system -import tui_curses -import utils +from . import config +from . import installer +from . import system +from . import tui_curses +from . import utils if system.have_dep("dialog"): - import tui_dialog + from . import tui_dialog class Screen: diff --git a/utils.py b/logos_on_linux/utils.py similarity index 99% rename from utils.py rename to logos_on_linux/utils.py index a8366bf0..bfef350d 100644 --- a/utils.py +++ b/logos_on_linux/utils.py @@ -15,20 +15,19 @@ import threading import time import tkinter as tk -from datetime import datetime from packaging import version from pathlib import Path from typing import List, Union -import config -import msg -import network -import system +from . import config +from . import msg +from . import network +from . import system if system.have_dep("dialog"): - import tui_dialog as tui + from . import tui_dialog as tui else: - import tui_curses as tui -import wine + from . import tui_curses as tui +from . import wine # TODO: Move config commands to config.py diff --git a/wine.py b/logos_on_linux/wine.py similarity index 99% rename from wine.py rename to logos_on_linux/wine.py index 8cf5ab82..c5560a08 100644 --- a/wine.py +++ b/logos_on_linux/wine.py @@ -6,13 +6,13 @@ import subprocess from pathlib import Path -import config -import msg -import network -import system -import utils +from . import config +from . import msg +from . import network +from . import system +from . import utils -from config import processes +from .config import processes def get_wine_user(): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ec806d64 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +dependencies = [ +# "altgraph", +# "certifi", +# "charset-normalizer", + "distro", +# "idna", + "packaging", + "psutil", + "pythondialog", + "requests", +# "tkinter", # actually provided by a system package, not a python package +# "urllib3", +] +name = "logos_on_linux" +dynamic = ["readme", "version"] +requires-python = ">=3.12" + +[project.optional-dependencies] +build = ["pyinstaller"] + +[project.scripts] +logos-on-linux = "logos_on_linux.main:main" + +[tool.setuptools.dynamic] +readme = {file = ["README.md"], content-type = "text/plain"} +version = {attr = "logos_on_linux.config.LLI_CURRENT_VERSION"} + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools.package-data] +"logos_on_linux.img" = ["*icon.png"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ba3bd95e..00000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -altgraph==0.17.4 -certifi==2023.11.17 -charset-normalizer==3.3.2 -distro==1.9.0 -idna==3.6 -packaging==23.2 -psutil==5.9.7 -pythondialog==3.5.3 -requests==2.31.0 -urllib3==2.1.0 diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh index 5e495a96..8ad72218 100755 --- a/scripts/build-binary.sh +++ b/scripts/build-binary.sh @@ -5,4 +5,4 @@ if ! which pyinstaller >/dev/null 2>&1; then echo "Error: Need to install pyinstaller; e.g. 'pip3 install pyinstaller'" exit 1 fi -python3 -m PyInstaller --clean "${repo_root}/LogosLinuxInstaller.spec" +python3 -m PyInstaller --clean "${repo_root}/logos_on_linux.spec" diff --git a/scripts/run_app.py b/scripts/run_app.py new file mode 100755 index 00000000..0a65389d --- /dev/null +++ b/scripts/run_app.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +""" +This script is needed so that PyInstaller can refer to a script that does not +use relative imports. +https://github.com/pyinstaller/pyinstaller/issues/2560 +""" +import re +import sys +from logos_on_linux.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) From 8094338cc83386ba3245a6a06853b56cd3fe702d Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 21 Oct 2024 05:53:14 +0100 Subject: [PATCH 243/253] Update project name to Ou Dedetai --- .github/workflows/autobuild-main.yml | 2 +- .github/workflows/build-branch.yml | 8 +++--- .github/workflows/build-release.yml | 2 +- .github/workflows/build-test.yml | 4 +-- Dockerfile | 4 +-- README.md | 20 ++++++++++----- logos_on_linux.spec => ou_dedetai.spec | 6 ++--- {logos_on_linux => ou_dedetai}/__init__.py | 0 {logos_on_linux => ou_dedetai}/cli.py | 0 {logos_on_linux => ou_dedetai}/config.py | 15 ++++++++--- {logos_on_linux => ou_dedetai}/control.py | 0 {logos_on_linux => ou_dedetai}/gui.py | 0 {logos_on_linux => ou_dedetai}/gui_app.py | 2 +- .../img/logos4-128-icon.png | Bin .../img/step_01.png | Bin .../img/step_02.png | Bin .../img/step_03.png | Bin .../img/step_04.png | Bin .../img/step_05.png | Bin .../img/step_06.png | Bin .../img/step_07.png | Bin .../img/step_08.png | Bin .../img/step_09.png | Bin .../img/step_10.png | Bin .../img/step_11.png | Bin .../img/step_12.png | Bin .../img/step_13.png | Bin .../img/step_14.png | Bin .../img/step_15.png | Bin .../img/step_16.png | Bin .../img/step_17.png | Bin .../img/step_18.png | Bin .../img/step_19.png | Bin .../img/step_20.png | Bin .../img/step_21.png | Bin .../img/step_22.png | Bin .../img/verbum-128-icon.png | Bin {logos_on_linux => ou_dedetai}/installer.py | 4 +-- {logos_on_linux => ou_dedetai}/logos.py | 0 {logos_on_linux => ou_dedetai}/main.py | 8 +++--- {logos_on_linux => ou_dedetai}/msg.py | 6 ++--- {logos_on_linux => ou_dedetai}/network.py | 8 +++--- {logos_on_linux => ou_dedetai}/system.py | 4 +-- {logos_on_linux => ou_dedetai}/tui_app.py | 24 +++++++++--------- {logos_on_linux => ou_dedetai}/tui_curses.py | 0 {logos_on_linux => ou_dedetai}/tui_dialog.py | 0 {logos_on_linux => ou_dedetai}/tui_screen.py | 0 {logos_on_linux => ou_dedetai}/utils.py | 19 +++++++------- {logos_on_linux => ou_dedetai}/wine.py | 2 +- pyproject.toml | 8 +++--- scripts/build-binary.sh | 2 +- scripts/run_app.py | 4 +-- 52 files changed, 83 insertions(+), 69 deletions(-) rename logos_on_linux.spec => ou_dedetai.spec (83%) rename {logos_on_linux => ou_dedetai}/__init__.py (100%) rename {logos_on_linux => ou_dedetai}/cli.py (100%) rename {logos_on_linux => ou_dedetai}/config.py (92%) rename {logos_on_linux => ou_dedetai}/control.py (100%) rename {logos_on_linux => ou_dedetai}/gui.py (100%) rename {logos_on_linux => ou_dedetai}/gui_app.py (99%) rename {logos_on_linux => ou_dedetai}/img/logos4-128-icon.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_01.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_02.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_03.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_04.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_05.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_06.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_07.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_08.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_09.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_10.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_11.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_12.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_13.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_14.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_15.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_16.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_17.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_18.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_19.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_20.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_21.png (100%) rename {logos_on_linux => ou_dedetai}/img/step_22.png (100%) rename {logos_on_linux => ou_dedetai}/img/verbum-128-icon.png (100%) rename {logos_on_linux => ou_dedetai}/installer.py (99%) rename {logos_on_linux => ou_dedetai}/logos.py (100%) rename {logos_on_linux => ou_dedetai}/main.py (98%) rename {logos_on_linux => ou_dedetai}/msg.py (97%) rename {logos_on_linux => ou_dedetai}/network.py (98%) rename {logos_on_linux => ou_dedetai}/system.py (99%) rename {logos_on_linux => ou_dedetai}/tui_app.py (98%) rename {logos_on_linux => ou_dedetai}/tui_curses.py (100%) rename {logos_on_linux => ou_dedetai}/tui_dialog.py (100%) rename {logos_on_linux => ou_dedetai}/tui_screen.py (100%) rename {logos_on_linux => ou_dedetai}/utils.py (98%) rename {logos_on_linux => ou_dedetai}/wine.py (99%) diff --git a/.github/workflows/autobuild-main.yml b/.github/workflows/autobuild-main.yml index 74ed164c..e9dc8abf 100644 --- a/.github/workflows/autobuild-main.yml +++ b/.github/workflows/autobuild-main.yml @@ -23,7 +23,7 @@ jobs: - name: Run shell tasks run: | echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV - find -name LogosLinuxInstaller -type f -exec chmod +x {} \; + find -name oudedetai -type f -exec chmod +x {} \; - name: Upload release to test repo uses: softprops/action-gh-release@v1 with: diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index dad654a0..e0ec9918 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -52,12 +52,12 @@ jobs: - name: Build with pyinstaller id: pyinstaller run: | - pyinstaller logos_on_linux.spec --clean - echo "bin_name=logos-on-linux" >> $GITHUB_OUTPUT + pyinstaller ou_dedetai.spec --clean + echo "bin_name=oudedetai" >> $GITHUB_OUTPUT - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: logos-on-linux - path: dist/logos-on-linux + name: oudedetai + path: dist/oudedetai compression-level: 0 diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 2e5348f9..48bccc06 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -29,7 +29,7 @@ jobs: - name: download uses: actions/download-artifact@v4 with: - name: LogosLinuxInstaller + name: oudedetai - name: release uses: softprops/action-gh-release@v1 env: diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 11dd3e29..46e51e76 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -25,8 +25,8 @@ jobs: name: ${{ needs.build.outputs.bin_name }} - name: Fix file permissions run: | - find -name LogosLinuxInstaller -type f - find -name LogosLinuxInstaller -type f -exec chmod +x {} \; + find -name oudedetai -type f + find -name oudedetai -type f -exec chmod +x {} \; - name: Upload release to test repo uses: softprops/action-gh-release@v1 with: diff --git a/Dockerfile b/Dockerfile index ce814b8b..cf154972 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM ubuntu:focal # Prevent popups during install of requirements ENV DEBIAN_FRONTEND=noninteractive -# LogosLinuxInstaller Requirements +# App Requirements RUN apt update -qq && apt install -y -qq git build-essential gdb lcov pkg-config \ libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \ libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \ @@ -28,4 +28,4 @@ RUN pyenv install --verbose ${PYTHON_VERSION} RUN pyenv global ${PYTHON_VERSION} WORKDIR /usr/src/app -ENTRYPOINT ["sh", "-c", "pip install --no-cache-dir .[build] && pyinstaller logos_on_linux.spec"] +ENTRYPOINT ["sh", "-c", "pip install --no-cache-dir .[build] && pyinstaller ou_dedetai.spec"] diff --git a/README.md b/README.md index b0e28f5b..5f4928bd 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,23 @@ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f730f74748c348cb9b3ff2fa1654c84b)](https://app.codacy.com/manual/FaithLife-Community/LogosLinuxInstaller?utm_source=github.com&utm_medium=referral&utm_content=FaithLife-Community/LogosLinuxInstaller&utm_campaign=Badge_Grade_Dashboard) [![Automation testing](https://img.shields.io/badge/Automation-testing-sucess)](https://github.com/FaithLife-Community/LogosLinuxInstallTests) [![Installer LogosBible](https://img.shields.io/badge/Installer-LogosBible-blue)](https://www.logos.com) [![LastRelease](https://img.shields.io/github/v/release/FaithLife-Community/LogosLinuxInstaller)](https://github.com/FaithLife-Community/LogosLinuxInstaller/releases) -# Install Logos Bible Software on Linux +# Ou Dedetai -This repository contains a Python program for installing and maintaining FaithLife's Logos Bible (Verbum) Software on Linux. +>Remember Jesus Christ, risen from the dead, the offspring of David, as preached in my gospel, for which I am suffering, bound with chains as a criminal. **But the word of God is not bound!** +ἀλλʼ **ὁ λόγος** τοῦ θεοῦ **οὐ δέδεται** +> +> Second Timothy 2:8–9, ESV + +## Manages Logos Bible Software via Wine + +This repository contains a Python program for installing and maintaining [FaithLife](https://faithlife.com/)'s [Logos Bible (Verbum) Software](https://www.logos.com/) via [Wine](https://www.winehq.org/). This program is created and maintained by the FaithLife Community and is licensed under the MIT License. -## LogosLinuxInstaller -The main program is a distributable executable and contains Python itself and all necessary Python packages. +## oudetai binary + +The main program is a distributable executable binary and contains Python itself and all necessary Python packages. When running the program, it will attempt to determine your operating system and package manager. It will then attempt to install all needed system dependencies during the installation of Logos. @@ -121,11 +129,11 @@ $ cd LogosLinuxInstaller # docker run --rm -v $(pwd):/usr/src/app logosinstaller ``` -The built binary will now be in `./dist/LogosLinuxInstaller`. +The built binary will now be in `./dist/oudedetai`. ## Install guide (possibly outdated) -NOTE: You can run Logos on Linux using the Steam Proton Experimental binary, which often has the latest and greatest updates to make Logos run even smoother. The script should be able to find the binary automatically, unless your Steam install is located outside of your HOME directory. +NOTE: You can run **Ou Dedetai** using the Steam Proton Experimental binary, which often has the latest and greatest updates to make Logos run even smoother. The script should be able to find the binary automatically, unless your Steam install is located outside of your HOME directory. If you want to install your distro's dependencies outside of the script, please see the [System Dependencies wiki page](https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki/System-Dependencies). diff --git a/logos_on_linux.spec b/ou_dedetai.spec similarity index 83% rename from logos_on_linux.spec rename to ou_dedetai.spec index 50a0baba..7626d5d1 100644 --- a/logos_on_linux.spec +++ b/ou_dedetai.spec @@ -6,8 +6,8 @@ a = Analysis( pathex=[], #binaries=[('/usr/bin/tclsh8.6', '.')], binaries=[], - datas=[('logos_on_linux/img/*-128-icon.png', 'img')], - hiddenimports=['logos_on_linux'], + datas=[('ou_dedetai/img/*-128-icon.png', 'img')], + hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -22,7 +22,7 @@ exe = EXE( a.binaries, a.datas, [], - name='logos-on-linux', + name='oudedetai', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/logos_on_linux/__init__.py b/ou_dedetai/__init__.py similarity index 100% rename from logos_on_linux/__init__.py rename to ou_dedetai/__init__.py diff --git a/logos_on_linux/cli.py b/ou_dedetai/cli.py similarity index 100% rename from logos_on_linux/cli.py rename to ou_dedetai/cli.py diff --git a/logos_on_linux/config.py b/ou_dedetai/config.py similarity index 92% rename from logos_on_linux/config.py rename to ou_dedetai/config.py index 6b308117..26e31cbd 100644 --- a/logos_on_linux/config.py +++ b/ou_dedetai/config.py @@ -5,6 +5,12 @@ from datetime import datetime +# Define app name variables. +name_app = 'Ou Dedetai' +name_binary = 'oudedetai' +name_package = 'ou_dedetai' +repo_link = "https://github.com/FaithLife-Community/LogosLinuxInstaller" + # Define and set variables that are required in the config file. core_config_keys = [ "FLPRODUCT", "TARGETVERSION", "TARGET_RELEASE_VERSION", @@ -27,7 +33,7 @@ 'DEBUG': False, 'DELETE_LOG': None, 'DIALOG': None, - 'LOGOS_LOG': os.path.expanduser("~/.local/state/FaithLife-Community/Logos_on_Linux.log"), # noqa: E501 + 'LOGOS_LOG': os.path.expanduser(f"~/.local/state/FaithLife-Community/{name_binary}.log"), # noqa: E501 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 'LOGOS_EXE': None, 'LOGOS_EXECUTABLE': None, @@ -57,7 +63,7 @@ APPIMAGE_FILE_PATH = None authenticated = False BADPACKAGES = None -DEFAULT_CONFIG_PATH = os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json") # noqa: E501 +DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{name_binary}.json") # noqa: E501 FLPRODUCTi = None GUI = None INSTALL_STEP = 0 @@ -67,7 +73,7 @@ LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" LLI_CURRENT_VERSION = "4.0.0-beta.1" LLI_LATEST_VERSION = None -LLI_TITLE = "Logos Linux Installer" +LLI_TITLE = name_app LOG_LEVEL = logging.WARNING LOGOS_BLUE = '#0082FF' LOGOS_GRAY = '#E7E7E7' @@ -77,7 +83,7 @@ LOGOS_FORCE_ROOT = False LOGOS_ICON_FILENAME = None LOGOS_ICON_URL = None -LOGOS_LATEST_VERSION_FILENAME = "LogosLinuxInstaller" +LOGOS_LATEST_VERSION_FILENAME = name_binary LOGOS_LATEST_VERSION_URL = None LOGOS9_RELEASES = None # used to save downloaded releases list LOGOS9_WINE64_BOTTLE_TARGZ_NAME = "wine64_bottle.tar.gz" @@ -91,6 +97,7 @@ PACKAGE_MANAGER_COMMAND_QUERY = None PACKAGES = None PASSIVE = None +pid_file = f'/tmp/{name_binary}.pid' PRESENT_WORKING_DIRECTORY = os.getcwd() QUERY_PREFIX = None REBOOT_REQUIRED = None diff --git a/logos_on_linux/control.py b/ou_dedetai/control.py similarity index 100% rename from logos_on_linux/control.py rename to ou_dedetai/control.py diff --git a/logos_on_linux/gui.py b/ou_dedetai/gui.py similarity index 100% rename from logos_on_linux/gui.py rename to ou_dedetai/gui.py diff --git a/logos_on_linux/gui_app.py b/ou_dedetai/gui_app.py similarity index 99% rename from logos_on_linux/gui_app.py rename to ou_dedetai/gui_app.py index 1165786b..4cd60f71 100644 --- a/logos_on_linux/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -668,7 +668,7 @@ def configure_app_button(self, evt=None): self.gui.app_button.config(command=self.run_installer) def run_installer(self, evt=None): - classname = "LogosLinuxInstaller" + classname = config.name_binary self.installer_win = Toplevel() InstallerWindow(self.installer_win, self.root, class_=classname) self.root.icon = config.LOGOS_ICON_URL diff --git a/logos_on_linux/img/logos4-128-icon.png b/ou_dedetai/img/logos4-128-icon.png similarity index 100% rename from logos_on_linux/img/logos4-128-icon.png rename to ou_dedetai/img/logos4-128-icon.png diff --git a/logos_on_linux/img/step_01.png b/ou_dedetai/img/step_01.png similarity index 100% rename from logos_on_linux/img/step_01.png rename to ou_dedetai/img/step_01.png diff --git a/logos_on_linux/img/step_02.png b/ou_dedetai/img/step_02.png similarity index 100% rename from logos_on_linux/img/step_02.png rename to ou_dedetai/img/step_02.png diff --git a/logos_on_linux/img/step_03.png b/ou_dedetai/img/step_03.png similarity index 100% rename from logos_on_linux/img/step_03.png rename to ou_dedetai/img/step_03.png diff --git a/logos_on_linux/img/step_04.png b/ou_dedetai/img/step_04.png similarity index 100% rename from logos_on_linux/img/step_04.png rename to ou_dedetai/img/step_04.png diff --git a/logos_on_linux/img/step_05.png b/ou_dedetai/img/step_05.png similarity index 100% rename from logos_on_linux/img/step_05.png rename to ou_dedetai/img/step_05.png diff --git a/logos_on_linux/img/step_06.png b/ou_dedetai/img/step_06.png similarity index 100% rename from logos_on_linux/img/step_06.png rename to ou_dedetai/img/step_06.png diff --git a/logos_on_linux/img/step_07.png b/ou_dedetai/img/step_07.png similarity index 100% rename from logos_on_linux/img/step_07.png rename to ou_dedetai/img/step_07.png diff --git a/logos_on_linux/img/step_08.png b/ou_dedetai/img/step_08.png similarity index 100% rename from logos_on_linux/img/step_08.png rename to ou_dedetai/img/step_08.png diff --git a/logos_on_linux/img/step_09.png b/ou_dedetai/img/step_09.png similarity index 100% rename from logos_on_linux/img/step_09.png rename to ou_dedetai/img/step_09.png diff --git a/logos_on_linux/img/step_10.png b/ou_dedetai/img/step_10.png similarity index 100% rename from logos_on_linux/img/step_10.png rename to ou_dedetai/img/step_10.png diff --git a/logos_on_linux/img/step_11.png b/ou_dedetai/img/step_11.png similarity index 100% rename from logos_on_linux/img/step_11.png rename to ou_dedetai/img/step_11.png diff --git a/logos_on_linux/img/step_12.png b/ou_dedetai/img/step_12.png similarity index 100% rename from logos_on_linux/img/step_12.png rename to ou_dedetai/img/step_12.png diff --git a/logos_on_linux/img/step_13.png b/ou_dedetai/img/step_13.png similarity index 100% rename from logos_on_linux/img/step_13.png rename to ou_dedetai/img/step_13.png diff --git a/logos_on_linux/img/step_14.png b/ou_dedetai/img/step_14.png similarity index 100% rename from logos_on_linux/img/step_14.png rename to ou_dedetai/img/step_14.png diff --git a/logos_on_linux/img/step_15.png b/ou_dedetai/img/step_15.png similarity index 100% rename from logos_on_linux/img/step_15.png rename to ou_dedetai/img/step_15.png diff --git a/logos_on_linux/img/step_16.png b/ou_dedetai/img/step_16.png similarity index 100% rename from logos_on_linux/img/step_16.png rename to ou_dedetai/img/step_16.png diff --git a/logos_on_linux/img/step_17.png b/ou_dedetai/img/step_17.png similarity index 100% rename from logos_on_linux/img/step_17.png rename to ou_dedetai/img/step_17.png diff --git a/logos_on_linux/img/step_18.png b/ou_dedetai/img/step_18.png similarity index 100% rename from logos_on_linux/img/step_18.png rename to ou_dedetai/img/step_18.png diff --git a/logos_on_linux/img/step_19.png b/ou_dedetai/img/step_19.png similarity index 100% rename from logos_on_linux/img/step_19.png rename to ou_dedetai/img/step_19.png diff --git a/logos_on_linux/img/step_20.png b/ou_dedetai/img/step_20.png similarity index 100% rename from logos_on_linux/img/step_20.png rename to ou_dedetai/img/step_20.png diff --git a/logos_on_linux/img/step_21.png b/ou_dedetai/img/step_21.png similarity index 100% rename from logos_on_linux/img/step_21.png rename to ou_dedetai/img/step_21.png diff --git a/logos_on_linux/img/step_22.png b/ou_dedetai/img/step_22.png similarity index 100% rename from logos_on_linux/img/step_22.png rename to ou_dedetai/img/step_22.png diff --git a/logos_on_linux/img/verbum-128-icon.png b/ou_dedetai/img/verbum-128-icon.png similarity index 100% rename from logos_on_linux/img/verbum-128-icon.png rename to ou_dedetai/img/verbum-128-icon.png diff --git a/logos_on_linux/installer.py b/ou_dedetai/installer.py similarity index 99% rename from logos_on_linux/installer.py rename to ou_dedetai/installer.py index c303ec81..857c8e92 100644 --- a/logos_on_linux/installer.py +++ b/ou_dedetai/installer.py @@ -658,7 +658,7 @@ def ensure_launcher_executable(app=None): ) # Copy executable to config.INSTALLDIR. - launcher_exe = Path(f"{config.INSTALLDIR}/LogosLinuxInstaller") + launcher_exe = Path(f"{config.INSTALLDIR}/{config.name_binary})") if launcher_exe.is_file(): logging.debug("Removing existing launcher binary.") launcher_exe.unlink() @@ -809,7 +809,7 @@ def create_launcher_shortcuts(): logos_icon_path = app_dir / logos_icon_src.name if system.get_runmode() == 'binary': - lli_executable = f"{installdir}/LogosLinuxInstaller" + lli_executable = f"{installdir}/{config.name_binary}" else: script = Path(sys.argv[0]).expanduser().resolve() # Find python in virtual environment. diff --git a/logos_on_linux/logos.py b/ou_dedetai/logos.py similarity index 100% rename from logos_on_linux/logos.py rename to ou_dedetai/logos.py diff --git a/logos_on_linux/main.py b/ou_dedetai/main.py similarity index 98% rename from logos_on_linux/main.py rename to ou_dedetai/main.py index c7ce6e28..84cca545 100755 --- a/logos_on_linux/main.py +++ b/ou_dedetai/main.py @@ -384,8 +384,8 @@ def check_incompatibilities(): question_text = "Remove AppImageLauncher? A reboot will be required." secondary = ( "Your system currently has AppImageLauncher installed.\n" - "LogosLinuxInstaller is not compatible with AppImageLauncher.\n" - "For more information, see: https://github.com/FaithLife-Community/LogosLinuxInstaller/issues/114" # noqa: E501 + f"{config.name_app} is not compatible with AppImageLauncher.\n" + f"For more information, see: {config.repo_link}/issues/114" ) no_text = "User declined to remove AppImageLauncher." msg.logos_continue_question(question_text, no_text, secondary) @@ -466,7 +466,7 @@ def main(): def close(): - logging.debug("Closing Logos on Linux.") + logging.debug(f"Closing {config.name_app}.") for thread in threads: # Only wait on non-daemon threads. if not thread.daemon: @@ -477,7 +477,7 @@ def close(): wine.end_wine_processes() else: logging.debug("No extra processes found.") - logging.debug("Closing Logos on Linux finished.") + logging.debug(f"Closing {config.name_app} finished.") if __name__ == '__main__': diff --git a/logos_on_linux/msg.py b/ou_dedetai/msg.py similarity index 97% rename from logos_on_linux/msg.py rename to ou_dedetai/msg.py index f9d5b506..034a5dfc 100644 --- a/logos_on_linux/msg.py +++ b/ou_dedetai/msg.py @@ -165,7 +165,7 @@ def logos_warn(message): def ui_message(message, secondary=None, detail=None, app=None, parent=None, fatal=False): # noqa: E501 if detail is None: detail = '' - WIKI_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki" # noqa: E501 + WIKI_LINK = f"{config.repo_link}/wiki" TELEGRAM_LINK = "https://t.me/linux_logos" MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 @@ -191,7 +191,7 @@ def ui_message(message, secondary=None, detail=None, app=None, parent=None, fata def logos_error(message, secondary=None, detail=None, app=None, parent=None): # if detail is None: # detail = '' - # WIKI_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki" # noqa: E501 + # WIKI_LINK = f"{config.repo_link}/wiki" # TELEGRAM_LINK = "https://t.me/linux_logos" # MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" # help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 @@ -215,7 +215,7 @@ def logos_error(message, secondary=None, detail=None, app=None, parent=None): logging.critical(message) if secondary is None or secondary == "": try: - os.remove("/tmp/LogosLinuxInstaller.pid") + os.remove(config.pid_file) except FileNotFoundError: # no pid file when testing functions pass os.kill(os.getpgid(os.getpid()), signal.SIGKILL) diff --git a/logos_on_linux/network.py b/ou_dedetai/network.py similarity index 98% rename from logos_on_linux/network.py rename to ou_dedetai/network.py index 6ea931eb..2d9af840 100644 --- a/logos_on_linux/network.py +++ b/ou_dedetai/network.py @@ -410,7 +410,7 @@ def set_logoslinuxinstaller_latest_release_config(): json_data = get_latest_release_data(releases_url) logoslinuxinstaller_url = get_latest_release_url(json_data) if logoslinuxinstaller_url is None: - logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 + logging.critical(f"Unable to set {config.name_app} release without URL.") # noqa: E501 return config.LOGOS_LATEST_VERSION_URL = logoslinuxinstaller_url config.LOGOS_LATEST_VERSION_FILENAME = os.path.basename(logoslinuxinstaller_url) # noqa: #501 @@ -563,8 +563,8 @@ def get_logos_releases(app=None): def update_lli_binary(app=None): lli_file_path = os.path.realpath(sys.argv[0]) - lli_download_path = Path(config.MYDOWNLOADS) / "LogosLinuxInstaller" - temp_path = Path(config.MYDOWNLOADS) / "LogosLinuxInstaller.tmp" + lli_download_path = Path(config.MYDOWNLOADS) / config.name_binary + temp_path = Path(config.MYDOWNLOADS) / f"{config.name_binary}.tmp" logging.debug( f"Updating Logos Linux Installer to latest version by overwriting: {lli_file_path}") # noqa: E501 @@ -579,7 +579,7 @@ def update_lli_binary(app=None): logos_reuse_download( config.LOGOS_LATEST_VERSION_URL, - "LogosLinuxInstaller", + config.name_binary, config.MYDOWNLOADS, app=app, ) diff --git a/logos_on_linux/system.py b/ou_dedetai/system.py similarity index 99% rename from logos_on_linux/system.py rename to ou_dedetai/system.py index 474264c4..c2d3a40a 100644 --- a/logos_on_linux/system.py +++ b/ou_dedetai/system.py @@ -715,7 +715,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) message = "The system needs to install/remove packages, but it requires manual intervention." # noqa: E501 detail = ( "Please run the following command in a terminal, then restart " - f"LogosLinuxInstaller:\n{sudo_command}\n" + f"{config.name_app}:\n{sudo_command}\n" ) if config.DIALOG == "tk": if hasattr(app, 'root'): @@ -739,7 +739,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) 17, app.manualinstall_q, app.manualinstall_e, - f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\nLogosLinuxInstaller:\n{sudo_command}\n", # noqa: E501 + f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\n{config.name_app}:\n{sudo_command}\n", # noqa: E501 "User cancelled dependency installation.", # noqa: E501 message, options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) # noqa: E501 diff --git a/logos_on_linux/tui_app.py b/ou_dedetai/tui_app.py similarity index 98% rename from logos_on_linux/tui_app.py rename to ou_dedetai/tui_app.py index 12df17d1..16918cbb 100644 --- a/logos_on_linux/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -26,11 +26,11 @@ class TUI: def __init__(self, stdscr): self.stdscr = stdscr - #if config.current_logos_version is not None: - self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" - self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" - #else: - # self.title = f"Welcome to Logos on Linux ({config.LLI_CURRENT_VERSION})" + # if config.current_logos_version is not None: + self.title = f"Welcome to {config.name_app} {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 + self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" # noqa: E501 + # else: + # self.title = f"Welcome to {config.name_app} ({config.LLI_CURRENT_VERSION})" # noqa: E501 self.console_message = "Starting TUI…" self.llirunning = True self.active_progress = False @@ -233,15 +233,15 @@ def end(self, signal, frame): def update_main_window_contents(self): self.clear() - self.title = f"Welcome to Logos on Linux {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" - self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" - self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) + self.title = f"Welcome to {config.name_app} {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 + self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" # noqa: E501 + self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) # noqa: E501 self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) - #self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) + # self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) self.refresh() - #ERR: On a sudden resize, the Curses menu is not properly resized, + # ERR: On a sudden resize, the Curses menu is not properly resized, # and we are not currently dynamically passing the menu options based # on the current screen, but rather always passing the tui menu options. # To replicate, open Terminator, run LLI full screen, then his Ctrl+A. @@ -531,7 +531,7 @@ def utilities_menu_select(self, choice): utils.change_logos_release_channel() self.update_main_window_contents() self.go_to_main_menu() - elif choice == "Change Logos on Linux Release Channel": + elif choice == f"Change {config.name_app} Release Channel": self.reset_screen() utils.change_lli_release_channel() network.set_logoslinuxinstaller_latest_release_config() @@ -1037,7 +1037,7 @@ def set_utilities_menu_options(self, dialog=False): if utils.file_exists(config.LOGOS_EXE): labels_utils_installed = [ "Change Logos Release Channel", - "Change Logos on Linux Release Channel", + f"Change {config.name_app} Release Channel", # "Back Up Data", # "Restore Data" ] diff --git a/logos_on_linux/tui_curses.py b/ou_dedetai/tui_curses.py similarity index 100% rename from logos_on_linux/tui_curses.py rename to ou_dedetai/tui_curses.py diff --git a/logos_on_linux/tui_dialog.py b/ou_dedetai/tui_dialog.py similarity index 100% rename from logos_on_linux/tui_dialog.py rename to ou_dedetai/tui_dialog.py diff --git a/logos_on_linux/tui_screen.py b/ou_dedetai/tui_screen.py similarity index 100% rename from logos_on_linux/tui_screen.py rename to ou_dedetai/tui_screen.py diff --git a/logos_on_linux/utils.py b/ou_dedetai/utils.py similarity index 98% rename from logos_on_linux/utils.py rename to ou_dedetai/utils.py index bfef350d..ab0f8f66 100644 --- a/logos_on_linux/utils.py +++ b/ou_dedetai/utils.py @@ -123,14 +123,13 @@ def update_config_file(config_file_path, key, value): def die_if_running(): - PIDF = '/tmp/LogosLinuxInstaller.pid' def remove_pid_file(): - if os.path.exists(PIDF): - os.remove(PIDF) + if os.path.exists(config.pid_file): + os.remove(config.pid_file) - if os.path.isfile(PIDF): - with open(PIDF, 'r') as f: + if os.path.isfile(config.pid_file): + with open(config.pid_file, 'r') as f: pid = f.read().strip() message = f"The script is already running on PID {pid}. Should it be killed to allow this instance to run?" # noqa: E501 if config.DIALOG == "tk": @@ -151,7 +150,7 @@ def remove_pid_file(): os.kill(int(pid), signal.SIGKILL) atexit.register(remove_pid_file) - with open(PIDF, 'w') as f: + with open(config.pid_file, 'w') as f: f.write(str(os.getpid())) @@ -167,7 +166,7 @@ def die(message): def restart_lli(): logging.debug("Restarting Logos Linux Installer.") - pidfile = Path('/tmp/LogosLinuxInstaller.pid') + pidfile = Path(config.pid_file) if pidfile.is_file(): pidfile.unlink() os.execv(sys.executable, [sys.executable]) @@ -809,7 +808,7 @@ def set_appimage_symlink(app=None): if not check_appimage(selected_appimage_file_path): msg.logos_error(f"Cannot use {selected_appimage_file_path}.") - # Determine if user wants their AppImage in the Logos on Linux bin dir. + # Determine if user wants their AppImage in the app bin dir. copy_message = ( f"Should the program copy {selected_appimage_file_path} to the" f" {config.APPDIR_BINDIR} directory?" @@ -849,7 +848,7 @@ def update_to_latest_lli_release(app=None): status, _ = compare_logos_linux_installer_version() if system.get_runmode() != 'binary': - logging.error("Can't update LogosLinuxInstaller when run as a script.") + logging.error(f"Can't update {config.name_app} when run as a script.") elif status == 0: network.update_lli_binary(app=app) elif status == 1: @@ -884,7 +883,7 @@ def get_downloaded_file_path(filename): def send_task(app, task): - #logging.debug(f"{task=}") + # logging.debug(f"{task=}") app.todo_q.put(task) if config.DIALOG == 'tk': app.root.event_generate('<>') diff --git a/logos_on_linux/wine.py b/ou_dedetai/wine.py similarity index 99% rename from logos_on_linux/wine.py rename to ou_dedetai/wine.py index c5560a08..36093480 100644 --- a/logos_on_linux/wine.py +++ b/ou_dedetai/wine.py @@ -440,7 +440,7 @@ def install_icu_data_files(app=None): icu_url = network.get_latest_release_url(json_data) # icu_tag_name = utils.get_latest_release_version_tag_name(json_data) if icu_url is None: - logging.critical("Unable to set LogosLinuxInstaller release without URL.") # noqa: E501 + logging.critical(f"Unable to set {config.name_app} release without URL.") # noqa: E501 return icu_filename = os.path.basename(icu_url) network.logos_reuse_download( diff --git a/pyproject.toml b/pyproject.toml index ec806d64..766f400c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ # "tkinter", # actually provided by a system package, not a python package # "urllib3", ] -name = "logos_on_linux" +name = "ou_dedetai" dynamic = ["readme", "version"] requires-python = ">=3.12" @@ -24,14 +24,14 @@ requires-python = ">=3.12" build = ["pyinstaller"] [project.scripts] -logos-on-linux = "logos_on_linux.main:main" +oudedetai = "ou_dedetai.main:main" [tool.setuptools.dynamic] readme = {file = ["README.md"], content-type = "text/plain"} -version = {attr = "logos_on_linux.config.LLI_CURRENT_VERSION"} +version = {attr = "ou_dedetai.config.LLI_CURRENT_VERSION"} [tool.setuptools.packages.find] where = ["."] [tool.setuptools.package-data] -"logos_on_linux.img" = ["*icon.png"] \ No newline at end of file +"ou_dedetai.img" = ["*icon.png"] \ No newline at end of file diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh index 8ad72218..13f1affd 100755 --- a/scripts/build-binary.sh +++ b/scripts/build-binary.sh @@ -5,4 +5,4 @@ if ! which pyinstaller >/dev/null 2>&1; then echo "Error: Need to install pyinstaller; e.g. 'pip3 install pyinstaller'" exit 1 fi -python3 -m PyInstaller --clean "${repo_root}/logos_on_linux.spec" +python3 -m PyInstaller --clean "${repo_root}/ou_dedetai.spec" diff --git a/scripts/run_app.py b/scripts/run_app.py index 0a65389d..80d348b1 100755 --- a/scripts/run_app.py +++ b/scripts/run_app.py @@ -6,7 +6,7 @@ """ import re import sys -from logos_on_linux.main import main +import ou_dedetai if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) + sys.exit(ou_dedetai.main()) From e55d868a9b666389993b6c255e3d7a9aa4cbba15 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 21 Oct 2024 16:47:30 +0100 Subject: [PATCH 244/253] use ./scripts/build-binary.sh for all automated builds --- .github/workflows/build-branch.yml | 2 +- scripts/build-binary.sh | 9 ++++++--- scripts/run_app.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index e0ec9918..762f2542 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -52,7 +52,7 @@ jobs: - name: Build with pyinstaller id: pyinstaller run: | - pyinstaller ou_dedetai.spec --clean + ./scripts/build-binary.sh echo "bin_name=oudedetai" >> $GITHUB_OUTPUT - name: Upload artifact diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh index 13f1affd..9efa669d 100755 --- a/scripts/build-binary.sh +++ b/scripts/build-binary.sh @@ -1,8 +1,11 @@ #!/usr/bin/env bash +start_dir="$PWD" script_dir="$(dirname "$0")" repo_root="$(dirname "$script_dir")" +cd "$repo_root" if ! which pyinstaller >/dev/null 2>&1; then - echo "Error: Need to install pyinstaller; e.g. 'pip3 install pyinstaller'" - exit 1 + # Install build deps. + python3 -m pip install .[build] fi -python3 -m PyInstaller --clean "${repo_root}/ou_dedetai.spec" +pyinstaller --clean --log-level DEBUG ou_dedetai.spec +cd "$start_dir" \ No newline at end of file diff --git a/scripts/run_app.py b/scripts/run_app.py index 80d348b1..e1b8bfab 100755 --- a/scripts/run_app.py +++ b/scripts/run_app.py @@ -6,7 +6,7 @@ """ import re import sys -import ou_dedetai +import ou_dedetai.main if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(ou_dedetai.main()) + sys.exit(ou_dedetai.main.main()) From 919bbec6d178b1e4e66095460e58f79c0566ecc5 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 21 Oct 2024 18:43:41 +0100 Subject: [PATCH 245/253] fix #171; add placeholder icon --- ou_dedetai.spec | 2 +- ou_dedetai/gui_app.py | 4 +- ou_dedetai/img/icon.png | Bin 0 -> 65544 bytes ou_dedetai/img/icon.svg | 125 ++++++++++++++++++++++++++++++++++++++++ ou_dedetai/installer.py | 37 ++++++++---- scripts/build-binary.sh | 2 +- scripts/run_app.py | 4 +- 7 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 ou_dedetai/img/icon.png create mode 100644 ou_dedetai/img/icon.svg diff --git a/ou_dedetai.spec b/ou_dedetai.spec index 7626d5d1..8616f8ef 100644 --- a/ou_dedetai.spec +++ b/ou_dedetai.spec @@ -6,7 +6,7 @@ a = Analysis( pathex=[], #binaries=[('/usr/bin/tclsh8.6', '.')], binaries=[], - datas=[('ou_dedetai/img/*-128-icon.png', 'img')], + datas=[('ou_dedetai/img/*icon.png', 'img')], hiddenimports=[], hookspath=[], hooksconfig={}, diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 4cd60f71..2757f31a 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -77,7 +77,7 @@ def __init__(self, *args, **kwargs): # Set panel icon. app_dir = Path(__file__).parent - self.icon = app_dir / 'img' / 'logos4-128-icon.png' + self.icon = app_dir / 'img' / 'icon.png' self.pi = PhotoImage(file=f'{self.icon}') self.iconphoto(False, self.pi) @@ -913,7 +913,7 @@ def stop_indeterminate_progress(self, evt=None): def control_panel_app(): utils.set_debug() - classname = "LogosLinuxControlPanel" + classname = config.name_binary root = Root(className=classname) ControlWindow(root, class_=classname) root.mainloop() diff --git a/ou_dedetai/img/icon.png b/ou_dedetai/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..01261242e933e1041edf62d8b368d99b0b3fbb63 GIT binary patch literal 65544 zcmXtfcQjnz_x2rKbRtCWEu!~e2BY^NB8ie9h!(w@QIhC2BDx?5f~Zl45Js<2!-(E{ zH)h`Xe1GfxW7eHD>&`v*?6dcC_I~zrqKynRDef@c0RVtPTMPOa0Pt{M@ct}($gOB^YasNa&z{wefid2#NE>& zYhRHGcN6u0H<>zlyVwJUJ{(YcJA3b!PJ%|po&dlBXhT&@{j+yN{IXeo*Pngc|Fi!l zjgD3|S|A0`CP?*?xtz5VyLaEEW9Hx}+qL7ebHl|Y+%-H(M#g4^fJBdohJcGCNxO%G zl$U@c$=AGP;_UXv#9U@(zvY5b+jb$UWB7Yec^Us{)3+d!uXjU8BjHO9j=wb-fqeRh zj_M50s0-MIHa`Hj009ZaNagcIsP0Dr;{s>`iLna79S+qz5>4IE8 zEw@mBdmquew*XR|WHgQj&;T(SYh3bPlgDW6wi_$l;k~}2Y6?^U)hy1{;Z|^ggNIwA zqm)1siK}BLqxZ`Y>eGQBFt3hWCvl%iCrkV?QS2+W<**sujeMxF%4j?=P)&3&PIjoh zIG_CG9}A!gc`Nw7fNTXK#&vM}QQ##xL%~LLo3WPC!^7e5*g_)(BHkE+m^8;E{G7*nZcB5+@s ze>x?Q08x_zRv*WyMpEc5i;g9U@miY~h%zI`R-)QnN4l zT-#RkJCilxMske&{Qjua22UV-P}Zn`fQ(h8ME_h_hd@2dIR^aRuq5A`L}!)aPE6?2 z$oQjsNQE~gzXj*utsSl5<+IcZF@8jxpJsV(h{QYSKZ`MRNr$9~^tworP z^s_?=CGhPce3BLLr0L1OpY_S_;6bv2cTryCp?;sTHdROSF@=Zz3I4hgpq+{5bN*G* zh|854%Hf|AN)~Hk$@h>rKk#O8$f)zP={*5syd*leaRF51q2)edQsipvV<9coB$44T zHzm3c9nz1C6!3Y+f4?$441Cr|Z}1z$(a71oGgH>uS_Kt)%XwQAX?+e{YUU~dB9tmy zb6-{j7<;6sL?V7>0UP{v)KW!fDX)1m{~wHpq!a1gM32Jd-D&!fF} za=3t>)Xn);kpAN^gfuVzl96V8jI6q6qt5ET2|gQ(bnSn1opy-#eB7}1XyjwCW%OcC zhJb<#(}n-%qMMguh=cbXr|uNm1`MV^#OtZFJu&i(E+N43tP3y(ZMT)QB>u==8!(mnTQ)2R*)vYU@y==Tar)MUxTejqKwSbe ze2TPd4Cd`@4>2LBaWuQ^TTUGP;mE77@vwKh^ zxD<3N5U~q}qKZ%?xW{{YUJ#n|e=$A)*{qP?{RtJTrthRl6%X9_F-DX{-t}|L)%$Oh zOdRt}97U!Yx$B)lx6>)smp>=at-W@dBj@9_q91dOluo-UYESTT0?TewrF6@WU z-etG-Qp{VGCCL77Met6$rq;fO^2WVn+LOJrl?AWCX2i5 zuXD`ndj7+QarLIqmF9OQl>!ohx#i5o?8SECOKubgIkbkq62GOi@)7s;1dxg1 zL^(5m?K^3ANXDbir_X{wcfaT**;7G`_d%me7GYaEwjCF~iM~0?@Astx0bic@pc2yH zztN4>0eOSXt4B6dV5{DnfP<2k)Hceu_dd;KT08<)x#~DOPVus1bXLQJ3qvfrE4iMT zeLPqhholCi3@$Jv`BA~&u$lQnUh~&FEmc5F0MFklW0b`1P%36g>PLhGx50y1^aAR2 z!P4u>Xv(YJK0iV#zY3?g8f`Zc<4z_;UD0ainmXL;tCN(a-=rb7%?s|eN2AQyA>0dO z9mIXIf!{a;Rj&3yT03lj#UHHFBfRfJ9OkUld%>4>*>Xi0=h4FZ+tN?byAqzgeK0Z% zCXZ??<08}(|F_U!*Ndz?+6Es$M)T>^t*)3D=V{Sc=#!#@)I(Phs=AEEs*X<`!7uiS z?fmAgnU`h8^fyW3SR+HtSy}elHv9vZJ6^|0{t#qRcyM|6_3Pz8z{LkY#U^U@jkdfO z)XSFF`mL|a+;)MaSTS182H(Y}(d~q})i~R$^K56GrhX*w-f12dREt_1;AN&Kn7l3E znZZJga1omR5%FEp<{6h(3VqUUef|B_gb@wm1L@@4McP-TZ`ni07igY<+dlujoCOp4 zRiviheg9b#5lzomra=kFGjTTjrp&+cjr`;Y6zQXtCGaIea^m|%-aFa3>z=A}_*uX8 zpw(l=pQeO$>PortL?(0V*l5#=NqX5BbSN6yqeEzd9qF?<=yVNiA&vHaHT#0IbJh-s}8oq|{IIo2jDFMab zLMQ4|pQc<` zNp#PjKJwQsDe}QBxuY6g1Mx$!)(%fF4UB0GU-JmQ zr%&D&i$AAe4MVUnv-&S-oW|;_1o%CfQXqDlVJwlO}{!ju}i*(2Fpj(1Ag z@q6YTL079=C3L0h-Q@ zORvOmrQw^(C_KvfKBTA^1KDpMEmZ=X9^ngqy+!=+s@szFEkxbNp;Kg#pmD_T9p%<5 zUeCS^KSHnLMcwVvCEh^LVk(6$D=tAf#NUM)&a$sU9#nV zF+~S_dIY4~NO8F&Z?Ee}5shQ@gb3L_^VgV zEkenUxRRL0zi^N+s;>8fCgZXOzhw4m7=BbwvS&8EmXYClp40Y*ZR|x^fJNk>T3v2m z1e5ZM4Tp=;@BwF=W2US@`#0{+wgn?QH3wg#t)zpb0_3j2K{{(UyjT3ar5NvRwY)?T z?B7p}85dTM)nc;;-ra{E7lU^I|M4mHf9*^F^A{>edW<;{epKJ)b=V+GQ|ntnag#y273)3g-R`*f)KCm{WFmImA0dRDHg{jk;=jJ=`kc3C z()8*tF! z?ZNZUVL!V0xn1g4co)A3*m8K@F{KF|Ia&0;!M%OS0D0t}S^MUK3b>9a!SKBD++|AJ z*4KxeM1{~Y64L8D@pvm$t<$|)2f~r{HukE`4o1MerXZ6Xa<&ob2TJ=$q;r?7VYQO` zyewSq2yr0x-kBZj8X@fph$Xkkv?ffg2-E)5v5|wZ+u>~zg5>)xCXBsL-!(~r zO+^VPRD~v-KIDfNkkRDkGo0SLbdGMn&ut{whd4aU*}qUhx7XrTR66OS%(Wrj3OI)t zj8ahcMJWLV!XU%a_JZJ0rJQ!BDqx!baBZ(JUS#%z)#DV{ISI)#gFGE9)uvhoJU2+4 zoH)>(+*J2`;c1nM;X_`%FI?`Z1P~a}$5MPUv)Siuc?zl}LHSv9cv~_5oWcZr*4W_- zGM6Wn*y!M0KygdzUY(pb48wIL^ZE9$YpezFA`2}%Sj92^95{7hhaBLn`@bAM#?t7> zcTOOgHwAj8&WouKAv2xLoXZ;i`iwg|VimDIUYNp7w&=~xvB6kT3YwWX zq>Ke(tP6*sw+vmF*BihI^rlQ(bAiMYlCAa2v~x3?OK;2M~h&B zpk56z`kBSz`Sf}X*kqw>aduvl8RG~J^w7rqhU9IMGKTYSHf`?q0jFI_s*{Y< zAI?jS9sKHV*o(PSRbRreO}VRjZ_2PrP9xtYoBs6M5T_f*@kHHYQJy;o1a&rIQwjix zh@PwesW~V2WgSwfN*S1tLb#sf2}|bJkS50+S_T2$Cz%}IodZk-{jAThY;9Ii1wo+? zaRm|YBO5O&AbBN!6z4#f0(Epo6xUfKpiW-Kq>E4;5*wXyZDbp6)l)v=+ch_Y0Gy|G z-?@XCYYc0NIUYJ#)!U}B0e{2?vhgi!^S`Tc)SAoVb9p+rdh_CG2Q0--6l#MP6Rjp0 z`ttD7DoNP={0iIfYJ_4SH-z3cj0N@9a23$jbHB`o6?8^h|ANEGoS0R2aYTjA+r#8z z_aY({b5`cIqM{KsnRM5QJbkB**NX)zfABfNoow?f1&ySs4)hMaV%BvInE~r;Hhpv- zCL{v=fJWg-U8?z_RWyV3vjo{(3nt;8d%VH{2)YFyL<}RG(n-UA^%G;;%8X;gL(Nw! z6d%T2HO_lK76bSvkM4l~!x#Tg!7Jq#gd-bm47R3+Je}{nc~o6;F06FKaPMWfQ`1p_ zLoRtp5%bP`N;}2BtdE3x8=jW@R5RX3u{MDWDycrElqz$>@Z~aiZLvmbXdp*hHF6cj z`tlDKoODh%RVer=*AFnGWcr9r2$?t&C- zuXFX`Ff2L4VS!n3akjppQN>_0xtmlBNmTuFxx?}L$McmqE(N^umg>XW#t$2dSoYx0 z7gkbFA%`K&^duT2d-3Q!re)@YioA|hTEig%g3Nn6;7<^*4kwhLwm1W zs@jtxz~^EoqN=?HZ!h3L{Z7mp0jk7AMU3a5ts?bMdvnFM*} zlLxVvuFV+Nf)M6$L!C=)X3Pr`<#N=q_ZI$J4{E(Y0GEe;;u=E=SU36V+>a$Mw4hqz5sY-WH|z$a?`42f-Om4A0WEj6@Wa1#73qYko` zBc07r&;hnTf`gXL>014T0a(_=oOnU?O7CUcX3xu)2W1l{&(1dgU~CLq+pWyMvYWb= zb`&ofxacwl(I-!j@aCDHMon{tOHTwJVoif~mA(%onw{T?o{F1J85$V*Jy-5R>d;kL zGS0AhFB#1pvyUrB@>ni!4h>lsHk(oj>=Pt`vOMw0cp7S`PnGroqcdG1A$(G!75w** zD+AmLUe{-jSilco?3|`RTQ44e;2WjBc_Bwr`X!Fm=~vOByUwA) zhtBDzk9#}#j|QS!pNzae4H*A4ze#FMAU`%e%**9RPa}IrFPm^od^;O|jE2vH1lpjT z12VX%a~dnMz8i;raF^A5m=i2_vcIGac#c1bZ;~VVWB>qz*RmEhZzw`uB_7Sx3qqf# ztPJB3kQB#vP6nawyndJvB*;WU0L~ABf1-r&iD~cMxZ#<8-E|$m4LApVF1iZ&uXrf9 z&6un8>fU^Cfp-`{1#~Daj0~^g>fK@|7+&Dqq~o&ac?s$v?UkQd^>(->(l1o%kv}Ws zHt1h%kNcZKL1Lie+x*L2`y2Dn#kt5c@8)rZSloxWUy^MIPn*uVUM9F9$>09csL$ zLG)|zq%Sq`Uiy1z9$tb{H}~~bRxYY3oPNKb=zVqcT{N|)N!tieeJj53;2`AGiO2Q) zWP!kOqC5S}KCHbkWIh4Q6Au&7hw&vemOUyl7GW?|cj2RmD85f%X_D`(X7lR-zQKY> zrvGf%>HPPXKakiKs?>wq)o)AnnIM$qZOxUUG;yvX#SZojs$VoFiFC7KsjJPM)G#=Z zM!7FYRJ?x@mTC0$d`0}uD}9Ut^O!=x)us6TE8_68l(-WGk1066l=5TWvDB@{uU*rW z-Mi+G8iX9wK7mLG6Xkxz-A&X8dz0u=Gzfn6`CRt!*9IGp4jw}z@?I)+J!Q^k-U8IO3r^bpJJ=>mI>X)IQxF+QS+8nze**Q`KGCECLFI?tX1D;!EC`-w3Jz89}qnwbpM=GQ{+@y9j z#D_aA;&(LMH_C_gPdfe*rOHPMF|#7RrJF6%^YO%j_Ddj((KD~PgAqZgZ? z)4WkkaUkvm;Hm&T|ctVJD!qh zV`!d)+;DpSGt48gs*3&v3W#)j)YHF0vM9C8FtnCL`gEKIM<@1lVDuT={sj7OG+eEI z$Ds?38;&fyH`4EV8c@>VErPNwV9M8LhnKQ+Oi%ipg|x4`m2I?8!Ru+xfX}Bi)n3uf zyR+pSrVa7HA2tjAFYV|rp_Wgh0?AC30EwXKYnT~!)wMAC9XIyQ6WE1{=h#Rt<)YSF zc4xm&eXylySbRI6zC|kcmR8Bt#uK&)P&~~Hv-oE;Z9?s`VTut+LtVioT=UnvVOy-2Wn0KHp5v09L)OyBzB{PTF+<*a!60LEnv@9r1=eCsF`W~$%M{m`+kIUCBJ7m#a$ zaV}VfVtE`~XgjgjUo?+(gWD%P%DTk1AYDo_e}M9^p$2JncdR@=ro!ibq%fOIkd%^! zaRcQ1`uJ@hCF5%%gE_v;w^yB~%(!`f=u-!Wlm({d)p28lheNoUfX2bD@!Hq#tX)_2 zIzY@TKU*)1UIQtr7a;pWpXuiI=xoNh%cyHJpHLJHLPryIme_oyCyDQIYpfwxKmsle z=t&%4B{sVUF|$suN*z)&$MPrnot8_IZ2P!uW0g>n0S5rhO!qKEdO2I1H`D(&=WlMT zJ6Nk7TU!z#o21cwAX&q3Mt11Apok|G$d@nOjWl-kKiZe=OKCm-?@b`kx(aiaykLJz ztKn__vzQ4a39>bK@d^D=@3}U2sD)Hz?78|$G*^fPt2INw$yggn15IlsF@(X=0knXz zJA;`KsWei8OB~tm?FPL`z0biR*$~h7!b9LTkh2f_Z7IH96*n`-K`0wyxcC;2>sI+A zsoX2Cz~J1S^!tE0k_*jPyegW?KXmuA9l>tzn~l}N(0htU4$1UOXL=GOWmdnXumU=cuvWC> zg(>SiQ6FvR4mGxO586ZMi40Jaj3Uj6$sK!U&fTsZtu6=!Bmt4?_(LDhKlJz_XQ7O9 zrQ3ddws3{*kus_d-Z|Om(;zF!Pdt;nAWOSETt;Sf1{-r;PWdX<$B4ADo>dfjey;%M zeh3|m^#jJMD$G>GDsj4!$=2Pa+Q8x=7@t!J-7i_*Up%XBsvS$~_ualV+9GSB~ z5sL^d#5TjrGg&r{^cUwYXXdwoIeBc|y?YXTx(6*YJZV`UNoV8<{C!E4?~vxAT=C8+ zNUr;&Yt>Am8af~P|I1~`1PzeG1)1W}7VwET2MNC)*_!$A>k`yDl=Qzh3J8UPx-rqKBA5kmAQDl!Hd;U;dds;0OQQ-CBy5 zi}>L?Dz!XqkN;FYh;4AC227O!KLFor4ZMwARRtPk>kKGxRR2Yv?ZP-nOoclYp|@XX z)thh>Dt*K2!m~X6^->+$saE3til1AKnO2%}iR%LR%x0J}z0fTJ82puCf~|3$!pJl* z}j764H7H=@bqY=K~rqyGIaM@{Er66nkd%-gBcZn(R z=xp9q|G4y)!75KYsMp)4O+;x`V3rrO#udppGi>cle6y&sPyi&Nl!KL#dHexc1K8lA zWjzy2F^*B&FIuTc=vxa_>V2V0&ZT5BZl0W|&r`;AQ$$e3{jZKq`(l0j?$37q?4gau z=pJhyf!jQ;WLWk&BG};YK;d}B-HnIo#EdqnPklo^bvP8G=+Im_NGjH9F*?}v6#eV* zUm@lZ+LaGUt~FM*-T+i96>VXWvK!{~#+r{I#0d~r>D@v0EFaZHqhyBM%SyQJJOMjt zckpee4fQE6!<3z`WfB*eyMM})RvLuidPhX>I!Tq1VPB9ieZuk)MBWMee~g#KM=Lrq zmq0!pZ;;M?`nw{Y^IcmWqK+<;LWter7(Yoq zcIYvfQDDOEsBOX+qR}l}6_~l(RjlQa%b`2x*a?9E^Mo`>jaRj2a(wxUjZmW?9nV|5 zMSrTCS`=@SECDTtqQeKra^Y!VR^VRQoScmzCcye0GT~b`o|&Edqhc;MnxmH2ggC~m zqS9F#A~oFge$)QC*e+8>@8VOp_~=kc4MVFTzg~0?eKL9kk&ZU{e7=$6n=YO@Xg6mk znmUkqt(Off1d2h#@Tu8#EkEQ;qRT8CRUR?CpBqRy4r(;r=i!~Qtud>chi?x8LW|VE;~f@dFBVDDmN4@(X@N4 zY{CyoBPD#h5nS5ox7<%4KOHF@#qfBzneqHi%t&dwBSGtW$_C8ahTBl|PG8S_FF1O~gT>juD)5u; z4L*iD6k;hddPayu<~IINs>|B`!X;df-|6G@Eb)a)nTr=& zf>}w3-PqJ3O-TB_LucM;2rWYEgSD=vkce?R>A%<|)x-Y<_zM<-)MiaoBtslPupFiag)QvH?1!m@;Kku zkN&Xu{^#Q#RSJBDp zIlJ^exnno*<1H!aNxOP_plZ&g%5WJ=Jc_k|>$(1$P- zonf}ba(aBNf^szK-Ko7TUxUOR4>YenwIj8jq(-TaH9|qve7IMhpx2EpzUA5|&<)fF zuDpuvW7*`C3LX43s&0!87N4!PRf0)>qRXF1lLxg*@>awA=q=>_EUo0+%gofmR!lwf z(*c?llH)dAE3pjU)1l3*#`Fg0YgNJr@ zo@d!y$+oAn`*9;7&k$qB6}0};+9Jj;nLAU>ZXKw2ejk9KUrvMoA))Y)BwXmA8Ws;8 zad#Y`q_*8ZdcyUlq2Pvp^?HR~R-#s@ZiNGKxbP4aBx{?v`$ksl3co}Axd!u#5CNqU zn^hDh&0=-K9Bi$Ba}l~Oz_s4wB-7#7T{R$lNF$ewoIn!r=pB*m-JfkK4>yFz+-7Z) zayo7S{_5ib);wN)wB12sfAmw)6Ixg0Z*D(6+LL8Esttq|4h*>J;zN=%AjC##o z)5oCAVFCB9uB1nZv7oZ+L1W12;E1Q)!Gd45z)yO|8(*b4fxC@a%@Vnakfl^q6Nl9X)bwSPt1KY@}jd@C4KJbxr|3$p9m3u z-t1rZ* zRZqDxoM6C%kxaDy&aH?6=ut5@mIv3gtgLnL@pcNGzY7~rfg_(3JK*Q!>=7cGtpH0Z zxC&3G`dJ24316Xk^BLL?5t z`!yW+Cu}bqU*(u(K%o0IQs8`pGyG>IOUPU#HrAD}d9tdOd2eG;SEObCYRPqT90QkI z_G3LUrqxJ)gw<&sq^NpO$&7{?3+2P`Lq&ytI0)x zEh}h31DyJUhc`^a8!vGC*ewK4gaRJtatf*8HNqwj-g)rohZN4867ZKEUyPK|7(qAH zxnU;Z7p!MH*MMo!{$olp&d~d~qN82w5$V`~i|Y1jkyFNbN6ti(q;&8Fad z{fpidZaz)hsz=$m7PusDSK~&$7FqSW-AMl?i&}|irPx-6H>^M&RLL98SnMTHBiRt$ zz`H9iON@3Vyk}qk1oakp6=fizfG3-_6=lU!`+%m%5VO0J`Vk#sM_}rU{@mv;EXf&~ z>WJc%?ARZ9cW4iF_tfbx)8pXwvn;cIJ+OBZHB@-!a4!x$Z8{pdFYI}xXF11S%IlS0 z4)V5$}foe1$$%X%PFuO05SxB-r z7o}@tqpwf4alP~FX7aFMD`F2K;rlLbN9FTm?gM84%MXh>%;3n~SoEWoG0fK>B)x1a z^6M=~{AD?p-?VsEJACmZSt*RakGk9IkcWIS)jx#@(mMyn8)e4{?Ec>6n9c&e@AJhr zU#;lT{PqQ26`5UJ{2}AeJ&N^CO@S?*h34H4^EXKQPI`1A@kddp&0pDM{6hb-!1h(L zMgpYulRvMSIgA|RU*4?g!uB0}QvE6!n|Wt$BtyNj^&g?i9I6nRhV3BcI3z?{Acv>&v3syLGXdBny z!WJ2X4SjxNK=*vns>D_m-B6TQw`Hhts<6W|i_g3n1iZQU()9PhD;4J#Y#N55IQDr< z#PiRYs30!j73%YNmNzykc!O$%!x9w`do0bHxjuLysg^RJ5K0Z@gv~7!Pr0;WyvZ`ZX9gDTAUv)R!3`hnOigRI~CbE*ONCwRqU~TpLN6Q!X$(iTjJL=8V=H)*(-I0f&ja~3K8-% zb>G(STP$Tw7MGp}`tBzW=06}s0Od#W&#>2BTTbt|ux{SjO0h3|x?(%Sn@66z-y%x- zrp#|q=5+abxxReBUyqt*ndDZX9K#Hsr#FmuX5A@cm8AoL9KPT#NoWwmr@s=SEm;6a zeFy-SQ{(z$T~;vw&DBmIsD=Y(VaGLLNc#i1dnteAks6hkXML|Twjd-q*XcF9am=2? zJ2_ZU!Fb@5FJN8W-kQ;-^{5375TR9*=a_t(5XhJQQ~sQAgOTlB{2 z%8Z+N;r+U8Lw?Dhp*;Lg6~AI}oJj>aT;yzIXI6n!+rHrzRPcNc z?{z$!D}(H6zKBfU&`8)$M!R?U47$?VW}x{=K56Tl_f>ts!_>`Oa62Pk0BCwQOwe=; z@sg^;fnRe6b$CDU%@-sh{JeIb3(>yyb&0jRH@#>qv3@NGKUfA7GnKr>-$&4$v=t*l z_|YEkaoI_%9Zadr^H;}uFbvJ#<9v|1QEOEzcOPK?2E)~;9jT`0#K=lK>g2j1Gaz0S zBSI5Dz=+6o`CCr~kxJBl-2t_>=kKTed5Y71Vk(1dB#1w_K6`qM9nwDuCzQr7i^^p; zx5szsb&>Rv@l4l)e-laGj9EYSaB9#vpSUe>nwaV}b_9%KHqA$f3}3go{g43Q3S}36 z*x>{c^zyYybZuIn291-F5qr_$;CuAZvFnd#KPU7R_@sKA%R&F=Ip|RR(*Wkh;chG9 zF^_9_A8#m{O>S3#P0d{~JJ(d}81^mmeEQFY{Yty&O{)5NK?QX0g=dp9A4kHzzW2(e zmEA~zgZtt(y4B?%UVL?MZwtE`GLK>$Ft^=Ta$kz5n0}~1)FD-t_{EbJT2m1ON}0N_ z?8}R<+oOd7OYw#qU^}%CyxVD|BnquYkMhk`=$MH{}0(xv=4fBbm*;1v1 z5hgR1LYuo9y$lCjh&Csy>iqQ?y&bHuw{hE>L7wHtE4TjDUB!kBz{1;ooHiTnkl9MsM@)e&AB?c4L+-KjwId#L}%QOZNNZOU2h-%i^runI2<| zxWp>`J0~v0W2V;XO?gDK)T9yzMfBxPfjAd+L`!0<0n4BF##^p~q6#fz9#cxIjj}%$ zFp!XL(XxLf$rMTI=pXvy4?s}Qc|Ev5GsD*Qu1Uo$m8&{J6G0u~Y{$I7LJ=PIYDhhi zC~@tF?}`JU$8`PTF}Cb#VwZ4~M#-If!jDuGq2WO|QZQy4>FsNBL>e*CoMha{kEf>q8Y zNY@`sXQ~pDVt-_uG_a2FYIhnXp&}e?lbR^oDj=q!?1SJD=_ZO>xJ>TAe>+W z+)#e3thdvv|GZ5`TC^#0=Mc6PdI{+RiLAYyz6?ZYYR_Il! zlO1d3;#Y;92Bab?79F1-IBfWF!g5%_>)gv5h3*!14>CufC@HmkFE*woVtA+FMd5_+ zea({M>MYYy3zc3Azhq~(0Qsj+FRosKCuGiGe;D&IB7C=t8Ji9nq||4m)RV9&^$nd+ z{S3XNcvolj%UztgTqMYYriQX4airy?t{-vj$uoq9B(R&s;%O`Nsxf}sM=&HblsThp z%h2GI%VJNVcmbnyqs{j3CZ%_$-vyf7@+QuDr1z)Rhv^A>B2(P%vCt0niryWMY$t{T z9m=U<%Q$V7+T(>-P46e89_y$+X<5YMwn;-X)O7q#E$CQWW;wCfvn*-F+D$1-=(*ecsH+6%JX#!NudtUSn z_MBg3YKcGPo0g@jgH?90x+eCU%#hc?D(rnEaW3HOKC4`=u@JMn7rUtlV6X&hE2dO3x z*$Oq@PaV=GuoVw<)N>Kdw3abcWUzTcX>%69O!s_Narx-9qv+E9a$RI>wT=Jfhr53N zC~!I!xs`*}In*M6!Mk#ZXKiG8C$gg<|kK^G+5uSL6Y4D0EYDJh4+_Cs-Di zf&s>&pFy{w9J-!W!2PhV`k3}?Jesf_q~#hTaCY!+&`NW?VA{O+!L#Y^o=Gdhv5mjP zmj=$1g~a(+=$y?j-zxU*JNIGJY&d7!u({Y?lw|ES;#;cCV{9XL(0$__(!W!r)U1v1 zbT-d!803k5u#su+Az*aKT7I+Pd2cyiP5hNL`^y1a_m!2_i?OGaaP5`65v~6kpI#XX zq+E|Lv~e|Q6x zpa1-MVYL}GM0M{E4X@D-$4PQnY7$8#abJw<`tSMYpPOI^MKOg^!BVdY<~z=P;P&CX zknhXWY43*Quya{)M_x5S{ROilX6C+>|LmKIAjxqQHBgDpKwK0kdg-iX_i;1jp~0IR zqU=6FN%QeAe<=+jj`$w1$)i6X2I?3fAFnrh?Igyog)+9{LsO>E6CAMQzmr5_vDfNe zJq@kx$VI6K`4e>C`+ni{1`ZsjT9Wu29n-$ieD2aUKMu~PaoD+q<(S6q(?`_7692Xd z+t3{itk=a~T1pUTd%N!6n>l%t+4r>c8nj#ogD+*6Ja8}+fvg$|Zi?ppTEm@iC2O;Y zV;RFGNRZ&={$HG1|VS0k_9AQY5r&?;+DH!^^0ZL<|1 zt%zg>I0B0ZJD=%84%1jS+rNF^0{s_}XYQ-lsJ6kPTXkc3ti{LpfnBqPiX0b%4E<-G zs0~u%Ny^fyBSVfILuzHOXKR1ClE6UgxiZH0sIb#H;2=UBgKI%Ml`~;qMMW)ad=1+r zR?eN5Im?4iwaEI|4b9ykX^RBSX%2=1Jo#4z$mp{#OFZuu$fjR1r}cW z`>JKMZ-=5oYc1CkDt(OjHzjxMU=7aU&gTvsyU^6^HX582Fo5f3h`rZW0Ebn;B*W!P z%!hLo@mjtBPZ!?#C1Q2`>&5&BX|5gt;{J~b(SNQ&G_DJpyqz%qBGwWUqHeg7U7FkG zRquR%k84hZKbvu#2oBgI-BnL1rtsg+na)+m!~O%zYgx~g1KZ<7PA`I|r9JvNks7g= z_Bcz>rq3Y^dh%gWHp7BxwX*DEGYc4TfzjJq{=ToG!WuzlNso?tk&VP1MFU{N4{%gO z>5lc?rWiXu>CmhM*u4VL=6$k(`Ie*xEseXLp=_Us%z{qE?^O~1mP42)CpfY}_J?*J zUkM#nc_hZOIpx^=WP_VDMDrOisrG)Yd7|3A3fl?p`ad{gJ-LRV8=d3x4(?N1l8)Y3 z7C#P1$pvOH`l5t!o&A&MM3enMNFv0C_!d9X%b+~?hrlWoK!wl`4f@C3iLS>xFB3bD zdCxM+cpspj?bSNbf%ANWg$hUO)qT?Hp^b>!RF_C|5WSxFo8V|EMruEv1 zLKO|?Nj*&p!KypQ*uAgEkv<5cRm3HoV#w(C&Kze#p~`;Hl|0a3H_}x!e6UUZvlvsm zNm$JpXmZ@h>&H2c}n;LlGNL&c2P$7q)~x$k1KeRx61ZTsCcZ-b7` zL~|QsB4lDTT;)^TD*ozHZfj{UN3p9?bFexoj2iWeMe!3c&Y)D>DaL|W|HW;D=pLkz zn}Vk6w4jigwFDji_p}#A7&~>xa?xR71Nm{(EN`m_*V=5611VT$)nCdhO=6y_5&XAu zPA4=o^n@z7qbuXReeY@+mJ6Fz=pF3if?0rRk_BjQ@vuA7Ryhk^S<+TELB?S5*2rWn z!|j5=Y898q>>>`t4L1usAo^%wX!j6p!{&*b?s@U5h$ml)BBRs?j+TVpPZ~E!5@(@J zg@6oS9Nz6X>ni@rD|o zig3TRd=0VdqHB-YeMmB|R^D^x|7!urJ~uh&Me|WObwUoDgDlL<)Rk@NBBR-}<7O1f+5u4R|qcYp77{lD(>VdtD@&YYQh?t7l#q+EXXYK9#lrM^tF-7C%w|oC;$90 zl*Z%D{BYcv%94!<*%bn+61)0bB5l7(e2xNiUGV|3Jy2od#7dmkMSB)=cLND!4A^F~ z5H`P+wSNJyTWibe&a!dAKqr)eNLu$fA-1xlE}&w$(1A|0D53 zzcm=o_m;Z$Y917cuJ8a%y85U-S9`JjG09nf#G#nn8$aXN{P13N*@bt zekMuO!Rz9@cj1U+VbYZb3Tq;8lpQWb-a+4~T|wZSF9f%txfDDe$*Duumr=9&3mJ>e z6rE9nDnLOG3ak6JxcDPd!X^4&mnY)6&U-=5_Uu|LW1xB?jY*AC=DsLl(t1^iy-0;co26>{`Hr zG(dew!6mXwt%&(BLyE9KNSMJ&c#4r&NidGi8p7r*H*MRxTAKK#4wE_3#w{o#C4l3@)9%>GQW&4n z2Z0|~0yk1h^v37iIiB9kPO)J?-;>OnrL(zI0x{u@5h->Rt30eTl^a2_Q2QLv7xja@ zm4xV&e2s_OqoI|h8)LR8*p{_@B8ZRHnrI%bFHs=Rsf9j>vM?k|+&{orbunCn|9*<% zidi&q7u5@5uI~QjaYD@ej_j;esW>B74D6chI^BdXa0NIwMN;w}UQ zehat{@uFQ6@{brTr~EF1T@aPQnD8Nsl%kT)gF^2@43o1`g<=RWDT)FPoCJAt?d~QR z=HcQQk&r!_j+f~r<-RQ7h*sSP8bV!b*bc6m?2L{ltjUrAK5{M#(IsEg>wlp-#g@qj zFMnxXPd@hzM7&g*XEE^Y&T?J?ok`KbGlajaq{rn7H_UO#n&h~(Kp_qtxz`Kdc@%muz~RR43qBj% zdQ@J>nAS^7T-nnHR(nvGDk@@Qi`W1dH=MLfoFJ$p9+7Inq(s9ai{G_Fiw;yl zbIC}MVReH3tp=8$v^x9LJC9^843CqTbuRnpMy!*v1cS1^Vd!wq8uwfgLTrAFcYbEf zfCi@MPywyI5;IYiwvI6Vj8-6XXjPE-_9^4~F2P{GsRp*f4wt<*JvW%+GAMWB=HF8` z-_7meGF{cjaQg4}9!zzNp;CwURgrfQP&Nci>|V7VY@687@8IWaC%2J6UH3YC?f zHQGDklntkz%m(Zi_B|#c%xZ2w;Yf6tY_@=R?uJ!{nNV1tMu2aX{nY-UJvg^3o1_#q~}}ZA$sK;li4T*+QF$pIn00D+39xPph@8C~#r# zLD@?SwL7+gImPWqO>zJXN9)U{IStB7!$hB1g%2XsfMrIk%*)CVyuSCK5D|J~k9;lN zydxJ(^Vso$!z)<*Y;wvYva=@%O|0Pty*lhEkDWsCX1MB*E{Z`z@7!sD?7!+E_9%Nvle8)yd z(MbC`v0Gs8)JXxqts$&}q9d3q9um7}88B(&$oJGkfbh(np1Vt5vdr>5TS|7GiGRz3 z4zJ1L|L@QM#u!lz#F7=69Qz6hAOuoC{)#;nN7;&5X}9#w4k?PE^=gNGBAeCbj=8mF zqqcr^!4_g*A)e05J1vc;9%WI)J@to&FiLxn=LS&!1#XUYb`Wp}fMjfY#Y3X@;I~Uf zGeu8FdI=}S9C$_DXjp#lSjU=aK;GY|r>4Zvz&o#)PO^f{?y|n9BM_e3xG(3hC{+5v zmT5SeL#xVaFbb=jd)O_1N_5ns*k6|ydj*!~>Zl4+aV}Y!l=_;lcYbUzFFlNCQT0Y7 zc}|)@9js{b-L8tPt8M2>X|EuNy1?+~yctMGI8Yj^xTWQ^XwW?M{Pht3D=f@qJ+xG1 zSTTtH{IkjSUVPLG#eeY%QDTZ`ccDYzPC3iy#tZLCAn)A!Dmz&B`C zTP)opq6vgzRBf+PL?5Szec#-S8iT2I#RnxcIzaE1#}F_Jh#Sz0wuRUkbn zM_9AI^Vv z1^KiIwlz?Nh~iN683QI09su&N8zTR;%X!t$cYhL`jL0@956>i+HrBABHr)5~`2U=? z-mrZ|nGLQ=+@Bi13wkTT>E<2V=;1}>xVuG|m@ZBQzbpYy3scr`vtpu0b;#I1S59A) zD2ptiKFnnd&ILwj7g@$H5j}8|#pAg=&mb|cCJk3>nCwO6j_g7A4OD$3?s;-PSKR*L)FmmC!7m40@7*;di zZB%YOdds48biM(y-TC0P;ZhDdV&^tq<;nl26HU;jyZ+)%SWkjb5hHLKS#sou?+=>J z%-dpy9P}o)YesUDQ6?d#HgtVk_Dog!xZcPMZDEk=x7uuV#%i3WL2G?)+IU-hh6v8Q zW!}*BHs6asVDqrC$E$&5h*8<*tA5M@J~-lbX@bL#NxOI?%8@f&H9;{-(| z#I&NW>SUa)CYB6e>iFC$H*v0CdfUU?-2S_e0C6KC2@%UWU^^C|G6~Z=GalB&d|Dx)ywpZ z2y2h|Zv@Kmq2;5+kHB-u=;)<59)m^n-=9q(k(vK6TxK;jVD4E!ht7e^`{er0QLdjN z5D=zwa_^(OR4l%ZsylsUmS0s+Li~SmWKG{W-RTBN23{35yM7yk zVlM#)i0;M!XZThZJ=QyB`wU_Hg(1Nf#;W(ckznrWoIz!g$rc5+q9XTH(Vm2wh@6N! zOGaG`j8EM`+C9R6Gp+WRF;xVI1&6guqHy14j#=BbE!f+0%Fz3(1lD@mMsg~aSw{eJ znK#qUOwk5&7M>%vG2EVI&L)?E%nfl&6?I8JGvuhr=1;5r2V#NWH9$pOXcuJ}Rg{$K5Z?pF6H&yFk(mk%IN8DT$n?_^x`$%ZqlkCM5g8_J20q$KR_=Y8$ode zX#0~~Rk5T&$Rk0oD2Y52zPbW2>o2&BvK0GG%@7V2j90TF|H#yHAEWnThY3|;ka-qi z_&(WB%wgF#&|i`)>IN)WNJ*^4Hpq2r*&ksy-a9YNRbX&1^9~?yva?d@{R@mA(b(T$ zM_IL>X8Bq)aJcX_mR(<3IlMyhGU`i1{uw3(;mHGB0x7;Qw~#4wL(d$TdxSg=swK!A?2sLo(yEhF9wmYnD$~GI7=~7elh_IMwHh zc#~mX)&u`&8L^WzVI1?rM)TQsnu8YgX)(I_U+;siuregj@B=IgY0Batz$k+UO^OIk zB1MYY1Szv@jr%M-@2f=kC}n)oc7V;xlUGkFiyw5V7$hs-i=%nr&^T~7iN zhy>+qdSvm=Qm|f%E;ib?tk3-^yVi}?n$1BA%GB1-Rh6OZ{jp5n=3!jqzcN_mgsmrj zK(7x<-TYEb*SS6}wNY8|qg7}^%pI2Pg4QiYKu*e_am#e40OMkN)EL!sTLle#orA>@E{ z$;I@sjVb;Q$**|cN_Q2HSJzQj4)m%CK{PH?j!ugVN%pdd^=GONabEv~rz9#kSiU-| zdcR9R@KTMoB^$x_?!bh- zw_2LeE`R?+>llfP=C>Q4W_JPgGLr_itO_u8lPo~}Eu}&g-bDL*w)vtPYYDm9uc#nN zROVt?b_B<@I{k&tuMtVaK7j%gH6=*aMpSTWa{qiB=z=kZ7uMC~Mz_HHz=VF`IZ=v5 zS7BBjA*@QCU+4@<&b?oe-!t$ESNMc3fYw6J7HZM}-R#^d12l2Symw)d-mSuIlhVg7 zy_O0ia|*TCgu{R#Q9#R8rv`9y>u=3@z9u8~Boy&$eVTQBZgh$@aqQ=JCFPk0d+p3B z0eCA_+n}ngtuEPf%%B273zcomoC2Gb%DLXI674vJx?Uq0^(NnKKE5G^Vwqfbla%8LD;sdvdQ`x!5irEiuUiZ)wV3DmoYE=j@@kADVu+tAV zl$(VwKpjOD?*?P{GVC#$EJ89)k5RMONS~}Hs})*^x&h}!`otT~Be}b4-3~&E^#j0m z4hnX=ku3aM{Wxp!%%V><3gyQ*8SJHvO|H-TR(V)m$m?y+o?c7CHgv@A!XR!THaunj ztjY5_FWCYj*LR? z;`3fcfeOllc55TYg`~Ps8!_;Eqgr)CAefAn%w}B@C;y^l)JIP?GGQiKAN6m!C4s1 zLc+h)r1r1Nq|g1N7Ek1>-1i10NW>#H(wrRF=4grKQW{TD-lM&7%PQA3$=ANdTLq5^DO0i{)J1+;ymFYOFQEGhn+n;bdnQY zQBH~SE@p55Wr$3z`@N5MTthvI$=z}cxY27770=57hmyD?7e82*4o?16pJ0utkmzJi zum)gIw4Y0PA-TK6JMOm}?;^WyraE(jGAVsXEdIl3oVM~X6ApQ=^R@+T*#>wqGRz%? z{;omOdw^#|^)SG-@!FdC;XC+5MsP78Wy4c%C7bgp-Wcn5q2+LD`>f9)h}KqpyRE$ty@k!uPDGdwsDJ@ z?crc6AurB*)r~*rwPZ-NDp1-{^yp^!$s{557gs~OCK zRxJ0_BMWspuuwU`;8$a|2Xc3bOR)=7AP)ZuQ{_9P6dDtFl=i(Nom3ks;!^EQ9fiZ3 z@2`eZMadVRndrcqtAjz`ae(AO3Nr5hsR}b)B(QRgW7TfkgputM$IQJy$C=p)ZGR@M z#D5qri?@bsYe5SBPHSfMty@r--LI+D=hefjZS!pU#;SUur|kDS2NY4e+R68ZV|l94 zEQhfWdjoLsK*>3&dWUoF!PdLhDJ`8vF5a0xYn=YVwWPhLQeSg#I%U;T=7@0Da&`gT zO5CbyM@@<~Y-n4Wtw}Q%5*M0eP)M#Wr9uLSaVTBfur1khOajSNF>d+F-L*I;3v&~$ zf6zEV`rFF@3)&AnP3hwUP3tPy{ge0!;UiI|bHLJJf+%wwdKiQ=2m?3Eea*%WlYwnC zRqo%fEy*vYa-g}=2Pp+uO#~ORm;!bbJxf{?7gKIR>{Nw^LedX=-|BIR)CYE z6tu=;HWL53LIHzvWn%5DdHa|I2^4(d9?R?U@2{#M8O44^V$j>jE{XSKCC`KwHf?vUbG$ zFsoKgUF4^2^yfVf*(wYEi@ZxgrX_2eyVj`o_G-fU%ND2lqe9P0_B%0O;i2I2-yp)K z)d#4WJjWNPXY?UTIk)T>f3~6QZ^V%wqhW2j%5Q$QeHQc7KooNV##jg~aKiVW03`qMQtNdYFi%3L-+dMCr)8xqDts(1?lS6EN{VgP;0a z`f3#Yf?uu~W73gW4oR%9NqF4@iEg$;KDdsxk~I-0pD!mVpY(D)FstVx2JM<;fIx3% z$5jk(1O+$C+-thQ{~ZuFSCBng59*4?G~Z<}=7tvh{+law=a#BXl38a9xyH+fGY_aB z;_P|>x>;0zduAp*%S*K57Y(Yq59|x~4g5|A@O1e5dIMG;p;!+dc5m`wvM(rqz-Y`Q z^7XSy{nLs@8CW4ig(IN!F-ra0EgUQYHkDH2$&L=oFWZ>_Ua-CnEtw1}Lksevi zTDL;p6jw=qNYvX5Qsbg1d?O)|n6{Y2J+0dH#PGIO2wSF%$9g#Ctn>G4_N&OWnF1$` z09P(Ao*&-Xof3xcyOudcD}=^HPLf`(h707HM-2RTge+5$$Ao$Az%^aGd{rQGxDVAP zOd?VGv0OL8lkH=lM=e6qh}{?uOe0W|c|!YB#TUPO#=(6>NJoxss>awt`6X9ekjSQu z*R(5vDLs0W5nGqLdqzV3qH&C%&$|G_s?+vrF$+M-(;}OwH`E?uWCRD`%~0zmw(~&d z>3o^BK0et=P_1yfx_+7<#8O>ZmM?qyQ0d_7Psy})+<$i4^F3tB!;a*i^hz33Q)5)U z>vXQ6b8ISyvg7E7 z`{NPC=C}GKM6i~Xir(!?X!gy&Lx;k-#Xrhqp~KpLn5YId>wqGD4EE*Me_!<0qT)`!=?x;F7{V6AJ93CWRcSqEt`hv3RZ3c&pXUwNSETh^VA5E8BKq zT;$XboUCSxHNFkTI@*mSd1n&zYfkj42OFaD^ogQ=L9n3A+LvrHw5bGl$T-bs-u5-a zR;@4XV*ld;z}(uHnEB9h&og>&nz8-yA)K9nKa}2Y-P7a^El_g~y&zlIdUPZ=$CJ!M z24HTHas-<@Ky#>lc1fJw=dl~Yo8a!Y^roCOI8L7|v@A!+`0Yic@_1)MdyyHUZMN!G z|21{zpknEqZNE;|s{ylGpZa(CpGHm96bXJ$8_BpC(eGzpa~TssDo#{)P>q8ZBfG#$ zg-_h4T1=mRD7_p0G9-33!#28lS$szR2Ivt68){y)-{aJomi7y)mY1A;ag%K!8Z5}} zTOm&t0)(ci5xy3PJA=8fUXej!N&F&N$nirH$W$g<51Q>rAKUK@n%+G9ry6wg1c$M? zb-t9%JyYpD)k7;)Ygx|ME~luD)n*Uwkfb(mreMuux~teZRRkOU5A!?-0N+VdZB$Nf zv82Lfq}vAjY64Ynlape zsya_?n*%WKEKkyMS!#z49ozYXlIhFD1hwRJT!4C(h(-gc2n22{eYk*KOH#C|6gL$P#bjlJT5 zYx<7w=tBSqkXJ*zGrOrXTD+cBY1)dmMwELI9L&uu470R;8|_oo9NdJPdMQuJfVh$~ z6I;Gcs5lHBSwF!ws2av}8RxyQwe#lorCDE|K9UNiFhRR}TIcJ*zY<5-=w1?A6zf23co9f?5ALP{UW60^59!+OSw z5xp5>CoOZavpl7h#>d8I1%`DpQy;hdB`_&2qka#K_Pl5lK| z-5$(Lw)0`I&01-_mzxxK@hD96Paak-a@u@0ulx`w27MtWIQ4-cA`8X$yz9 z;zSyF%yvuZ+7E`at)Tr~H5YZs>Hx-9QiamW(|)q50d1(Cdl{CEMV2agsV?%$<}w@F z?ubZ6qtq<}dKw+U9%{t$Bh=8n;e+x^zPqnUxZPPdSVeceAU3NxpdGeFXPDLHX3_pm znL@pN5!$aw41HeOIrXAbjXDziC^~De)Jh~0%#qSAQE-jXb2~5tXpieIrW*^l#+O*X z8~6m05**x)!!qA5v&dW&LzVLa?!WC#4>&3gV3)!jm(M@_wkpZo5I~zK((E=vReK9h z*U#mhE4&dg`7*cBVP!>UNsQh*`EnybyhIDUjDhQi7FZxr%Ur<6HO$t-*K)# z`p{jPWn=bz-YtXfaZB+Ws?KAu>d>e8O6O)3xSXpeQA6g?+u6a(h!U^xA?V{KPl^pW z_LZLuJZgFQj5gt1JL;;oD_pXe-G+(gc;V5bEfX{|h;sDDqf|nr|7J|iLnz)qgwpx< z?Rt)EHS|YJ;(*cDSjKR5ZeGA3e$HOYjr7IAHl^6I`=kEd_O-V2Jz3wtwKaYA5U?jD zUr5UL`>fe3Imu3>P_Q8AbkSnX#eWWbBURhN3_0;@!w;v&9%2MM`Efy}$Sy@7B0`xr zKX!660E)CSRaKTC3msQ{FK%UZ;@${e$Y$HDN{_a1ND08nNNj(pro4%H9;YdmCvg5}u+Oz4bkL{B<&!3;lH@{a7 zAz2Kyr>;sYVKMvWJ(Onct5?!052Rk20oa;(+?WsWAhsA8 zo@`_(7*^)>XScv4JO<5evoPr8hVFp!e=M4B+Y5iRu2d`w+yE(G^3hwOyodxQBND3i z)3xnNB{bb={4eh6Hha#VD}x(Ny$Bt~{DtP1)Oh$eKvL z?WpZzcd@()7KYy*21q#HBR+PV^ok^x`z~+&W zTD|#|{HUe=H&I!4{+sM@Ii!)C`9JtD|HblG?|R~d=FqcoHLINx7gDCvJsa)Xe_QZ; z>J`kiCAq5uq6_o*cl;Yz3H)70?aqWQ33?GZFnEgD2)AVE&BS_K2!CJ6o}BZUCGmob zC?dGVN!B2!RZq}@X(2ZykW!z=&ye1*R+7_uRvSh!KbUMoxY-Fm37O25hOKY-3`Qc> zs8Al=YImh4QuIK+#YxmY2uLnq94Fp0mlDSBqBiF&3B2uW%jO@kP3KJhc!MqXXKidF zr_3A%#f-xc9m{Tc(JxM_c_w2n77*<@>C4^$@H^4JVSOx3P{q8+VIdJr`X&Uzqd911 zv&DCRyFoEOe0QB9hJV0-63wL4Z{N=o0Bjpx`GQl5A^5b0ze&8Zy_lf6iD$O>$7V#H%M z_mwLd%0c@M&O(P-f2%)SdP|5{Y(ro8fB?H(;26UXV^xxPWd5I;UP9cO@-f$S?(H5+!^)DtFtg1A_AS>;^x?E??GP zW_z4Uubo!Q!5qgjbOyWV*3qaHW7H;|XOzm7{eZF`jyoGmz}cj^k-`Q%_GfTl-@d}I zh;r|4Myd33OBpw1y(+rdOZ2`|QVeft-4H@?iGm<_8Qlgii@>oOC}1PlJJ-VveFe&V zxqf|pNSR28^GREscD-XR`%fpb&$u#BmkbbMykCL%_qJY5`Rd6kB%5H6_xaG;Zo4{e z%k6uA*}Vaqb1A-fbJwVHKq6}e5KMgscT-Iw2ZzH@b2T@9mj$;U76%fpbxxI3CnjgNCb`~={-~%C6t>EWuf+(T1 zqtM6H<}XLx+O6$>-O{B516W`}%;+*s|u|799cU50Wn zfD`EJS=Li)?uQ5StQRg9V3E8OwXDj+}g2PIaRX# zh`-!{g-GQ*q$Qr60`mqHjB3+aLd<|}I-)A^of*GrvCqo$d$jvstEWY6kxLCWpD{fg zvJ!Z?C{f(82DHR|0;P$UYoM&&C8>eG;ZNK(p9|@?+&s^2Kg#UuiBp?&J2}7a+u)Uw zg{!~{ho9QLACH0E(Ei&|23I7qp_@%FptA_endQyf&=)jNA#@tkjAG2{6R@)yX=lx^ z{LKeBpdaN$zl1a%bwcQP%|b0oA2+l#Rx-D=a*(}?EqGBU@qQ@;@1WXu-1y>L!86h4 zsis>o*+FsV_dB~eo>JWRqZ-qj-0|Gp*dN2Z)Fz9$E*gVeYI0~PuG;_6_?oJDe25BT z7cAkT7J2-L_6JKC^XDT&@Na^bK4c;piOmN_a`jh8z-b-k&$v-y*$Vh4;zCj*HZF>- zBJm>>*Mc0Uc`$ynjywWkUlbcN4XC=U7B9JZ053A3a$o{g907WF%SO4mtu^tPpGUrb zfWiVRpD6QnlxPofvV0e3UdrXyKw%29MC6M_t)abAS{YTlApEi-SbWpv0vmvB_Y%9~ zQr~t=C|xG+1HfVgC=5WM0>(=&8^u4WqJSc8kga~b{XQgW2s*NXo{@>Kgat#jo_`{^R#s|XKPm>@C!#DgyrFzNN+ zQRI|KO;q0Tf%DZKivNnAU%MRVmBkR4JxXHHtI*kITJ z*4kO}n!>NR zPp%}$-KYq}^x}sS1|a9{+uWG6;$QnVE%mYk(IrcD1;yfM`*hAs72sm(K57?yqQ*s_ zrvzlXunj2=@Vl?ljA(MU=T^NBPc9^hb=82fg7b zc`8d*PcD)CEBN|bd0PAne#yS}frBJ$&LaR4TVfq8qSX?j?Ko^!#0Nt;Cx&1cD0MW%*4=k+mX95vZ z&Q3pGFFqf?{guzURb1M${gq)=!~o06p(>2D@4TQ+akb6qZ_OnmSV+@L2o4AOeEeX} z+b;yh4!wj8o%~ahAxg%6*u@_lHrf_?g*{_NGf08wMG861-U__A8{Rb}niB(Gj4zDW z>f?)E0Owyo^r2OjF#zKui$_ddknvfi{~ngC`Og^}>C>(TP?%umi^{LW*R%});|zLu z1e5@!FaD8ECkM~FwfH)Xsnp8-wtezQFrcgbyy}N9aEh+^KksfGX)iWo-f4IHR5b>FLl>WL{;6vgV~;yiEQ(dh1cpnC!4$B5Q3!x z!EI3Ru$8Ob*72-SZ{n${=NGa!&~Bo=H5~^s5Vf5dHkg*b;G=b3@?eA0>LuD(@!#k(rR5Qk8y5|V-xggFr6FDlw$8S>;#K$%$IC;V zi4J1)ccZgThl!0?iLn_cZRCEQeRmHunuXPmrf+xawfwn??K$nP%;}MQ9Pj=;!`1VA z9EECo_Je>_ea0P~EPSN0h4tG_=eqUL@5d|%T(EQW`+9xCE*E2 zs&6MzhvaPg_Bd;ks0Z0LD%!6o)Ox>e6JZdibn->IPmgk4T(>dOQPNkVT_!vx zinic!2(HB!h&Ds>Pl>V5??OM41J)R;53!!Z`S~0o21UUxd?*_Q5a}O_plRc2m`-$N zr^N=ptO2R5%CQIF*{dQTSYpj`ljt3EFto z;r}A%XaNVw>k7?y#{upFi|@XZM+Zc1IX9B^1y$`u2hegHZ6{mF5>e?9fH$66=N;3K zoJ#H%2agD{xW4U_pKTD}0I*zAu9zgE2e(dY6Esf32*XU@(gNAYq&)>g!~7asQ~Eu7 z+ryX1i4#l@;*C89rNupnWs)(J`wc*@R@DLSYma70mpT_|&v%+mtg4JzchjJ{;k}32 zzW}X8Hup)EdokC3IbHIh0mFj!gz6UoE%l*4b@4aH4RaD&<4=gp?&}8_8Bi?=;MYq4 z%-t}FI{De8gkU?@fQ|G6)V_H>5h1t~Pi&`{&q5)B3tlEzl?V$Yc-=th|C#{& zEcO(4M@Umg`(%-3*hxzlpAa~1;p|>*8X|W-TU_WGzYxVqU^hY2*1|Hu8Y14<+QcM! zpxmbuY*2^FQh(# zWS9IElEqxKSd?)%Z4BHjOeqAMJxjOF-*4X8MH)vllqwWhaHOX zT)=c>FJvHH(MB6TO;4L}X1j3|r58KiPCXM%swF254VBzXVFX*DnQ%sQ=|In3~y z>a|ob*+HIZ_|1Qgphbzx!2pSoQCf;=S@#a-%MVhafA(y=7x(Aq#5FGFi=}O)0w;w> zT*B)I{-xzr;VmBOrPWmf*S4Q>7TIlo_&>ZcZN5nKa9=vt6ZtrO3;FGXN6eUAEo+fg zs@3oNS9wdmDXFRbG<_BC^0Zx?g&Uwe@g}P+t9{TcJDegO=PZ55cP6}dWu)ip?Z`xl zykMSNdB6~DiTC^n2LOnWW8I-_vZ7aqnS3j0A`xPlX?`Y$#X{}OwN{7Iv9&Gmr1CG%*q0+7 zh$7pjum9czNrI;{KCZ^8kBk3a!Chi!qyu7IA;Byhzv%Jzzz}}LE*I+qTySkt<1tS;N26GbiiQ0(7WqR z!gtSs8K z9J(dG?tA{c>N~?4&u?OZ`6GIGctg#{R zzfV$t{im_rL2FI2eztl;!M|RzxH5H4wgJ<%g;$Oc7HNZDCPx^^?TTo3S=vpqyg+UEYkFS*W_FtD-Y7P zZvl|ks&e;|Da%=f+Bdo?h!@Yk`;-J&+lQu_e)kl4l;_hy%aVg!NK^Q7b&DnDaw&(y z%LW0hX`V)+*Yt>4e4q4tO9mAF)MAj0(NU9+H2U$`P1BngLUUtn^x2760o-Vj-thWt z=;7oWzFJed+VRDV3NMc<31FU+^N%aoKTGq1`_ON;J|@jo`Cr5*^nb;i-oLe2M%Ws@ zQ4e*T;IMgNgX{>rC_!BE2N^6~LGPo#ip)!KFOc^Caq|{j=`HNVqG5hE9X0$-uey!^f+9m2fQGn=uoHJ+UwdW)sF%P}T=n-2^S;Mp9Y zQKJ5L%u#360DGtKB4dI{)Za!polMOM!LM35{wy(ivxi?n^Laz4edVIo{yb-`#>Y^J zs(fZye*Z3#_G6zpvaTY9=kuph!Ee$dTEx}XW4#BY6jmj9IRGz$(+X%d1pp?ZCUhGC zt_%>vV(#i}1%iHMdD?frPJD}QP*YodEv1)@^6f=32Ekx|v-{&k_CLGkNVhCPCR^4? z1SXn2K%T^qXZ!g+I$8QbH+&Rp6JPjmFC#`EutZvs;$0u+Lwfuunz#nsL|+8c?H9L| z5ASC@YC`Whni7Jx1Mlk+CaUz_%&d~6A^`?LEfnul{E!0v`Y#%-%7{0VC<1S(C&yD>;~W@(1PJDrcni zOFp|V$NI)rovoh@MfMdMuAjewvg68GrqpSyjslt!!H$X40BPXYuWW-M2;!5Ov=^vq z7p0}s%)<&fL*{vbSlTtocY*^LHDOe~G4MBH_-2YhCh;K)rg@7`S0< zRaSOYhgv3NiA37nzNb%NVx#KE3FjMVZ?A3S`S^c}Kf^4Z0XuXH(B>8y@bEcj6Y+vs zKf5&aKnUtX(^m%zg+eM z5-vqUE_xoWH83%0FPifglkajRGWy=s^H5l4jx>ngj%Pj2q-9x>2w$^w!NT|&+^1>I z$BDSc^#KPU*7O>EgvSlTvcvt)oQf@+(?Vjm9GtfZVh$6@WP2V-lR%> z5#;i`vCAwBuoMed**(}Ery)Rwc)a|J&TEmDKG}4h04)v`S&n_E_rsKlasEuwz=Ui- z6tO9R7bn<_iKGA4QAJRlQZyG~J zsl)75kEi2gKWM)Fj|*`0t52UkLApee@$&&5iSJvB#w}264|R_$6~fMy6@HnJ{TSUr z=M>pr(zO5<4V{i4Qy?^}AN#I^sa+oK*zcBnY&4V!ar|39Z`o3xO$0r1xmowa z0`i|S?!S&&f^pa0`gj%K{bKfCF}_+f@HCx&9`qpH&Lr<4`}B7oZVYPZ<_fkqNcW}( zeB+X>XpICtXShXn|8zczxY#$5dPq-|z726uY;&Gbx0^iN=k%A{}MMkOBM{g~&f%Rh5xXVTqx1^0*W%&e$xGHg;ct7gUfA z(P2i{X@H!SvTaj-1;OF~+?-Let%lL5Js`dgX&Q8pD&(eW!vuNxEs+Ma5`T@uI%IK$ zGwiu$y96Cec%9GlHc2Eh2j9u!`N_ZD=|h>vjF+meBp}_uSWIcxO8lPt7*RhcMt~Q{4ODr_J)E2ygwX z$>Y8kP6`PL^D+Zw^`}zVVU-!@TQ9KH-LReu-|4oxg?aAkxJ8po`>P9(a1bF1pAdo) z{fCl{T}O)1oj>;s97ZfnWSi20;-x#$+#3vt_L#;2TGCP+lAhzRXTI`~joX(xIUomK&7#C=Bp_$ypQYCn5}bn(*=G zguOIbZ7#|YP!65Zxjs&6pu)CJ&SL|1@~#Kpv`$F6no%NcagKUL6D>T|QQkOYEK z7v7P%7A-y7u^90ye34zbB{h3ir1ya1nzjfn{9BETKY;n07BHGmsQCW72oXvAHrP&2 z$IXRpmYRy{C4(@#%Pul&|HE#;GjrnKCVXw=e1)NW0dF|uck*s-NqIbqS*xxcY5_c@ zkiT#%8K&1|P%B{&PhHCyq}wy{&4{*_i=YOwby`kYt%dB=GI+xG-T~V$O@YyoR}OaJ zdfe07_uV(;s0NAzvB4&dKN*%Xt{){_?-pu36=Qq_B#7xim2{eG$F-0YVD!(Jwj z+PnFac~KkALYqZZh{I;H2SSQSB~Oa(j@3_RfkC(Eh8wu0KbwQ-q7)c)Mv677H4)cVY2($c(V7wZhyx2H(f)gl$*DWax;OmrV?_S>@*0)fSOmP0^CKc9gbw!t}i|@U! zbtm6chzmTqGQxDs5jRPGtVaY%#D)N)|3RKVEOizS+aw#fwT?NhBbM)Z^WZ49s9Og+ zjelVK#3dGDcW!(@=eZzyMPyx?d0r4Cl!^zIFS+l=PbrXyJC=S%zK2;1o7r|H-?bU? zFJ{=>&*~u*rYC=$;SX%;y3s<#m5SZ=cjR8`>3cYoP(d*>a`#YDHcWwq(G!jXeA^ql$Ix0cDk`<7!En}@4acg+|+ zfnUsTSSZx8>=n%_JRF<@a}53Y$4Y8vuSnykum3lto`i^?{EP!!DGascm;Dl69u~~B zdlq=caGm&9b@c9bLyel}YqHOxOWTX*^M>BqZn?|MVuv&bmnr!zz`rLF$yHZxW0;H5 zz7v9S2$B|+bfC6p1&HIs3?pvtRk1Sv%-BDK=WDhBW0Pc6+}`$cBsDv&zA)lRyJ+CRP^0$iSJc7H_r3Nd^VqgWNxi?WK8A+@3-`z^>o;EQ>13Q~ z^O9_vp0InWgnnQ|nW&xdk{H>#Re$q;0<)!QEk@`;lRA~A$VSOCPe&X-rgFgapPeUb zxwTa!x{Lm}Ydk7tMo@&Wg)gS=qXsAfH&!<~l+(zR4_dRx0D~F-d1X@2s%5!n)J*^w z`y>cVT9~Uv8Nc~7Qa0!yVjb@%OG*U+s-@cG>)PUwMu_8ZU}LhhhB}5KXT&iVgjfOC zO>NHMtxb{3<%pL4oM<@qLY6}0zT7p#hlalbhOfNdk%VXKi$%qeDMMAIy7=!Y)lvM> z?0ejC;Ig(rhTBg&?|{6;%q~wyheFon{TlqY@O+pIdfpH0G=ptpnGHAhb_@}j)NRrK zw*1oYhL^>Gx=UIwZaE63l7X3Pj|rsqf~ahol>e-yF6zf)g6A1l|H&!@O zc*1rn2f&-cl^9%OEp6L?(TIOq_ca}K+yRIFW?0DT3-+)o z^dbSKm|`M0bVeQveDKY>im$!U-|#MD?Ns-rrfc(S^1ty4bW?p2A+j>@I@sdYPU_b= z4ozYIs>CNv+8xhDAiXrnJC+f>I*^b(S~-KjZBWV%<=V&o8?LUe%u@H9BaizFCiMB8 zvnlp`+VT@`YvnT1-hUDaME{}rsYIOZ-L|Rl`zsR$>d_|sTh&d0uKE5#%UKn>{tNAM zTZGvyJ;iARfr=%nBI{1Wun$6@lgw*%2n zTHvsk)2I2dN$G3SfqZ39Y84*Wa;V+B@N(j`8X>^XdEYRFS@v)RUsmg8eua+?QRTO; z1>eA&u~Cval8(%DWjjg9T}juAO%Bs6bJ^SlyLjVTg{S`wVZuYXqOyDe&uVU+8Oax% zdn|ir&ENB6rMG?8t*Gc#^{+R~`*uXO$(`fZt_#KeSd)KIWy`eAS^l~-ah@~fqawDQ zKJ3l&qBI;Zgn%d}TmfEa?j`wdjs(Hb*Rqi6@m5Zd4a!o~!y@?kkLo*X7=|c+$5(Vv zc2DUM(H_B0T`2N&*)Y$QZoFeo4NW6(KJ?>Uk-u)f;#Yz4-FJg+_oTmjXgbE^Yi8%M zU7@M7m=6au^9UKnN^9)WVr#Pyr73NWB1oncfMjuLm$hx>b5|+xaMxN|`{?<)WJs2&^chtkma@(On>RPU)gn|s))AYwRFgtM-K^z%=_aMRcmBAGu3j2=g(&#cs=o*#Ej2s zFNp9XOl9X^dM_gg^G3R{hky5yzAr5I!;9W4pVWV1**h%i^LhVR0L9odO%K}g)T*0E z#C_z8Z9c5<&59blE5C}4AaJnGSH_+^+ZvG|1pQHaaZ|lVBGTSEp)Y&pGJKtQR`_kc z74&@JQB7d~o#DBV#k%SaK@2?W)ia72c!%^^`gc-fIPBSNG`%vG+vboF1V0;rjC3H_ znYS!XMm6RsfJyV~`^ks1RsDZBLB{d}G$!A-y=En2i53Gk{Zif;^)6kNF>}F4z*TYo zij>ZnFF;0KN!bU5$HT`vRt+^Rs`$wgrLJE`ruhVtmGdTRt!2B9uSyf{1MM4Px5uZn zXhomZzT4-G<4VOzb%O=G04RUSV#h+d@!q>Hq~k)JHR3?6*z%9h0~d>^ZfEMP;|jpb z8g*~>7&B6JX~!cf$oPZ)2SLdo(Vsi@ndXekAr~=rZI@kU%SU-lt;p3bB`Fv8-tX z<-SY|)xOLYE8;Q5)Rpd93HOx#aR$o)8@+RW;G{(w-!p8S4{mALzBImV%bCTH;| zsh=Qn&EAIv?Kn3+(D7L4+gmL9k^;_J@7sqhgO^k~q-5eAt<& zEULD(k;EmZ>5uf}@*{fSer_-X1;~4ARq?EDbu$nM9{9Gsk0w?)VxJ{X2>)%Q3Lx(( zu6=pMRjn%okt?QcVG7tsMW6dZL`w`fgBcW$HDtLI52ENDdeHo1rFsjYMei7pckA*% z%_dhO^MpMlTQdQ1HDZTkbDx~};NvY$LKQ53ENa;3eZU=`Yg3bgu50T^ej5}CoK|y_ zSY(!fpZP1nXIe@} z?C@I}6e&%^3}fuUfrIyY><;afFRPSoh4j>+ZGD*j>Y%CP}&J!y$; zp0mv1Nk6^K5IJkb^sjythL?S_Vj3^@7|^2~8Ov#Z>iCPiL_&@R87|v_bN<+zi;)WT ztM?R}zP-7)ePXw&Szj>pZi3XN-DZ#p-GLkw<0obPYtDVbrAZ}6B+5LQq)t%|w zYAbi&z?>_uni$ygE}GXcCJ+F!it$Lzpf}Asau4Zv;QJMl4K`_O*Pgd@;>z4MWsH|L z6qro4ZpK5TgRj&7t+x&Oj>ZJ7f3Hjdbb2ixAW#p88wb~M}9=>#xmkL4t+B^P*1X*X`Fo8LHFn}9AHSTV6_`_k|Q({6-(Ejr0rNI`ugFEonVfA*O zjB!KUb)R(Q@Q<>%y;lY>v0Q4mMh0fS=H=$Y={GfzvgyDb%EF<#k%vMsiNav5nkxkK zl?9P`2Rs&|PVwjJWfl1$9q{O@SQY~*{kLJTk86Dg#PQbvLsfDEW{JlZ8@fJGLcohJriP}C4p1s%JSg_*< zXms{uzmXdO79!=?+tx>9L>h%l6D^bBuYNK|&Fgw9iOPED zPU$=QPPbB6BXhN;6`bSPczmvGlU;wa(mo)5T)6}4-?V2raVH9GZ~QpCH&a_UB`7&iJjO>w%AU0>u@SNP7C;Rq z0a`psT_RkspTI^tQ5f~QjCj42h==Sb0mV^6x`Wy6cgbtTq+r4-^pA_bk84gtE*#X1 zIe3e#@w0{G;m>^Y)E&m$Jwp#@^ipRt&#h(!-qFX&C8BpIEy7I-RQGm z@kKVX$FK_GHP&xxQRV)34>LYrEq{7U{7bBT_#t-awh-OA;`2*95z$VMKi8~^(K)6` zdHGDy$2ITlD_W`eExK|b$+PQvoKBzY-XYHEAyCuR=LCYG z2{15YF8C>T&Lm5>Dccgi?sWTMjHr}enLOD31F0|LhmDA*W(23FmGXRuQ zb!3+2c`P=1i+pf9FDo340aSgHFx92=1v-RW)bJ-~=+iY4kURMk@}ylO5{n`Z(Tl{~ zBFSK57WeG@-n_*kT{Df)k@m~k$rptf8g~Kg|kb(hfAXv27TVwPU_(%+|RXI z#Y)l>mB2_K)2uqp>|C}s;QVmLwM+=qs=V8&Tf3OUedclRzDYt935J7fJ4OF^d1rGg zUj4C%5|xBV*QCy0CpN>YoFa~F6R;iI!w(#$4{SbqxNcg~h&IHBq73^Vg`A=XL1RP|f4PUicysiCwI@XZhq7cN69p$=W9ARo535>XOib*6Pa@n(?}d)xazJ(+J>mRInSnt_qveRIoYaNVDwu+br}PKqr|@}P<6cnO z-5WUcacvI31gX=6GXg1#LW7X?LTrHDxvrGBCD{b?W2GaX`x890Yvg3;8D!Wks{O!zcz665K`+`rdZ#F>h zliG-%HUMSfv7!03l=GItyzC60Xm^VG%%he~(djBod%-yYK-RLG5}k3AlL&~Z=aL;I zl_N%_7)!yJUjxt(+*#(jvv$!{AXu}%*bw7APHHE~l;+VggodelQJQ5- zU9%YrviO67UzW*kik>&VZi_f(v&0MKP&Yv&eBap}Ephf`vXyXa%r^_ZcBTXF{hx#P zetb3CfyoCx5}4PoH7Weq%D+piEVG)sOZt`3gjzDh+gqk*FDq(e>ORu^{yp0@*^Fhf z@z=U?g&2kEGMn7bdWmZ>A(ifckB1dCVy7Nh?`$W!n6QbJvldcHrfzyW)2;$5HpTI+ z0Dem{r69xo2VCMC@z%=lgYFZH8LduZ2Wj2h*MG9#>#RW*yTUGyOs`f0+-UuKxG(l6 zln=HBSaJd%BE)F#gmupTJWpyisua*bE}Ez>t#K=<@?#hhqHm9b60cIF@vp z{}|>oqb0v{J)VnI@J=|-uB8vKHhqZtUKT@_YaVqb|Goc0V9+;4|1m%~LA;_1B@~(lSfZof>Va^z_ zd7L7eAY5_0mKH4R_UQwlzu^0B4>otjm*D0kDNriVxp1R1ib&s$(#zmL8Qh|mVf@f5 z{@m)+S}L%d^HQaKCC4rA(L#YR02QuxX$vzJT}V9dOXjrc2j;^x`D)j2g3@@j`n0Hp zuuwW>*UZ`H^ph27%JtU8`E738=~-Xtu~qE9t#w4ID=OxP-p{U^ec~FpRiNY-+rG6F zu`-{9Ko(L6UW>xI%2ulz-G7Oj{EbO;B_=>lx?q2{UGjW~0$q$rB__!FqLW(U68&`x zfEeWG`OKXMsLX&-+hK;h7;?+wd-_oWe#`7YJHMoRR=J7UnRv(j# zz;L{@xe3xa(u(IsL~?uMTx3J=UI-Kk8ln%=Vl^lJ)~`Vnise@T{FI65Zn2FZwE>Lu zcOa=_Q1zf0-NwAn<{0H{;y?1wYIu@+yf*o$mxbk1X&~3!&7hrleOPzb`$(<%lr@V2;JdF8Re%| z_^$M@IGn&#DF#nMCA4jLs}Jw_GFS3$Z0-9QwW<_K&PA>`rRgCDv*2_qiGyOh%*YoT zBI^^izCfB{GGLAhQbjU>0SmBBa?;bu`hq|d&7-FH$3vbITtF(>CM*}^ha~>$z z$0@Sq`}_^t^O8LIbprw(9u>H5IPXo_sdhcq@oOvT5vEZuDFmc;b($f3cK20cf@>im zn-=Q(%5;>=3->~+&Qxv7$^3A912~89i)$Q=1DI^qGB}_!NP10~`S94|xs=V4L~s23 zP1_FLZ0#`*^~A>4XJdrLTW9=N*~Jl!yJ-%IN8tqFVeJF;?JqMM#hU2YpFPim#0I{n|U$(F8?e;WK)KQMSuRSe(fP@m(u!@ z9Ti>E7bN(%YBZ$y+mbl5H5AP5mby#=PICn)QWdyHLs6&ZTXBT>FBW#(qlbS7Q6njH zp8+b=J08mI+crx;ni&#KN1c)^pow%mVBTU(J1w?1D`r zjU(h*$UOxjYteO4o#aQ4VsNPpVrG1_kQ%YdB!Yvynbi_HQk*t|eR?LFaWIURTKh49Nq%a?qoU>(+2P!E}Y4b0Y|dOmI3F7K+6G|B$^k zNd#)IbQPNsPaken(S$`_eE|u@V6&R-!|&Jo9NqPS$85DfXc>klJNss8JFM;M z+BA5I<+QT7Nf#GE>U!B>4^wTRU=reNTq9`delfv2042s@g_W?k*?_LBuD;gq_z`CY z8R51yo&!99$Ty*@MwV_BnAIC`Da`QmiT~#UKu7oa8cY&4!OjzB-)JadIUKObuPM#N z*^7(|)1S2O*Agc`wd8?1>@2a01ZAQ^5^bABV>tIhB+29nj0*A{qQ8|T0;H% zJ8!&|uDE(9WQ^&p+e--4ax@#tzB%3kt{RV6(2OKt+A1nRR^-q0A9{IO(-+1OLp`9V z!+>nmSpgr=Ik=vvIKMR4Gs+}~#iSAxKLRQZUE9O;>MlUyT#VB@{fCPnIWRbn`;$v$ zC52nYudJFuUWSy!s&SH!R)F6&*8B?mHZ(+jp6NBr@LG6BX)Ojq(5aV4vYbY|a`I)( zp=oyV=g)ff^H9bj{tBzb-p6)Yn9e=um+9~$u4jWh(&=+~uRn4T*-C1sOIcl~(V>pJ z<@f9oEjBFpVle}*UM}+(@6_lFhp`?`KU3JiHfGz|A7VIrba~vbQK|9lzaZ@ioe>pK zv|63DCjpzg&(K3oSat9kw!2O#-sHK&`N1JQE5aHl?}Kt_dNY@1)at{ik)2B_H!BVg zWa>9y*)Ik4IRnyShg9f39rP0o6U13Tma2if~DA6Ae1xT2<7L=mafF=;v((2^J!a1P(4I zdh*?!V^qMUaZ?t5GjTz^xl@t3QD5QktM{f?LB|A6Ld4|aZA16xo#9M_^g&5(RO}sB zeeWjec-H(?IdGQV5SR4sMnjblRnc~rbm^zMGt`AVYCb-3{->Cp&nZ*I$F;UDHT(Bm zVfhxj`TDVtK5_Qs0|dSTQWym$m5~TOTM7wfjrfX z_D$u4qKDWLm5Oa?nE9(k`?M~EpKfyypRLRDo2&TKtdjyX-H)rh9y7&*!n}(w`67T5 zd-Ols3%5X1Qi2oFC~MeVhCQXsyRmDkY$&hlhX;O0aS(>+Jj zU=G0J>5N&!4e5jk{xB`ASbXJ08GZ{g5T6l7|DdOqCo!Tial?b-AjQAL#jrXG#eA(j3l8dr z3H|T6)oCN-R&U0lF8sRSKY{y?Z59#p@$;8U-bA}{K1AC9Qh3;9l~63;qW6qe|7JrX z`j^L7rCyEtfJ&xKw6fGnn7(qELaL{mxm-LadwYg)7} zACV8E>_Q|bWsnRzm7nzoe;a-LZ)SyT)}_wB>h=tK(~tj zoO!Y^yO9u&u5I(+Vj!S+_ySH*?n*6HZ)h#Zx|O$2jS1v=DzZJ<{WsoR@OBc2K!3yi`Yvf( z>bw%B{2jXTZz)Qyi_JYPt78KT1IO90TpZ^IEd{#a=BXUR75UrxYS-eWOLH%#4lfTc zNB@?rQ)M^&?xk|&SxY`%jsJ|-_TDw>%k^?8oJ+bArywrI+k=;cBBM$>#hkV-Ob78` zK6m9UOx=oL0a|fHJ>>>*8oF7H^UhYvdL=JE(AY65#v9N`K|+XGUlA-0^bM&PqEu%+%AP4DM8a>Xpol@Q#nmLWf{xW9wtNqDl(K6K1 zAASB3Y|QI(xf@%hAhTZa@Z`kQQqp?g|k- zT1!#x%sjr&;|Ys=a&!U9CzDkb1&sgc$>R_Xos4FpW&0U!1eepYo2d&$qwK@lHenSB zt`*yVC}mWni8D^QSW%ud25uMC(Yds^wm|sa^-4yH!Btum$9~32zePPR*fLQR3`pa{ zP{p{d#7hs`?OWI$e!bN&p67dG7ARL)h4oSe2kMljv^-=nSA=u>H&X1dt1Cy$8(og- z2lnQWIPuk8!5~;Cu8AV>hxQSY)Z0aqOuMo03jdcU#^Z5A!+d~M({v#`K(Q^7XE|!= z&4xt%Soe$_5}I#vYme?PYYNk|kLPUy+U?s}m;V2ltCpCP*leb#@;nQJ>A=iMyX zV*DM^pvve!i352IgjHwkrqi+&-^$U*cc*$vD*qmlf!V4UJ8I#}W>+${%*0{SXm@0j z?jye=zHkl|Cgj~KT-L`0kS`^jyFQMZZ(uq{>VI zkje)FeP7CLalDHKJ=|rS%Nr4r0@i4DtCPh>Wi*4(wsCr zPi~4{HtJidc@}l;ybiAHXPU7ogW%_dGx+OUbI%+c1CN`5*ALpVgWoT(UUzXhmGEU* zfPnYEi7kuei3fhWvq}`vpZ{-=(ce+!p@Yhu5IylccdAL8i57%Fxk{H3)se=-T*1KiG(9B-j5m`V z7Y%-20)B*AC2^vl4G9qv+UCE|3$H(5kdGuGE~2nuQ53j+3HN&UuKON{$Xg|u43&6R z5I@e*q9clWtFM1RZaAfMM7-beY_Lt`=>svM-#+BS&_462zavLLJ>X5=X^y90HUF^m zttp_SJ#mbZ9d7uU1Ltu;dAeh2`(&t-clbctDQ$!O_U5T&`zAH(dTOhJ>8-@+0}8{_ zh!)ZGQ@y$=d_D>E)R^i%Li*uA@;8a$!5~=I5rU;(1Dy##50(@3=f5*yrhlh;S^aMl zHXZj?3~Kq}mq0k!Vt(^q;LDa{(ADM>$>B+5FCz+`w0SJ>JOe?HsM8*nm$j`MUYkT@-2DEP{Sz%Ycu`)!N~QVi`9_a>?UF2}_z^*H{l904{On>M>M;GyD zW{Xi|^naEwOvF_n-b%XPQ7vpITB$x$S6fbd;cG;H3kkDzbvDs63;Qybx$T^iN6 z-->Py8j(YP{6u#2H$@H~K|1nyY-9rO!=r+vz9?-xaYhp#Z1+l7C$+pi-RGYcot4ys zZ62iPyvobX&^xCEWcYu5F=lZ>hk&n(W@~+qFWMg-vGU%WHaEI!n^25X6WF};M;yvt*m(RI}!NS)+to!_K#iY8r znVIh|is&5}GwNH!&Dc@B+cT%o;%Kn>k$rNdz*gdQ+0!rEosvJWTNmAZmy%|eMJdUh~*7oJlSwAQI#@<9SCe~lA%?g@;U$}o%Lvj-i zldUwDXsjIgfk*kPjqBt4fFT=^N{wbkm|@P$NQ7EL&$fE{n0=Jd#*g%OH*OKtf!Ze5 zSjMP^nU{G<43Ta7YEfCk)!sjyYcEaFtRJ=?@2bCv7)KX1&QShCHAUP-iydq!)*Pp2 zx^zTp+hu|L82Z#$)IIB>Ult zwC?t1S0#EJ)Mi&2mAeFI5EOBuj}CN+`;{L#b#qopf(E>&2c`BVMgiM)CN|5&yfSg4 z;q$HUd%!z+F%IFqC*WPavk-^X8$KwFzQ7?Cs(jvkS8wx zm|#@|YS?S>nc@yB+@mzh=bPPhD>VIEAj35oz*e%htNh!t%k1giVN}QW_dL`4_{98<18$=f8`@q@jAA4jr6yt5 zRtfFh)uVMN*_}~qXo)?_H+-oJT6sob*wWnh2ak`fp24d(UhapK+aryMy6TzjyhQ3} zT9yZ&VUw=@TI{+~HQ9?|O}&{o7#eSG-xd1=ei-B<%`Rhyl5IBUtRe9zS6s8A&#t$B zy?V;)|CpVcis$y2ou@4Q>dcSJ7MbZ*9nD-55$<4iyu{wZ;psc@+>v@`A@LAJGAAdDw;tOq` zysT3^E0f^U!gcmjIF2cpY7X753^{qjBPp|4i0|%=8M@Mc9rRuhQ0uoUMR;AoiG5`F~$2bhVjXrm-Y4xgeqL_pLWa6I znLdBpG7Q!eQ8*z;_N8-!;ZqA)W0o{O3$}$sc3LK{+CEFQlQQv31*@jG@PrxE>yf5a zGYmBq-wxJjZgjNoL^9^I$7(nDa#Rudp!@k#ud%gR zOE%(HinRI2nYflSIApW(O#e0~-+fIf48~hyo7?1SH&!eROt9DTW zgKjD}TdmDOTpjJ(c)_AXU!H1fZMlsfN11pstf1!sHKc5;MUC~ zZ7VEB^!^8l0vkrs9@`;Uyn;a(9T3SYQF!naz4HFlP|XxVD4`*&zz|40F9-#H(;N(vJ#QbzBn@gt>xR?lo81TH9T99+$4R4;yoz;{7N{8U=)XMw_fH`VQC~h);1N_; zK%R=Kuoa)WgfPljA+A#vyuEtjTcZ&{vWKwBbaceao2Tnp_ea;uX(*u@FbEnX=Bt?0 zM;2nsp9&eZV7mth><5_`hGi@Z_ z2db{kFua%Np>zH`?VMQM_ZJVUAWDYktssswJ8w)F&8cA@_ACMv0@`=1SU#|997 z(@sbsip&#d;$suFYOafo#?3V*8H$KlWIF!KJ2g{`S)`1V08+ zepgvLzPA2V&!6LarKc#Iae#L;v`e1rB@p{@i7$0DUt&;;_EVK05~ETD=ucG>n99qW zSSte<#NkGAPeNmqxaTr7Ot;Ibroo=c^UiIQ?rI-#ED@t)o(1|J0N@yN9LTHk%E*z3 zvyl(?Duj1TqmOv~3yw-F3eejrU-^Iig*~0*E7-%Y*_he$d+_}esNIMwfNdPp?;J_s zW3y>`qnO$jUF;PR>Uc|7z%Y*5IJ58OEiY7sYUl6{)Rg4Ief$+8hW`ogbj5R?E2J2> zy=#}l@LxoTEKw7_vq2zYYO{+YBfo^g>3-;D#+d5!yveD(Cn-yewWIdqy&WuRLP@=% zkFQ5tXSj=VBwnHM4ZoOjG_{X5~WO*=vB?#0gZXQbX z8Y!?IxGeJ3{w47FsyG@t0L8BG+;zY5cNY0a7kG`U=C0%%CDW>ltD`cYZoe!O00$k% zRG%&(RPH?sdZJHBZM{of<1ud#lYwnz(Y;TfVq30sEYOICuPe7*L+tm~yEk`W1Z zk4GfN-O$MFSL27QZJ5}6a{6R?QE>*NS%}?DEbPQxF%PN>bPewA7wCYbpcKsB-BdyDX9@$_YVdZP8ay+Z6&+0C5yy8q)Yui0qfH(-ghGg+N6m@422rt^VD z`pWt(a$AX*E1P5mGdxEpSq8;?SlW21>cv%RsdjW^n&*$D!{vwKoM2$9tMJHyNd_e&%GS#GCMoPw>li%1W(nQ)sWtIU9&r0^~fN)53##hu7`c7pJ;iO8D_ zP_ipPaM6u$$s$Z+1h$%G-cM{qf?SiAejdfL@mndUG*_BCWQ$%Lu~(5l@&XoBKc8(N z9^K+PQ_OfU!rh`>)Mi5V;xEF7avR1qS8kG!W8?QAgSH{Nwy}Y?ePqvALfJhKA!Oe`==u5(v zHv@~y_9+}NSSUTfV_QMUa9R)nA$wbBWu)-VfkDNm`mX)e#SbFBKgMULRsu|`Cs zHf!|umQ3!H=r$wI#qrsG#wK2Pj*lk41kcRq-)2_HEyZ~c*`Mf7x5dDr%in_K!ZDHJ z^r|0^R4;l7Hy0@14&Uh}sM%lxEKK*E9^M&kc<+=Rb4HdFMDYCwX?x|h|9*jyWiWxf zS8cAJ`*X_akh|^whAIQ*4Tk5#jP!}2;|o_O!DXxf(7UNl@W`g}^84%+1@Y!OC@Ch$ zlkHDp#=o9zo5!y`=usx+Sum$3FSuw7%Y2@$08c}aya_HxJnaB=GvLZ(nLwN-)ix{$ zP8L9-+GMBMw(}j96fMp0bl6#f9!`=>gh-ANNTOe3a~^sf4REY3e;vslDH_t2OZIta3yFSiIpDgHhGN|SDFUNU( zv)1*7%`umCk6vtFn()41Q(Vrl(f$@}#_~wqsvUnVHV-FB7CeT%V}z5qno+SfTm=uf zH5a@LqXSjNrU*?Kt(V0@b8OIHh5aQ%M6H>~Wu5ONq&i5hThXfzEB&3! z7$a-Dgg6fsuO~NaBvnH#(@y$=s?Nk$F|*Oe^Sxz3MxiF09&9Q2irD<>{p->v8y!51 zD5Ediml%8!R4b-zs~iQF;kp+hs~MZZT?9xkP4asQ+oSwP{Qy{MlOXjDOrlg9UjX_; zX+VYUtC4jdX-Y(~9O%qn{>uiv9i+zWDsv*(r>lQ32=4^PRcH zWFRzWty2*LYt)nS%5_4}83c)T4LannwDF^BQhlJh3|~Ygin4 zD(=GSF6ZaOIG(52zBBTaRN%?kOu*XF2GHs57qlqlDLrLBa!V>>e;IrsnM8oM4#myM zj`oWV;+ZL5jcR0l!pp~_c3!(Rk^m1_nkV_$Tlh(Pn=QzkM#c``tZhy(;u_Y`N^@2Z5f)oA>1mnV^`+l_%y?C(yOrn_*=GffTcfr}7 z1c8v02*o|X3R&mRS{e>>yQI2n$1RzrPor}cUsvFDS`ZYNGB08cVHUS|wIgUR3H)x_ zDo&FBlORbHd*XZJIB|?)BiGDZ^18r!9{c zX=4nHj+xp_#ZNKrRg5vf;B6C5|Ab$9n9O+&%*3_K6Sm<8$e}@h;oZ zV*ysRH;Oms?-Qd0`gAMeHv8W2ae^4Oj%Np=^X&eXq%=vk zmJcjVlLB+=q95|MNV$w}X%3gzu^Xy}M*sGhM$nG=0sz?iir{{Nd_7oQ1ej(IIUW4n z_EG^=99Oev*4ZBSdp8z%)X=`lAECT2F0egD={<8H@&vy)Iobfvet-j-DvE|grrr?e zU{BEXO-|16qa}h88oAOul9lSqx0jU=u&8Xd`eKz|vRLI7H8l0w&RAiWHy?3z+raI( zx*z{0-|T`P{v4VcMzA?SvF}3;7b%PJP_l?-CyS+gtZpqFn zp(w(|-|JG;W;np@0Kk)DZ@r8KN3e#1=p9rhNuCd2KC|Pf3?Aj*f%lFyT7c6&AsQ2l2k$eYgeHER=zQ}@#&4zaPknV8P`LxB zgyZMGZ>1s1GDyv+)n4{_p(Bn5UT5iV zXff6;EHWI`xf)<-`V1gcB@u9X3aA3W)T#c&GPt}vYPON*{v>Y!)=*dhb8)-KeTU>( z!&$UJ!!|to>r41;2E2J1gu0{YD}y3ZqGUT>hmXhS(+HR2Vo;W_T99m}l>WDw|EcM! z!=n11?%iErK{^!a4v_{ydTBvYT2cW40qL#s8UEUTu(np;lM>jn$vF%-}&#l*IO)m3-m=$SX#zwe{w|b zSn_H*+1ob}aE{UW_WV-#;Jbb#v7+D0w{y7$05;Ee%XIah5iGA$e&WByqjmq)R_yOXoR|B+K00HFDHRYv-Zz zUR@Tb{F8tm=^xil8Y6e+l7h$-!#_)qZ1h;w{ZSLzHk)F)=qBbOK0PYVlP(^k{ECMk zQ~X9)cp&FpFPbI6bRDl`hbB^(l=%%KWZnYGc(vPot5(%->=u%4Q2BCu{F>`J315M% zvjYtWDuO6v_01$o`WS(u4&{Sl=Av(J-`b6&%cSXi6zXBO!2tUGO5zgZos0{0!2{cL zOqHY?^gbnT^fSQh@Y9QeeT`pv3F9zMOu;{3=+gSuidIWCe(&#{#|Lq<`zY|E8IIwp+0)%~{4@-K)?B=FAF z0p*z7vWH-o@uF=*{g=wV`!!;pvjN0X=eP{OXkRLmq9I)FKNT02aWQ7L^w^(FhsNM+ zqnp*ALYn3GZV#ndwYTm%q__gGn*5ep8ts`a>reA--t)Hf`I$T@N#c>ucN@uc zHK5pn18kqWus6Q)eJ5~-x-iid@Ny=kelO{RXR(Br`zR?}wHDZBMHt=pwIYB?4plyc z0?3bYc@Ifi+ySzK41PNXLIWS{`!~6u1nyuv4Hi_0ix-%Ma{=;_jir$gppiN6IzWfT zIO~bv`)}*4Yyp7mzZi5)xm=^IdL1DTX@_@9E~8?^TA#Zf(lr zyQLOJ?8Tf!sFs<+$I(e=q-5d{gu?qH2@B+**~&Rz|65emSB-pr0=wMP#m0kB4ye-| z*azM|3=b=FtC=QF;(A=qlNbUi3uykEXG>dPiYUG#zKD3v<6kHjUa#z+^CU=1j|yea zZ|MU=C1mE2?P_FMkjW5IjS*PrKCDKV9tZm`$tTGjHoRr`rzjIP_(k&4ftldLjCI>y zf=Zw}AZ_1y1wmfnE=e<^_pq&BtrjE>pIJT7q9}VTR~q|=z$(pez0T}?oOtQRZC}6N z>QVppcjKV@UcaRZ2$^5Y^SQ!=oj&9uHeFB_ZXOD>Wbw` z6UxeSr2!P0ybEnIov}4nPzm$b@XJKp&i(L`I|BPxSqDD8+mq#{RqZ4$u@!!r%P%rW z8cDvg4M{Yn!5du)NUb;^T)c&t&w)rp>8>Ezwuv<*Y3QnI;~+pPEfj$21G?E7A@O8 z2H9y}B_Ja()?n!};k&zxk8S1sp-yRZ&u?rNV80h2Pr8U1na{ITu6^_s9-V9q)^xU7 zuaYO1aA?Q>MibI?Hj7yoeYT-c^HgAk5|DOgGRz|2iLS z6L6mu32*Bf47ab4OmBM=VAuvc^{m2qr{3zE03>`e335u2BLHig##exMGUmyp+sQ}H zt8bM!k+tBFwga6YTj1;+BzwnY&SS8kGL%xqS&;br8vyvLh4K&}Tk4uvfye8THrnw^ zzGW-ljlE)n-|{e{F0swc&yK-K-n$JqChxjE&Q9Jz7KZCw5;E?qD6C~95m2_Xgf_f~ z>F@OVoAw2_e82HW>GxVr87iH!8;2=$%>&EBgDyMh;p$E~3Q*~&=^;S2+_pbjT%1c* z?+md z_+Q%3Ja}8dWo@1zd2`Vtyr1?I9)VNjS5wH_?0HkbgHj5+0h2WQ2D?s?VGUsO`ClfX zeqdq+Qzo2cP-(qA?y(uiq}M42M_A)_aLD92o4oi@8QFFJXlY7fw^4}2;|_5x^8$(v@ox@wrc-tsLbsTqdnAXxXN{d-PA!v@fc_P@3wuPJT^>vtr_r5ljMGfuA;QP8`| z_1i0UXVWbH1_OUpf`Q%oudEa=_&le3Z%tC>+c0dWj~#=Gh1U70|x!px&?{9`WZH%{;eTes<0w922tt-k+GLRmL!9e5 zXxDLAUzRPgEQB*gNXzQVI0lYOE}r4t>-Xx*RUML42z_Lz-w1eSIR$}ia2rVV0E`(Q zH_$CpigT*{&%RJ4jkx+y$x`W;VqcnhaI)TiiTliX%V%12?xLD67rTJ`5(JO zj*ew#=6)mvTkH8;jkEuZ!HQ2oYMh^fnU&fvJtsR}t*{pnFyk}UUGs@)u<3R8fmN+I zOUUFImiOV3;TRLayln|9%YYe{vHB^HXn#ZTfq&^I!+%K(={tkRA%=Uo8C1Q`8Mnl> z5%r*$9EiOq@3((I^zOld>^a1XglB)}dyK4c1zgyUQ=CAd&peC8p~Dd4rRxBJpL8{Zp{J;?7ySH2Jc;0nQfhVT=NfC}Ye5 zJA#NN0jI6V=kx&UrD|n5rx{LS3%2VS_3S844=6!o?h*2+g^A@k9v3IKI^8fQWA*3z z=6X97?+-v*o18d9C?u5*x#WNcsozlz)iY;@EK;~&KffDm$Mo6zQC)4j!w1HuM!%W8 zk*^d|{opqh$E442M2Be92o@$*?AGxLyXmJ0h@;Wl@gYlbbwxu@+4Q>rS9BE`IvM|l zZvYZGcb7(q1ejGxJ8f-h0Bzz~oeZ=j=oe=QAf%hJUKg43F3t=vG)YgELx>Rq(A$B0 zYQQlee-dD>J1|f}=03t$(A2?i+A)8mOiQcWuAJT@v|oHzfD=kIDRxpOlbO9sudNI~|S>*t0`ZglIQ&+Rm3g zW-KKlwX%JPz}Oq#hpbrlsnv`RUQpYG=!EFqM5VI-+=5N(z8z3_aRMI~sa%Nz(aWMS zxaqY^*GoUNrq@aHFcY#ss~1Lc&E@5KiVlaPoKWy&HLo)GHo)aqw}ba2 zVK>~Hu1)vOL8;03&Bv2X`El3u}M5Vu>5Ejg@aAMbb43^`jEex11If6}E z)h$gKAv6xxwMPbqkVKCk+4xTs#Jek>cLx+bO=sQ3?U;G)Z{wWe3JspmeOr zO_r*U)QJw0F*sqxxkee1aS zM#B78k2hZXUx&&Ansz@9-y)1|gWL7$U6xL6oek_>v&nRA(sJuy#+vLY0om`c9)cfR zS_s>Poi=3uhZk{vIj1cvi#s9P!C33q!bZ*b=)mr}^+KW)5y3x@;_ZR#{8gHN`Gb$M~K|BYp)2~nyTf}0mPa%n_mv!60x+S@3>tD_^qh>i~ z+(Y?CYpPkB^Lw+ux-|2Rs0`sA$80!nDs}RHEagHC&*6hOpiK7RS6VtQ?jJD0`bjJX zYkA0=5SRGXlB=U@F~mCpsSaOOJuC9)+tHhDeytp+ZcQ+Fk10xop0VR6lXF*_F2#!| z^JyBZ&6;zdaKGeq#&JhMA4^@XDt?itc<#_B{UkRnqwiJRrLwS}Esp-X{phuzly+_o z%NPvO?cFa)U^*x6YLNKx&rRBJYx?G^pIh5W0-=oGH`j3@yKRRcE>d}=$$$(0cRBA0 zbP(ByoJ^X(HdZbO&^9P1BvZYzCirkjR@D?GtF84a;XRMKP^*_MALo{mChtGH2LuyM z^m(g@33pSg^wf5~zH3(OX{|Z=El$bxS6yTHGKvGkgl(=Q{pcSdh+|I?5cXUO(sJBy z?5qw=yLgpK{Mds;;`hZtJa$ek2T$krTKzZsdZjzQuP~$(c?uQk<%dPuj zNibeIhG>(q>Db2z?XM@wnoD&$Q|z!k%HdntQDcU?%m@ z=x!6PJXWL6=I41#fOYpn>q!(c_#5y+(m`U}38C)u(^J&z$Cxr_t};p9l0U2eTnQ#O zr(Onzw0lTA4#!<5w&+7dY?+uOE%0(c(gJ13+Fvhnu9G&nKsAQH^vrJkO}{btb|Fm% z89%xWDj~nK4Ty?|eHw`t394L$YZtge5Rc+UGwec311y2~(6%1=O|p5L zR5Qsf_J2D=$;iFZDdhc~pq~>-5It1xO`9Zc{?`S%UbpS29_#4|yH%WCr-9x@ug=`~ z;IkUYuj&p{ixs)%niI0wwoO62(q5H&>+?=m<6d_PhEfLwpmZFHW4d7!}+2 z>n<8yp}>_lf||Vjk33LT199BYJtXM}zqRq0JS6H(M`cLhL=hW;S2w~x)r$&bb*Rxsy!Uy7Qb8c<%u$wncVmdNtAR>{OqcH{rzFEy~ry1B!O9D<1&t*q|%9Q>t{fhRvN zRqK~~wc+#unlKQxPC3uI@Lb_Xkf2FMML<4lU{b98&;~D%)IzwT-EA}7PkXj$epuPk zzPFZ}L9ELs=Xv9y`xtKJEixBmkqccPmb>q~l3Ym$)lR9`sC#!us0!8R`&?CND^0v` ziP}meqb=u;S{`!--_NIi9NQMSG%#(k^yABmeIc? zfN|PeS-}4AMU%z*oWeC=fFlJqyC&X z;<`GL&O#9y%Xh~Hrdl$leQp1=5eR0>3y$lh;ks(t1-@2%$C*9MEI?0-xEQW`A%_pz zLXDlBRP2nLVG1Kaqd65o3gl^@MwRt@rza!TzZ%X%ZOI#6TNv=@Or8c!T${(mygsiB zxaIOaC8ZjH*z=D{$X2#D(lx7uER(x%oPF`()+z5+4uTYEBJUD=y|xtPdO&E_5b0lu zb&)`FsfM_wfgT&i%qA^~UW%H9b$lFlG|h`CL%zwDbF>*AveUe(@xb)vzDg!ulfwh= z2d%pOncpq(bpb!e@l*_@4h`i6E`Z`hOv#fCa-7iYU>`MF(J$Jnbogg z8}G8~^~k~QvGI|l1Zy*`a~&W0dT4>rkLGg*5aNfFQG;>v78r9tmV5=L;`yak4tUUau( zj+$#`}_=+ zvQ~_v^17yJhq7X5Oq;5D{5U(#z^iVqhEO_9qgnkCBD^#!-})6Re!!)zPaK&KUWpL! zyIZ%KFd_hFQ$;=Ajt$&%Z8z%DfCl!l*iwwzqrxShvbX(>E;k$i@WMw${F3G1%YP_W zD0Sk`V_5=nwE!0`WJP?R8D9S8ptu;6u;^M2i;XbdqGVpXd_F#+d9}-MWxAl#Tl2d= zBYO|z0x)s>#|0?W89u|}tPn+HwLIxAVo%gMj`8$$2+nenIfE2V=k_-J!EfNF+=>U! z&7I0eUUWq1Y6-);l^wpmeJ1xS;j(c_&q&0?RPfUg1ES?_o#IQLncwax*i02 z|Hym7-PTMKRHew-Sa9zMqh88qHa&@XDucDneH=afAfqd%P&t7)iG zm^+oS%^q^dJ6l~HMgK8+b9OwbubMA04m}%y+&4qhns`#H8+aw(!Es%c&aTWYWP(=ekXSUBJ!<9CzwS+?_X4-Tu+9oW4)8tb9?D z9NKFb7BvIwND=2FteS-w6q07DUXP@f+A<;houizlz=h9H%|b-1DX+G*6XGg?PN|K> zcM$lK6Dy(;H8KHGvPpdBAt@3mwCzq>UoisJS)X~2lD)Ac9ZeuI3rQCNF1InSBf{)d zU!_E_UI0g@Ccmay<2BWoZ~aPorIRY_-GReaz`HhhzL9qxYZeDb)hql{2O`jJ9h+xg z#a{fg6zKUhD%YSM?Xk!7*&z$busLhzWwUA`6*rR5C(n3*Rq<3#oF1y01TKzCBTgb8ZJ^Qw8Q!@;ch+UA0WK3>{7vvL|B7D8mfC zY|K(MYlt~YAUw-OZMK7FZ=qFOzRmj~J%=$BeYv$vR_{RTAAdlgsGyk9q4Hud1nK5y)W!mgj0#$DfjO{$%sN{(g~TH)^hoBRPbOQ|zc=alhmF z2}TJWqAWQ(ezKUlXCiU;-+W?QY_a1cJkMR2f^Iu^TxW8_oUFz9wH!^o*PStxFc)VB zKImbBPs~nZ4F?--}Z6)Jw7zZWlg6Z5h7wk|Z%%HJi z&t){4C8z@Z4N@wngU~j*0E7;U3P$Zg99pyty=YuPhWhP}+N_j)yeFQ1qdNHy-Eh+X zFc18rA#Fi0R?;R)!1@Bn7d$25t>0Zd}q~n71%fI2Ln!>QXfW*Gp z-r`8hh`+XR!Thj0DcMgddSGQ>=#>{jb~*yg9{AC$eHcP@8L-tdUyqiW2FlkAeHSmn zDX9i}$R9g07X5KLzPYqyZu1H{rBdn~Vx!Lyv05Pm(Bv)rfy!9^sRyh9vxfq4r*_P2 zuGEEKi**j{lPVLEcVT#_ITo#1aCvIR_bCji0mhmDaD82TA>2_t+hF4-#c4#mlL*Rj zPxGt{e;`gA{JM)RT{U12D#?TI9kLC}Ua`iZbwSl7Ll z>wp)Edwu;V3N?=ycW^u0}qGSx>%< z7NQhJX<4bC%d!T~(hKa2W`E7w9R4cs)=qj>`Qbd)Ge7WN^&V0!9clM8O=wwyKo$** zVfAgH*%y5ZD(8~A!E@pK+gy^e{gA%HWb=TzioF1n(&8miw#2RnWPR|*iYyGsL+QKa z7p-`Ju^;p{=H#BcR`s|W)MmBfBVbD&xERI zHIc|aw@l@zawAf*Su4P}G*5~8wo_Vo3S7&aIAtZb5o9&9W0b;cbL$@NUh58i6vOA# zQQk56a69H9DFqmtLWXpnz?b8RXFP1khOaDCK`2^pZq^nq<*JN6N#TPhTjs;a`ir3$r#+0B*^!R zXFEDKXrT8(Hj7!0XUfxDeXxQ1(m*M{B3>D(d=jP@T1~Sp=he4y(Yt3w@M&^$yU)|N zx<$0Tn7*SzfY^Mco)5ldL!`0kBn%bPl1Jo3vc)=YKfr0a7HX*{CB3##4QteMI|wrB zc1gdpog7sD-@!ig=M=xYF@Wha>Uy}LU66egTkv-4_Poe`dMj>L^p}w>+&IWYR3K*2 zD^Ibd7ToJ}<&id!s~_9G|R~k)Wq?ZwvyX^q;7lZy!Hby{T@DJ>er}HmdASD~9boj*VMC zU22J*FtB;EN>)q{OY_ii+18z$Gw=1 z&AoHWmXwV;bI+O|R9Eucogr(9`%R`JMMk8`O+5|Xrs2O#x9_4|o?0OA z^9k>b__*B6qiXd&uq?iQz-DPg66Q4287dehtkQN9Q}p?&i10Xg=^8`nbl7DbK=ML| zb{zyvAC*qT;zRc&Wi2H~z}n{&!bZeV4N=(BzpHZ1`m1j6j?F$Sk-UK*%8tc1jgmsg zG*yU-r5kzZW%ILL?tMj2P;mxdsDbfR{KGm6%8>Ku@lpP}Hht5!ow<+btR$AkBDWUB z)0XC^l&E_b{rQgPH~SRTpV_gnCP)VWp3kn&@D^zezJ%d$YQ2v>G6|3l`nZSG`aw#M z)uJjVWzNzb?)!xIF%}Vt-X+AjB}WD??e<19xo|9jc9VS0bZbWn+Y&f`!JmLN<}r@> z4{|Y&*-o>KU~fMT81dP^y%51trdksPo&Vg@1>B!ad*OL9KxgB$GA=!Lu(dldvfJ?o zcQap++RQ>1hB?0fj`?e1&cI;uWB8inxNW*3N-jw)FXh1J?wMU5KF(#AuNzQt<#5^? z8Z7MX;**`n@@ry1`zyBLQ091WI@i>Kmr6g$8#*6p$yF=B>Yw%|pLDr>@mcUdKS{5T z;>3L^(tqEb7asSw`o-kC8*K-JciYA&Lo{dtTT?-kn9>?kjRdQWAlcpd$6*tFM?mGa zANlOgV>~b|@Z^e`lpOuE;k1BCF>^D3&%|v-zsYVe>QZEBH}M6E{m*TI@+A{!-?$#7 zZ5>?uD9f11Ly=*zm=K~c`!%qbG4T_;?Nor;?Vp`H8&Ao5*^JV#bsqfu14oYixR&Os zZElAo)s;KvdcTe@(vXD~R*N3b6I?w_f|3}(#+;XHe%@Xct0!OH+OK7ERG#A5EFOjn zqVjjyy(t5PrKA^rFuIgGB$E4`i^M+8eNH6#ich$aS6K4oU6!_vn?_00hsT+Ox*wPT9;!&Sla0J#56_R{rjNzthG2ON>{06yuy9*;{Lzmm6Y-mpzo$YX_hD=0r@yST7l8hgO!1uX*i5fe zFXv;Y8o@?GzK-UfAAfA@h*5A(KeJ_XI#p&@CR(56HfMkMbal6XO`?)VWa|`iR6(=M zL}iO6ziA77rsK|rCbRx<_{g@)IpN>=?vVTodgS@NrXAves-0+&{vL0p?zg0eEbYzf z|LhmuHoM;MI2O=yuc`FCx?5eT#IF`;5~WU2tu=gnYW#6z;_jdJeT_fO+uHq1ZydV= zecBm(E+;)eKzMhtjOXxu97r%5qImkD(+}-%&1=KnB?%9#rrh0J?;m(#6tSoIL{?rh zo3|xm(4d*G$;6PB^?bodLexbnTtwmFvgsA9(SC$U*hSq#DB61aN9AY*$S8 zWVHE{2kAxSnVj%VQN6XLw?KUvd(=#lu;-m5Qz@@B#Urep7rkTKZmO4QL-ra%=dNw- zq&|mvF8x*BlkXm9p+NF9i>JASp^EYl4;_fM7aEahMVR;K?vX|pq=A4dk7*#VvJZE$ zje_g8U5;5D$Nj9K6ER*h{~0-5E;#FNg< zNqSfAze^1{*7-$w>hEblkF2#vB_&uB<3E=c z7$BIV)%8DLIF_q_H09&D16758-+a$ze6bZ=joFCE_e$97CTGhxk|3b6LD?uV$qC4w zRDU^L!5u%k;~t1bn6LgcZ`f^-dg^nhZ)%Dral6QQtM|KW+MExVNK)7k&P>1K*(=M( z)%+%>kaVJrKCzY*p}fqp!<5VXSY6J{FZsX?Zbg3v0Xoh$)2T!Dpn+Ly(-a0id5FfZ zxlY07fH}6=+~zg|L*^V*;Zt~CE`Pdxl`*8U3%6kOsgQ`usm&vH{Ggs`@wOhEU1>|d z*HY`bKQ6GcwUrk;V?f?UBDA?qRaIOM<|mAT0FyphlNinLjtO1Rr$&##$?`GBaY8NRr7v|MI z>+};+o@zO1MnBV!44OdOKf-cW)>-*s7RKcp{PdvzBKVPLk06^gUXWQ%6XOR&ZL|9TR-Z zFD?petbQ5m0Nsk6E$Z*R`#SVwMAr8&#_4!^EaRr!w|#U~U`@B>e`LVe>_*&2m5G5n zq4MG`bI*Q4(tJG8%gg}1rxp(Es_C2pv&YqvXjVE_Q`Y9aNBYB%-rYu^3F{e8ROffN zEpqDDb66tFltY{qOzXfv>6cUX%?#z!RrJ?nC4E+R)+%9Fb7TD6cAAv?9(NNZ3jU0w z1Hb8Y%csA`1$yy33EC<1jFhq6XFoBg4L5z{~eqA9gU$*aSruG zT9h<)>1-}#DIt2#Z+hkD)Qc*P+vgPRkqusbQl-{pIvS}}a<|rslWWXPqmeAByQ^3G z6-SE!`8|d{ot8me(rBNyX^KRqc;e63aaa?yum@$C?4%&1Swx}m_YUMHRZgz;q2#Db znUjO3O7@}vDlW4SUxixo(8z#7XI4mtklFEsNn0$5GJBSkUb|<&#uE+L4CCs|l5%r$ zhYLoQY!2(&0$aq9@>P{}r6t7tm#9#VaQ-;gu^(0o@sU&sjD%%_= z7Gm=KQ@9^T)y-~Y3^Lm6{}`s+eiXeJ!CPvDx_QsV@SZ-*cU0ezKotAjgsbh)?wm;W*=jKc*7@;kmBI)S&dWJ+? z1X}H;<%#YPOl;5q$Yv3M!gS)sO5_69?^nD9(M?}{KIz4I3Rh4i2eVYT^{Kg!z}0ERnp=N`9>!$sme=H^Y}3QLbi3zPPyIa#%8x>rl_>cJ8qZ<-s~q+_6dV?;Hw}ahC5=0OtWXCp-Jo z?cJH`2Lx+rG_K^wyE6pfa=?_CZt*c4A1QLTwT{f*5SGBo=HIqGY@-gUs?T*g2Nz%J z#{S{eCJze!r`{C=M@>9ISqwRR3;2?)eA*LMU;W7kGs=S&%HN8$!?KsY$Mcy7=`k?) z>9QZPt{6EJPJAa9lqHX8H2VZ$X1=^X1eJGuLs$y?^!RSdL38yDDPhq{0=O~=rjVJq z!yFIQ?f4S;d2YF<-iR-`;oDK6CKc<1CIrg$|FkcjSI}XV{5c0h&L-ewf^G7n5h1_W zQc-dqFYu7hbU-l<@QW$;WMe!axP^g7cI;@~mKF{d_xEC#Yt3c8V5H%Hz;)bOnImsQFT zu~|dZh7k?d#K&FK*$PXlzVmFb9cVMjzt4{>1r{0(@x!|{(8bu@8NphvuoxnRMZu_L zKS36515#vsYs)p@S#ZniE|&Ecd7OX`--@Wx<;?d6-pCWiuQ=(r)mcgZi=pl+U91ZW zZt$acb!G7s@u==>dD?Et5ZxXT{)}qXe?FHm0@0&&Z(ccqm}Wb%HQsreBeYCQ0VH@q zDB(Cl;m7!`EN72Bb29rO0DvO+L}c~3w02Tz$TaesPg~s$#!t^J^sp93|HxB3xDI_j zwh70Rk7ElmwL^R_j1_U2RY>B>>RCw!}U2` z1`z%=RqLQfA?QVAmH`7_yjSNJr@u^-mr^YVn866{8Q|eOZJ;lz#JtoZ+P0iLzBS30 zGV(7#h7zJEM6%G76S%V=moP=3HdQNvcVM=sm_WL#o!vI-1sGC0vR3P=`2f;O+e-aYt%~D9)KhIpnH0gN4)wFagF?q5 ztFnUeuQevYu34*PVp>ZHF9(v6o@cyqbG;_T#=lYgmf z;gmfL!#zztH!LIKy3_CGgdc$QVryKVvs6mZ*UU_Ac22e26#=SMj+EUfs>w2T83AUd zU)0<0?d9^)^S0z;H90{(N!foVAtZ{za7Cjt^OdvBr*3DPsItLuRUUI4vLlVPVUiKLxO zR$=%NRrl$(EN5I=2Eo22;EI;>Ghr!0(WnJ~U|#5oZL`O5=pb)%Fe`d8j=bpFB83zL z&`t;4q>I-vW9_$SKYE-FuBS#0*p^)^Vm}84Q2kabwj~9YUjun_rlZBa(7YM**c`37H^iLn9Cs@EgmK)Aou2L&W)R-HATCR`z{eiU zsAK{`v@0cUO$^WgO=9L2!_=>5VR;^{Ekn@d?e|n6h#c-00K6(G_+AkI9|@3=h%8eU zp{-&~h3y^pD`%b$r5*32dDw5ZeA{x$(r?JV=Qb^b+H&z;?wP(}N8kSZNd*7hq;&NMuZ2^{3|^*PsM>kHRN5Xe$Q;NqL?0Sg9%c={SoI2 zmh^~jgt+gu`X|q>>V}iktREg)4-F?*-$rrk9BgxGJ4qnnZ~PxO^>EKkcPYuim>U`M zycGiFGXlK?7l)wu_`T5R-FJs0&zU|CJIQI}G#>gjoe}P;51-d0Y@R{r><~p6Q$Dx! zz^uQg4wWj4BPpO`ISflI=SmFzZ&So8dJLljF+=VifQrD;gCA_&&qiFiwfF?viQ_W( zWKlROwy#$Sp@dHh7ANP$HJY!{ZdERsr`}A87j3eq zH{NzXR>5TW>T2u#f?hiSIQux~6UX}{%pVLrnL9YR`?+7l3?8>}_AY??Q++RG6lt58 zr%g_Dgp{U(j(fkpqDPAE44PP_q@0~g16i8$-R(%EG-5PD(Ra+?Ydg^+H_%2VBPg|g z3T{lUg+U+$W}cT3jKIYac;u`fg1>A3J1O1w_rcRat+^2&B!B|y8>&|<-7j@Sr%ypS z6N1;Wn1BWe#a@|_7^u0x!goAU)1Ue)jOP+61ZF3A1XOFsLbLcApNxUii& zO`4&IV6D(Ce}bJsUDpK|(7+L39sq&VDy*von{E?7-f<+bU_g#4mvuQQJ*L4a2g7WWoVbY=d=Mmbw*MvCnHetpTrr&azfZ>lp3RdBmR~G7^qq|K`Ia5XLt9(%E!Q&@v*45%q + + + + Ou Dedetai + + + + + + + + + + + + + + + + + + + + + Ou Dedetai + + + + diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 857c8e92..dfedf2ec 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -658,7 +658,7 @@ def ensure_launcher_executable(app=None): ) # Copy executable to config.INSTALLDIR. - launcher_exe = Path(f"{config.INSTALLDIR}/{config.name_binary})") + launcher_exe = Path(f"{config.INSTALLDIR}/{config.name_binary}") if launcher_exe.is_file(): logging.debug("Removing existing launcher binary.") launcher_exe.unlink() @@ -796,6 +796,7 @@ def create_launcher_shortcuts(): flproducti = get_flproducti_name(flproduct) src_dir = Path(__file__).parent logos_icon_src = src_dir / 'img' / f"{flproducti}-128-icon.png" + app_icon_src = src_dir / 'img' / 'icon.png' if installdir is None: reason = "because the installation folder is not defined." @@ -807,23 +808,31 @@ def create_launcher_shortcuts(): return app_dir = Path(installdir) / 'data' logos_icon_path = app_dir / logos_icon_src.name + app_icon_path = app_dir / app_icon_src.name if system.get_runmode() == 'binary': lli_executable = f"{installdir}/{config.name_binary}" else: script = Path(sys.argv[0]).expanduser().resolve() + repo_dir = None + for p in script.parents: + for c in p.iterdir(): + if c.name == '.git': + repo_dir = p + break # Find python in virtual environment. - py_bin = next(script.parent.glob('*/bin/python')) + py_bin = next(repo_dir.glob('*/bin/python')) if not py_bin.is_file(): msg.logos_warning("Could not locate python binary in virtual environment.") # noqa: E501 return - lli_executable = f"{py_bin} {script}" + lli_executable = f"env DIALOG=tk {py_bin} {script}" - if not logos_icon_path.is_file(): - app_dir.mkdir(exist_ok=True) - shutil.copy(logos_icon_src, logos_icon_path) - else: - logging.info(f"Icon found at {logos_icon_path}.") + for (src, path) in [(app_icon_src, app_icon_path), (logos_icon_src, logos_icon_path)]: # noqa: E501 + if not path.is_file(): + app_dir.mkdir(exist_ok=True) + shutil.copy(src, path) + else: + logging.info(f"Icon found at {path}.") # Set launcher file names and content. desktop_files = [ @@ -836,19 +845,23 @@ def create_launcher_shortcuts(): Icon={logos_icon_path} Terminal=false Type=Application +StartupWMClass={flproduct.lower()}.exe Categories=Education; +Keywords={flproduct};Logos;Bible;Control; """ ), ( - f"{flproduct}Bible-ControlPanel.desktop", + f"{config.name_binary}.desktop", f"""[Desktop Entry] -Name={flproduct}Bible Control Panel -Comment=Perform various tasks for {flproduct} app +Name={config.name_app} +Comment=Manages FaithLife Bible Software Exec={lli_executable} -Icon={logos_icon_path} +Icon={app_icon_path} Terminal=false Type=Application +StartupWMClass={config.name_binary} Categories=Education; +Keywords={flproduct};Logos;Bible;Control; """ ), ] diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh index 9efa669d..217cc15f 100755 --- a/scripts/build-binary.sh +++ b/scripts/build-binary.sh @@ -3,7 +3,7 @@ start_dir="$PWD" script_dir="$(dirname "$0")" repo_root="$(dirname "$script_dir")" cd "$repo_root" -if ! which pyinstaller >/dev/null 2>&1; then +if ! which pyinstaller >/dev/null 2>&1 || ! which oudedetai >/dev/null; then # Install build deps. python3 -m pip install .[build] fi diff --git a/scripts/run_app.py b/scripts/run_app.py index e1b8bfab..1247ef89 100755 --- a/scripts/run_app.py +++ b/scripts/run_app.py @@ -6,7 +6,9 @@ """ import re import sys -import ou_dedetai.main +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parents[1])) +import ou_dedetai.main # noqa: E402 if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(ou_dedetai.main.main()) From 327fa06816f6dacc13dc8260d4cefa2515ce4c6a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 21 Oct 2024 16:14:20 -0400 Subject: [PATCH 246/253] More name updates to Ou Dedetai --- ou_dedetai/gui.py | 2 +- ou_dedetai/gui_app.py | 10 +++++----- ou_dedetai/installer.py | 3 ++- ou_dedetai/main.py | 4 ++-- ou_dedetai/network.py | 4 ++-- ou_dedetai/tui_app.py | 4 ++-- ou_dedetai/utils.py | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index aed45ec8..a370744c 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -218,7 +218,7 @@ def __init__(self, root, *args, **kwargs): self.backups_label = Label(self, text="Backup/restore data") self.backup_button = Button(self, text="Backup") self.restore_button = Button(self, text="Restore") - self.update_lli_label = Label(self, text="Update Logos Linux Installer") # noqa: E501 + self.update_lli_label = Label(self, text=f"Update {config.name_app}") # noqa: E501 self.update_lli_button = Button(self, text="Update") # AppImage buttons self.latest_appimage_label = Label( diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 2757f31a..7436cd17 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -87,7 +87,7 @@ def __init__(self, new_win, root, **kwargs): # Set root parameters. self.win = new_win self.root = root - self.win.title("Faithlife Bible Software Installer") + self.win.title(f"{config.name_app} Installer") self.win.resizable(False, False) self.gui = gui.InstallerGui(self.win) @@ -560,7 +560,7 @@ class ControlWindow(): def __init__(self, root, *args, **kwargs): # Set root parameters. self.root = root - self.root.title("Faithlife Bible Software Control Panel") + self.root.title(f"{config.name_app} Control Panel") self.root.resizable(False, False) self.gui = gui.ControlGui(self.root) self.actioncmd = None @@ -745,7 +745,7 @@ def open_file_dialog(self, filetype_name, filetype_extension): def update_to_latest_lli_release(self, evt=None): self.start_indeterminate_progress() - self.gui.statusvar.set("Updating to latest Logos Linux Installer version…") # noqa: E501 + self.gui.statusvar.set(f"Updating to latest {config.name_app} version…") # noqa: E501 utils.start_thread(utils.update_to_latest_lli_release, app=self) def update_to_latest_appimage(self, evt=None): @@ -827,10 +827,10 @@ def update_latest_lli_release_button(self, evt=None): state = '!disabled' elif config.logos_linux_installer_status == 1: state = 'disabled' - msg = "This button is disabled. Logos Linux Installer is up-to-date." # noqa: E501 + msg = f"This button is disabled. {config.name_app} is up-to-date." # noqa: E501 elif config.logos_linux_installer_status == 2: state = 'disabled' - msg = "This button is disabled. Logos Linux Installer is newer than the latest release." # noqa: E501 + msg = f"This button is disabled. {config.name_app} is newer than the latest release." # noqa: E501 if msg: gui.ToolTip(self.gui.update_lli_button, msg) self.clear_status_text() diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index dfedf2ec..34a4eb1c 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -854,7 +854,8 @@ def create_launcher_shortcuts(): f"{config.name_binary}.desktop", f"""[Desktop Entry] Name={config.name_app} -Comment=Manages FaithLife Bible Software +GenericName=FaithLife Wine App Installer +Comment=Manages FaithLife Bible Software via Wine Exec={lli_executable} Icon={app_icon_path} Terminal=false diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 84cca545..bffe114f 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -25,7 +25,7 @@ def get_parser(): - desc = "Installs FaithLife Bible Software with Wine on Linux." + desc = "Installs FaithLife Bible Software with Wine." parser = argparse.ArgumentParser(description=desc) parser.add_argument( '-v', '--version', action='version', @@ -138,7 +138,7 @@ def get_parser(): ) cmd.add_argument( '--update-self', '-u', action='store_true', - help='Update Logos Linux Installer to the latest release.', + help=f'Update {config.name_app} to the latest release.', ) cmd.add_argument( '--update-latest-appimage', '-U', action='store_true', diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 2d9af840..1433e818 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -566,7 +566,7 @@ def update_lli_binary(app=None): lli_download_path = Path(config.MYDOWNLOADS) / config.name_binary temp_path = Path(config.MYDOWNLOADS) / f"{config.name_binary}.tmp" logging.debug( - f"Updating Logos Linux Installer to latest version by overwriting: {lli_file_path}") # noqa: E501 + f"Updating {config.name_app} to latest version by overwriting: {lli_file_path}") # noqa: E501 # Remove existing downloaded file if different version. if lli_download_path.is_file(): @@ -591,5 +591,5 @@ def update_lli_binary(app=None): return os.chmod(sys.argv[0], os.stat(sys.argv[0]).st_mode | 0o111) - logging.debug("Successfully updated Logos Linux Installer.") + logging.debug(f"Successfully updated {config.name_app}.") utils.restart_lli() diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 16918cbb..10924ce8 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -439,7 +439,7 @@ def main_menu_select(self, choice): daemon_bool=True, app=self, ) - elif choice.startswith("Update Logos Linux Installer"): + elif choice.startswith(f"Update {config.name_app}"): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": self.reset_screen() @@ -922,7 +922,7 @@ def set_tui_menu_options(self, dialog=False): status = config.logos_linux_installer_status error_message = config.logos_linux_installer_status_info.get(status) # noqa: E501 if status == 0: - labels.append("Update Logos Linux Installer") + labels.append(f"Update {config.name_app}") elif status == 1: # logging.debug("Logos Linux Installer is up-to-date.") pass diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index ab0f8f66..801342e6 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -165,7 +165,7 @@ def die(message): def restart_lli(): - logging.debug("Restarting Logos Linux Installer.") + logging.debug(f"Restarting {config.name_app}.") pidfile = Path(config.pid_file) if pidfile.is_file(): pidfile.unlink() From af706e8043073d73e616166b6ceada15120822df Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 21 Oct 2024 16:38:42 -0400 Subject: [PATCH 247/253] Update version and changelog --- CHANGELOG.md | 3 +++ ou_dedetai/config.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba89a57c..03d02df4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +- 4.0.0-beta.2 + - Fix #171 [N. Marti] + - Fix #185 [N. Marti] - 4.0.0-beta.1 - Migrate .config and logs from `~/.config/Logos_on_Linux` and `~/.local/state/Logos_on_Linux` to `~/.config/FaithLife-Community` and `~/.local/state/FaithLife-Community` - Add Logos State Manager [T. H. Wright, N. Marti] diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 26e31cbd..b78d8c15 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -71,7 +71,7 @@ L9PACKAGES = None LEGACY_CONFIG_FILE = os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-beta.1" +LLI_CURRENT_VERSION = "4.0.0-beta.2" LLI_LATEST_VERSION = None LLI_TITLE = name_app LOG_LEVEL = logging.WARNING From 9a14ecf87cbee4f14b15654e5cdcceca2b755c8a Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 21 Oct 2024 16:46:47 -0400 Subject: [PATCH 248/253] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f4928bd..557adb18 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This repository contains a Python program for installing and maintaining [FaithL This program is created and maintained by the FaithLife Community and is licensed under the MIT License. -## oudetai binary +## oudedetai binary The main program is a distributable executable binary and contains Python itself and all necessary Python packages. From 0a767bcbd6dc87adb92d03dd1ed809bd7f4bbe48 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 21 Oct 2024 23:14:05 -0400 Subject: [PATCH 249/253] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 557adb18..8c8e91f5 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ # Ou Dedetai ->Remember Jesus Christ, risen from the dead, the offspring of David, as preached in my gospel, for which I am suffering, bound with chains as a criminal. **But the word of God is not bound!** -ἀλλʼ **ὁ λόγος** τοῦ θεοῦ **οὐ δέδεται** +>Remember Jesus Christ, risen from the dead, the offspring of David, as preached in my gospel, for which I am suffering, bound with chains as a criminal. But **the word** of God **is not bound!** > -> Second Timothy 2:8–9, ESV +>ἀλλʼ **ὁ λόγος** τοῦ θεοῦ **οὐ δέδεται** + +—Second Timothy 2:8–9 [ESV](https://biblia.com/bible/esv/2-timothy/2/8-9), [NA28](https://biblia.com/bible/ubs5/2-timothy/2/9) ## Manages Logos Bible Software via Wine @@ -15,7 +16,6 @@ This repository contains a Python program for installing and maintaining [FaithL This program is created and maintained by the FaithLife Community and is licensed under the MIT License. - ## oudedetai binary The main program is a distributable executable binary and contains Python itself and all necessary Python packages. From ac7f186660c3b813d17d082ba76869676852e90c Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Mon, 21 Oct 2024 23:15:08 -0400 Subject: [PATCH 250/253] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8c8e91f5..5ea04711 100644 --- a/README.md +++ b/README.md @@ -137,3 +137,6 @@ NOTE: You can run **Ou Dedetai** using the Steam Proton Experimental binary, whi If you want to install your distro's dependencies outside of the script, please see the [System Dependencies wiki page](https://github.com/FaithLife-Community/LogosLinuxInstaller/wiki/System-Dependencies). +--- + +Soli Deo Gloria From bb3e2d5cdae614a1075546d40032deca040296bc Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:25:40 +0000 Subject: [PATCH 251/253] docs: update README with new invocation There is no ./main.py anymore. Perhaps we should re-word this documentation to use the pypi package instead --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ea04711..b64771ca 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ LogosLinuxInstaller$ source env/bin/activate # activate the env Python 3.12.5 (env) LogosLinuxInstaller$ python -m tkinter # verify that tkinter test window opens (env) LogosLinuxInstaller$ pip install -r requirements.txt # install python packages -(env) LogosLinuxInstaller$ ./main.py --help # run the script +(env) LogosLinuxInstaller$ python -m ou_dedetai.main --help # run the script ``` ### Building using docker From 1fc367a35af4966f88d6397c606ef2e73378a484 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:15:19 +0000 Subject: [PATCH 252/253] fix: complete wine refactor Looks like some renames didn't get picked up in the first wave --- ou_dedetai/tui_app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 10924ce8..14d32d81 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -486,10 +486,10 @@ def winetricks_menu_select(self, choice): self.go_to_main_menu() elif choice == "Install d3dcompiler": self.reset_screen() - wine.installD3DCompiler() + wine.install_d3d_compiler() elif choice == "Install Fonts": self.reset_screen() - wine.installFonts() + wine.install_fonts() self.go_to_main_menu() elif choice == "Set Renderer": self.reset_screen() @@ -566,7 +566,7 @@ def utilities_menu_select(self, choice): self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) elif choice == "Install ICU": self.reset_screen() - wine.installICUDataFiles() + wine.install_icu_data_files() self.go_to_main_menu() elif choice.endswith("Logging"): self.reset_screen() From 0def2e5b99225db5de97fea97acaaaa0d8efa52b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 23 Oct 2024 03:57:18 -0700 Subject: [PATCH 253/253] fix: pull latest github release In some cases the first release may be a pre-release or otherwise may not be the latest. Use the /latest url to get the "latest" according to github --- ou_dedetai/network.py | 31 ++++++++++++++----------------- ou_dedetai/wine.py | 6 +++--- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 1433e818..9c55b9a7 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -367,8 +367,9 @@ def same_size(url, file_path): return res -def get_latest_release_data(releases_url): - data = net_get(releases_url) +def get_latest_release_data(repository): + release_url = f"https://api.github.com/repos/{repository}/releases/latest" + data = net_get(release_url) if data: try: json_data = json.loads(data.decode()) @@ -376,20 +377,16 @@ def get_latest_release_data(releases_url): logging.error(f"Error decoding JSON response: {e}") return None - if not isinstance(json_data, list) or len(json_data) == 0: - logging.error("Invalid or empty JSON response.") - return None - else: - return json_data + return json_data else: logging.critical("Could not get latest release URL.") return None -def get_latest_release_url(json_data): +def get_first_asset_url(json_data): release_url = None if json_data: - release_url = json_data[0].get('assets')[0].get('browser_download_url') + release_url = json_data.get('assets')[0].get('browser_download_url') logging.info(f"Release URL: {release_url}") return release_url @@ -397,18 +394,18 @@ def get_latest_release_url(json_data): def get_tag_name(json_data): tag_name = None if json_data: - tag_name = json_data[0].get('tag_name') + tag_name = json_data.get('tag_name') logging.info(f"Release URL Tag Name: {tag_name}") return tag_name def set_logoslinuxinstaller_latest_release_config(): if config.lli_release_channel is None or config.lli_release_channel == "stable": # noqa: E501 - releases_url = "https://api.github.com/repos/FaithLife-Community/LogosLinuxInstaller/releases" # noqa: E501 + repo = "FaithLife-Community/LogosLinuxInstaller" else: - releases_url = "https://api.github.com/repos/FaithLife-Community/test-builds/releases" # noqa: E501 - json_data = get_latest_release_data(releases_url) - logoslinuxinstaller_url = get_latest_release_url(json_data) + repo = "FaithLife-Community/test-builds" + json_data = get_latest_release_data(repo) + logoslinuxinstaller_url = get_first_asset_url(json_data) if logoslinuxinstaller_url is None: logging.critical(f"Unable to set {config.name_app} release without URL.") # noqa: E501 return @@ -421,10 +418,10 @@ def set_logoslinuxinstaller_latest_release_config(): def set_recommended_appimage_config(): - releases_url = "https://api.github.com/repos/FaithLife-Community/wine-appimages/releases" # noqa: E501 + repo = "FaithLife-Community/wine-appimages" if not config.RECOMMENDED_WINE64_APPIMAGE_URL: - json_data = get_latest_release_data(releases_url) - appimage_url = get_latest_release_url(json_data) + json_data = get_latest_release_data(repo) + appimage_url = get_first_asset_url(json_data) if appimage_url is None: logging.critical("Unable to set recommended appimage config without URL.") # noqa: E501 return diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 36093480..ac096385 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -435,9 +435,9 @@ def set_win_version(exe, windows_version): def install_icu_data_files(app=None): - releases_url = "https://api.github.com/repos/FaithLife-Community/icu/releases" # noqa: E501 - json_data = network.get_latest_release_data(releases_url) - icu_url = network.get_latest_release_url(json_data) + repo = "FaithLife-Community/icu" + json_data = network.get_latest_release_data(repo) + icu_url = network.get_first_asset_url(json_data) # icu_tag_name = utils.get_latest_release_version_tag_name(json_data) if icu_url is None: logging.critical(f"Unable to set {config.name_app} release without URL.") # noqa: E501