Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add abstract app module and refactor config #206

Merged

Conversation

ctrlaltf24
Copy link
Contributor

@ctrlaltf24 ctrlaltf24 commented Oct 24, 2024

Improvements

Type Enforcement

Types are checked using a static analysis tool (mypy) to ensure nothing is None when it shouldn't be (or a Path when it should be a string)

Network Caching

All network requests are cached

Config Enforcement

Wrapper struct to ensure all config variables are set, if not the user is prompted (invalid answers are prompted again).

Additionally configuration hooks were added so all UIs can refresh themselves if the config changes underneath them. A reload function has also been added (TUI only as it's for power users and offers no benefit to the cli)

Config Format Change

In the interest of consistency of variable names, the config keys have changed. Legacy values will continue to be read (and written). New keys take precedence. This new config can be copied to older versions of the installer and it should (mostly) work (not aware of any deficiencies however downgrading is hard to support). Legacy paths are moved to the new location.

Graphical User Interface (tinker)

Ensured that the ask function was never called. We want to use the given UI, as it's prettier. However if the case should arise where we accidentally access a variable before we set it, the user will see a pop-up with the one question. Data flow has been changed to pull all values from the config, it no longer stores copies. It also now populates all drop-downs in real-time as other drop-downs are changed, as a result there was no longer any need for the "Get EXE" and "Get Release List" buttons, so they were removed. Progress bar now considers the entire installation rather than "completing" after each step.

Terminal User Interface (curses)

Appears the same as before from the user's standpoint, however now there is a generic ask page, greatly cutting down on the number of queues/Events. There may be some more unused Queues/Events lingering, didn't see value in cleaning them up.

Command Line Interface

Prompts appears the same as before, progress bar is now prepended to status lines

Misc other changes

  • renamed arch p7zip package to 7zip
  • use winecfg rather than winetricks to set win version

Closing Notes

One goal of this refactor was to keep the code from being understood by someone who is already familiar with it. There is a couple new concepts, the abstract base class, reworked config, and network cache, but the core logic of how it works remains unchanged. If something isn't clear please ask.

Screenshots

GUI Sample

GUI-sample-prompt

TUI Sample

TUI-prompt

CLI Sample

CLI-prompt

GUI behavior

Before Product is selected
GUI-before-product-selected
After product is selected
GUI-after-product-selected

Fixes: #147, #234, #35, #168, #155

Works on all three UIs offers a generic function to ask a question that platform independent.
If the user fails to offer a response, the installer will terminate.

In the GUI this still works, however it may not be desirable to prompt the user for each question.
So long as we don't attempt to access the variable before the user has had a chance to put in their preferences it will not prompt them
Changed the GUI to gray out the other widgets if the product is not selected.
start_ensure_config is called AFTER product is set, if it's called before it attempts to figure out which platform it's on, prompting the user with an additional dialog (not ideal, but acceptable)
@ctrlaltf24 ctrlaltf24 force-pushed the feat-platform-independent-prompts branch from 8042817 to 82d0c94 Compare October 24, 2024 07:45

def get_version(self, dialog):
self.product_e.wait()
question = f"Which version of {config.FLPRODUCT} should the script install?" # noqa: E501
question = f"Which version of {self.conf.faithlife_product} should the script install?" # noqa: E501
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge, I need to wait on self.product_e.wait() as if not, the TUI charges through the installer process; should this way now be handled by the TUI's implementation of app._hook_product_update()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surprisingly enough it didn't charge through - it waited for the question to be answered before going to the next screen probably because we have two main threads in the TUI:

  1. The input processing thread (main, don't block me)
  2. When events are triggered they're spawned on a new thread (this is the installation thread). Since the ask call is blocking, this gets stopped while the user is inputting their value (which the processing is done on the first thread)

Then if you notice in TUI's ask implementation there is an event wait to communicate between the two threads

@thw26
Copy link
Collaborator

thw26 commented Oct 24, 2024

Comments on Demonstration

Thanks for this! The framework you have here looks great.

The TUI is a Frankenstein of my own thought, so anything that simplifies it and makes it less bloated is great—I do like seeing lines of code removed.

As mentioned to Nate, I see tui_screen.py as a library that could feasibly be used outside of our project, so also the same with certain aspects of the display code in tui_app.py, say lines 1–350.

import logging
import os
import signal
import threading
import time
import curses
from pathlib import Path
from queue import Queue
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 = ""
# TODO: Fix hitting cancel in Dialog Screens; currently crashes program.
class TUI:
def __init__(self, stdscr):
self.stdscr = stdscr
# 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
self.logos = logos.LogosManager(app=self)
self.tmp = ""
# 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()
self.screen_q = Queue()
self.choice_q = Queue()
self.switch_q = Queue()
# 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.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.wines_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.confirm_q = Queue()
self.confirm_e = threading.Event()
self.password_q = Queue()
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 = []
self.menu_options = []
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]))
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)
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)
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)
@staticmethod
def set_curses_style():
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)
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 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 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()
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():
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()
curses.cbreak()
self.stdscr.keypad(True)
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))
#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()
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)
curses.nocbreak()
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…")
self.llirunning = False
curses.endwin()
def update_main_window_contents(self):
self.clear()
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.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()
self.refresh()
msg.status("Window resized.", 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:
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()
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:
tui_curses.write_line(self, self.resize_window, i, margin, line, self.window_width - config.margin, curses.A_BOLD)
self.refresh()
def display(self):
signal.signal(signal.SIGWINCH, self.signal_resize)
signal.signal(signal.SIGINT, self.end)
msg.initialize_tui_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:
config.margin = 2
if not config.resizing:
self.update_windows()
self.active_screen.display()
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 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()
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:
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)

(Given the hope of simplifying our queue/event code, many of these lines could be squashed/removed.) The task processor code in tui_app and in gui_app could also be brought into the abstract class. I would also hope that the choice_processor code in tui_app.py might find its way there.

@n8marti has handled the GUI, so I will leave that to him. (Given your review, you might be the third person we've needed: someone who understands both the TUI and the GUI, haha.)

I originally tried to code for the various UIs particularly in msg.py. This eventually got away from me and found its way back in msg.status(). There's likely a fair chunk of room for messaging to be brought into the abstract class given how much code is relatively unused in that module and how msg.status is accounting for each UI. The abstract class lets us do that without all the if/elif.

General Comments

I had also tried this in installer, but the GUI needed enough odds and ends to be separated at the time. Now that we have our working base, I think it'd be great to try to reel in the various odd bits and make these more united, all in the spirit of #147.

As mentioned elsewhere, this would also be helpful for #2 and #87, and for drastically improving code reusability/maintenance. Given your further comments about the suggested config changes, that would go well with #187. While I think all these issues are too much for one PR, I do think we could lump #147 and #187 into this PR's scope as a way of refactoring our code.

Thinking Out Loud

This framework might enable me to further simplify the various calls within tui_app to tui_screen. tui_curses and tui_dialog are fairly static at this point. There are some parts of tui_screen that need to be abstracted, particularly in the console_screen. I've also considered changing the tui_screen class to utilize a method of the tui_app that flags the need for tui_app.refresh to be run again.

@n8marti
Copy link
Collaborator

n8marti commented Nov 8, 2024

Am I understanding correctly that in the GUI the user will see the "Choose which FaithLife product" window first, then they will see the GUI installer window, with all the options pre-populated with defaults? So then they can make adjustments, or just click "Install"?

@ctrlaltf24
Copy link
Contributor Author

Am I understanding correctly that in the GUI the user will see the "Choose which FaithLife product" window first, then they will see the GUI installer window, with all the options pre-populated with defaults? So then they can make adjustments, or just click "Install"?

Not quite, I liked the current GUI flow so much I didn't want to modify it. Before and after this PR it behaves the same, however if someday in the future there was a code path that tried to retrieve the faithlife product before the prompt showed up, it would open a separate dialog asking that question. In the GUI's case we probably want to avoid this, however the code will handle that case and avoid an error if such a code path were to exist in the future.

@n8marti
Copy link
Collaborator

n8marti commented Nov 11, 2024

Am I understanding correctly that in the GUI the user will see the "Choose which FaithLife product" window first, then they will see the GUI installer window, with all the options pre-populated with defaults? So then they can make adjustments, or just click "Install"?

Not quite, I liked the current GUI flow so much I didn't want to modify it. Before and after this PR it behaves the same, however if someday in the future there was a code path that tried to retrieve the faithlife product before the prompt showed up, it would open a separate dialog asking that question. In the GUI's case we probably want to avoid this, however the code will handle that case and avoid an error if such a code path were to exist in the future.

Okay, I'm happy with that.

@n8marti n8marti self-requested a review November 11, 2024 13:59
@n8marti
Copy link
Collaborator

n8marti commented Nov 11, 2024

If you @ctrlaltf24 can take care of the potential merge conflicts, then I'll look it over again for approval.

@ctrlaltf24 ctrlaltf24 marked this pull request as draft November 12, 2024 21:53
@ctrlaltf24
Copy link
Contributor Author

Expanding usage of this framework....

@thw26 thw26 mentioned this pull request Nov 13, 2024
@n8marti
Copy link
Collaborator

n8marti commented Jan 13, 2025

With a finished install and restored backup files if I click on "Run Logos" I'm not seeing the Logos window open. Here's what I have in System Monitor, several minutes after clicking the button:
image
I'll try again without using the -P flag to make sure Logos installs correctly, but all the files seem to be there after a quick visual check.

Before the install button wouldn't enable when the network cache returned [], this new logic is more robust

Tested:
- editing network cache manually and setting the releases to an empty array
- editing config to remove the release
- editing both of the above to remove both

System recovered in all cases
@ctrlaltf24
Copy link
Contributor Author

Further GUI testing on a clean install. I ran it once successfully, but then the 2nd time the "Install" button never became activated. Here are the two log files. Maybe there's a race condition for the network check? oudedetai_1st-run.log oudedetai_2nd-run.log

Fixed in 9bac0f1

@ctrlaltf24
Copy link
Contributor Author

ctrlaltf24 commented Jan 13, 2025

With a finished install and restored backup files if I click on "Run Logos" I'm not seeing the Logos window open. Here's what I have in System Monitor, several minutes after clicking the button:

what were the results of looking at the wine.log?

@ctrlaltf24
Copy link
Contributor Author

Missing confirmation question during "restore". Should say something like "Restore backup from [latest folder path]?"

2025-01-13 18:00:07 INFO: Found 2 backup folders.
2025-01-13 18:00:07 INFO: Latest folder: /media/nate/Storage-250/LLI-data-backups/Logos10-20250113T175000

: Yes [default], No, Exit:

fixed in 5e14603

@n8marti
Copy link
Collaborator

n8marti commented Jan 14, 2025

With a finished install and restored backup files if I click on "Run Logos" I'm not seeing the Logos window open. Here's what I have in System Monitor, several minutes after clicking the button:

what were the results of looking at the wine.log?

Nothing in wine.log besides the subprocess command. But here's what I see in LogosBible10/data/wine64_bottle/drive_c/users/nate/AppData/Local/Faithlife/Logs/Logos/LogosError.log after a failed run with --run-installed-app:

Program Version: 38.1 (38.1.0.0002)
Time: 2025-01-14 14:15:50 +01:00 (2025-01-14T13:15:50Z)
SQLite Error 283: recovered 10 frames from WAL file C:\users\nate\AppData\Local\Logos\Documents\dsck2rmb.rvb\LocalUserPreferences\PreferencesManager.db-wal

Program Version: 38.1 (38.1.0.0002)
Time: 2025-01-14 14:15:50 +01:00 (2025-01-14T13:15:50Z)
SQLite Error 283: recovered 10 frames from WAL file C:\users\nate\AppData\Local\Logos\Data\dsck2rmb.rvb\Errors\Application\ErrorReportManager.db-wal

Program Version: 38.1 (38.1.0.0002)
Time: 2025-01-14 14:15:50 +01:00 (2025-01-14T13:15:50Z)
SQLite Error 2058: os_win.c:49869: (33) winUnlockReadLock(C:\users\nate\AppData\Local\Logos\Data\dsck2rmb.rvb\Errors\Application\ErrorReportManager.db) - Violation de verrou.

*** Application Crash ***
Program Version: 38.1 (38.1.0.0002)
Windows Version: 10.0.19043.0
.NET Framework Version: 8.0.10
Time: 2025-01-14 14:15:50 +01:00 (2025-01-14T13:15:50Z)
Installed memory: 15,697 MB
Install path: C:\users\nate\AppData\Local\Logos\System\Logos.dll
Free install space: 0 MB
Data path: C:\users\nate\AppData\Local\Logos\Data\dsck2rmb.rvb
Free disk space: 0 MB
Temp path: C:\users\nate\AppData\Local\Temp\
Free temp space: 0 MB

Error ID: 8409
Error detail: AggregateException: One or more errors occurred. (Could not load file or assembly 'Logos.BibleStudyBuilderApi.v1, Version=3.6.0.0, Culture=neutral, PublicKeyToken=null'. Fichier introuvable.)

System.AggregateException: One or more errors occurred. (Could not load file or assembly 'Logos.BibleStudyBuilderApi.v1, Version=3.6.0.0, Culture=neutral, PublicKeyToken=null'. Fichier introuvable.)
 ---> System.IO.FileNotFoundException: Could not load file or assembly 'Logos.BibleStudyBuilderApi.v1, Version=3.6.0.0, Culture=neutral, PublicKeyToken=null'. Fichier introuvable.
File name: 'Logos.BibleStudyBuilderApi.v1, Version=3.6.0.0, Culture=neutral, PublicKeyToken=null'
   at LDLS4.Startup.StartupManager.Steps.CreateServiceClients(AsyncMethodContext context, LicenseManager licenseManager, ResourceManager resourceManager, LibraryCatalog libraryCatalog, Boolean shouldWorkOnline, User user, ApplicationFamily family, WebServiceRequestSettings requestSettings, Func`2 createFacilityClientSettings)
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at LDLS4.Startup.StartupManager.Steps.CreateServiceClients(AsyncMethodContext context, LicenseManager licenseManager, ResourceManager resourceManager, LibraryCatalog libraryCatalog, Boolean shouldWorkOnline, User user, ApplicationFamily family, WebServiceRequestSettings requestSettings, Func`2 createFacilityClientSettings)
   at LDLS4.Startup.StartupManager.StartServices(AsyncMethodContext context, User user, DigitalLibraryServices services, Boolean shouldWorkOnline, FeedbackLevel effectiveFeedbackLevel, ApplicationFamily family)
   at LDLS4.Startup.StartupManager.DoRun(AsyncMethodContext context)
   at LDLS4.Startup.StartupManager.Run(AsyncMethodContext context)
   at LDLS4.AppModel.<>c__DisplayClass691_0.<<Run>b__0>d.MoveNext()
   --- End of inner exception stack trace ---

Running directly from the terminal opens correctly and does not show any "Could not load" errors. However, in this same installation subsequent attempts to open with the --run-installed-app option seem to consistently throw this System.OutOfMemoryException:

Program Version: 38.1 (38.1.0.0002)
Time: 2025-01-14 15:04:03 +01:00 (2025-01-14T14:04:03Z)
SQLite Error 2058: os_win.c:49869: (33) winUnlockReadLock(C:\users\nate\AppData\Local\Logos\Data\dsck2rmb.rvb\Errors\Application\ErrorReportManager.db) - Violation de verrou.

*** Application Crash ***
Program Version: 38.1 (38.1.0.0002)
Windows Version: 10.0.19043.0
.NET Framework Version: 8.0.10
Time: 2025-01-14 15:04:03 +01:00 (2025-01-14T14:04:03Z)
Installed memory: 15,697 MB
Install path: C:\users\nate\AppData\Local\Logos\System\Logos.dll
Free install space: 46,158 MB
Data path: C:\users\nate\AppData\Local\Logos\Data\dsck2rmb.rvb
Free disk space: 46,158 MB
Temp path: C:\users\nate\AppData\Local\Temp\
Free temp space: 46,158 MB

Error ID: 3650
Error detail: AggregateException: One or more errors occurred. (Exception of type 'System.OutOfMemoryException' was thrown.)

System.AggregateException: One or more errors occurred. (Exception of type 'System.OutOfMemoryException' was thrown.)
 ---> System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Threading.Thread.StartCore()
   at Logos.Sync.Client.SyncManager.Start()
   at LDLS4.AppModel.AppStartupSteps.StartPostSetFactoryServicesWorkCore(AsyncMethodContext context)
   at LDLS4.Startup.ExternalStartupSteps.StartPostSetFactoryServicesWork(AsyncMethodContext context)
   at LDLS4.Startup.StartupManager.StartServices(AsyncMethodContext context, User user, DigitalLibraryServices services, Boolean shouldWorkOnline, FeedbackLevel effectiveFeedbackLevel, ApplicationFamily family)
   at LDLS4.Startup.StartupManager.DoRun(AsyncMethodContext context)
   at LDLS4.Startup.StartupManager.Run(AsyncMethodContext context)
   at LDLS4.AppModel.<>c__DisplayClass691_0.<<Run>b__0>d.MoveNext()
   --- End of inner exception stack trace ---

I have no idea what memory that would be referring to. There are no low-memory errors in journalctl -b, and I hardly see the total memory usage move at all when observing System Monitor while using --run-installed-app.

@n8marti
Copy link
Collaborator

n8marti commented Jan 14, 2025

This patch solves both the hung process issue and the issue of Logos not starting after a clean install. Take a look at it and see if you're happy with it:
fix-hung-procs-and-no-logos-start.diff.zip

ctrlaltf24 and others added 3 commits January 14, 2025 09:54
Also remove -p flag to check_wineserver,
both found by Nate to be ineffective

Co-Authored by: Nate Marti <[email protected]>
@thw26 thw26 linked an issue Jan 14, 2025 that may be closed by this pull request
@thw26 thw26 mentioned this pull request Jan 14, 2025
@thw26 thw26 linked an issue Jan 14, 2025 that may be closed by this pull request
Comment on lines +79 to +83
os.makedirs(os.path.dirname(app_log_path), exist_ok=True)

# Ensure log file parent folders exist.
log_parent = Path(config.LOGOS_LOG).parent
if not log_parent.is_dir():
log_parent.mkdir(parents=True)
log_parent = Path(app_log_path).parent
log_parent.mkdir(parents=True, exist_ok=True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't running os.makedirs redundant with log_parent.mkdir?

@n8marti
Copy link
Collaborator

n8marti commented Jan 15, 2025

I'm very happy with the simplifications and strong foundation that this refactor brings in. I see some typos and other minor fixes that should be done, as well as a full conversion to ruff linting, but those shouldn't hold up this merge. We can do a cleanup PR in the near future.

was removed due to unused before, now it's used to filter out v39
observed when window was resized
```
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/main.py", line 447, in <module>
    main()
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/main.py", line 442, in main
    run(ephemeral_config, action)
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/main.py", line 380, in run
    action(ephemeral_config)  # run control_panel right away
    ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/main.py", line 339, in run_control_panel
    raise e
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/main.py", line 328, in run_control_panel
    curses.wrapper(tui_app.control_panel_app, ephemeral_config)
  File "/home/user/.local/lib/python3.12/curses/__init__.py", line 94, in wrapper
    return func(stdscr, *args, **kwds)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/tui_app.py", line 1235, in control_panel_app
    TUI(stdscr, ephemeral_config).run()
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/tui_app.py", line 463, in run
    self.display()
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/tui_app.py", line 422, in display
    self.active_screen.display()
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/tui_screen.py", line 244, in display
    time.sleep(0.1)
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/tui_app.py", line 364, in signal_resize
    self.resize_curses()
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/tui_app.py", line 360, in resize_curses
    logging.debug("Window resized.", self)
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 2226, in debug
    root.debug(msg, *args, **kwargs)
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 1527, in debug
    self._log(DEBUG, msg, args, **kwargs)
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 1684, in _log
    self.handle(record)
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 1700, in handle
    self.callHandlers(record)
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 1762, in callHandlers
    hdlr.handle(record)
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 1022, in handle
    rv = self.filter(record)
         ^^^^^^^^^^^^^^^^^^^
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 858, in filter
    result = f.filter(record)
             ^^^^^^^^^^^^^^^^
  File "/home/user/LogosLinuxInstaller-bravo/ou_dedetai/msg.py", line 42, in filter
    current_message = record.getMessage()
                      ^^^^^^^^^^^^^^^^^^^
  File "/home/user/.local/lib/python3.12/logging/__init__.py", line 392, in getMessage
    msg = msg % self.args
          ~~~~^~~~~~~~~~~
TypeError: not all arguments converted during string formatting
```
Setting switch_q to 1 when contents were changed removed the pending ask screen, causing install to stop halfway

Also it's possible for the TUI to ask for the installed_faithlife_product_release before install_dir is set, causing two threads to both be asking, causing a invalid response. The application would probably recover, but it's not ideal.
@ctrlaltf24
Copy link
Contributor Author

Tested clean installs on TUI, GUI, and CLI one last time, all get to logos login page (needed a couple very simple bug fixes)

PR ready to Squash and merge @thw26 (using this PR's description as the commit body). Don't want these individual commits on main, as the code isn't stable in between all the commits, no reason to have the history for what in essence is a scratch pad. Commit history will remain in this PR if we need it later.

@thw26 thw26 merged commit b5e677a into FaithLife-Community:main Jan 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants