sync: auto-sync from HOWARD-HOME at 2026-06-25 12:53:21
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-25 12:53:21
This commit is contained in:
@@ -123,9 +123,7 @@
|
||||
- [pfSense 25.07 ops quirks](reference_pfsense_25_07_ops.md) — Cascades pfSense Plus 25.07: logs are PLAIN TEXT (use tail/grep, NOT clog → clog returns empty); clean dhcpd restart = `services_dhcpd_configure()` via slow pfSsh.php (needs 50s+ timeout); dirty boot can leave 2 dhcpd → DISCOVER/OFFER but no ACK; reboot the Cox modem after a config restore; ZFS survives power loss. From the 2026-06-17 power-outage incident.
|
||||
- [feedback_ascii_only_api_payloads](feedback_ascii_only_api_payloads.md) -- On Windows/Git-bash, non-ASCII chars (em-dash, arrow, smart quotes) in JSON payload TEXT passed to curl get mangled and rejected — Discord bot-alert returns 400, the coord API returns "error parsing the body". Use ASCII-only in API payload text, or a single-quoted heredoc.
|
||||
- [feedback_bitdefender_unattended_install](feedback_bitdefender_unattended_install.md) -- Bitdefender unattended RMM install must use the FULL KIT as SYSTEM (silent, no UAC) — the downloader stub fails headless and triggers UAC
|
||||
- [Broken [[backlinks]] are write-me-later markers — flesh out from session history, don't delete](feedback_broken_backlinks_are_writeme_markers.md) -- A [[name]] link in a memory body whose target file doesn't exist is NOT an error to clean up — it's an intentional marker that that memory is worth writing. When you hit one (or memory-dream lists them), flesh the missing memory out from the session logs / session history, don't strip the link.
|
||||
- [feedback_rmm_longops_fire_and_forget](feedback_rmm_longops_fire_and_forget.md) -- Long-running RMM endpoint ops (software installs, big downloads) must be fire-and-forget, not live-monitored
|
||||
- [Broken [[backlinks]] are write-me-later markers — flesh out from session history, don't delete](feedback_broken_backlinks_are_writeme_markers.md) -- A [[name]] link in a memory body whose target file doesn't exist is NOT an error to clean up — it's an intentional marker that that memory is worth writing. When you hit one (or memory-dream lists them), flesh the missing memory out from the session logs / session history, don't strip the link.
|
||||
|
||||
## Machine
|
||||
- [GURU-5070 Workstation Setup](reference_workstation_setup.md) — Mike's primary (owner confirmed 2026-05-26). Windows 11 Pro. Renamed from OC-5070 → ACG-5070/acg-guru-5070 → GURU-5070; all the same box, all Mike's.
|
||||
|
||||
@@ -23,9 +23,11 @@ import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
@@ -111,6 +113,11 @@ CACHE_TTL_SECONDS = 86400
|
||||
SKILL_DIR = Path(__file__).resolve().parent.parent
|
||||
CACHE_DIR = SKILL_DIR / ".cache"
|
||||
CACHE_FILE = CACHE_DIR / "inventory.json"
|
||||
CACHE_LOCK_FILE = CACHE_DIR / "inventory.lock"
|
||||
# Best-effort advisory lock for read-modify-write of the cache. Short timeout:
|
||||
# losing a write-through update is acceptable; hanging the CLI is not.
|
||||
CACHE_LOCK_TIMEOUT_SECONDS = 5.0
|
||||
CACHE_LOCK_STALE_SECONDS = 30.0
|
||||
|
||||
|
||||
class GravityZoneError(RuntimeError):
|
||||
@@ -1136,10 +1143,60 @@ class GravityZoneClient:
|
||||
return None
|
||||
|
||||
def _write_cache(self, cache: dict) -> None:
|
||||
"""Atomically replace the cache file (temp write + os.replace) so a crash
|
||||
mid-write or a concurrent reader can never see a truncated file."""
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CACHE_FILE.write_text(
|
||||
json.dumps(cache, indent=2, sort_keys=True), encoding="utf-8"
|
||||
)
|
||||
payload = json.dumps(cache, indent=2, sort_keys=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=str(CACHE_DIR), prefix=".inventory.",
|
||||
suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
fh.write(payload)
|
||||
fh.flush()
|
||||
os.fsync(fh.fileno())
|
||||
os.replace(tmp, CACHE_FILE) # atomic on the same filesystem
|
||||
except BaseException:
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
@contextmanager
|
||||
def _cache_lock(self):
|
||||
"""Best-effort cross-platform advisory lock around a read-modify-write of
|
||||
the cache, so two concurrent gz.py invocations don't lose each other's
|
||||
write-through update. Steals a stale lock; on timeout proceeds unlocked
|
||||
(a lost update is tolerable, a hang is not)."""
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
deadline = time.monotonic() + CACHE_LOCK_TIMEOUT_SECONDS
|
||||
acquired = False
|
||||
while True:
|
||||
try:
|
||||
fd = os.open(str(CACHE_LOCK_FILE),
|
||||
os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||
os.close(fd)
|
||||
acquired = True
|
||||
break
|
||||
except FileExistsError:
|
||||
try:
|
||||
age = time.time() - os.path.getmtime(CACHE_LOCK_FILE)
|
||||
if age > CACHE_LOCK_STALE_SECONDS:
|
||||
os.unlink(CACHE_LOCK_FILE)
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
if time.monotonic() >= deadline:
|
||||
break # give up the lock, proceed unlocked
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if acquired:
|
||||
try:
|
||||
os.unlink(CACHE_LOCK_FILE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _cache_is_fresh(self, cache: dict) -> bool:
|
||||
fetched = cache.get("fetched_at")
|
||||
@@ -1226,27 +1283,29 @@ class GravityZoneClient:
|
||||
return self.refresh_inventory()
|
||||
|
||||
def _cache_add_group(self, group_id: str, name: str) -> None:
|
||||
cache = self._read_cache()
|
||||
if cache is None:
|
||||
return # no cache yet - next refresh picks it up
|
||||
cache.setdefault("companies", {})
|
||||
# Groups live in the inventory tree; store under a 'groups' map.
|
||||
cache.setdefault("groups", {})[group_id] = name
|
||||
self._write_cache(cache)
|
||||
with self._cache_lock():
|
||||
cache = self._read_cache()
|
||||
if cache is None:
|
||||
return # no cache yet - next refresh picks it up
|
||||
# Groups live in the inventory tree; store under a 'groups' map.
|
||||
cache.setdefault("groups", {})[group_id] = name
|
||||
self._write_cache(cache)
|
||||
|
||||
def _cache_add_package(self, package_name: str, create_result: Any) -> None:
|
||||
cache = self._read_cache()
|
||||
if cache is None:
|
||||
return
|
||||
packages = cache.setdefault("packages", [])
|
||||
pkg_id = create_result if isinstance(create_result, str) else None
|
||||
if isinstance(create_result, dict):
|
||||
pkg_id = create_result.get("id")
|
||||
if not any(
|
||||
(isinstance(p, dict) and p.get("name") == package_name) for p in packages
|
||||
):
|
||||
packages.append({"id": pkg_id, "name": package_name})
|
||||
self._write_cache(cache)
|
||||
with self._cache_lock():
|
||||
cache = self._read_cache()
|
||||
if cache is None:
|
||||
return
|
||||
packages = cache.setdefault("packages", [])
|
||||
pkg_id = create_result if isinstance(create_result, str) else None
|
||||
if isinstance(create_result, dict):
|
||||
pkg_id = create_result.get("id")
|
||||
if not any(
|
||||
(isinstance(p, dict) and p.get("name") == package_name)
|
||||
for p in packages
|
||||
):
|
||||
packages.append({"id": pkg_id, "name": package_name})
|
||||
self._write_cache(cache)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -1258,9 +1317,4 @@ def main() -> int:
|
||||
"httpx" if _HAS_HTTPX else "urllib")
|
||||
return 0
|
||||
except GravityZoneError as exc:
|
||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
prin
|
||||
Reference in New Issue
Block a user