fix(lib,install): update locking doc to SQLite transaction, cache NFS check, verify PyYAML
This commit is contained in:
+27
-27
@@ -3,10 +3,10 @@
|
||||
#
|
||||
# Single source of truth for the four things that were inconsistently
|
||||
# re-implemented across create/resume/delete/monitor (REVIEW.md §4.1):
|
||||
# - derive_session_name : the tmux session slug (P0-A)
|
||||
# - atomic_dump_yaml : flock + temp+rename + .bak + validate (P0-B)
|
||||
# - env_python : env-safe Python (no heredoc injection) (P0-B / P1-B)
|
||||
# - find_workspace_uuid : workspace-SCOPED resume id lookup (P0-C)
|
||||
# - derive_session_name : the tmux session slug (P0-A)
|
||||
# - atomic_dump_yaml : SQLite db transaction + temp+rename + .bak + validate (P0-B)
|
||||
# - env_python : env-safe Python (no heredoc injection) (P0-B / P1-B)
|
||||
# - find_workspace_uuid : workspace-SCOPED resume id lookup (P0-C)
|
||||
#
|
||||
# Source it from each script with a path computed from the script location:
|
||||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
||||
@@ -234,32 +234,43 @@ env_python() {
|
||||
# atomic_dump_yaml <yaml_path> [KEY=VALUE ...] (mutation source from stdin)
|
||||
#
|
||||
# The ONLY sanctioned way to write agent-sessions.yaml. It:
|
||||
# 1. takes an exclusive flock on <yaml_path>.lock (serialises all writers)
|
||||
# 2. loads the YAML into `d`
|
||||
# 1. takes an exclusive SQLite BEGIN IMMEDIATE transaction lock on
|
||||
# agent-sessions.db (serialises all writers)
|
||||
# 2. loads the current state into `d` (seeds from YAML if DB is empty)
|
||||
# 3. exec()s the caller's mutation source (sees d, yaml, os, datetime,
|
||||
# timezone, glob, subprocess; reads values via os.environ). The mutation
|
||||
# may print and may `raise SystemExit(n)` to abort *without* writing.
|
||||
# 4. validates the resulting schema
|
||||
# 5. backs up to <yaml_path>.bak, then writes atomically (temp + os.replace)
|
||||
# 5. backs up to <yaml_path>.bak, then writes YAML atomically (temp + os.replace)
|
||||
# when a session transitions to a finished state.
|
||||
#
|
||||
# The mutation source is passed via env and exec()'d — it is never string
|
||||
# spliced and untrusted data never lands in Python source (P0-B / P1-B).
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check if the workspace is on NFS — flock is unreliable on NFS
|
||||
_atomic_dump_yaml_check_nfs() {
|
||||
# Check if the workspace is on NFS — locking behaves differently on NFS
|
||||
_check_is_nfs() {
|
||||
local f="$1"
|
||||
local mountpoint
|
||||
mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)" || return 0
|
||||
mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)" || return 1
|
||||
if mount | grep -q "$mountpoint.*nfs\|$mountpoint.*cifs\|$mountpoint.*fuse.sshfs"; then
|
||||
echo "WARNING: $mountpoint appears to be a network filesystem (NFS/CIFS/SSHFS)." >&2
|
||||
echo "WARNING: fcntl.flock-based atomic writes are unreliable on network filesystems." >&2
|
||||
echo "WARNING: SQLite journal_mode automatically falls back to DELETE." >&2
|
||||
return 0 # is NFS
|
||||
fi
|
||||
return 1 # not NFS
|
||||
}
|
||||
|
||||
atomic_dump_yaml() {
|
||||
local yaml_path="$1"; shift
|
||||
local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN")
|
||||
if [ -z "${MAM_IS_NFS:-}" ]; then
|
||||
if _check_is_nfs "$(dirname "$yaml_path")"; then
|
||||
export MAM_IS_NFS="true"
|
||||
echo "WARNING: $(dirname "$yaml_path") appears to be a network filesystem (NFS/CIFS/SSHFS)." >&2
|
||||
echo "WARNING: SQLite journal_mode automatically falls back to DELETE." >&2
|
||||
else
|
||||
export MAM_IS_NFS="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN" "MAM_IS_NFS=$MAM_IS_NFS")
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
*=*)
|
||||
@@ -312,19 +323,8 @@ for f in [db_path, db_path + '-wal', db_path + '-shm']:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def is_nfs(path):
|
||||
try:
|
||||
df_out = subprocess.check_output(['df', '--output=target', path], text=True, stderr=subprocess.DEVNULL)
|
||||
target = df_out.strip().split('\n')[-1].strip()
|
||||
mount_out = subprocess.check_output(['mount'], text=True)
|
||||
for line in mount_out.split('\n'):
|
||||
if f" on {target} " in line and (' type nfs ' in line or ' type cifs ' in line or ' fuse.sshfs ' in line):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
if is_nfs(os.path.dirname(db_path) or '.'):
|
||||
is_nfs = os.environ.get('MAM_IS_NFS') == 'true'
|
||||
if is_nfs:
|
||||
conn.execute('PRAGMA journal_mode=DELETE')
|
||||
else:
|
||||
conn.execute('PRAGMA journal_mode=WAL')
|
||||
|
||||
@@ -106,7 +106,7 @@ WRAPPER="$LOCAL_BIN/$SESSION_NAME"
|
||||
spawn() {
|
||||
case "$AGENT" in
|
||||
claude)
|
||||
if [ -x "$WRAPPER" ] || [ "$USE_WRAPPER" = "1" ]; then
|
||||
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
|
||||
nohup "$WRAPPER" >/dev/null 2>&1 &
|
||||
disown
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user