From d1b8c590f25a91b170090ea1040b84d7138c36e3 Mon Sep 17 00:00:00 2001 From: "T. H. Wright" Date: Tue, 27 Aug 2024 23:57:35 -0400 Subject: [PATCH 01/18] 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 732a3ba6..007c526d 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': 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': 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': 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': 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': 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 5908cdda073b12e60273915fdf3a327503c0328b Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 05:59:47 +0100 Subject: [PATCH 02/18] 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 f92d531f83d4b639960bd559af941e5ed23ad170 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 06:00:51 +0100 Subject: [PATCH 03/18] 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 5d3774c1e332d0866b4855de7848b427aeb99403 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 07:21:03 +0100 Subject: [PATCH 04/18] 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 2f0b0ffd7e3e82ea01cb2fb950828b9ea74d993a Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 07:22:22 +0100 Subject: [PATCH 05/18] 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 b43e6dc7e1219604fe3b90e2f314cdc00c3e0661 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 14:52:40 +0100 Subject: [PATCH 06/18] 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 007c526d..e0bc5379 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 c918db560c624c944b07cd3f719390a1bf8e4845 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 15:08:14 +0100 Subject: [PATCH 07/18] 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 8a82699517007242607350087d1b9d542432f00f Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 15:08:41 +0100 Subject: [PATCH 08/18] 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 1008fc5bf2fcb2a552c21f8280ef0f0310cce32c Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 15:10:15 +0100 Subject: [PATCH 09/18] 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 b63ed02eab3e1975fb52acf3106cb4abb0cbde0e Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Sat, 5 Oct 2024 19:24:01 +0100 Subject: [PATCH 10/18] 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 075da8f6c19c2eb145341ff41fc4e1ee05325d21 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 10:06:59 +0100 Subject: [PATCH 11/18] 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 aa8fc026543be8f65762d2b02e86819db5f93804 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 10:53:48 +0100 Subject: [PATCH 12/18] 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 e0bc5379..98dd3c7c 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 11315d1464a683364801ee6e322ac398cf145dde Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 11:12:22 +0100 Subject: [PATCH 13/18] 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 b3fce6e8f0b4f38d079cc1be0c056d30852638fc Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 13:00:41 +0100 Subject: [PATCH 14/18] 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 98dd3c7c..9a840149 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 0814ecdb5fd96a30f0933c7aea8594ea20a66ffb Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 13:41:05 +0100 Subject: [PATCH 15/18] 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 96280ded1157de48a871f50827cc6cb908325ee0 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 14:32:56 +0100 Subject: [PATCH 16/18] 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 a28a838d7f5c64330c79d364f167e77543b4cd3e Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 14:59:02 +0100 Subject: [PATCH 17/18] 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 f71731e1a95caed855d892c12159bcd118e69593 Mon Sep 17 00:00:00 2001 From: Nate Marti Date: Mon, 7 Oct 2024 15:23:44 +0100 Subject: [PATCH 18/18] 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()