Skip to content

feat(persist): default persistency path to systemd $STATE_DIRECTORY#191

Open
viktorbeck98 wants to merge 1 commit into
developmentfrom
feat/persist-state-directory-default
Open

feat(persist): default persistency path to systemd $STATE_DIRECTORY#191
viktorbeck98 wants to merge 1 commit into
developmentfrom
feat/persist-state-directory-default

Conversation

@viktorbeck98

@viktorbeck98 viktorbeck98 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Task

#190

Description

PersistConfig.path defaulted to the CWD-relative "./state". Under systemd
(where the service CWD is typically / unless WorkingDirectory= is set), this
resolves to /state — the wrong location, and writable only by root. Worse, the
resulting save failure is swallowed as a logger.warning in PersistencySaver.save(),
so the detector keeps running and silently persists nothing.

This PR makes the default environment-aware:

  • PersistConfig.path now defaults to systemd's $STATE_DIRECTORY (set by
    StateDirectory= in the unit file, which also creates the directory with the
    correct service-user ownership), falling back to ./state outside systemd.
  • A colon-separated $STATE_DIRECTORY (systemd's format for multiple
    StateDirectory= entries) uses the first path.
  • An explicit PersistConfig(path=...) still wins — default_factory only runs
    when path is omitted, so no existing caller changes.

Implemented as a one-line Field(default_factory=...); no hardcoded system path,
which keeps the library portable across containers, non-root, and dev.

Docs (docs/detectors.md): added a "Running under systemd" note with a unit-file
example and updated the path field table.

Out of scope (worth separate issues): an XDG_STATE_HOME fallback for
non-systemd user daemons, and making PersistencySaver.save() fail fast / escalate
instead of silently swallowing permanent save failures.

How Has This Been Tested?

Added unit tests in detectmatelibrary_tests/test_common/test_persist_config.py
(class TestPersistConfigStateDirectoryDefault), written test-first:

  • default reads $STATE_DIRECTORY when set,
  • takes the first entry of a colon-separated list,
  • falls back to ./state when unset,
  • explicit path= overrides the env var.

Also pinned $STATE_DIRECTORY unset in the existing test_defaults (the change
made it env-sensitive). uv run pytest green: 19 passing in the persist-config
suite, 143 across the persistency-related suites. prek hooks (mypy, flake8,
bandit, …) all pass.

Checklist

  • This Pull-Request goes to the development branch.
  • I have successfully run prek locally.
  • I have added tests to cover my changes.
  • I have linked the issue-id to the task-description.
  • I have performed a self-review of my own code.

PersistConfig.path defaulted to the CWD-relative "./state". Under systemd
(CWD typically /), this resolves to /state — wrong location and needs root,
and the save failure is swallowed as a warning, so state is silently lost.

Default now reads systemd's $STATE_DIRECTORY (set by StateDirectory= in the
unit file, which also creates the dir with correct ownership), taking the
first entry of a colon-separated list, and falls back to ./state outside
systemd. Explicit path= still overrides.

Docs: systemd setup note + updated field table in docs/detectors.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Falls back to CWD-relative ./state outside systemd. Explicit path= wins.
path: str = Field(
default_factory=lambda: os.environ.get("STATE_DIRECTORY", "./state").split(":")[0]
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what happens when STATE_DIRECTORY="", or if it starts with a colon? overwrites the path with an empty string, then the complete path will become /MyDetector and also have permission errors (root)

# unit file) so services persist to /var/lib/<dir> with no explicit path=.
# Falls back to CWD-relative ./state outside systemd. Explicit path= wins.
path: str = Field(
default_factory=lambda: os.environ.get("STATE_DIRECTORY", "./state").split(":")[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
default_factory=lambda: os.environ.get("STATE_DIRECTORY", "./state").split(":")[0]
default_factory=lambda: (os.environ.get("STATE_DIRECTORY") or "./state").split(":")[0]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants