mirror of
https://github.com/aclist/dztui.git
synced 2026-11-03 16:42:13 +01:00
Compare commits
17
Commits
7.0.0b14
...
78151b3bb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78151b3bb3 | ||
|
|
2418d8be68 | ||
|
|
9fa7172056 | ||
|
|
d75f87768e | ||
|
|
5bb4271d36 | ||
|
|
6c86bff869 | ||
|
|
92ba7c2267 | ||
|
|
6c56f16633 | ||
|
|
2b87272b52 | ||
|
|
4de5147899 | ||
|
|
4c31cd7ecd | ||
|
|
61f90d6016 | ||
|
|
cd1fd1b179 | ||
|
|
1b25c8fadb | ||
|
|
18146bf66f | ||
|
|
8ae54d436a | ||
|
|
4d94551f87 |
No files matched your search
@@ -26,7 +26,7 @@ You can install it via the following set of commands:
|
|||||||
.. code:: console
|
.. code:: console
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install libgirepository-2.0
|
sudo apt install libgirepository-2.0-0
|
||||||
|
|
||||||
Next steps
|
Next steps
|
||||||
---------------
|
---------------
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ from dzgui.strings import boot
|
|||||||
# from dzgui.util.map_count import get_map_count
|
# from dzgui.util.map_count import get_map_count
|
||||||
from dzgui.util.deck import is_steam_deck, is_game_mode
|
from dzgui.util.deck import is_steam_deck, is_game_mode
|
||||||
from dzgui.util.localize import set_locale
|
from dzgui.util.localize import set_locale
|
||||||
|
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||||
from dzgui.util.strings import init
|
from dzgui.util.strings import init
|
||||||
|
|
||||||
from dzgui.views.base import App
|
from dzgui.views.base import App
|
||||||
@@ -39,17 +41,63 @@ def make_parents(path: "Path") -> None:
|
|||||||
|
|
||||||
|
|
||||||
def setup_logger(log_path: "Path") -> None:
|
def setup_logger(log_path: "Path") -> None:
|
||||||
|
# TODO: put in consts?
|
||||||
_format = (
|
_format = (
|
||||||
"%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s"
|
"%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s"
|
||||||
)
|
)
|
||||||
|
|
||||||
fh = logging.FileHandler(log_path)
|
fh = logging.FileHandler(log_path)
|
||||||
formatter = logging.Formatter(_format)
|
formatter = logging.Formatter(_format)
|
||||||
fh.setFormatter(formatter)
|
fh.setFormatter(formatter)
|
||||||
fh.setLevel(logging.DEBUG)
|
fh.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
_filter = RedactionFilter(patterns=REDACTION_PATTERNS)
|
||||||
|
fh.addFilter(_filter)
|
||||||
|
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
logger.addHandler(fh)
|
logger.addHandler(fh)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
|
||||||
|
# NOTE: temporary workaround for #375
|
||||||
|
conf = "config.json"
|
||||||
|
state = [
|
||||||
|
"dzg.history",
|
||||||
|
"dzg.versions",
|
||||||
|
"dzg.res.json",
|
||||||
|
"dzg.filters.json",
|
||||||
|
"dzg.columns.json",
|
||||||
|
"dzg.notes.json",
|
||||||
|
"ips.csv",
|
||||||
|
".month",
|
||||||
|
]
|
||||||
|
config_changed = False
|
||||||
|
state_changed = False
|
||||||
|
if config.parent.exists() is False:
|
||||||
|
new_file = config
|
||||||
|
old_file = config.parent.parent / conf
|
||||||
|
if old_file.is_file():
|
||||||
|
try:
|
||||||
|
make_parents(new_file)
|
||||||
|
shutil.copy(old_file, new_file)
|
||||||
|
config_changed = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
if resolution.parent.exists() is False:
|
||||||
|
state_path = resolution.parent
|
||||||
|
for state_file in state:
|
||||||
|
old_file = state_path.parent / state_file
|
||||||
|
if old_file.is_file():
|
||||||
|
try:
|
||||||
|
new_file = state_path / state_file
|
||||||
|
make_parents(new_file)
|
||||||
|
shutil.copy(old_file, new_file)
|
||||||
|
state_changed = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return (config_changed, state_changed)
|
||||||
|
|
||||||
|
|
||||||
def load_gui(version: str, is_debug: bool) -> None:
|
def load_gui(version: str, is_debug: bool) -> None:
|
||||||
lock = lock_acquire() # noqa
|
lock = lock_acquire() # noqa
|
||||||
|
|
||||||
@@ -58,6 +106,8 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
xdg_paths = get_xdg_paths()
|
xdg_paths = get_xdg_paths()
|
||||||
XDG = parse_filepaths(xdg_paths)
|
XDG = parse_filepaths(xdg_paths)
|
||||||
|
|
||||||
|
copy_bare_configs(XDG.config, XDG.resolution)
|
||||||
|
|
||||||
if XDG.resolution.parent.is_dir() is False:
|
if XDG.resolution.parent.is_dir() is False:
|
||||||
make_parents(XDG.resolution)
|
make_parents(XDG.resolution)
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -29,9 +29,9 @@ def is_writeable(path_str: str) -> bool:
|
|||||||
if not path.exists():
|
if not path.exists():
|
||||||
try:
|
try:
|
||||||
path.mkdir(parents=True)
|
path.mkdir(parents=True)
|
||||||
|
path.unlink()
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
path.unlink()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -55,11 +55,12 @@ def get_xdg_paths() -> dict:
|
|||||||
|
|
||||||
resolved_paths = {}
|
resolved_paths = {}
|
||||||
for path in xdg_paths:
|
for path in xdg_paths:
|
||||||
rp = os.environ.get(path)
|
real_path = os.environ.get(path)
|
||||||
if rp is not None and is_writeable(rp):
|
if real_path is not None and is_writeable(real_path):
|
||||||
resolved_paths[path] = Path(rp)
|
new_path = Path(real_path)
|
||||||
else:
|
else:
|
||||||
resolved_paths[path] = xdg_paths[path] / APP_NAME_LOWER
|
new_path = xdg_paths[path]
|
||||||
|
resolved_paths[path] = new_path / APP_NAME_LOWER
|
||||||
return resolved_paths
|
return resolved_paths
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -1,11 +1,12 @@
|
|||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pathlib import Path
|
from ctypes.util import find_library
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
LIB = "libgirepository-2.0"
|
LIB = "libgirepository-2.0"
|
||||||
|
|
||||||
|
|
||||||
@deprecated("currently unused")
|
@deprecated("currently unused")
|
||||||
def is_debian() -> bool:
|
def is_debian() -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -39,9 +40,7 @@ def is_debian() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def has_libgi() -> bool:
|
def has_libgi() -> bool:
|
||||||
parent_dir = Path("/usr/lib64")
|
return bool(find_library("girepository-2.0"))
|
||||||
exists = any(LIB in subdir.name for subdir in parent_dir.iterdir())
|
|
||||||
return exists
|
|
||||||
|
|
||||||
|
|
||||||
def test_libgi_missing() -> None:
|
def test_libgi_missing() -> None:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from dataclasses import dataclass
|
|||||||
from typing import Any, Sequence, TYPE_CHECKING
|
from typing import Any, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.enum import HELP_MENU_ROWS
|
from dzgui.const.enum import HELP_MENU_ROWS
|
||||||
from dzgui.util.redact import redact_log
|
|
||||||
from dzgui.util.strings import delimiter
|
from dzgui.util.strings import delimiter
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -134,9 +133,7 @@ class ModelFactory:
|
|||||||
with open(path, "r") as f:
|
with open(path, "r") as f:
|
||||||
lines = [line.split(delimiter) for line in f.read().splitlines()]
|
lines = [line.split(delimiter) for line in f.read().splitlines()]
|
||||||
for record in lines:
|
for record in lines:
|
||||||
# NOTE: strips PII and API keys
|
store.append(record)
|
||||||
clean = redact_log(record)
|
|
||||||
store.append(clean)
|
|
||||||
return store
|
return store
|
||||||
|
|
||||||
def new_model_from_class(self, cls: type) -> FastInsertListStore:
|
def new_model_from_class(self, cls: type) -> FastInsertListStore:
|
||||||
|
|||||||
+13
-6
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
@@ -9,7 +10,9 @@ from dzgui.const.constants import APP_NAME
|
|||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
from dzgui.init.prefix import get_version
|
from dzgui.init.prefix import get_version
|
||||||
from dzgui.util.redact import redact
|
from dzgui.util.redact import redact_home
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def get_cpu_model() -> str:
|
def get_cpu_model() -> str:
|
||||||
@@ -41,12 +44,16 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
# TODO: test availability on other distros
|
# TODO: test availability on other distros
|
||||||
date = datetime.now().isoformat()
|
date = datetime.now().isoformat()
|
||||||
|
|
||||||
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
try:
|
||||||
|
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(e)
|
||||||
|
distro = "Unknown"
|
||||||
|
|
||||||
kernel = os.uname().release
|
kernel = os.uname().release
|
||||||
cpu = get_cpu_model()
|
cpu = get_cpu_model()
|
||||||
version = get_version()
|
version = get_version()
|
||||||
|
|
||||||
branch = lookup(config, Preferences.BRANCH)
|
|
||||||
debug = lookup(config, Preferences.DEBUG)
|
debug = lookup(config, Preferences.DEBUG)
|
||||||
install = lookup(config, Preferences.INSTALL)
|
install = lookup(config, Preferences.INSTALL)
|
||||||
default = lookup(config, Preferences.DEFAULT)
|
default = lookup(config, Preferences.DEFAULT)
|
||||||
@@ -54,8 +61,8 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
steam_path = Path(default)
|
steam_path = Path(default)
|
||||||
workshop_path = get_local_mod_path(steam_path)
|
workshop_path = get_local_mod_path(steam_path)
|
||||||
|
|
||||||
steam_redacted = redact(default)
|
steam_redacted = redact_home(default)
|
||||||
workshop_redacted = redact(str(workshop_path))
|
workshop_redacted = redact_home(str(workshop_path))
|
||||||
|
|
||||||
mods = get_local_mod_ids(steam_path)
|
mods = get_local_mod_ids(steam_path)
|
||||||
mods_pretty = print_mods(mods)
|
mods_pretty = print_mods(mods)
|
||||||
@@ -65,7 +72,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
|
|
||||||
# FIXME: extraneous newlines in lists of mods
|
# FIXME: extraneous newlines in lists of mods
|
||||||
template = f"""\
|
template = f"""\
|
||||||
{APP_NAME} version {version} ({branch})
|
{APP_NAME} version {version}
|
||||||
Date: {date}
|
Date: {date}
|
||||||
===============================
|
===============================
|
||||||
Distribution: {distro}
|
Distribution: {distro}
|
||||||
|
|||||||
+26
-16
@@ -1,20 +1,30 @@
|
|||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
def redact(text: str) -> str:
|
api_filter = r"(.*&key=)([^&]*)(.*)"
|
||||||
r = r"(/home/)([^/])*"
|
# FIXME: handle whitespace after directory name ($HOME root)
|
||||||
cleaned = re.sub(r, r"/home/REDACTED", text)
|
home_filter = r"(/home/)([^\s'\/]*)(.*)"
|
||||||
|
REDACTED = r"\1REDACTED\3"
|
||||||
|
REDACTION_PATTERNS = [api_filter, home_filter]
|
||||||
|
|
||||||
|
|
||||||
|
def redact_home(text: str) -> str:
|
||||||
|
pat = re.compile(home_filter)
|
||||||
|
cleaned = pat.sub(REDACTED, text)
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
def redact_log(record: list) -> list[str]:
|
|
||||||
"""
|
class RedactionFilter(logging.Filter):
|
||||||
requests library includes Steam API key in URL params
|
def __init__(self, patterns: list[str] | None = None) -> None:
|
||||||
"""
|
super().__init__()
|
||||||
clean = []
|
self._patterns = [re.compile(pat) for pat in (patterns or [])]
|
||||||
for item in record:
|
|
||||||
if "&key=" in item:
|
def filter(self, record: logging.LogRecord) -> Literal[True]:
|
||||||
pat = r"(.*&key=)(\S+)(.*)"
|
for pattern in self._patterns:
|
||||||
scrubbed = re.sub(pat, r"\1REDACTED\3", item)
|
try:
|
||||||
clean.append(scrubbed)
|
record.msg = pattern.sub(REDACTED, record.msg)
|
||||||
else:
|
except TypeError:
|
||||||
clean.append(item)
|
exception_text = f"{type(record.msg).__name__}: {record.msg}"
|
||||||
return clean
|
record.msg = pattern.sub(REDACTED, exception_text)
|
||||||
|
return True
|
||||||
+2
-1
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b14"
|
version = "7.0.0b15"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -110,6 +110,7 @@ markers = [
|
|||||||
"mods: tests mod metadata/link creation",
|
"mods: tests mod metadata/link creation",
|
||||||
"pefile: validate PE files",
|
"pefile: validate PE files",
|
||||||
"post_install: requires a completed installation",
|
"post_install: requires a completed installation",
|
||||||
|
"redact: log redaction mechanisms",
|
||||||
"slow: long-running tests",
|
"slow: long-running tests",
|
||||||
"webtest: checks remote endpoints"
|
"webtest: checks remote endpoints"
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import pytest
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dzgui.app_init import copy_bare_configs
|
||||||
|
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||||
|
|
||||||
|
|
||||||
|
CONF_STRING = "DZGUI_CONF\n"
|
||||||
|
STATE_STRING = "DZGUI_STATE\n"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def state_files():
|
||||||
|
state = [
|
||||||
|
"dzg.history",
|
||||||
|
"dzg.versions",
|
||||||
|
"dzg.res.json",
|
||||||
|
"dzg.filters.json",
|
||||||
|
"dzg.columns.json",
|
||||||
|
"dzg.notes.json",
|
||||||
|
"ips.csv",
|
||||||
|
".month",
|
||||||
|
]
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def xdg_paths():
|
||||||
|
routes = {
|
||||||
|
"XDG_CONFIG_HOME": "",
|
||||||
|
"XDG_STATE_HOME": "",
|
||||||
|
"XDG_DATA_HOME": "",
|
||||||
|
"XDG_CACHE_HOME": "",
|
||||||
|
}
|
||||||
|
for route in routes:
|
||||||
|
tmp = tempfile.TemporaryDirectory(delete=False)
|
||||||
|
routes[route] = tmp.name
|
||||||
|
for k, v in routes.items():
|
||||||
|
os.environ[k] = v
|
||||||
|
env = get_xdg_paths()
|
||||||
|
return parse_filepaths(env)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mods
|
||||||
|
def test_config_file_import(xdg_paths, state_files):
|
||||||
|
|
||||||
|
# NOTE: write bare files in root
|
||||||
|
tmp_conf = xdg_paths.config.parent.parent
|
||||||
|
tmp_conf_file = tmp_conf / xdg_paths.config.name
|
||||||
|
tmp_conf_file.write_text(CONF_STRING)
|
||||||
|
|
||||||
|
tmp_state = xdg_paths.resolution.parent.parent
|
||||||
|
for file in state_files:
|
||||||
|
tmp_state.joinpath(file).write_text(STATE_STRING)
|
||||||
|
# NOTE: function should move files into "dzgui" subdirectory
|
||||||
|
config_changed, state_changed = copy_bare_configs(
|
||||||
|
xdg_paths.config, xdg_paths.resolution
|
||||||
|
)
|
||||||
|
|
||||||
|
assert xdg_paths.config.read_text() == CONF_STRING
|
||||||
|
for file in state_files:
|
||||||
|
expected = xdg_paths.resolution.parent.joinpath(file)
|
||||||
|
assert expected.read_text() == STATE_STRING
|
||||||
|
|
||||||
|
assert config_changed is True
|
||||||
|
assert state_changed is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mods
|
||||||
|
def test_config_file_no_import(xdg_paths, state_files):
|
||||||
|
for subdir in xdg_paths.config.parent, xdg_paths.resolution.parent:
|
||||||
|
subdir.mkdir()
|
||||||
|
|
||||||
|
tmp_conf_file = xdg_paths.config
|
||||||
|
tmp_conf_file.write_text(CONF_STRING)
|
||||||
|
|
||||||
|
for file in state_files:
|
||||||
|
xdg_paths.resolution.parent.joinpath(file).write_text(STATE_STRING)
|
||||||
|
|
||||||
|
config_changed, state_changed = copy_bare_configs(
|
||||||
|
xdg_paths.config, xdg_paths.resolution
|
||||||
|
)
|
||||||
|
assert config_changed is False
|
||||||
|
assert state_changed is False
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import logging
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||||
|
|
||||||
|
|
||||||
|
class RecordsListHandler(logging.Handler):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.records_list = []
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
self.records_list.append(record)
|
||||||
|
|
||||||
|
def pop(self) -> None:
|
||||||
|
return self.records_list[-1].msg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.redact
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"log_error, expect",
|
||||||
|
[
|
||||||
|
("/home/SENSITIVE_USERNAME/subdir", "/home/REDACTED/subdir"),
|
||||||
|
(
|
||||||
|
"https://url.com/?api&key=SENSITIVE_KEY&results=10",
|
||||||
|
"https://url.com/?api&key=REDACTED&results=10",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://url.com/?api&key=SENSITIVE_KEY",
|
||||||
|
"https://url.com/?api&key=REDACTED",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Error in directory: '/home/SENSITIVE_USERNAME/'",
|
||||||
|
"Error in directory: '/home/REDACTED/'",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Error in directory: '/home/SENSITIVE_USERNAME'",
|
||||||
|
"Error in directory: '/home/REDACTED'",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_log_redaction(log_error: str, expect: str) -> None:
|
||||||
|
logger = logging.getLogger("TEST")
|
||||||
|
|
||||||
|
handler = RecordsListHandler()
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
_filter = RedactionFilter(patterns=REDACTION_PATTERNS)
|
||||||
|
logger.addFilter(_filter)
|
||||||
|
|
||||||
|
logger.critical(log_error)
|
||||||
|
redacted = handler.pop()
|
||||||
|
assert expect == redacted
|
||||||
Reference in new issue
Block a user