Fix critical race conditions and unsafe disk report command
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
LOCK_DIR = Path("/var/run/tg-bot")
|
LOCK_DIR = Path("/var/run/tg-bot")
|
||||||
@@ -11,9 +12,14 @@ def lock_path(name: str) -> Path:
|
|||||||
|
|
||||||
def acquire_lock(name: str) -> bool:
|
def acquire_lock(name: str) -> bool:
|
||||||
p = lock_path(name)
|
p = lock_path(name)
|
||||||
if p.exists():
|
try:
|
||||||
|
fd = os.open(str(p), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||||
|
except FileExistsError:
|
||||||
return False
|
return False
|
||||||
p.write_text(str(time.time()))
|
try:
|
||||||
|
os.write(fd, str(time.time()).encode("ascii", errors="ignore"))
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,48 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from services.runner import run_cmd
|
from services.runner import run_cmd
|
||||||
|
|
||||||
|
|
||||||
def _top_dirs_cmd(path: str, limit: int) -> list[str]:
|
def _top_dirs_cmd(path: str, limit: int) -> list[str]:
|
||||||
return ["bash", "-lc", f"du -xhd1 {path} 2>/dev/null | sort -h | tail -n {limit}"]
|
_ = limit
|
||||||
|
return ["du", "-x", "-h", "-d", "1", path]
|
||||||
|
|
||||||
|
|
||||||
|
_SIZE_RE = re.compile(r"^\s*([0-9]+(?:\.[0-9]+)?)([KMGTP]?)(i?B?)?$", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _size_to_bytes(value: str) -> float:
|
||||||
|
m = _SIZE_RE.match(value.strip())
|
||||||
|
if not m:
|
||||||
|
return -1.0
|
||||||
|
num = float(m.group(1))
|
||||||
|
unit = (m.group(2) or "").upper()
|
||||||
|
mul = {
|
||||||
|
"": 1,
|
||||||
|
"K": 1024,
|
||||||
|
"M": 1024**2,
|
||||||
|
"G": 1024**3,
|
||||||
|
"T": 1024**4,
|
||||||
|
"P": 1024**5,
|
||||||
|
}.get(unit, 1)
|
||||||
|
return num * mul
|
||||||
|
|
||||||
|
|
||||||
|
def _format_top_dirs(raw: str, limit: int) -> str:
|
||||||
|
rows: list[tuple[float, str]] = []
|
||||||
|
for line in raw.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(maxsplit=1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
continue
|
||||||
|
size, name = parts
|
||||||
|
rows.append((_size_to_bytes(size), f"{size}\t{name}"))
|
||||||
|
rows.sort(key=lambda x: x[0])
|
||||||
|
return "\n".join(line for _sz, line in rows[-max(1, limit):])
|
||||||
|
|
||||||
|
|
||||||
async def build_disk_report(cfg: dict[str, Any], mount: str, usage: int) -> str:
|
async def build_disk_report(cfg: dict[str, Any], mount: str, usage: int) -> str:
|
||||||
@@ -15,24 +52,27 @@ async def build_disk_report(cfg: dict[str, Any], mount: str, usage: int) -> str:
|
|||||||
|
|
||||||
rc, out = await run_cmd(_top_dirs_cmd(mount, limit), timeout=30)
|
rc, out = await run_cmd(_top_dirs_cmd(mount, limit), timeout=30)
|
||||||
if rc == 0 and out.strip():
|
if rc == 0 and out.strip():
|
||||||
|
top_out = _format_top_dirs(out, limit)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Top directories:")
|
lines.append("Top directories:")
|
||||||
lines.append(out.strip())
|
lines.append(top_out)
|
||||||
|
|
||||||
docker_dir = cfg.get("disk_report", {}).get("docker_dir", "/var/lib/docker")
|
docker_dir = cfg.get("disk_report", {}).get("docker_dir", "/var/lib/docker")
|
||||||
if docker_dir and os.path.exists(docker_dir):
|
if docker_dir and os.path.exists(docker_dir):
|
||||||
rc2, out2 = await run_cmd(_top_dirs_cmd(docker_dir, limit), timeout=30)
|
rc2, out2 = await run_cmd(_top_dirs_cmd(docker_dir, limit), timeout=30)
|
||||||
if rc2 == 0 and out2.strip():
|
if rc2 == 0 and out2.strip():
|
||||||
|
top_out2 = _format_top_dirs(out2, limit)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"Docker dir: {docker_dir}")
|
lines.append(f"Docker dir: {docker_dir}")
|
||||||
lines.append(out2.strip())
|
lines.append(top_out2)
|
||||||
|
|
||||||
logs_dir = cfg.get("disk_report", {}).get("logs_dir", "/var/log")
|
logs_dir = cfg.get("disk_report", {}).get("logs_dir", "/var/log")
|
||||||
if logs_dir and os.path.exists(logs_dir):
|
if logs_dir and os.path.exists(logs_dir):
|
||||||
rc3, out3 = await run_cmd(_top_dirs_cmd(logs_dir, limit), timeout=30)
|
rc3, out3 = await run_cmd(_top_dirs_cmd(logs_dir, limit), timeout=30)
|
||||||
if rc3 == 0 and out3.strip():
|
if rc3 == 0 and out3.strip():
|
||||||
|
top_out3 = _format_top_dirs(out3, limit)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"Logs dir: {logs_dir}")
|
lines.append(f"Logs dir: {logs_dir}")
|
||||||
lines.append(out3.strip())
|
lines.append(top_out3)
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
|
import tempfile
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
_PATH = "/var/server-bot/runtime.json"
|
_PATH = "/var/server-bot/runtime.json"
|
||||||
_STATE: Dict[str, Any] = {}
|
_STATE: Dict[str, Any] = {}
|
||||||
|
_LOCK = threading.RLock()
|
||||||
|
_LOADED = False
|
||||||
|
|
||||||
|
|
||||||
def configure(path: str | None):
|
def configure(path: str | None):
|
||||||
@@ -13,40 +17,57 @@ def configure(path: str | None):
|
|||||||
|
|
||||||
|
|
||||||
def _load_from_disk():
|
def _load_from_disk():
|
||||||
global _STATE
|
global _STATE, _LOADED
|
||||||
if not os.path.exists(_PATH):
|
if not os.path.exists(_PATH):
|
||||||
_STATE = {}
|
_STATE = {}
|
||||||
|
_LOADED = True
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
with open(_PATH, "r", encoding="utf-8") as f:
|
with open(_PATH, "r", encoding="utf-8") as f:
|
||||||
_STATE = json.load(f)
|
_STATE = json.load(f)
|
||||||
except Exception:
|
except Exception:
|
||||||
_STATE = {}
|
_STATE = {}
|
||||||
|
_LOADED = True
|
||||||
|
|
||||||
|
|
||||||
def _save():
|
def _save():
|
||||||
os.makedirs(os.path.dirname(_PATH), exist_ok=True)
|
directory = os.path.dirname(_PATH) or "."
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
try:
|
try:
|
||||||
with open(_PATH, "w", encoding="utf-8") as f:
|
fd, tmp_path = tempfile.mkstemp(prefix=".runtime.", suffix=".json", dir=directory)
|
||||||
json.dump(_STATE, f)
|
try:
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(_STATE, f, ensure_ascii=False)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
os.replace(tmp_path, _PATH)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
try:
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_state() -> Dict[str, Any]:
|
def get_state() -> Dict[str, Any]:
|
||||||
if not _STATE:
|
with _LOCK:
|
||||||
_load_from_disk()
|
if not _LOADED:
|
||||||
return _STATE
|
_load_from_disk()
|
||||||
|
return _STATE
|
||||||
|
|
||||||
|
|
||||||
def set_state(key: str, value: Any):
|
def set_state(key: str, value: Any):
|
||||||
if not _STATE:
|
with _LOCK:
|
||||||
_load_from_disk()
|
if not _LOADED:
|
||||||
_STATE[key] = value
|
_load_from_disk()
|
||||||
_save()
|
_STATE[key] = value
|
||||||
|
_save()
|
||||||
|
|
||||||
|
|
||||||
def get(key: str, default: Any = None) -> Any:
|
def get(key: str, default: Any = None) -> Any:
|
||||||
if not _STATE:
|
with _LOCK:
|
||||||
_load_from_disk()
|
if not _LOADED:
|
||||||
return _STATE.get(key, default)
|
_load_from_disk()
|
||||||
|
return _STATE.get(key, default)
|
||||||
|
|||||||
Reference in New Issue
Block a user