Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 88 additions & 4 deletions bin/fm-ff-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,45 @@ dirty_status() {
fi
}

# True when an attributes stream activates, disables, or resets the external
# `filter` attribute. A startup pull must not run any candidate-selected
# clean/smudge/process command, so every declaration is a refusal rather than
# trying to decide whether the local filter configuration currently names a
# command.
ff_attributes_define_filter() {
LC_ALL=C awk '
/^[[:space:]]*#/ || /^[[:space:]]*$/ { next }
{
for (i = 2; i <= NF; i++) {
if ($i ~ /^[!-]?filter($|=)/) { found = 1; exit }
}
}
END { exit found ? 0 : 1 }
'
}

# True when revision $2 or this checkout's repository-local attributes would
# activate an external checkout filter. System/global attributes are disabled
# on the actual startup merge; info/attributes cannot be disabled by config, so
# it is inspected explicitly here.
ff_external_filter_present() { # <repo> <revision>
local dir=$1 revision=$2 path info
info=$(git -C "$dir" rev-parse --git-path info/attributes 2>/dev/null) || return 0
case "$info" in /*) ;; *) info="$dir/$info" ;; esac
if [ -e "$info" ] || [ -L "$info" ]; then
[ -f "$info" ] && [ ! -L "$info" ] && [ -r "$info" ] || return 0
ff_attributes_define_filter < "$info" && return 0
fi
while IFS= read -r -d '' path; do
case "$path" in .gitattributes|*/.gitattributes) ;;
*) continue ;;
esac
git -C "$dir" show "$revision:$path" 2>/dev/null \
| ff_attributes_define_filter && return 0
done < <(git -C "$dir" ls-tree -r -z --name-only "$revision" 2>/dev/null)
return 1
}

secondmate_update_reconcile_marker_path() { # <state> <id>
local state=$1 id=$2
case "$id" in *[!A-Za-z0-9._-]*|'') return 1 ;; esac
Expand Down Expand Up @@ -383,10 +422,24 @@ FF_STATUS=""
FF_INSTR=""
ff_target() {
local dir=$1 label=$2 base_mode=$3 allow_detached=${4:-no} ignore_seed_marker=${5:-no}
local secondmate_id=${6:-} reconciliation_state=${7:-}
local secondmate_id=${6:-} reconciliation_state=${7:-} operation=${8:-normal}
FF_STATUS="skipped"
FF_INSTR=""

case "$operation" in
normal) ;;
startup-check|startup-update)
if [ "$base_mode" = origin ] || [ -n "$secondmate_id$reconciliation_state" ]; then
echo "$label: skipped: startup pull requires one pinned local commit"
return 0
fi
;;
*)
echo "$label: skipped: unknown fast-forward operation"
return 0
;;
esac

if [ ! -d "$dir" ]; then
echo "$label: skipped: not a directory"
return 0
Expand All @@ -396,7 +449,7 @@ ff_target() {
return 0
fi

local default base cur instr local_rev base_rev before after out
local default base cur instr local_rev base_rev before after out dirty
default=$(default_branch "$dir") || {
echo "$label: skipped: cannot determine default branch"
return 0
Expand Down Expand Up @@ -432,7 +485,17 @@ ff_target() {
return 0
fi

if [ -n "$(dirty_status "$dir" "$ignore_seed_marker")" ]; then
if [ "$operation" = normal ]; then
dirty=$(dirty_status "$dir" "$ignore_seed_marker")
else
dirty=$(GIT_ATTR_NOSYSTEM=1 git -C "$dir" \
-c core.attributesFile=/dev/null -c core.hooksPath=/dev/null \
-c core.fsmonitor=false status --porcelain --untracked-files=all 2>/dev/null) || {
echo "$label: skipped: cannot inspect working tree"
return 0
}
fi
if [ -n "$dirty" ]; then
echo "$label: skipped: dirty working tree"
return 0
fi
Expand Down Expand Up @@ -484,9 +547,30 @@ ff_target() {
return 0
fi

if [ "$operation" != normal ]; then
if ff_external_filter_present "$dir" HEAD \
|| ff_external_filter_present "$dir" "$base"; then
echo "$label: skipped: external checkout filter declared"
return 0
fi
if [ "$operation" = startup-check ]; then
FF_STATUS="updated"
FF_INSTR=$(changed_instr "$dir" "$base")
echo "$label: update available"
return 0
fi
fi

instr=$(changed_instr "$dir" "$base")
before=$(git -C "$dir" rev-parse --short HEAD)
if ! out=$(git -C "$dir" merge --ff-only "$base" 2>&1); then
if [ "$operation" = startup-update ]; then
out=$(GIT_ATTR_NOSYSTEM=1 git -C "$dir" \
-c core.attributesFile=/dev/null -c core.hooksPath=/dev/null \
merge --ff-only "$base" 2>&1) || {
echo "$label: skipped: fast-forward failed: $(first_line "$out")"
return 0
}
elif ! out=$(git -C "$dir" merge --ff-only "$base" 2>&1); then
echo "$label: skipped: fast-forward failed: $(first_line "$out")"
return 0
fi
Expand Down
92 changes: 92 additions & 0 deletions bin/fm-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,94 @@ publish_lock_session_or_die() {
exit 1
}

PRELAUNCH_MATCHED=0
PRELAUNCH_HOME=$(CDPATH='' cd -- "$FM_HOME" 2>/dev/null && pwd -P) || {
echo "error: cannot resolve the physical firstmate home; operate read-only until resolved" >&2
exit 1
}

# Classify the pre-harness reservation while the ordinary claim mutex is held.
# A raw launch may proceed when none exists, but a live reservation belongs only
# to the authenticated child harness below its recorded launcher. Unknown
# state refuses, and a dead or PID-reused owner is the only reclaimable state.
prepare_prelaunch_reservation() {
local owner=${FM_PRELAUNCH_OWNER_PID:-} token=${FM_PRELAUNCH_TOKEN:-} head dirty
PRELAUNCH_MATCHED=0
fm_prelaunch_reservation_inspect "$STATE" "$PRELAUNCH_HOME"
case "$FM_PRELAUNCH_INSPECT_STATE" in
free) return 0 ;;
stale)
rm -f -- "$STATE/$FM_PRELAUNCH_RESERVATION_FILE" || {
echo "error: cannot reclaim the dead prelaunch reservation; operate read-only until resolved" >&2
exit 1
}
return 0
;;
live) ;;
*)
echo "error: prelaunch reservation ownership is unreadable or unknown; operate read-only until resolved" >&2
exit 1
;;
esac
case "$owner" in ''|*[!0-9]*|0)
echo "error: another live prelaunch owner reserved this home; operate read-only until resolved" >&2
exit 1
;;
esac
if [ "$FM_PRELAUNCH_RECORD_OWNER_PID" != "$owner" ] \
|| ! fm_prelaunch_attached_child_authenticated "$STATE" "$PRELAUNCH_HOME" "$token"; then
echo "error: another live prelaunch owner reserved this home; operate read-only until resolved" >&2
exit 1
fi
case "$FM_PRELAUNCH_RECORD_UPDATE_STATUS" in current|updated) ;;
*)
echo "error: authenticated prelaunch child started before its source update completed; operate read-only until resolved" >&2
exit 1
;;
esac
[ -n "$FM_PRELAUNCH_RECORD_TARGET_COMMIT" ] || {
echo "error: authenticated prelaunch child has no verified source target; operate read-only until resolved" >&2
exit 1
}
head=$(fm_prelaunch_git -C "$PRELAUNCH_HOME" rev-parse --verify 'HEAD^{commit}' 2>/dev/null) || {
echo "error: cannot verify prelaunch source HEAD; operate read-only until resolved" >&2
exit 1
}
[ "$head" = "$FM_PRELAUNCH_RECORD_TARGET_COMMIT" ] || {
echo "error: prelaunch source HEAD changed before session-lock handoff; operate read-only until resolved" >&2
exit 1
}
dirty=$(GIT_ATTR_NOSYSTEM=1 fm_prelaunch_git -C "$PRELAUNCH_HOME" \
-c core.attributesFile=/dev/null -c core.hooksPath=/dev/null \
-c core.fsmonitor=false status --porcelain --untracked-files=all 2>/dev/null) || {
echo "error: cannot verify prelaunch source cleanliness before session-lock handoff; operate read-only until resolved" >&2
exit 1
}
if [ -n "$dirty" ]; then
echo "error: prelaunch source became dirty before session-lock handoff; operate read-only until resolved" >&2
exit 1
fi
PRELAUNCH_MATCHED=1
}

complete_prelaunch_handoff() { # <verified-lock-pid>
local lock_pid=$1 owner=${FM_PRELAUNCH_OWNER_PID:-} token=${FM_PRELAUNCH_TOKEN:-}
[ "$PRELAUNCH_MATCHED" -eq 1 ] || return 0
# Re-read under the still-held claim mutex immediately before publishing the
# exchange. This makes an interrupted prior attempt idempotent without ever
# trusting the environment alone.
fm_prelaunch_reservation_inspect "$STATE" "$PRELAUNCH_HOME"
if [ "$FM_PRELAUNCH_RECORD_OWNER_PID" != "$owner" ] \
|| ! fm_prelaunch_attached_child_authenticated "$STATE" "$PRELAUNCH_HOME" "$token"; then
echo "error: prelaunch reservation changed before session-lock handoff; operate read-only until resolved" >&2
exit 1
fi
fm_prelaunch_handoff_publish "$STATE" "$lock_pid" || {
echo "error: cannot publish the verified prelaunch session handoff; operate read-only until resolved" >&2
exit 1
}
}

# This session already holds the lock, recorded as pid $1. Line 1 stays exactly
# as recorded while that pid is alive; only the sidecar is refreshed, under the
# claim lock, so a /clear re-key inside the same process replaces the old id.
Expand All @@ -167,7 +255,9 @@ confirm_own_lock() { # <recorded-pid>
fi
recorded=$(cat "$LOCK" 2>/dev/null || true)
if [ "$recorded" = "$me" ] || fm_session_lock_owned_by_self "$STATE"; then
prepare_prelaunch_reservation
publish_lock_session_or_die
complete_prelaunch_handoff "$recorded"
commit_lock_session
release_claim_lock
echo "lock acquired: harness pid $recorded"
Expand Down Expand Up @@ -227,6 +317,7 @@ if [ -e "$LOCK" ] || [ -L "$LOCK" ]; then
fi
fi
fi
prepare_prelaunch_reservation
# The sidecar goes first: a fresh pid beside a previous session's id would let
# that session's resume own this lock. If the sidecar changes before line 1 is
# written, a failure restores the previous sidecar. If line 1 is written but
Expand Down Expand Up @@ -270,6 +361,7 @@ if [ ! -f "$LOCK" ] || [ -L "$LOCK" ] || [ "$written" != "$me" ]; then
echo "error: session lock ownership verification failed; operate read-only until resolved" >&2
exit 1
fi
complete_prelaunch_handoff "$me"
commit_lock_session
release_claim_lock
echo "lock acquired: harness pid $me"
Loading