redfetch 1.4.1__tar.gz → 1.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {redfetch-1.4.1 → redfetch-1.4.3}/.github/workflows/pipeline.yml +3 -3
- {redfetch-1.4.1 → redfetch-1.4.3}/CHANGELOG.md +10 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/PKG-INFO +2 -2
- redfetch-1.4.3/pyapp_launcher.py +99 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/__about__.py +2 -2
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/config.py +6 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/config_firstrun.py +25 -1
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/main.py +3 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/processes.py +19 -21
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/cli_reference.py +1 -1
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_close_gating.py +44 -2
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_self_heal_eqpath.py +3 -12
- redfetch-1.4.1/tests/test_readme_cli_reference.py +0 -36
- {redfetch-1.4.1 → redfetch-1.4.3}/.gitignore +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/AGENTS.md +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/LICENSE +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/README.md +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/pyproject.toml +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/redfetch.ico +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/__init__.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/api.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/auth.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/desktop_shortcut.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/detecteq.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/download.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/listener.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/meta.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/navmesh.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/net.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/post_update.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/push.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/runtime_errors.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/settings.toml +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/shortcuts.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/special.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/store.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_discovery.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_executor.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_planner.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_remote.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_types.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/terminal_ui.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/terminal_ui.tcss +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/update_status.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/utils.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_autologin_eqpath.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_check.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_env_selection.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_fetch_sync_info.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_first_run_setup.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_is_special_or_dependency.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_licensed_resources_filtering.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_path_reset.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_process_lock.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_remote_snapshot.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_settings_local_deltas.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_shortcuts.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_staged_extraction.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_sync_pipeline_regressions.py +0 -0
- {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_sync_queue_rules.py +0 -0
|
@@ -107,7 +107,7 @@ jobs:
|
|
|
107
107
|
id: set_env_vars
|
|
108
108
|
shell: pwsh
|
|
109
109
|
run: |
|
|
110
|
-
"
|
|
110
|
+
"PYAPP_EXEC_SCRIPT=$env:GITHUB_WORKSPACE\pyapp_launcher.py" >> $env:GITHUB_ENV
|
|
111
111
|
"PYAPP_PASS_LOCATION=1" >> $env:GITHUB_ENV
|
|
112
112
|
|
|
113
113
|
if ($env:GITHUB_REF -eq 'refs/heads/develop') {
|
|
@@ -165,11 +165,11 @@ jobs:
|
|
|
165
165
|
Remove-Item Env:PYAPP_PROJECT_VERSION -ErrorAction SilentlyContinue
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
$env:
|
|
168
|
+
$env:PYAPP_EXEC_SCRIPT = "${{ env.PYAPP_EXEC_SCRIPT }}"
|
|
169
169
|
$env:PYAPP_PASS_LOCATION = "${{ env.PYAPP_PASS_LOCATION }}"
|
|
170
170
|
$env:PYAPP_PIP_EXTRA_ARGS = "${{ env.PYAPP_PIP_EXTRA_ARGS }}"
|
|
171
171
|
|
|
172
|
-
Write-Host "
|
|
172
|
+
Write-Host "PYAPP_EXEC_SCRIPT: $env:PYAPP_EXEC_SCRIPT"
|
|
173
173
|
Write-Host "PYAPP_PASS_LOCATION: $env:PYAPP_PASS_LOCATION"
|
|
174
174
|
Write-Host "PYAPP_PIP_EXTRA_ARGS: $env:PYAPP_PIP_EXTRA_ARGS"
|
|
175
175
|
|
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.3] - 2026-07-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- A failed install will attempt to repair itself, rather than closing immediately.
|
|
12
|
+
|
|
13
|
+
## [1.4.2] - 2026-07-06
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- The update button wouldn't launch MQ if eqbcs.exe was running.
|
|
17
|
+
|
|
8
18
|
## [1.4.1] - 2026-07-05
|
|
9
19
|
|
|
10
20
|
### Changed
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redfetch
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.3
|
|
4
4
|
Summary: Download and publish EverQuest scripts and software using the RedGuides API
|
|
5
5
|
Project-URL: Homepage, https://www.redguides.com
|
|
6
6
|
Project-URL: Documentation, https://www.redguides.com/community/resources/redfetch.3177/
|
|
7
7
|
Project-URL: Repository, https://github.com/RedGuides/redfetch
|
|
8
8
|
Project-URL: Issues, https://github.com/RedGuides/redfetch/issues
|
|
9
9
|
Project-URL: Changelog, https://github.com/RedGuides/redfetch/blob/main/CHANGELOG.md
|
|
10
|
-
Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/
|
|
10
|
+
Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/9161a88b101d6df6ce9a697736e3fcc57b1c3122.zip
|
|
11
11
|
Author-email: Redbot <ask@redguides.com>
|
|
12
12
|
License-Expression: GPL-3.0-or-later
|
|
13
13
|
License-File: LICENSE
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""PyApp entry point for redfetch.exe: run the app, and self-heal a broken install.
|
|
2
|
+
|
|
3
|
+
PyApp treats half-installed environments as complete, so if something fails or
|
|
4
|
+
interrupts install, it stays broken unless someone opens a terminal and runs `self restore`.
|
|
5
|
+
This wrapper runs `self restore` automatically.
|
|
6
|
+
|
|
7
|
+
Embedded via PYAPP_EXEC_SCRIPT. Stdlib-only, and must not import redfetch at module
|
|
8
|
+
scope, since redfetch is exactly what may be missing!
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _import_entrypoint():
|
|
15
|
+
from redfetch.main import main
|
|
16
|
+
return main
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Repair runs after we exit, in a detached script: on Windows `self restore` can't
|
|
20
|
+
# delete the install dir while our python.exe is still running from inside it.
|
|
21
|
+
_REPAIR_BAT = r"""@echo off
|
|
22
|
+
title redfetch - finishing setup
|
|
23
|
+
echo.
|
|
24
|
+
echo redfetch setup didn't finish!
|
|
25
|
+
echo Trying to finish it now, this may take a few minutes.
|
|
26
|
+
echo Please don't close this window.
|
|
27
|
+
echo.
|
|
28
|
+
timeout /t 3 /nobreak > nul
|
|
29
|
+
"<EXE>" self restore
|
|
30
|
+
if %errorlevel% neq 0 goto redfetch_failed
|
|
31
|
+
echo.
|
|
32
|
+
echo All set -- starting redfetch...
|
|
33
|
+
set "REDFETCH_SELFHEAL=1"
|
|
34
|
+
"<EXE>"
|
|
35
|
+
goto redfetch_cleanup
|
|
36
|
+
:redfetch_failed
|
|
37
|
+
echo.
|
|
38
|
+
echo Automatic repair didn't complete. You can repair manually by running:
|
|
39
|
+
echo "<EXE>" self restore
|
|
40
|
+
echo.
|
|
41
|
+
pause
|
|
42
|
+
:redfetch_cleanup
|
|
43
|
+
(goto) 2>nul & del "%~f0"
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _schedule_repair() -> "int | None":
|
|
48
|
+
"""Schedule `self restore` + relaunch; return 0 if scheduled, else None."""
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
# PYAPP is always set (to "1" when PASS_LOCATION is off), so require a real path.
|
|
52
|
+
exe = os.environ.get("PYAPP")
|
|
53
|
+
if sys.platform != "win32" or not exe or not os.path.exists(exe):
|
|
54
|
+
return None
|
|
55
|
+
if os.environ.get("REDFETCH_SELFHEAL"): # already tried once this chain
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
import subprocess
|
|
59
|
+
import tempfile
|
|
60
|
+
|
|
61
|
+
script = _REPAIR_BAT.replace("<EXE>", exe.replace("%", "%%")) # %% so cmd won't expand the path
|
|
62
|
+
path = None
|
|
63
|
+
try:
|
|
64
|
+
fd, path = tempfile.mkstemp(prefix="redfetch-repair-", suffix=".bat")
|
|
65
|
+
with os.fdopen(fd, "w") as handle:
|
|
66
|
+
handle.write(script)
|
|
67
|
+
subprocess.Popen(
|
|
68
|
+
["cmd.exe", "/c", path],
|
|
69
|
+
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
|
70
|
+
close_fds=True,
|
|
71
|
+
)
|
|
72
|
+
except Exception:
|
|
73
|
+
if path:
|
|
74
|
+
try:
|
|
75
|
+
os.unlink(path)
|
|
76
|
+
except OSError:
|
|
77
|
+
pass
|
|
78
|
+
return None
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> int:
|
|
83
|
+
try:
|
|
84
|
+
entrypoint = _import_entrypoint()
|
|
85
|
+
except ImportError:
|
|
86
|
+
code = _schedule_repair()
|
|
87
|
+
if code is None:
|
|
88
|
+
raise
|
|
89
|
+
print("redfetch needs to finish setting up; opening a repair window...",
|
|
90
|
+
file=sys.stderr)
|
|
91
|
+
return code
|
|
92
|
+
|
|
93
|
+
sys.argv[0] = "redfetch" # so typer's --help shows "redfetch", not this script's path
|
|
94
|
+
entrypoint() # errors past here are app logic, not install problems -> don't self-heal
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
raise SystemExit(main())
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 4,
|
|
21
|
+
__version__ = version = '1.4.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 4, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -493,3 +493,9 @@ def write_env_to_file(new_env):
|
|
|
493
493
|
raise RuntimeError("Configuration has not been initialized. Call initialize_config() first.")
|
|
494
494
|
|
|
495
495
|
env_loader.write(env_file_path, {"REDFETCH_ENV": new_env})
|
|
496
|
+
|
|
497
|
+
# Env changed -> re-shows server/config banner.
|
|
498
|
+
try:
|
|
499
|
+
os.remove(os.path.join(os.path.dirname(env_file_path), ".banner_shown"))
|
|
500
|
+
except OSError:
|
|
501
|
+
pass
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import os
|
|
5
5
|
import platform
|
|
6
6
|
import json
|
|
7
|
+
import time
|
|
7
8
|
|
|
8
9
|
# Third-party
|
|
9
10
|
from platformdirs import user_config_dir
|
|
@@ -21,6 +22,25 @@ from redfetch.detecteq import find_everquest_uninstall_location
|
|
|
21
22
|
|
|
22
23
|
console = Console()
|
|
23
24
|
|
|
25
|
+
# server type and config dir banner
|
|
26
|
+
BANNER_COOLDOWN = 12 * 3600
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _banner_due(config_dir):
|
|
30
|
+
"""True unless the banner was shown within the cooldown window."""
|
|
31
|
+
marker = os.path.join(config_dir, ".banner_shown")
|
|
32
|
+
try:
|
|
33
|
+
return (time.time() - os.path.getmtime(marker)) > BANNER_COOLDOWN
|
|
34
|
+
except OSError:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _mark_banner_shown(config_dir):
|
|
39
|
+
try:
|
|
40
|
+
open(os.path.join(config_dir, ".banner_shown"), "w").close()
|
|
41
|
+
except OSError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
24
44
|
|
|
25
45
|
# zork-like prompt
|
|
26
46
|
class CustomPrompt(PromptBase):
|
|
@@ -260,7 +280,11 @@ def first_run_setup():
|
|
|
260
280
|
if pypi_url_override:
|
|
261
281
|
notice_lines.append(f"[bold yellow]REDFETCH_PYPI_URL:[/bold yellow] {pypi_url_override}")
|
|
262
282
|
|
|
263
|
-
|
|
283
|
+
# Env overrides always spam the status/config banner.
|
|
284
|
+
has_overrides = len(notice_lines) > 2
|
|
285
|
+
if has_overrides or _banner_due(config_dir):
|
|
286
|
+
console.print(Panel("\n".join(notice_lines), expand=False))
|
|
287
|
+
_mark_banner_shown(config_dir)
|
|
264
288
|
return config_dir
|
|
265
289
|
else:
|
|
266
290
|
console.print("[bold red]Environment file (.env) not found. Rerunning setup.[/bold red]")
|
|
@@ -539,6 +539,9 @@ def config_show_command(server: Optional[Env] = typer.Option(None, "--server", "
|
|
|
539
539
|
if not panel_lines:
|
|
540
540
|
panel_lines.append("[dim]No paths are currently configured or opted in for this environment.[/dim]")
|
|
541
541
|
|
|
542
|
+
# Lead with the server type so users always know which env they're on.
|
|
543
|
+
panel_lines.insert(0, f"[bold yellow]Server type:[/bold yellow] {current_env}")
|
|
544
|
+
|
|
542
545
|
console.print(Panel("\n".join(panel_lines), expand=False))
|
|
543
546
|
|
|
544
547
|
# Optional: Show full settings dict with a label
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Helpers for managing external processes."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import configparser
|
|
4
5
|
import os
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
@@ -95,22 +96,20 @@ def is_executable_running(exe_path: str, running: set[str] | None = None) -> boo
|
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
def _spawned_loader_name(mq_folder: str) -> str | None:
|
|
98
|
-
"""Loader-copy filename from MacroQuest.ini
|
|
99
|
-
ini_path
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
except OSError:
|
|
113
|
-
pass
|
|
99
|
+
"""Loader-copy filename from MacroQuest.ini."""
|
|
100
|
+
for ini_path in (
|
|
101
|
+
os.path.join(mq_folder, "config", "MacroQuest.ini"),
|
|
102
|
+
os.path.join(mq_folder, "MacroQuest.ini"),
|
|
103
|
+
):
|
|
104
|
+
|
|
105
|
+
parser = configparser.ConfigParser(interpolation=None, strict=False)
|
|
106
|
+
try:
|
|
107
|
+
parser.read(ini_path, encoding="utf-8")
|
|
108
|
+
name = parser.get("Internal", "SpawnedProcess", fallback="").strip()
|
|
109
|
+
except (configparser.Error, OSError):
|
|
110
|
+
continue
|
|
111
|
+
if name:
|
|
112
|
+
return name
|
|
114
113
|
return None
|
|
115
114
|
|
|
116
115
|
|
|
@@ -136,13 +135,12 @@ def macroquest_session_running(mq_folder: str, running: set[str] | None = None)
|
|
|
136
135
|
if running is None:
|
|
137
136
|
running = running_executable_paths()
|
|
138
137
|
folder = _norm(mq_folder)
|
|
138
|
+
alias = _spawned_loader_name(mq_folder)
|
|
139
|
+
alias = alias.lower() if alias else None
|
|
139
140
|
for path in running:
|
|
140
|
-
|
|
141
|
+
base = os.path.basename(path)
|
|
142
|
+
if base == alias or (base == "macroquest.exe" and os.path.dirname(path) == folder):
|
|
141
143
|
return True
|
|
142
|
-
spawned = _spawned_loader_name(mq_folder)
|
|
143
|
-
if spawned:
|
|
144
|
-
spawned = spawned.lower()
|
|
145
|
-
return any(os.path.basename(path) == spawned for path in running)
|
|
146
144
|
return False
|
|
147
145
|
|
|
148
146
|
|
|
@@ -5,7 +5,7 @@ changing a command or option, refresh the docs:
|
|
|
5
5
|
|
|
6
6
|
hatch run dev:gen-docs # or: python tests/cli_reference.py --write
|
|
7
7
|
|
|
8
|
-
``
|
|
8
|
+
Run ``hatch run dev:check-docs`` in CI or before release to catch drift.
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
@@ -194,10 +194,11 @@ def _norm_set(*paths):
|
|
|
194
194
|
return {os.path.normcase(os.path.normpath(p)) for p in paths}
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
def
|
|
197
|
+
def test_session_ignores_unrecorded_exe_in_folder(tmp_path, monkeypatch):
|
|
198
|
+
# With no loader recorded (fresh install), a stray exe in the folder is NOT a session.
|
|
198
199
|
monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: None)
|
|
199
200
|
running = _norm_set(str(tmp_path / "LV3XGukn.exe"))
|
|
200
|
-
assert processes.macroquest_session_running(str(tmp_path), running) is
|
|
201
|
+
assert processes.macroquest_session_running(str(tmp_path), running) is False
|
|
201
202
|
|
|
202
203
|
|
|
203
204
|
@windows_paths
|
|
@@ -222,6 +223,39 @@ def test_session_not_running_when_nothing_matches(tmp_path, monkeypatch):
|
|
|
222
223
|
assert processes.macroquest_session_running(str(tmp_path), running) is False
|
|
223
224
|
|
|
224
225
|
|
|
226
|
+
@windows_paths
|
|
227
|
+
def test_session_ignores_renamed_eqbcs_copy_when_loader_not_running(tmp_path, monkeypatch):
|
|
228
|
+
# The reported bug: EQBCS runs as a random-named RunFromTemp copy next to MQ and survives it.
|
|
229
|
+
monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: "LV3XGukn.exe")
|
|
230
|
+
running = _norm_set(str(tmp_path / "wUUayFYjXM.exe")) # an EQBCS copy; loader not running
|
|
231
|
+
assert processes.macroquest_session_running(str(tmp_path), running) is False
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@windows_paths
|
|
235
|
+
def test_session_matches_recorded_loader_amid_other_folder_exes(tmp_path, monkeypatch):
|
|
236
|
+
# The recorded loader name is authoritative: MQ is up when that copy runs, whatever else
|
|
237
|
+
# (LootSort, MeshGenerator, an EQBCS copy...) happens to share the folder.
|
|
238
|
+
monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: "LV3XGukn.exe")
|
|
239
|
+
running = _norm_set(str(tmp_path / "LV3XGukn.exe"), str(tmp_path / "LootSort.exe"))
|
|
240
|
+
assert processes.macroquest_session_running(str(tmp_path), running) is True
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@windows_paths
|
|
244
|
+
def test_session_ignores_folder_tools_when_loader_recorded(tmp_path, monkeypatch):
|
|
245
|
+
# A folder tool running is not a session when a loader name is recorded and its process is gone.
|
|
246
|
+
monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: "LV3XGukn.exe")
|
|
247
|
+
running = _norm_set(str(tmp_path / "LootSort.exe"))
|
|
248
|
+
assert processes.macroquest_session_running(str(tmp_path), running) is False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@windows_paths
|
|
252
|
+
def test_session_detects_macroquest_exe_direct(tmp_path, monkeypatch):
|
|
253
|
+
# RunFromTemp off: MacroQuest.exe runs directly from the folder -> unambiguously a session.
|
|
254
|
+
monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: None)
|
|
255
|
+
running = _norm_set(str(tmp_path / "MacroQuest.exe"))
|
|
256
|
+
assert processes.macroquest_session_running(str(tmp_path), running) is True
|
|
257
|
+
|
|
258
|
+
|
|
225
259
|
def test_spawned_loader_name_parsed_from_ini(tmp_path):
|
|
226
260
|
(tmp_path / "MacroQuest.ini").write_text(
|
|
227
261
|
"[Other]\nSpawnedProcess=wrong.exe\n[Internal]\nSpawnedProcess=Ab12Cd34.exe\n"
|
|
@@ -230,6 +264,14 @@ def test_spawned_loader_name_parsed_from_ini(tmp_path):
|
|
|
230
264
|
assert processes._spawned_loader_name(str(tmp_path / "missing")) is None
|
|
231
265
|
|
|
232
266
|
|
|
267
|
+
def test_spawned_loader_name_prefers_config_subfolder(tmp_path):
|
|
268
|
+
# Current MQ writes its INI under config/; the reader must look there, and it wins over root.
|
|
269
|
+
(tmp_path / "config").mkdir()
|
|
270
|
+
(tmp_path / "config" / "MacroQuest.ini").write_text("[Internal]\nSpawnedProcess=fromconfig.exe\n")
|
|
271
|
+
(tmp_path / "MacroQuest.ini").write_text("[Internal]\nSpawnedProcess=fromroot.exe\n")
|
|
272
|
+
assert processes._spawned_loader_name(str(tmp_path)) == "fromconfig.exe"
|
|
273
|
+
|
|
274
|
+
|
|
233
275
|
# --- terminate/restart hardening --------------------------------------------------------
|
|
234
276
|
|
|
235
277
|
def test_terminate_reports_hung_process_and_continues(fake_windows):
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
"""config.self_heal_eqpath fills a blank/stale EQPATH from MacroQuest autologin at startup.
|
|
2
|
-
|
|
3
|
-
Covers the empty-or-invalid heal policy: heal when EQPATH is blank or its folder is gone,
|
|
4
|
-
never clobber a valid deliberate value, and keep one env's failure from aborting the others.
|
|
5
|
-
The @format linchpin test proves the payoff: a stored EQPATH makes maps resolve to <eq>/maps.
|
|
6
|
-
"""
|
|
1
|
+
"""config.self_heal_eqpath fills a blank/stale EQPATH from MacroQuest autologin at startup."""
|
|
7
2
|
import os
|
|
8
3
|
import sqlite3
|
|
9
4
|
|
|
@@ -138,7 +133,6 @@ def test_no_login_db_no_heal(tmp_path, spy, monkeypatch):
|
|
|
138
133
|
|
|
139
134
|
assert spy == []
|
|
140
135
|
|
|
141
|
-
|
|
142
136
|
def test_only_blank_env_heals(tmp_path, eq_dir, spy, monkeypatch):
|
|
143
137
|
vvmq = _make_vvmq(tmp_path, "vvmq_live", "live", eq_dir)
|
|
144
138
|
envs = {
|
|
@@ -169,11 +163,7 @@ def test_exception_in_one_env_does_not_abort_others(tmp_path, eq_dir, spy, monke
|
|
|
169
163
|
# --- the @format linchpin ---------------------------------------------------
|
|
170
164
|
|
|
171
165
|
def test_maps_resolve_under_stored_eqpath(tmp_path, monkeypatch):
|
|
172
|
-
"""A stored EQPATH (where the heal writes it) makes maps resource 153 resolve to <eq>/maps.
|
|
173
|
-
|
|
174
|
-
This is the whole point of self-heal over the old read-through: fix the stored value once
|
|
175
|
-
and every `@format {this.eqpath}` consumer lights up for free.
|
|
176
|
-
"""
|
|
166
|
+
"""A stored EQPATH (where the heal writes it) makes maps resource 153 resolve to <eq>/maps."""
|
|
177
167
|
from dynaconf import Dynaconf
|
|
178
168
|
from redfetch import sync_discovery
|
|
179
169
|
|
|
@@ -196,3 +186,4 @@ def test_maps_resolve_under_stored_eqpath(tmp_path, monkeypatch):
|
|
|
196
186
|
|
|
197
187
|
resolved = sync_discovery.resolve_root_path("153", None, "LIVE")
|
|
198
188
|
assert resolved == os.path.normpath(str(eq / "maps"))
|
|
189
|
+
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""Guard against README drift: the Command Line Reference is generated from the
|
|
2
|
-
typer app (see cli_reference.py). This fails if README.md is out of date, i.e.
|
|
3
|
-
someone changed a command/option in main.py but didn't regenerate the docs.
|
|
4
|
-
|
|
5
|
-
Fix a failure with: hatch run dev:gen-docs
|
|
6
|
-
"""
|
|
7
|
-
import difflib
|
|
8
|
-
import sys
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent)) # import the sibling helper
|
|
12
|
-
|
|
13
|
-
from cli_reference import README, current_block, render_reference
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_readme_cli_reference_is_current():
|
|
17
|
-
readme = README.read_text(encoding="utf-8")
|
|
18
|
-
block = current_block(readme)
|
|
19
|
-
assert block is not None, (
|
|
20
|
-
"CLI reference markers are missing from README.md. Restore them and run "
|
|
21
|
-
"`hatch run dev:gen-docs`."
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
expected = render_reference()
|
|
25
|
-
if block != expected:
|
|
26
|
-
diff = "\n".join(
|
|
27
|
-
difflib.unified_diff(
|
|
28
|
-
block.splitlines(), expected.splitlines(),
|
|
29
|
-
fromfile="README.md (committed)", tofile="generated from main.py",
|
|
30
|
-
lineterm="",
|
|
31
|
-
)
|
|
32
|
-
)
|
|
33
|
-
raise AssertionError(
|
|
34
|
-
"README.md Command Line Reference is stale. Regenerate with "
|
|
35
|
-
"`hatch run dev:gen-docs` (command help lives in src/redfetch/main.py).\n" + diff
|
|
36
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|