Skip to content
Closed
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
8 changes: 3 additions & 5 deletions bin/fm-backlog-receive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ LOCK_STALE_SECS=30
. "$SCRIPT_DIR/fm-tasks-axi-lib.sh"
# shellcheck source=bin/fm-wake-lib.sh
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-stat-lib.sh
. "$SCRIPT_DIR/fm-stat-lib.sh"

die() { printf 'error: %s\n' "$1" >&2; exit 1; }
usage() { sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 2; }
Expand Down Expand Up @@ -56,11 +58,7 @@ list_keys() { # <file>

lock_age() {
local modified now
if [ "$(uname 2>/dev/null)" = Darwin ]; then
modified=$(stat -f '%m' "$1" 2>/dev/null) || return 1
else
modified=$(stat -c '%Y' "$1" 2>/dev/null) || return 1
fi
modified=$(fm_stat_mtime "$1") || return 1
now=$(date +%s) || return 1
case "$modified$now" in *[!0-9]*) return 1 ;; esac
printf '%s\n' "$((now - modified))"
Expand Down
18 changes: 11 additions & 7 deletions bin/fm-classify-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ _FM_CLASSIFY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)"
# or no-mistakes install; absent, it points at the real sibling script.
FM_CREW_STATE_BIN="${FM_CREW_STATE_BIN:-$_FM_CLASSIFY_LIB_DIR/fm-crew-state.sh}"

# shellcheck source=bin/fm-stat-lib.sh
. "$_FM_CLASSIFY_LIB_DIR/fm-stat-lib.sh"

# Captain-relevant status verbs. A status line carrying any of these is work
# firstmate must see. Lines without these verbs are no-verb signals: the watcher
# absorbs them only with positive provably-working evidence, while the daemon uses
Expand Down Expand Up @@ -346,14 +349,15 @@ _fm_open_decisions_cursor_path() { # <status-file>
printf '%s/.%s.open-decisions-cursor' "$dir" "${base%.status}"
}

# Portable device:inode identity for the rotation/recreation check below.
# Portable device:inode identity for the rotation/recreation check below. The
# flavor comes from the binary's own dialect (bin/fm-stat-lib.sh), not from
# `uname -s`: a Darwin kernel routinely resolves `stat` to GNU coreutils, and a
# wrong flavor here makes every read look like a rotation.
# Non-zero (with empty stdout) on I/O failure: the caller at
# status_open_decisions_incremental keys its "trust the cursor" early return on
# that exit status, so it must survive.
_fm_open_decisions_file_ident() { # <file> -> "dev:inode", empty on I/O failure
local f=$1
if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
LC_ALL=C stat -f '%d:%i' "$f" 2>/dev/null
else
LC_ALL=C stat -c '%d:%i' "$f" 2>/dev/null
fi
fm_stat_identity "$1"
}

status_open_decisions_incremental() { # <status-file>
Expand Down
18 changes: 11 additions & 7 deletions bin/fm-lock-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
# binary through the same function so one host can never answer "is lsof here?"
# two different ways.

_FM_LOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _FM_LOCK_LIB_DIR="."
# shellcheck source=bin/fm-stat-lib.sh
. "$_FM_LOCK_LIB_DIR/fm-stat-lib.sh"

fm_lock_log() {
echo "${FM_LOCK_LOG_PREFIX:-fm-lock}: $*" >&2
}
Expand Down Expand Up @@ -54,14 +58,14 @@ fm_lsof_bin() {
return 1
}

# Portable mtime in epoch seconds. Kept self-contained so this leaf lib drags in
# no wake-queue machinery when a caller only needs the staleness proof.
# Portable mtime in epoch seconds. Still drags in no wake-queue machinery when a
# caller only needs the staleness proof: fm-stat-lib.sh is a dependency-free leaf
# whose only job is answering which dialect this host's `stat` speaks. Asking
# `uname` instead is wrong - a Darwin kernel routinely resolves `stat` to GNU
# coreutils - and a wrong answer here reads every lock as unreadable, which fails
# safe but permanently refuses to reap an abandoned lock.
fm_lock_path_mtime() {
if [ "$(uname)" = Darwin ]; then
stat -f %m "$1" 2>/dev/null
else
stat -c %Y "$1" 2>/dev/null
fi
fm_stat_mtime "$1"
}

# fm_lock_lsof_holder <target>: 0 a process holds it, 1 provably none, 2 lsof
Expand Down
12 changes: 7 additions & 5 deletions bin/fm-pending-reply-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ _FM_PENDING_REPLY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/n
. "$_FM_PENDING_REPLY_LIB_DIR/fm-backend.sh"
# shellcheck source=bin/fm-tmux-lib.sh
. "$_FM_PENDING_REPLY_LIB_DIR/fm-tmux-lib.sh"
# shellcheck source=bin/fm-stat-lib.sh
. "$_FM_PENDING_REPLY_LIB_DIR/fm-stat-lib.sh"

FM_PENDING_REPLY_SCHEMA='fm-pending-reply.v1'
FM_PENDING_REPLY_CORR_RE='corr=[A-Fa-f0-9]{16}'
Expand Down Expand Up @@ -462,11 +464,11 @@ fm_pending_reply_find_resolve_line() { # <status-file> <corr_id>
fm_pending_reply_file_signature() { # <path>
local path=$1
[ -f "$path" ] || { printf 'missing'; return 0; }
if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
LC_ALL=C stat -f '%d:%i:%z:%m:%c' "$path" 2>/dev/null || printf 'unreadable'
else
LC_ALL=C stat -c '%d:%i:%s:%Y:%Z' "$path" 2>/dev/null || printf 'unreadable'
fi
# Field flavor from the binary's own dialect, not `uname -s`: a Darwin kernel
# routinely resolves `stat` to GNU coreutils, and the wrong flavor would pin
# every file at 'unreadable' - a signature that never changes, so no missed
# reply would ever be detected. See bin/fm-stat-lib.sh.
fm_stat_fingerprint "$path" || printf 'unreadable'
}

fm_pending_reply_status_set_signature() { # <status-dir>
Expand Down
40 changes: 12 additions & 28 deletions bin/fm-pr-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
# The receipt binds the terminal observation to the canonical registration and
# lets a restart finish fixed-path removal without executing state-file bytes.

_FM_PR_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _FM_PR_LIB_DIR="."
# shellcheck source=bin/fm-stat-lib.sh
. "$_FM_PR_LIB_DIR/fm-stat-lib.sh"

FM_PR_PROVIDER=
FM_PR_URL=
FM_PR_HOST=
Expand Down Expand Up @@ -213,37 +217,17 @@ fm_pr_head_valid() {
[[ "$head" =~ ^[0-9a-f]{40}$|^[0-9a-f]{64}$ ]]
}

fm_pr_file_mode() {
if [ "$(uname)" = Darwin ]; then
stat -f %Lp "$1" 2>/dev/null
else
stat -c %a "$1" 2>/dev/null
fi
}
# Field flavor comes from bin/fm-stat-lib.sh, which detects what this host's
# `stat` binary actually speaks. Keying on `uname` would be wrong: a Darwin
# kernel routinely resolves `stat` to GNU coreutils (nix-darwin, or Homebrew
# coreutils ahead of /usr/bin on PATH).
fm_pr_file_mode() { fm_stat_mode "$1"; }

fm_pr_file_device() {
if [ "$(uname)" = Darwin ]; then
stat -f %d "$1" 2>/dev/null
else
stat -c %d "$1" 2>/dev/null
fi
}
fm_pr_file_device() { fm_stat_device "$1"; }

fm_pr_file_link_count() {
if [ "$(uname)" = Darwin ]; then
stat -f %l "$1" 2>/dev/null
else
stat -c %h "$1" 2>/dev/null
fi
}
fm_pr_file_link_count() { fm_stat_links "$1"; }

fm_pr_file_inode() {
if [ "$(uname)" = Darwin ]; then
stat -f %i "$1" 2>/dev/null
else
stat -c %i "$1" 2>/dev/null
fi
}
fm_pr_file_inode() { fm_stat_inode "$1"; }

fm_pr_file_identity() {
local device inode
Expand Down
10 changes: 3 additions & 7 deletions bin/fm-remote-file.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# shellcheck source=bin/fm-wake-lib.sh
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-stat-lib.sh
. "$SCRIPT_DIR/fm-stat-lib.sh"

die() { printf 'error: %s\n' "$1" >&2; exit 1; }
usage() { sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 2; }
Expand Down Expand Up @@ -75,13 +77,7 @@ snapshot_bounded_file() { # <file> <max-bytes> <destination> <size-file>
)
}

directory_identity() {
if [ "$(uname)" = Darwin ]; then
stat -f '%d:%i' . 2>/dev/null
else
stat -c '%d:%i' . 2>/dev/null
fi
}
directory_identity() { fm_stat_identity .; }

put_handoff_file() { # <home-real> <name> <max-bytes> <relative-path> <bytes> <sha256> <generation>
local home_real=$1 name=$2 max=$3 rel=$4 expected_bytes=$5 expected_hash=$6 generation=$7
Expand Down
6 changes: 3 additions & 3 deletions bin/fm-remote-inherit-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
. "$SCRIPT_DIR/fm-secondmate-registry-lib.sh"
# shellcheck source=bin/fm-config-inherit-lib.sh
. "$SCRIPT_DIR/fm-config-inherit-lib.sh"
# shellcheck source=bin/fm-stat-lib.sh
. "$SCRIPT_DIR/fm-stat-lib.sh"

die() { printf 'error: %s\n' "$1" >&2; exit 1; }
sha256_file() {
if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'; else sha256sum "$1" | awk '{print $1}'; fi
}
file_link_count() {
if [ "$(uname)" = Darwin ]; then stat -f %l "$1" 2>/dev/null; else stat -c %h "$1" 2>/dev/null; fi
}
file_link_count() { fm_stat_links "$1"; }
shared_captain_header_valid() {
local head
head=$(sed -n '1,12p' "$1" 2>/dev/null) || return 1
Expand Down
6 changes: 3 additions & 3 deletions bin/fm-remote-inherit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-config-inherit-lib.sh
. "$SCRIPT_DIR/fm-config-inherit-lib.sh"
# shellcheck source=bin/fm-stat-lib.sh
. "$SCRIPT_DIR/fm-stat-lib.sh"

die() { printf 'error: %s\n' "$1" >&2; exit 1; }
usage() { sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 2; }
file_link_count() {
if [ "$(uname)" = Darwin ]; then stat -f %l "$1" 2>/dev/null; else stat -c %h "$1" 2>/dev/null; fi
}
file_link_count() { fm_stat_links "$1"; }
sha256_file() {
if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'; else sha256sum "$1" | awk '{print $1}'; fi
}
Expand Down
12 changes: 9 additions & 3 deletions bin/fm-remote-job-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
# an Aqua requirement. The launch-agent renderer and repair helpers here are
# shared by the entrypoint and remote doctor so their ownership cannot drift.

_FM_REMOTE_JOB_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _FM_REMOTE_JOB_LIB_DIR="."
# shellcheck source=bin/fm-stat-lib.sh
. "$_FM_REMOTE_JOB_LIB_DIR/fm-stat-lib.sh"

FM_REMOTE_JOB_LABEL=dev.firstmate.remote-job
FM_REMOTE_JOB_MAX_BYTES=${FM_REMOTE_JOB_MAX_BYTES:-1048576}
FM_REMOTE_JOB_QUEUE_TIMEOUT=${FM_REMOTE_JOB_QUEUE_TIMEOUT:-360}
Expand Down Expand Up @@ -571,9 +575,11 @@ fm_remote_job_reap() { # <account-home> <id>; only removes an exact completed re
}

fm_remote_job_path_mtime() { # <path>
# The platform override controls worker shape in isolated tests, not the host
# kernel's stat syntax.
if [ "$(uname -s 2>/dev/null || true)" = Darwin ]; then stat -f %m "$1" 2>/dev/null; else stat -c %Y "$1" 2>/dev/null; fi
# Neither the platform override nor the host kernel decides stat's syntax: the
# `stat` BINARY does, and a Darwin kernel routinely resolves it to GNU
# coreutils. Getting this wrong made the readiness probe fail permanently
# (robots-xw8p). bin/fm-stat-lib.sh feature-detects the binary instead.
fm_stat_mtime "$1"
}

fm_remote_job_reap_stale() { # <account-home>
Expand Down
118 changes: 118 additions & 0 deletions bin/fm-stat-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# fm-stat-lib.sh - ONE owner for "which dialect does THIS host's `stat` speak?".
#
# `stat` has two incompatible flavors and firstmate runs on both:
# BSD/macOS stat -f <fmt> <path> %m mtime, %z size, %Lp mode, %l links, %i inode, %d device
# GNU stat -c <fmt> <path> %Y mtime, %s size, %a mode, %h links, %i inode, %d device
#
# Two ways of choosing between them are BOTH wrong, and firstmate has been bitten
# by each:
#
# 1. `uname` is the wrong discriminator. A Darwin kernel routinely resolves
# `stat` to GNU coreutils - nix-darwin, or Homebrew coreutils ahead of
# /usr/bin on PATH. The kernel says Darwin, the binary speaks GNU, and every
# `if [ "$(uname)" = Darwin ]` branch picks the flavor the binary does not
# speak. That is robots-xw8p (remote-job readiness probe) and robots-e8x5
# (thirteen more call sites).
#
# 2. `stat -f <fmt> ... || stat -c <fmt> ...` is worse, and subtly: GNU's `-f`
# is --file-system, not --format. When `stat -f %m <path>` runs under GNU,
# `%m` becomes a second file operand; GNU stats the FILESYSTEM of <path>,
# writes a multi-line apfs/ext4 dump to STDOUT, and then EXITS 1. The `||`
# DOES fire, and `2>/dev/null` only silences stderr - so the fallback's
# correct integer is APPENDED to the dump already in the pipe. The caller
# receives a non-integer multi-line value at overall rc=0: invisible to
# error-handling, fatal to any arithmetic that follows.
#
# So: FEATURE-DETECT the binary, once per process, and dispatch. The probe must
# try `-c` first: BSD stat rejects `-c` with a non-zero exit and NOTHING on
# stdout, so the clean failure falls through to the `-f` probe; GNU stat rejects
# `-f` only AFTER polluting stdout, so probing `-f` first would contribute junk
# output even when it fails. Probing `-c` first guarantees the discarded probe
# cannot contribute output under either flavor.
#
# The dialect is cached in _FM_STAT_DIALECT after the first probe, because
# callers like fm_path_mtime run inside 0.2s confirm and 0.5s attach polls where
# forking a probe per call is a measurable cost.
#
# No side effects on source. set -u / set -e safe. Leaf lib: depends on nothing.
#
# Tunables (env):
# FM_STAT_DIALECT_OVERRIDE force 'gnu' or 'bsd' (tests); skips the probe

# fm_stat_dialect: prints 'gnu' or 'bsd'; non-zero when neither probe answers.
fm_stat_dialect() {
if [ -n "${FM_STAT_DIALECT_OVERRIDE:-}" ]; then
case "$FM_STAT_DIALECT_OVERRIDE" in
gnu|bsd) printf '%s\n' "$FM_STAT_DIALECT_OVERRIDE"; return 0 ;;
*) return 1 ;;
esac
fi
if [ -z "${_FM_STAT_DIALECT:-}" ]; then
local probe
# `/` is guaranteed to exist and to have an integer size under both flavors,
# so a bare-integer result is a positive identification of the dialect and
# anything else (empty, usage text, a filesystem dump) is a rejection.
probe=$(stat -c %s / 2>/dev/null) || probe=''
case "$probe" in
''|*[!0-9]*)
probe=$(stat -f %z / 2>/dev/null) || probe=''
case "$probe" in
''|*[!0-9]*) _FM_STAT_DIALECT=unknown ;;
*) _FM_STAT_DIALECT=bsd ;;
esac
;;
*) _FM_STAT_DIALECT=gnu ;;
esac
fi
[ "$_FM_STAT_DIALECT" != unknown ] || return 1
printf '%s\n' "$_FM_STAT_DIALECT"
}

# fm_stat_fmt <gnu-fmt> <bsd-fmt> <path>: print the formatted field, else fail.
# The two format strings are the SAME field expressed in each dialect; callers
# below name the field so no call site has to remember the letter pairs.
fm_stat_fmt() {
local gnu_fmt=$1 bsd_fmt=$2 path=$3 dialect out
dialect=$(fm_stat_dialect) || return 1
case "$dialect" in
gnu) out=$(LC_ALL=C stat -c "$gnu_fmt" "$path" 2>/dev/null) || return 1 ;;
bsd) out=$(LC_ALL=C stat -f "$bsd_fmt" "$path" 2>/dev/null) || return 1 ;;
*) return 1 ;;
esac
[ -n "$out" ] || return 1
printf '%s\n' "$out"
}

# _fm_stat_uint <gnu-fmt> <bsd-fmt> <path>: as fm_stat_fmt, but the result must
# be a bare unsigned integer. This is the belt to fm_stat_dialect's braces: if a
# host ever ships a third flavor that the probe misreads, a caller doing
# arithmetic gets a clean failure instead of a stray token.
_fm_stat_uint() {
local out
out=$(fm_stat_fmt "$1" "$2" "$3") || return 1
case "$out" in
''|*[!0-9]*) return 1 ;;
esac
printf '%s\n' "$out"
}

fm_stat_mtime() { _fm_stat_uint %Y %m "$1"; } # mtime, epoch seconds
fm_stat_ctime() { _fm_stat_uint %Z %c "$1"; } # inode change time, epoch seconds
fm_stat_size() { _fm_stat_uint %s %z "$1"; } # size in bytes
fm_stat_mode() { _fm_stat_uint %a %Lp "$1"; } # permission bits, octal
fm_stat_device() { _fm_stat_uint %d %d "$1"; } # device number
fm_stat_inode() { _fm_stat_uint %i %i "$1"; } # inode number
fm_stat_links() { _fm_stat_uint %h %l "$1"; } # hard link count

# fm_stat_identity <path>: "device:inode" - the rotation/recreation check.
fm_stat_identity() { fm_stat_fmt '%d:%i' '%d:%i' "$1"; }

# fm_stat_signature <path>: "size:mtime" change signature. BSD %Fm is the
# fractional-second mtime; the GNU side stays whole-second %Y, matching the
# pairing every caller already used - a signature only has to differ when the
# file changes, it does not have to mean the same thing across hosts.
fm_stat_signature() { fm_stat_fmt '%s:%Y' '%z:%Fm' "$1"; }

# fm_stat_fingerprint <path>: "device:inode:size:mtime:ctime".
fm_stat_fingerprint() { fm_stat_fmt '%d:%i:%s:%Y:%Z' '%d:%i:%z:%m:%c' "$1"; }
15 changes: 9 additions & 6 deletions bin/fm-supervise-daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,15 @@ AFK_FLAG_NAME=".afk"
# classifiers can take an explicit state arg without depending on globals.
_state_root() { printf '%s' "${FM_STATE_OVERRIDE:-$FM_HOME/state}"; }

# --- portable stat (same trap as fm-watch.sh: no `stat -f || stat -c`) -------
if [ "$(uname)" = Darwin ]; then
_stat_file_mtime() { stat -f %m "$1" 2>/dev/null; }
else
_stat_file_mtime() { stat -c %Y "$1" 2>/dev/null; }
fi
# --- portable stat: one owner, bin/fm-stat-lib.sh ---------------------------
# It closes both traps at once - never `stat -f || stat -c` (GNU's -f is
# --file-system: it writes a filesystem dump to STDOUT and only THEN exits 1, so
# the fallback DOES run and appends its correct integer to that dump, handing the
# caller a multi-line non-integer at overall rc=0), and never keyed on `uname`
# (a Darwin kernel routinely resolves `stat` to GNU coreutils).
# shellcheck source=bin/fm-stat-lib.sh
. "$FM_DAEMON_DIR/fm-stat-lib.sh"
_stat_file_mtime() { fm_stat_mtime "$1"; }
_now() { date +%s; }
_file_age() { # seconds since mtime; very large if missing
local f=$1 m
Expand Down
Loading
Loading