Compare commits

...
38 Commits
Author SHA1 Message Date
aclist 03bd92c24a Merge a2d1f9b0e1 into 90c7d9f73e 2026-07-07 10:52:50 +00:00
aclist a2d1f9b0e1 chore: simplify path concatenation logic 2026-07-07 19:52:44 +09:00
aclist b731fb5566 chore: drop test code 2026-07-07 19:43:41 +09:00
aclist e8d991ccb5 chore: drop legacy functions 2026-07-07 19:43:00 +09:00
aclist f4aa6c749e chore: move get_app_path() 2026-07-07 19:36:44 +09:00
aclist 3ed4747d08 chore: drop unused import 2026-07-07 18:38:25 +09:00
aclist d2042a3dde fix: clear typehinting errors 2026-07-07 18:38:14 +09:00
aclist 09ef0f4f9a chore: add reminder 2026-07-07 18:11:40 +09:00
aclist a91ce17501 feat: config parsing POC 2026-07-07 18:11:30 +09:00
aclist 48f0f3913c chore: add reminder 2026-07-07 18:11:07 +09:00
aclist 33e367182e feat: ACF class 2026-07-07 18:10:52 +09:00
aclist 0ce3ab3915 chore: add vdf dependency 2026-07-07 18:10:12 +09:00
aclist 90c7d9f73e Merge pull request #395 from aclist/chore/contributing-docs
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
docs: add contributing docs
2026-07-07 16:52:16 +09:00
aclist 5923d5d935 fix: typo 2026-07-07 16:46:01 +09:00
aclist 9f73c545ef docs: port contributing file from dzgui6, with changes 2026-07-07 16:42:25 +09:00
aclist 041438cb7f docs: update readme to reflect current release workflow 2026-07-07 16:42:03 +09:00
aclist c6e69f9aa0 Merge pull request #393 from aclist/feat/redaction-filter
feat: redaction filter
2026-07-07 16:33:22 +09:00
aclist 8f1e4ff9d0 chore: drop fixed comment
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-07 16:32:23 +09:00
aclist 92792b0b4f chore: drop extraneous comment 2026-07-07 16:29:10 +09:00
aclist a7cfd4c346 fix: monkeypatch system env 2026-07-07 16:28:52 +09:00
aclist fa5f638697 chore: add comment 2026-07-07 12:03:10 +09:00
aclist 78151b3bb3 fix: update regex
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-04 14:30:59 +09:00
aclist 2418d8be68 chore: update pytest marks 2026-07-04 14:12:46 +09:00
aclist 9fa7172056 chore: add log redaction test 2026-07-04 13:18:44 +09:00
aclist d75f87768e chore: drop extraneous newline 2026-07-04 08:48:03 +09:00
aclist 5bb4271d36 feat: log-level redaction filter 2026-07-04 08:47:34 +09:00
aclist 6c86bff869 fix: drop branch from diagnostic log 2026-07-04 08:46:54 +09:00
aclist 92ba7c2267 chore: drop unused imports, vars 2026-07-04 08:42:53 +09:00
aclist 6c56f16633 Merge pull request #391 from aclist/fix/copy-bare-configs
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: copy bare config files to named directory
2026-06-29 07:36:49 +09:00
aclist 2b87272b52 chore: update test conditions
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-29 07:28:35 +09:00
aclist 4de5147899 fix: parse parent directory
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-28 23:52:13 +09:00
aclist 4c31cd7ecd chore: add test for no import case 2026-06-28 00:07:51 +09:00
aclist 61f90d6016 fix: copy bare config files to named directory (#375) 2026-06-27 23:51:20 +09:00
aclist cd1fd1b179 Merge pull request #390 from aclist/fix/ctypes-libgi
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: platform-agnostic shared library check
2026-06-25 21:19:09 +09:00
aclist 1b25c8fadb chore: bump version 2026-06-25 21:17:30 +09:00
aclist 18146bf66f docs: fix typo 2026-06-25 21:15:54 +09:00
aclist 8ae54d436a chore: drop unused import 2026-06-25 21:15:36 +09:00
aclist 4d94551f87 fix: use platform-agnostic shared library check 2026-06-25 21:15:08 +09:00
29 changed files with 550 additions and 210 deletions

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
# Introduction
Thank you for your interest in DZGUI!
This guide goes over development conventions and best practices for contributors.
If you are a developer, you can skip to the end.
# Requesting help
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
(issue tracker)[https://github.com/aclist/dztui/issues] under the
`troubleshooting` tag.
# How can I help the project?
There are several ways to help this project.
1. Report bugs that you find
2. Request features that you would like
3. Contribute features and fixes to the codebase
4. Contribute documentation to the project
Before making any contribution, please read the `CODE_OF_CONDUCT.md` and act
accordingly.
## Submitting a ticket
Navigate to the GitHub (issue tracker)[https://github.com/aclist/dztui/issues].
From there, follow the onscreen prompts. You will be asked questions such as:
- What version are you using?
- What distribution are you using?
- What is the issue that you found?
- How can we reproduce the issue?
You can also attach screenshots, logs, or other data that can help us.
## Requesting a feature
You can also request features via the same issue tracker. It is good practice to
first search for your idea to see if a similar one has already been posted.
If not, open a ticket where you describe your feature and its possible benefits.
Please note that this is a community project, so it takes time for us to develop
features. Putting in a feature request does not mean that it will be implemented,
but we will do our best to support as many cool ideas as possible.
## Contributing code or documentation
If you would like to assist with an issue on the issue tracker or
contribute a new change, please follow the guidelines below.
Fork this repository and check out the code up to the `dzgui7` (development) branch.
The following naming conventions apply for PRs:
- fix/<your-fix> - patch/hotfix branches
- feat/<your-feature> - feature branches
- doc/<your-doc-branch> - documentation branches
- infra/<your-infra-branch> - infrastructure branches
Implement your changes and test them locally. If they work, you may
open a merge request, and then we review your changes. If everything is OK, it
will be merged to `dzgui7`. After sufficient changes are consolidated into a new release,
this branch will be tagged to a certain point in time and a binary release published.
It is recommended to follow
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), as this
integrates well with tooling and helps the project review your code.
Please see DEVELOPERS.md for further details.
+2 -7
View File
@@ -24,13 +24,8 @@ Please refer to the documentation for installation and setup instructions:
Geolocation records from [DB-IP](https://db-ip.com) under [CC 4.0 license](https://creativecommons.org/licenses/by/4.0/)
This tool uses [python-a2s](https://github.com/Yepoleb/python-a2s) and [dayzquery](https://github.com/Yepoleb/dayzquery) as submodules; licenses for these submodules can be found in the LICENSES file
of the project root.
Both the geolocation records and submodules listed above are not shipped with the source code, but are retrieved and assembled at runtime.
Finally, executable versions of DZGUI shipped as release binaries are thin wrappers around the Python interpreter, and also retrieve and assemble the above dependencies at runtime on the end-user's
machine, rather than using pre-compiled source code. Users wishing to review these dependencies can inspect the 'pyproject.toml' manifest in the project root.
Executable versions of DZGUI published as release binaries ship with various runtime dependencies and the Python interpreter built in.
Users wishing to review the licenses to these components can inspect the `LICENSE` file located in the release tarball.
## Disclaimer
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2015 Rossen Georgiev <rossen@rgp.io>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -26,7 +26,7 @@ You can install it via the following set of commands:
.. code:: console
sudo apt update
sudo apt install libgirepository-2.0
sudo apt install libgirepository-2.0-0
Next steps
---------------
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+39 -15
View File
@@ -1,12 +1,18 @@
import logging
import re
from collections.abc import Iterator
from pathlib import Path
from typing import Any
from warnings import deprecated
from dzgui.const.constants import APP_NAME
@deprecated("Use dzgui.api.steam.unsubscribe()")
class WorkshopACF:
def __init__(self, file: str) -> None:
logger = logging.getLogger(APP_NAME)
class ACF:
def __init__(self, file: Path) -> None:
super().__init__()
self.dict: dict[str, Any]
@@ -15,9 +21,10 @@ class WorkshopACF:
def as_dict(self) -> dict[str, Any]:
return self.dict
def load(self, file: str) -> None:
def load(self, file: Path) -> None:
delimiter = r"\t\t"
lines = []
# TODO: illegal file handling
with open(file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
@@ -27,6 +34,14 @@ class WorkshopACF:
lines.append(els)
self.dict = self.parse(iter(lines))
@classmethod
def enquote(cls, s: str) -> str:
return f'"{s}"'
@classmethod
def dequote(cls, s: str) -> str:
return s.rstrip('"').lstrip('"')
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
acf: dict[str, Any] = {}
try:
@@ -54,6 +69,24 @@ class WorkshopACF:
except StopIteration:
return acf
def get_allows_downloads(self) -> int | None:
try:
# TODO: consider coercing strs to ints on initial import
flag = int(self.dict["AppState"]["AllowOtherDownloadsWhileRunning"])
return flag
except Exception as e:
logger.critical(e)
return None
@deprecated(
"Serializing workshop files is no longer supported, use dzgui.api.steam.unsubscribe"
)
class WorkshopACF(ACF):
def __init__(self, file: Path) -> None:
super().__init__(file)
# TODO: break into workshop parser and generic acf parser
def unpack(self, d: dict, lines: list[Any] = []) -> str:
t1 = "AppWorkshop"
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
@@ -78,18 +111,9 @@ class WorkshopACF:
s += line + "\n"
return s
def to_file(self, file: str) -> None:
def to_file(self, file: Path) -> None:
s = self.unpack(self.dict)
with open(file, "w") as f:
f.write(s)
@classmethod
def enquote(cls, s: str) -> str:
return f'"{s}"'
@classmethod
def dequote(cls, s: str) -> str:
return s.rstrip('"').lstrip('"')
file.write_text(s)
def delete(self, modid: int) -> None:
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
+2 -3
View File
@@ -7,13 +7,12 @@ from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path
import dzgui.api.pefile as PeFile
from dzgui.api.steam import get_app_path
from dzgui.api.servers import get_rules, fqip_to_record
from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
DAYZ_COMMUNITY_ROOT,
LIBRARYFOLDERS_PATH,
WORKSHOP_PATH,
)
@@ -40,7 +39,7 @@ def get_local_mod_ids(steam_path: Path) -> list[int]:
def get_local_mod_path(steam_path: Path) -> Path:
p = PeFile.get_app_path(steam_path / Path(LIBRARYFOLDERS_PATH), APPID_DAYZ)
p = get_app_path(steam_path, APPID_DAYZ)
workshop_path = p / WORKSHOP_PATH
return workshop_path
+2 -47
View File
@@ -12,9 +12,8 @@ from dzgui.const.constants import (
APPNAME_DAYZ,
APPNAME_DAYZ_EXP,
DAYZ_BINARY,
LIBRARYFOLDERS_PATH,
)
from dzgui.api.steam import vdf2json
from dzgui.api.steam import get_app_path
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
endian = "<"
@@ -257,25 +256,6 @@ class PeFileError(Exception):
pass
class AppNotInstalledError(Exception):
"""App not present in user's libraryfolders"""
pass
class AppMovedError(Exception):
"""VDF points to a nonexistent location on disk"""
pass
class VDFLoadError(Exception):
"""Malformed VDF or JSON conversion"""
pass
def parse_version_number(data: BinaryIO) -> FileVersion:
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
@@ -419,36 +399,11 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path:
name = identifier[appid]
binary = DAYZ_BINARY
app_path = get_app_path(steam_path / LIBRARYFOLDERS_PATH, appid)
app_path = get_app_path(steam_path, appid)
pe_path = app_path / f"steamapps/common/{name}/{binary}"
return pe_path
def get_app_path(folders_path: Path, appid: int) -> Path:
app_path = None
try:
j = json.loads(vdf2json(folders_path))
except Exception:
raise VDFLoadError("Failed to parse libraryfolders")
for obj in j["libraryfolders"]:
if str(appid) in j["libraryfolders"][obj]["apps"]:
app_path = j["libraryfolders"][obj]["path"]
if Path(app_path).exists():
break
if app_path is None:
raise AppNotInstalledError(
f"Failed to find a libraryfolder for the appid {appid}"
)
if Path(app_path).exists() is False:
raise AppMovedError(
f"The location '{app_path}' pointed to by '{appid}' no longer exists and may have been changed on the disk."
)
return Path(app_path)
def get_pretty_version(steam_path: Path, appid: int) -> str | None:
try:
+167 -49
View File
@@ -1,21 +1,26 @@
import json
import logging
import os
import psutil
import requests
import subprocess
from typing import Union
from warnings import deprecated
import vdf
from shlex import shlex
from pathlib import Path
from typing import Any, Union
from warnings import deprecated
from dzgui.api.acf import ACF
from dzgui.init.prereqs import has_steam_client
from dzgui.const.constants import (
APPID_DAYZ,
APPID_DAYZ_EXP,
APP_NAME,
DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH,
LIBRARYFOLDERS_PATH,
UBUNTU_STEAM_PATH,
REQUEST_TIMEOUT,
VDF_PATH,
@@ -24,10 +29,27 @@ from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_END
from dzgui.strings import wizard
from dzgui.util.bash import concat_bash_args
logger = logging.getLogger(APP_NAME)
class AppNotInstalledError(Exception):
"""App not present in user's libraryfolders"""
pass
class AppMovedError(Exception):
"""VDF points to a nonexistent location on disk"""
pass
class VDFLoadError(Exception):
"""Malformed VDF or JSON conversion"""
pass
def get_steam_paths() -> list[tuple[Path, str]]:
paths = []
if has_steam_client():
@@ -183,57 +205,17 @@ def launch_offline(
def find_user_id(path: Path) -> str | None:
resolved_path = path / "config" / "loginusers.vdf"
try:
vdf = vdf2json(resolved_path)
j = json.loads(vdf)
for user in j["users"]:
if j["users"][user]["MostRecent"] == "1":
return str(user)
return None
with open(resolved_path, "r") as f:
v = vdf.load(f)
for user in v["users"]:
if v["users"][user]["MostRecent"] == "1":
return str(user)
return None
except Exception as e:
logger.warn(e)
return None
def vdf2json(path: Path) -> str:
def _istr(indent: int, string: str) -> str:
return (indent * " ") + string
jbuf = "{\n"
indent = 1
with open(path, "r") as f:
st = f.read()
lex = shlex(st)
while True:
tok = lex.get_token()
if not tok:
return jbuf + "}\n"
if tok == "}":
indent -= 1
jbuf += _istr(indent, "}")
ntok = lex.get_token()
if ntok is not None:
lex.push_token(ntok)
if ntok and ntok != "}":
jbuf += ","
jbuf += "\n"
else:
ntok = lex.get_token()
if ntok == "{":
jbuf += _istr(indent, tok + ": {\n")
indent += 1
else:
if ntok is not None:
jbuf += _istr(indent, tok + ": " + ntok)
ntok = lex.get_token()
if ntok is not None:
lex.push_token(ntok)
if ntok != "}":
jbuf += ","
jbuf += "\n"
def update_workshop(key: str, mod: int, endpoint: str) -> None:
payload: dict[str, Union[int, str]] = {
"publishedfileid": mod,
@@ -275,3 +257,139 @@ def gen_shortcut() -> None:
def enqueue_mod(client: str, mod: str, appid: int) -> None:
client_args = concat_bash_args(client)
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
def get_registry() -> dict[str, Any] | None:
home = os.getenv("HOME")
try:
with open(f"{home}/.steam/registry.vdf") as f:
registry = vdf.load(f)
return registry
except Exception as e:
logger.critical(e)
return None
def _is_dayz_running() -> bool:
registry = get_registry()
if registry is None:
return False
apps = registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"]["apps"].items()
for app in apps:
k, v = app
if k in (APPID_DAYZ, APPID_DAYZ_EXP):
try:
state = v["Running"]
# NOTE: 0 denotes False
return bool(int(state))
except Exception as e:
logger.critical(e)
return False
return False
def _get_running_app() -> int | None:
registry = get_registry()
if registry is None:
return None
try:
return registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"][
"RunningAppID"
]
except Exception:
return None
# TODO: write tests
# TODO: consider moving to proc module
def get_running_app() -> int | None:
PROC_NAME = "steam"
SUBPROC_NAME = "reaper"
FLAG = "AppId"
for proc in psutil.process_iter():
if proc.name() == PROC_NAME:
subprocs = proc.children()
filtered = (proc for proc in subprocs if proc.name() == SUBPROC_NAME)
try:
proc = next(filtered)
except StopIteration:
return None
args = proc.cmdline()
appid = (row for row in args if FLAG in row)
try:
return str(next(appid).split("=")[1])
except StopIteration:
return None
return None
# TODO: write tests
def get_app_allows_downloads(path: Path, appid: int) -> bool:
root_path = get_app_path(path, appid)
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
flag = ACF(acf).get_allows_downloads()
match flag:
# NOTE: adheres to global client setting
case 0:
return get_client_allows_downloads(path)
# NOTE: always allow
case 1:
return True
# NOTE: never allow
case 2:
return False
case _:
return True
def get_client_allows_downloads(path: Path) -> bool:
config = path.joinpath("config/config.vdf")
try:
with open(config) as f:
settings = vdf.load(f)
# NOTE: "1" denotes "allow"
allow = bool(
int(
settings["InstallConfigStore"]["Software"]["Valve"]["Steam"][
"AllowDownloadsDuringGameplay"
]
)
)
return allow
except Exception as e:
logger.critical(e)
return True
def is_dayz_running() -> bool:
appid = get_running_app()
return appid in (APPID_DAYZ, APPID_DAYZ_EXP)
def get_app_path(folders_path: Path, appid: int) -> Path:
app_path = None
try:
with open(folders_path / LIBRARYFOLDERS_PATH) as f:
folders = vdf.load(f)
except Exception:
raise VDFLoadError("Failed to parse libraryfolders")
for obj in folders["libraryfolders"]:
if str(appid) in folders["libraryfolders"][obj]["apps"]:
app_path = folders["libraryfolders"][obj]["path"]
if Path(app_path).exists():
break
if app_path is None:
raise AppNotInstalledError(
f"Failed to find a libraryfolder for the appid {appid}"
)
if Path(app_path).exists() is False:
raise AppMovedError(
f"The location '{app_path}' pointed to by '{appid}' no longer exists and may have been changed on the disk."
)
return Path(app_path)
+50
View File
@@ -1,5 +1,6 @@
import logging
import os
import shutil
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.deck import is_steam_deck, is_game_mode
from dzgui.util.localize import set_locale
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
from dzgui.util.strings import init
from dzgui.views.base import App
@@ -39,17 +41,63 @@ def make_parents(path: "Path") -> None:
def setup_logger(log_path: "Path") -> None:
# TODO: put in consts?
_format = (
"%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s"
)
fh = logging.FileHandler(log_path)
formatter = logging.Formatter(_format)
fh.setFormatter(formatter)
fh.setLevel(logging.DEBUG)
_filter = RedactionFilter(patterns=REDACTION_PATTERNS)
fh.addFilter(_filter)
logger.setLevel(logging.DEBUG)
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:
lock = lock_acquire() # noqa
@@ -58,6 +106,8 @@ def load_gui(version: str, is_debug: bool) -> None:
xdg_paths = get_xdg_paths()
XDG = parse_filepaths(xdg_paths)
copy_bare_configs(XDG.config, XDG.resolution)
if XDG.resolution.parent.is_dir() is False:
make_parents(XDG.resolution)
+6 -5
View File
@@ -29,9 +29,9 @@ def is_writeable(path_str: str) -> bool:
if not path.exists():
try:
path.mkdir(parents=True)
path.unlink()
except OSError:
return False
path.unlink()
return True
try:
@@ -55,11 +55,12 @@ def get_xdg_paths() -> dict:
resolved_paths = {}
for path in xdg_paths:
rp = os.environ.get(path)
if rp is not None and is_writeable(rp):
resolved_paths[path] = Path(rp)
real_path = os.environ.get(path)
if real_path is not None and is_writeable(real_path):
new_path = Path(real_path)
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
+3 -3
View File
@@ -2,11 +2,11 @@ import logging
from pathlib import Path
from dzgui.api.steam import get_app_path
from dzgui.config.query import lookup
from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH
from dzgui.const.constants import APPID_DAYZ, APP_NAME
from dzgui.const.enum import Preferences
import dzgui.api.pefile as PeFile
logger = logging.getLogger(APP_NAME)
@@ -14,7 +14,7 @@ logger = logging.getLogger(APP_NAME)
def is_dayz_installed(config: Path) -> None:
try:
path = lookup(config, Preferences.DEFAULT)
PeFile.get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ)
get_app_path(Path(path), APPID_DAYZ)
except Exception as e:
logger.critical(e)
raise e
+3 -4
View File
@@ -1,11 +1,12 @@
import platform
import sys
from pathlib import Path
from ctypes.util import find_library
from warnings import deprecated
LIB = "libgirepository-2.0"
@deprecated("currently unused")
def is_debian() -> bool:
"""
@@ -39,9 +40,7 @@ def is_debian() -> bool:
def has_libgi() -> bool:
parent_dir = Path("/usr/lib64")
exists = any(LIB in subdir.name for subdir in parent_dir.iterdir())
return exists
return bool(find_library("girepository-2.0"))
def test_libgi_missing() -> None:
-37
View File
@@ -20,21 +20,6 @@ from dzgui.util.format import format_exception
logger = logging.getLogger(APP_NAME)
# TODO: move to util.proc
def is_dayz_running() -> bool:
"""Subprocesses spawned from Steam will not show up in regular process tree"""
procs = []
substring = DAYZ_BINARY
for proc in psutil.process_iter():
try:
procs.append(proc.cmdline())
except Exception as e:
msg = format_exception(e)
logger.warning(msg)
continue
return any(substring in item for sublist in procs for item in sublist)
def is_steam_running(cmd: str) -> bool:
if cmd == STEAM_CMD:
if has_cmd(STEAM_CMD) is False:
@@ -69,25 +54,3 @@ def has_cmd(cmd: str) -> bool:
if shutil.which(cmd) is not None:
return True
return False
@deprecated("dropped in favor of Gtk native methods")
def foreground(cmd: str, pid: int) -> None:
if cmd == "wmctrl":
proc = subprocess.run(["wmctrl", "-ilp"], capture_output=True, text=True)
lines = proc.stdout.splitlines()
for line in lines:
els = line.split(" ")
if str(pid) in els:
wid = els[0]
break
subprocess.run(["wmctrl", "-ia", wid])
elif cmd == "xdotool":
args = [cmd, "search", "--pid", str(pid)]
proc = subprocess.run([*args], capture_output=True, text=True)
lines = proc.stdout.splitlines()
## NOTE: some forked subprocesses may fail, so skip over them
for line in lines:
subprocess.run(
["xdotool", "windowactivate", line], stderr=subprocess.DEVNULL
)
+2 -1
View File
@@ -32,8 +32,9 @@ from dzgui.const.constants import (
APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN,
)
from dzgui.api.steam import is_dayz_running
from dzgui.const.enum import NotebookPage, Preferences
from dzgui.init.proc import is_dayz_running, is_steam_running
from dzgui.init.proc import is_steam_running
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings.dialogs import (
waiting_for_launch,
+1 -2
View File
@@ -5,10 +5,9 @@ from typing import Callable, TYPE_CHECKING, Union
import dzgui.api.pefile as PeFile
from dzgui.api.mods import is_mission, get_custom_mods
from dzgui.api.steam import launch_offline
from dzgui.api.steam import is_dayz_running, launch_offline
from dzgui.const.constants import APP_NAME, APPID_DAYZ_EXP
from dzgui.const.enum import Preferences
from dzgui.init.proc import is_dayz_running
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import ModelFactory
from dzgui.strings import dialogs
+1 -4
View File
@@ -2,7 +2,6 @@ from dataclasses import dataclass
from typing import Any, Sequence, TYPE_CHECKING
from dzgui.const.enum import HELP_MENU_ROWS
from dzgui.util.redact import redact_log
from dzgui.util.strings import delimiter
import gi
@@ -134,9 +133,7 @@ class ModelFactory:
with open(path, "r") as f:
lines = [line.split(delimiter) for line in f.read().splitlines()]
for record in lines:
# NOTE: strips PII and API keys
clean = redact_log(record)
store.append(clean)
store.append(record)
return store
def new_model_from_class(self, cls: type) -> FastInsertListStore:
+13 -6
View File
@@ -1,3 +1,4 @@
import logging
import os
import platform
@@ -9,7 +10,9 @@ from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences
from dzgui.config.query import lookup
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:
@@ -41,12 +44,16 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
# TODO: test availability on other distros
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
cpu = get_cpu_model()
version = get_version()
branch = lookup(config, Preferences.BRANCH)
debug = lookup(config, Preferences.DEBUG)
install = lookup(config, Preferences.INSTALL)
default = lookup(config, Preferences.DEFAULT)
@@ -54,8 +61,8 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
steam_path = Path(default)
workshop_path = get_local_mod_path(steam_path)
steam_redacted = redact(default)
workshop_redacted = redact(str(workshop_path))
steam_redacted = redact_home(default)
workshop_redacted = redact_home(str(workshop_path))
mods = get_local_mod_ids(steam_path)
mods_pretty = print_mods(mods)
@@ -65,7 +72,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
# FIXME: extraneous newlines in lists of mods
template = f"""\
{APP_NAME} version {version} ({branch})
{APP_NAME} version {version}
Date: {date}
===============================
Distribution: {distro}
+25 -16
View File
@@ -1,20 +1,29 @@
import logging
import re
from typing import Literal
def redact(text: str) -> str:
r = r"(/home/)([^/])*"
cleaned = re.sub(r, r"/home/REDACTED", text)
api_filter = r"(.*&key=)([^&]*)(.*)"
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
def redact_log(record: list) -> list[str]:
"""
requests library includes Steam API key in URL params
"""
clean = []
for item in record:
if "&key=" in item:
pat = r"(.*&key=)(\S+)(.*)"
scrubbed = re.sub(pat, r"\1REDACTED\3", item)
clean.append(scrubbed)
else:
clean.append(item)
return clean
class RedactionFilter(logging.Filter):
def __init__(self, patterns: list[str] | None = None) -> None:
super().__init__()
self._patterns = [re.compile(pat) for pat in (patterns or [])]
def filter(self, record: logging.LogRecord) -> Literal[True]:
for pattern in self._patterns:
try:
record.msg = pattern.sub(REDACTED, record.msg)
except TypeError:
exception_text = f"{type(record.msg).__name__}: {record.msg}"
record.msg = pattern.sub(REDACTED, exception_text)
return True
+1
View File
@@ -288,6 +288,7 @@ class Options(Gtk.Box):
def _on_api_change_failed(self, emitter: "Emitter") -> None:
self.old_entry.set_text(self.old_text)
# TODO: use popover
dialog = ExceptionDialog(self.controller, errors.api_validation_error)
dialog.run()
+3 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0b14"
version = "7.0.0b15"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"
@@ -27,6 +27,7 @@ dependencies = [
"psutil==7.1.3",
"python-a2s",
"requests==2.32.5",
"vdf==3.4"
]
[project.scripts]
@@ -110,6 +111,7 @@ markers = [
"mods: tests mod metadata/link creation",
"pefile: validate PE files",
"post_install: requires a completed installation",
"redact: log redaction mechanisms",
"slow: long-running tests",
"webtest: checks remote endpoints"
]
+84
View File
@@ -0,0 +1,84 @@
import pytest
import tempfile
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(monkeypatch):
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():
monkeypatch.setenv(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
-5
View File
@@ -95,8 +95,3 @@ def test_missing_values(unset_values):
j = convert.rc2json(unset_values)
j = json.loads(j)
assert j.keys() == config_boilerplate.keys()
# TODO: test that when a config file is created from scratch, it contains all values
+54
View File
@@ -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
+3 -3
View File
@@ -2,7 +2,7 @@ import pytest
from pathlib import Path
import dzgui.api.pefile as PeFile
from dzgui.api.pefile import VDFLoadError, AppNotInstalledError, AppMovedError
from dzgui.api.steam import VDFLoadError, AppNotInstalledError, AppMovedError, get_app_path
from dzgui.config.query import get_config
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
@@ -56,11 +56,11 @@ def test_not_in_library(fixture, exception):
fixture = fixture_path(fixture)
with pytest.raises(exception):
try:
PeFile.get_app_path(fixture, APPID_DAYZ)
get_app_path(fixture, APPID_DAYZ)
except Exception as e:
raise e
def test_on_second_drive(second_drive):
path = PeFile.get_app_path(second_drive, APPID_DAYZ)
path = get_app_path(second_drive, APPID_DAYZ)
assert path == Path("/tmp")
-1
View File
@@ -20,7 +20,6 @@ def test_steam(config):
key = config["steam_api"]
assert probe.test_steam_api(key)
def test_bm(config):
key = config["bm_api"]
assert probe.test_bm_api(key)