aboutsummaryrefslogtreecommitdiffstats
path: root/pylogsentinel
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2026-07-30 17:58:16 +0300
committerJan Tuomi <jan@jantuomi.fi>2026-07-30 17:58:26 +0300
commit3bf398ecd5e7a97746464f14dd549e5a7117dfd8 (patch)
treebc2e5f2002d40014d9229ff6e7b79096986362ac /pylogsentinel
parent9c54af24fb787ca505c0b6fc8abcd6f3f43d753a (diff)
0.5.0: Key state files by device id + inode to avoid collisionsHEADmain
Diffstat (limited to 'pylogsentinel')
-rw-r--r--pylogsentinel/core.py25
1 files changed, 16 insertions, 9 deletions
diff --git a/pylogsentinel/core.py b/pylogsentinel/core.py
index 5ee033e..29ac0e4 100644
--- a/pylogsentinel/core.py
+++ b/pylogsentinel/core.py
@@ -351,11 +351,15 @@ class LockFile:
class StateManager:
"""
- Manage per-inode state files.
+ Manage per-file state files keyed by device and inode.
Format per file:
<byte_offset> <line_number>
+ The state file is stored at "<state_dir>/<device>/<inode>" to uniquely
+ identify files across different filesystems (which may have overlapping
+ inode numbers).
+
If file is truncated (size < stored offset) we reset to 0,0.
"""
@@ -364,11 +368,11 @@ class StateManager:
def __init__(self, state_dir: str) -> None:
self.state_dir = state_dir
- def _path_for_inode(self, inode: int) -> str:
- return os.path.join(self.state_dir, str(inode))
+ def _path_for_file(self, device: int, inode: int) -> str:
+ return os.path.join(self.state_dir, str(device), str(inode))
- def load(self, inode: int) -> tuple[int, int]:
- path = self._path_for_inode(inode)
+ def load(self, device: int, inode: int) -> tuple[int, int]:
+ path = self._path_for_file(device, inode)
try:
with open(path, "r", encoding="utf-8") as fh:
content = fh.read().strip()
@@ -393,11 +397,13 @@ class StateManager:
return 0, 0
return byte_offset, line_number
- def save(self, inode: int, byte_offset: int, line_number: int) -> None:
- path = self._path_for_inode(inode)
+ def save(self, device: int, inode: int, byte_offset: int, line_number: int) -> None:
+ path = self._path_for_file(device, inode)
+ device_dir = os.path.dirname(path)
tmp_path = f"{path}.tmp"
data = f"{byte_offset} {line_number}\n"
try:
+ os.makedirs(device_dir, exist_ok=True)
with open(tmp_path, "w", encoding="utf-8") as fh:
fh.write(data)
os.replace(tmp_path, path)
@@ -711,8 +717,9 @@ class Sentinel:
print(f"WARNING: Cannot stat {file_path}: {e}", file=sys.stderr)
return
+ device = st.st_dev
inode = st.st_ino
- prev_offset, prev_line_no = self.state_manager.load(inode)
+ prev_offset, prev_line_no = self.state_manager.load(device, inode)
lines, new_offset, new_last_line_no = read_new_lines(
file_path, prev_offset, prev_line_no, self.max_block_size
@@ -726,7 +733,7 @@ class Sentinel:
action = self.actions.get(event.rule.action_id) or self.actions["default"]
execute_action(action, event, skip_actions=self.skip_actions)
- self.state_manager.save(inode, new_offset, new_last_line_no)
+ self.state_manager.save(device, inode, new_offset, new_last_line_no)
def run(self) -> int:
"""