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

Open
wants to merge 141 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
141 commits
Select commit Hold shift + click to select a range
82d0c94
feat: generic prompts
ctrlaltf24 Oct 24, 2024
a3bfa78
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 Nov 12, 2024
60860f8
refactor: move constants to their own file
ctrlaltf24 Nov 12, 2024
a3244a7
refactor: move function from config to utils
ctrlaltf24 Nov 12, 2024
4676033
feat(wip): move the install prompts into the abstract class
ctrlaltf24 Nov 13, 2024
923cbbc
refactor: function never called with an argument
ctrlaltf24 Nov 13, 2024
c97b5f5
fix(wip): add prompting and started new storage implementation
ctrlaltf24 Nov 13, 2024
a2001e8
feat(wip): more progress
ctrlaltf24 Nov 13, 2024
e27d248
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 Nov 23, 2024
630bbcf
feat: load legacy envs into new framework
ctrlaltf24 Nov 23, 2024
c68e29f
fix: migrate CUSTOMBINPATH
ctrlaltf24 Nov 23, 2024
43d7644
fix: migrate LOGOS_VERSION
ctrlaltf24 Nov 23, 2024
49a3c8c
fix: migrate LOGOS_EXECUTABLE
ctrlaltf24 Nov 23, 2024
3246bc5
fix: remove unused LOGOS64_MSI
ctrlaltf24 Nov 23, 2024
d930d47
fix: migrate LOGOS64_URL
ctrlaltf24 Nov 23, 2024
197609c
fix: migrate WINEDLLOVERRIDES
ctrlaltf24 Nov 23, 2024
4b76466
fix: migrate SKIP_WINETRICKS
ctrlaltf24 Nov 23, 2024
65e5369
fix: remove unused REINSTALL_DEPENDENCIES
ctrlaltf24 Nov 23, 2024
ac5308b
fix: migrate WINEDEBUG, VERBOSE, DEBUG and LOG_LEVEL
ctrlaltf24 Nov 24, 2024
7c162e0
fix: migrate WINEPREFIX
ctrlaltf24 Nov 24, 2024
45067bf
fix: migrate wine_log and standardize config naming
ctrlaltf24 Nov 24, 2024
d53ba49
feat: migrate LOGOS_LOG
ctrlaltf24 Nov 24, 2024
b138e52
fix: migrate INSTALLDIR
ctrlaltf24 Nov 24, 2024
07df426
fix: migrate WINE_EXE
ctrlaltf24 Nov 24, 2024
29af5ad
refactor: passed config explicitly
ctrlaltf24 Nov 24, 2024
ac353bf
fix: migrate INSTALL_STEP
ctrlaltf24 Nov 24, 2024
6045412
fix: migrate WINETRICKS_UNATTENDED
ctrlaltf24 Nov 24, 2024
f87448b
fix: plum new configuration through to main
ctrlaltf24 Nov 24, 2024
443c6a8
fix: misc
ctrlaltf24 Nov 24, 2024
cdc8e09
fix: misc
ctrlaltf24 Nov 24, 2024
2c5a4fa
fix: migrate LOGOS_EXE
ctrlaltf24 Nov 24, 2024
ec6abe3
fix: migrate DELETE_LOG
ctrlaltf24 Nov 24, 2024
b06c38c
fix: migrate CHECK_UPDATES
ctrlaltf24 Nov 24, 2024
f3221eb
fix: migrate SKIP_DEPENDENCIES
ctrlaltf24 Nov 24, 2024
4a73f8e
fix: migrate SKIP_FONTS
ctrlaltf24 Nov 24, 2024
db38ed2
fix: remove unused LOGOS_ICON_FILENAME
ctrlaltf24 Nov 24, 2024
753af64
refactor: remove need for separate icon name
ctrlaltf24 Nov 24, 2024
fe6e061
fix: migrate LOGOS_ICON_URL
ctrlaltf24 Nov 24, 2024
2cb8323
fix: remove OS_NAME and OS_RELEASE
ctrlaltf24 Nov 29, 2024
89e8718
refactor: separate network cache
ctrlaltf24 Nov 29, 2024
d18544a
fix: typing/stype thanks to mypy/ruff
ctrlaltf24 Nov 29, 2024
d47eb53
fix: migrate MYDOWNLOADS
ctrlaltf24 Nov 29, 2024
4857014
fix: migrate LOGOS_FORCE_ROOT and PASSIVE
ctrlaltf24 Nov 29, 2024
6087e99
fix: migrate appimage vars
ctrlaltf24 Nov 30, 2024
ffe81f9
fix: migrate winecmd_encoding
ctrlaltf24 Nov 30, 2024
78e8479
fix: migrate package manager to new format
ctrlaltf24 Nov 30, 2024
a0253ef
refactor: move globally scoped tui variables into TUI
ctrlaltf24 Nov 30, 2024
965d197
fix: remove check if indexing
ctrlaltf24 Nov 30, 2024
77e4065
refactor: migrate current_logos_version
ctrlaltf24 Nov 30, 2024
e0aa905
refactor: migrate LOGS
ctrlaltf24 Nov 30, 2024
912fded
fix: installed_faithlife_product_release
ctrlaltf24 Nov 30, 2024
b4b33c4
refactor: migrate latest version
ctrlaltf24 Nov 30, 2024
13ed0d0
refactor: move network cache into persistent config
ctrlaltf24 Nov 30, 2024
b7147ad
refactor: limit temp workdir to scope
ctrlaltf24 Nov 30, 2024
40ad90d
fix: misc
ctrlaltf24 Nov 30, 2024
e903d57
refactor: move new_config into config.py
ctrlaltf24 Nov 30, 2024
c9cf534
refactor: migrate config.SUPERUSER_COMMAND
ctrlaltf24 Nov 30, 2024
dd4ef56
refactor: remove more DIALOG
ctrlaltf24 Nov 30, 2024
27f3a43
refactor: move DIALOG removing
ctrlaltf24 Nov 30, 2024
b4ca5fc
refactor: more DIALOG
ctrlaltf24 Nov 30, 2024
d18b907
fix: misc
ctrlaltf24 Nov 30, 2024
3a28e3f
refactor: migrate config.processes
ctrlaltf24 Nov 30, 2024
818b063
chore: remove unused
ctrlaltf24 Nov 30, 2024
92bf465
fix: misc
ctrlaltf24 Nov 30, 2024
e113149
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 Dec 1, 2024
d07c92f
chore: add comments for additional work
ctrlaltf24 Dec 1, 2024
be9f468
docs: add comment to architecture code
ctrlaltf24 Dec 1, 2024
d6c26a4
fix: migrate new code to config
ctrlaltf24 Dec 1, 2024
c566637
fix: validate responses from _ask
ctrlaltf24 Dec 1, 2024
ffe1362
refactor: migrate msg questions into app
ctrlaltf24 Dec 1, 2024
dbcfffa
refactor: msg.logos_error
ctrlaltf24 Dec 1, 2024
17f6ac9
refactor: replace logos_warning with app.exit
ctrlaltf24 Dec 1, 2024
755df3d
refactor: remove unused ui_message
ctrlaltf24 Dec 1, 2024
5e0526c
refactor: remove msg.progress
ctrlaltf24 Dec 1, 2024
5fd98d2
refactor: update_install_feedback to app.status
ctrlaltf24 Dec 1, 2024
9d95a0e
fix: include progress bar in cli output
ctrlaltf24 Dec 1, 2024
976ce27
refactor: msg.status
ctrlaltf24 Dec 1, 2024
0361d08
refactor: msg.logos_warn
ctrlaltf24 Dec 1, 2024
8fcfeaf
refactor: remove logos_progress
ctrlaltf24 Dec 1, 2024
d9a0685
refactor: msg.logos_msg
ctrlaltf24 Dec 1, 2024
5c46fd7
chore: remove unused functions
ctrlaltf24 Dec 1, 2024
923b531
fix: resolve misc warning and errors
ctrlaltf24 Dec 1, 2024
3b8273e
refactor: DIALOG and use_python_dialog
ctrlaltf24 Dec 1, 2024
4d732fc
refactor: migrate config.threads to the app
ctrlaltf24 Dec 1, 2024
703198d
refactor: console_log
ctrlaltf24 Dec 1, 2024
7edb53a
refactor: migrate ACTION
ctrlaltf24 Dec 1, 2024
6dc5a07
refactor: migrate CONFIG_FILE
ctrlaltf24 Dec 1, 2024
cdfd46e
fix: resolve remaining mypy/ruff lints
ctrlaltf24 Dec 1, 2024
3f4c873
chore: remove out of date comments
ctrlaltf24 Dec 1, 2024
a003b0d
fix: cli progress output and misc
ctrlaltf24 Dec 1, 2024
c7ff552
refactor: another way to call CLI lazily
ctrlaltf24 Dec 1, 2024
276b366
fix: bug where selection didn't reset going back to main menu
ctrlaltf24 Dec 2, 2024
7e8b2d5
fix: additional mypy lints from untyped functions
ctrlaltf24 Dec 2, 2024
7750e13
fix: implement TUI status
ctrlaltf24 Dec 2, 2024
be4166a
fix: type hints in GUI
ctrlaltf24 Dec 2, 2024
ef63188
fix: more type hints
ctrlaltf24 Dec 2, 2024
5ccea3b
refactor: condense redundant steps
ctrlaltf24 Dec 2, 2024
830fa10
refactor: cache all network requests
ctrlaltf24 Dec 2, 2024
50cfc23
refactor: cache more file traversals
ctrlaltf24 Dec 2, 2024
d0c4ba5
refactor: allow for multiple config update hooks
ctrlaltf24 Dec 2, 2024
773f8c4
refactor: load all GUI state from config
ctrlaltf24 Dec 2, 2024
81b205d
fix: increase robustness of spawning dialogs on tk
ctrlaltf24 Dec 3, 2024
dcd84c5
refactor: resolve some comments
ctrlaltf24 Dec 3, 2024
de3992c
fix: spawn tui actions on a new thread
ctrlaltf24 Dec 3, 2024
d29eed3
fix: winetricks binary storage
ctrlaltf24 Dec 3, 2024
8b2e2d3
fix: no need to display this so prominently
ctrlaltf24 Dec 3, 2024
d7f9b45
fix: logs state manager mypy lints
ctrlaltf24 Dec 3, 2024
9811e05
fix: display recent messages in tui log
ctrlaltf24 Dec 3, 2024
d435dc2
fix: migrate legacy config
ctrlaltf24 Dec 3, 2024
03ea230
fix: load DIALOG from config
ctrlaltf24 Dec 3, 2024
a7c7634
feat: add -y and -q flags
ctrlaltf24 Dec 3, 2024
6a0030a
feat: added a reload function
ctrlaltf24 Dec 3, 2024
9f675c4
fix: resolve remaining todos
ctrlaltf24 Dec 3, 2024
c0febe4
fix: resolve a couple fixmes
ctrlaltf24 Dec 3, 2024
e45f571
feat: add integration test
ctrlaltf24 Dec 5, 2024
b188f90
fix: change color scheme
ctrlaltf24 Dec 5, 2024
6e2e5dd
fix: console log de-dup
ctrlaltf24 Dec 5, 2024
fd44ae3
fix: de-dup wine binary options
ctrlaltf24 Dec 5, 2024
631f00f
fix: handle case where install is started after going back to the menu
ctrlaltf24 Dec 5, 2024
33bb556
Update ou_dedetai/utils.py
ctrlaltf24 Dec 5, 2024
db4f175
feat: handle tab completion if the user input is path like
ctrlaltf24 Dec 5, 2024
1e2072e
Merge remote-tracking branch 'refs/remotes/origin/feat-platform-indep…
ctrlaltf24 Dec 5, 2024
e8fed2c
fix: resolve review comments
ctrlaltf24 Dec 5, 2024
7f70380
docs: update changelog
ctrlaltf24 Dec 5, 2024
0a02d40
fix: reset user options when hitting install again
ctrlaltf24 Dec 5, 2024
becb67e
fix: handle single appimage duplicate rather than reordering
ctrlaltf24 Dec 6, 2024
d6bd941
docs: update comment as to why launcher shortcuts are skipped
ctrlaltf24 Dec 6, 2024
59ace4f
fix: restore app logging
ctrlaltf24 Dec 6, 2024
7fc7515
fix: logging
ctrlaltf24 Dec 6, 2024
671d563
chore: more descriptive message when file config not found
ctrlaltf24 Dec 6, 2024
6dd127b
chore: remove unused code
ctrlaltf24 Dec 6, 2024
4819bb4
feat: add a install button that doesn't prompt in GUI
ctrlaltf24 Dec 7, 2024
8a9a52e
fix: support env overriding
ctrlaltf24 Dec 10, 2024
99d7243
chore: fix spelling
ctrlaltf24 Dec 10, 2024
9d3b99a
fix: optional pid
ctrlaltf24 Dec 10, 2024
632e98b
fix: ensure wine_appimage_path is in appbin dir
ctrlaltf24 Dec 10, 2024
da85163
chore: ignore .vscode files as well
ctrlaltf24 Dec 10, 2024
baed48c
refactor: move post-install hooks
ctrlaltf24 Dec 10, 2024
eb11a91
fix: handle slow network case
ctrlaltf24 Dec 10, 2024
31571b8
fix: enable install button after release download is complete
ctrlaltf24 Dec 14, 2024
7ab46cd
fix: normalize appimage path into install_dir
ctrlaltf24 Dec 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ env/
venv/
.venv/
.idea/
*.egg-info
*.egg-info
.vscode
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

- 4.0.0-beta.5
- Implemented network cache [N. Shaaban]
- Refactored configuration handling (no user action required) [N. Shaaban]
- Fix #17 [T. H. Wright]
- Make dependency lists more precise [M. Marti, N. Shaaban]
- Fix #230 [N. Shaaban]
- 4.0.0-beta.4
- Fix #220 [N. Shaaban]
- 4.0.0-beta.3
Expand Down
257 changes: 257 additions & 0 deletions ou_dedetai/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@

import abc
import logging
import os
from pathlib import Path
import sys
import threading
from typing import Callable, NoReturn, Optional

from ou_dedetai import constants
from ou_dedetai.constants import (
PROMPT_OPTION_DIRECTORY,
PROMPT_OPTION_FILE
)


class App(abc.ABC):
# FIXME: consider weighting install steps. Different steps take different lengths
Copy link
Collaborator

Choose a reason for hiding this comment

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

This would be a good improvement; alternatively, split up long steps into smaller steps.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm in favor of the steps being as discreet as possible, as in, as small as possible. That way the installer can check for each one in turn and only run the step if needed. That being said, all the gathering of user responses could maybe be a single step? That would help the progress bar's output seem more intuitive, I think.

installer_step_count: int = 0
"""Total steps in the installer, only set the installation process has started."""
installer_step: int = 1
"""Step the installer is on. Starts at 0"""

_threads: list[threading.Thread]
"""List of threads

Non-daemon threads will be joined before shutdown
"""
_last_status: Optional[str] = None
"""The last status we had"""
config_updated_hooks: list[Callable[[], None]] = []
_config_updated_event: threading.Event = threading.Event()

def __init__(self, config, **kwargs) -> None:
# This lazy load is required otherwise these would be circular imports
from ou_dedetai.config import Config
from ou_dedetai.logos import LogosManager
from ou_dedetai.system import check_incompatibilities

self.conf = Config(config, self)
self.logos = LogosManager(app=self)
self._threads = []
# Ensure everything is good to start
check_incompatibilities(self)

def _config_updated_hook_runner():
while True:
self._config_updated_event.wait()
self._config_updated_event.clear()
for hook in self.config_updated_hooks:
try:
hook()
except Exception:
logging.exception("Failed to run config update hook")
_config_updated_hook_runner.__name__ = "Config Update Hook"
self.start_thread(_config_updated_hook_runner, daemon_bool=True)

def ask(self, question: str, options: list[str]) -> str:
"""Asks the user a question with a list of supplied options

Returns the option the user picked.

If the internal ask function returns None, the process will exit with 1
"""
def validate_result(answer: str, options: list[str]) -> Optional[str]:
special_cases = set([PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE])
# These constants have special meaning, don't worry about them to start with
simple_options = list(set(options) - special_cases)
# This MUST have the same indexes as above
simple_options_lower = [opt.lower() for opt in simple_options]

# Case sensitive check first
if answer in simple_options:
return answer
# Also do a case insensitive match, no reason to fail due to casing
if answer.lower() in simple_options_lower:
# Return the correct casing to simplify the parsing of the ask result
return simple_options[simple_options.index(answer.lower())]

# Now check the special cases
if PROMPT_OPTION_FILE in options and Path(answer).is_file():
return answer
if PROMPT_OPTION_DIRECTORY in options and Path(answer).is_dir():
return answer

# Not valid
return None

# Check to see if we're supposed to prompt the user
if self.conf._overrides.assume_yes:
# Get the first non-dynamic option
for option in options:
if option not in [PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE]:
return option

passed_options: list[str] | str = options
if len(passed_options) == 1 and (
PROMPT_OPTION_DIRECTORY in passed_options
or PROMPT_OPTION_FILE in passed_options
):
# Set the only option to be the follow up prompt
passed_options = options[0]
elif passed_options is not None and self._exit_option is not None:
passed_options = options + [self._exit_option]

answer = self._ask(question, passed_options)
while answer is None or validate_result(answer, options) is None:
invalid_response = "That response is not valid, please try again."
new_question = f"{invalid_response}\n{question}"
answer = self._ask(new_question, passed_options)

if answer is not None:
answer = validate_result(answer, options)
if answer is None:
# Huh? coding error, this should have been checked earlier
logging.critical("An invalid response slipped by, please report this incident to the developers") #noqa: E501
self.exit("Failed to get a valid value from user")

if answer == self._exit_option:
answer = None

if answer is None:
self.exit("Failed to get a valid value from user")

return answer

def approve_or_exit(self, question: str, context: Optional[str] = None):
"""Asks the user a question, if they refuse, shutdown"""
if not self.approve(question, context):
self.exit(f"User refused the prompt: {question}")

def approve(self, question: str, context: Optional[str] = None) -> bool:
"""Asks the user a y/n question"""
question = f"{context}\n" if context is not None else "" + question
options = ["Yes", "No"]
return self.ask(question, options) == "Yes"

def exit(self, reason: str, intended: bool = False) -> NoReturn:
"""Exits the application cleanly with a reason."""
logging.debug(f"Closing {constants.APP_NAME}.")
# Shutdown logos/indexer if we spawned it
self.logos.end_processes()
# Join threads
for thread in self._threads:
# Only wait on non-daemon threads.
if not thread.daemon:
try:
thread.join()
except RuntimeError:
# Will happen if we try to join the current thread
pass
# Remove pid file if exists
try:
os.remove(constants.PID_FILE)
except FileNotFoundError: # no pid file when testing functions
pass
# exit from the process
if intended:
sys.exit(0)
else:
logging.critical(f"Cannot continue because {reason}\n{constants.SUPPORT_MESSAGE}") #noqa: E501
sys.exit(1)

_exit_option: Optional[str] = "Exit"

@abc.abstractmethod
def _ask(self, question: str, options: list[str] | str) -> Optional[str]:
"""Implementation for asking a question pre-front end

Options may include ability to prompt for an additional value.
Such as asking for one of strings or a directory.
If the user selects choose a new directory, the
implementations MUST handle the follow up prompt before returning

Options may be a single value,
Implementations MUST handle this single option being a follow up prompt
"""
raise NotImplementedError()

def is_installed(self) -> bool:
"""Returns whether the install was successful by
checking if the installed exe exists and is executable"""
if self.conf.logos_exe is not None:
return os.access(self.conf.logos_exe, os.X_OK)
return False

def status(self, message: str, percent: Optional[int | float] = None):
"""A status update

Args:
message: str - if it ends with a \r that signifies that this message is
intended to be overrighten next time
percent: Optional[int] - percent of the way through the current install step
(if installing)
"""
# Check to see if we want to suppress all output
if self.conf._overrides.quiet:
return

if isinstance(percent, float):
percent = round(percent * 100)
# If we're installing
if self.installer_step_count != 0:
current_step_percent = percent or 0
# We're further than the start of our current step, percent more
installer_percent = round((self.installer_step * 100 + current_step_percent) / self.installer_step_count) # noqa: E501
logging.debug(f"Install {installer_percent}: {message}")
self._status(message, percent=installer_percent)
else:
# Otherwise just print status using the progress given
logging.debug(f"{message}: {percent}")
self._status(message, percent)
self._last_status = message

@abc.abstractmethod
def _status(self, message: str, percent: Optional[int] = None):
"""Implementation for updating status pre-front end

Args:
message: str - if it ends with a \r that signifies that this message is
intended to be overrighten next time
percent: Optional[int] - percent complete of the current overall operation
if None that signifies we can't track the progress.
Feel free to implement a spinner
"""
# De-dup
if message != self._last_status:
if message.endswith("\r"):
print(f"{message}", end="\r")
else:
print(f"{message}")

@property
def superuser_command(self) -> str:
"""Command when root privileges are needed.

Raises:
SuperuserCommandNotFound

May be sudo or pkexec for example"""
from ou_dedetai.system import get_superuser_command
return get_superuser_command()

def start_thread(self, task, *args, daemon_bool: bool = True, **kwargs):
"""Starts a new thread

Non-daemon threads be joined before shutdown"""
thread = threading.Thread(
name=f"{constants.APP_NAME} {task}",
target=task,
daemon=daemon_bool,
args=args,
kwargs=kwargs
)
self._threads.append(thread)
thread.start()
return thread
Loading