#!/bin/sh
#
# S82dcentrald - Start DCENTos mining daemon
#
# dcentrald is the Rust mining daemon providing:
#   - ASIC hash board control via FPGA UIO
#   - Stratum V1 pool connection with ASICBoost
#   - Thermal management (PID fan control)
#   - REST API + WebSocket on port 8080
#   - Dashboard is served by S80dashboard on port 80
#   - CGMiner-compatible API on port 4028
#   - WebSocket live stats
#
# Config: /data/dcentrald.toml (persistent) or /etc/dcentrald.toml (default)
#
# SAFETY: If dcentrald crashes while hash boards are powered, the wrapper
# commands the home safety fan cap (PWM 30). If hashboard power is already
# proven cut, the wrapper leaves the parked unit at idle PWM 10. The thermal
# PID loop inside dcentrald is the ONLY thing controlling fan speed during
# mining. Without it, ASIC chips can overheat in seconds.
#
# D-Central Technologies - DCENTos v0.4.0
#

select_dcentrald_paths() {
    if [ -x /data/dcentrald ]; then
        DAEMON="/data/dcentrald"
    else
        DAEMON="/usr/local/bin/dcentrald"
    fi
    if [ -f /data/dcentrald.toml ]; then
        CONFIG="/data/dcentrald.toml"
    else
        CONFIG="/etc/dcentrald.toml"
    fi
}

select_dcentrald_paths
PIDFILE="/var/run/dcentrald.pid"
CHILD_PIDFILE="/var/run/dcentrald-child.pid"
EXPECTFILE="/var/run/dcentrald.expected_exit.pid"
LOGFILE="/tmp/dcentrald.log"
FAN_BASE=0x42800000
# AM2 persistent fan-custodian pidfile (see am2_start_fan_custodian below).
FANHOLD_PIDFILE="/var/run/dcentrald-fanhold.pid"
FANHOLD_READYFILE="/var/run/dcentrald-fanhold.ready"
DEFAULT_COMPAT="/etc/default/dcentos-compat"
PERSISTENT_COMPAT="/data/dcentos-compat"
API_PORT=8080
SCRIPT="$0"
SESSION_LATCH_HELPER="/usr/libexec/dcentos/dcentrald-session-latch.sh"
FIRSTLIGHT_CONFIG_HELPER="/usr/libexec/dcentos/dcentrald-firstlight-config.sh"
PROCESS_IDENTITY_HELPER="/usr/libexec/dcentos/dcentrald-process-identity.sh"
FAN_CUSTODY_HELPER="/usr/libexec/dcentos/dcentrald-fan-custody.sh"
DEPLOY_RECOVERY_HELPER="/usr/libexec/dcentos/dcentrald-deploy-recovery.sh"
DEPLOY_RECOVERY_BASE="/data/.dcent-deploy-recovery"
DEPLOY_LOCK_HELPER="/usr/libexec/dcentos/dcentos-deploy-lock"
DEPLOY_LOCK_PATH="/run/dcentos-deploy-admission.lock"
DEPLOY_MAINTENANCE_MARKER="/run/dcentos-deploy-maintenance"
CRASH_LATCH_FILE="/data/dcent/dcentrald-hardware-session.crash-latched"
DCENT_PROCESS_IDENTITY_TEST_AUTHORITY=0
readonly DCENT_PROCESS_IDENTITY_TEST_AUTHORITY
PROCESS_IDENTITY_AVAILABLE=0
if [ -r "$PROCESS_IDENTITY_HELPER" ] && . "$PROCESS_IDENTITY_HELPER"; then
    PROCESS_IDENTITY_AVAILABLE=1
fi
FAN_CUSTODY_AVAILABLE=0
if [ "$PROCESS_IDENTITY_AVAILABLE" -eq 1 ] \
    && [ -r "$FAN_CUSTODY_HELPER" ] && . "$FAN_CUSTODY_HELPER"; then
    FAN_CUSTODY_AVAILABLE=1
fi
FAN_TRANSITION_LOCK_HELD=0

dcent_release_fan_transition_lock() {
    if [ "$FAN_TRANSITION_LOCK_HELD" -eq 1 ]; then
        dcent_fan_lock_release "${FANHOLD_READYFILE}.lock" || true
        FAN_TRANSITION_LOCK_HELD=0
    fi
}

dcent_acquire_fan_transition_lock() {
    [ "$FAN_CUSTODY_AVAILABLE" -eq 1 ] || return 1
    dcent_fan_lock_acquire "${FANHOLD_READYFILE}.lock" 10 || return 1
    FAN_TRANSITION_LOCK_HELD=1
    trap 'dcent_release_fan_transition_lock' 0
    trap 'exit 1' 1 2 15
}

dcent_verify_resolved_deploy_config() {
    return 0
}

dcent_verify_resolved_deploy_generation() {
    dcent_verify_resolved_deploy_config
}

dcent_verify_resolved_deploy_selection() {
    return 0
}

resolve_interrupted_persistent_deploy() {
    if [ -r "$DEPLOY_RECOVERY_HELPER" ]; then
        . "$DEPLOY_RECOVERY_HELPER" || return 1
        dcent_recover_pending_deploy "$DEPLOY_RECOVERY_BASE" \
            /data/dcentrald /data/dcentrald.toml /etc/dcentrald.toml \
            /usr/local/bin/dcentrald
        return $?
    fi

    # Older/incomplete rootfs overlays must not silently start through a
    # transaction that they cannot interpret.
    [ -e "$DEPLOY_RECOVERY_BASE" ] || [ -L "$DEPLOY_RECOVERY_BASE" ] || return 0
    [ -d "$DEPLOY_RECOVERY_BASE" ] && [ ! -L "$DEPLOY_RECOVERY_BASE" ] || {
        echo "  [FAIL] Persistent deploy recovery base is invalid"
        return 1
    }
    [ "$(stat -c '%u:%a' "$DEPLOY_RECOVERY_BASE" 2>/dev/null)" = "0:700" ] || {
        echo "  [FAIL] Persistent deploy recovery base metadata is invalid"
        return 1
    }
    if [ -e "$DEPLOY_RECOVERY_BASE/.deploy-lease" ] \
        || [ -L "$DEPLOY_RECOVERY_BASE/.deploy-lease" ]; then
        echo "  [FAIL] Persistent deploy recovery helper is missing with an active lease"
        return 1
    fi
    set -- "$DEPLOY_RECOVERY_BASE"/*/persistent-transaction.state
    { [ ! -e "$1" ] && [ ! -L "$1" ]; } || {
        echo "  [FAIL] Persistent deploy recovery helper is missing"
        return 1
    }
}

dcent_validate_deploy_lock_handoff() {
    ready_fd=${DCENTOS_DEPLOY_LOCK_READY_FD:-}
    lock_fd=${DCENTOS_DEPLOY_LOCK_FD:-}
    case "$ready_fd:$lock_fd" in
        *[!0-9:]*|:*|*:|*:*:*) return 1 ;;
    esac
    [ "$ready_fd" -ge 3 ] && [ "$ready_fd" -le 1024 ] \
        && [ "$lock_fd" -ge 3 ] && [ "$lock_fd" -le 1024 ] \
        && [ "$ready_fd" -ne "$lock_fd" ] || return 1
    [ "$(readlink "/proc/$PPID/exe" 2>/dev/null)" = "$DEPLOY_LOCK_HELPER" ] \
        || return 1
    stat -Lc '%F' "/proc/$$/fd/$ready_fd" >/dev/null 2>&1 || return 1
    named_lock=$(stat -c '%d:%i:%u:%g:%a:%h' "$DEPLOY_LOCK_PATH" 2>/dev/null) \
        || return 1
    inherited_lock=$(stat -Lc '%d:%i:%u:%g:%a:%h' "/proc/$$/fd/$lock_fd" 2>/dev/null) \
        || return 1
    [ "$named_lock" = "$inherited_lock" ] || return 1
    case "$inherited_lock" in
        *:0:0:600:1) ;;
        *) return 1 ;;
    esac
}

PLATFORM=""
[ -r /etc/dcentos/platform ] && PLATFORM=$(cat /etc/dcentos/platform 2>/dev/null)
BOARD_TARGET=""
[ -r /etc/dcentos/board_target ] && BOARD_TARGET=$(cat /etc/dcentos/board_target 2>/dev/null)
IS_AM2=0
case "$PLATFORM" in
    zynq-bm3-am2) IS_AM2=1 ;;
esac
case "$BOARD_TARGET" in
    am2-s17|am2-s17p|am2-s17pro|am2-s17plus|am2-t17|am2-t17plus|am2-s19|am2-s19j|am2-s19jpro|am2-s19jpro-zynq|am2-s19pro|am2-t19) IS_AM2=1 ;;
esac

migrate_legacy_api_port() {
    # A transaction-bound config is immutable admission evidence. Migration is
    # intentionally deferred to a future explicit config-management operation;
    # rewriting it here would invalidate the generation between proof and exec.
    [ "${DCENT_DEPLOY_CONFIG_BINDING_ACTIVE:-0}" -ne 1 ] || return 0
    [ -f "$CONFIG" ] || return 0

    # Rewrite legacy dcentrald API http_port 80 to 8080 so S80dashboard owns :80.
    if ! grep -Eq '^[[:space:]]*http_port[[:space:]]*=[[:space:]]*80([[:space:]]*(#.*)?)?$' "$CONFIG" 2>/dev/null; then
        return 0
    fi

    TMPFILE="${CONFIG}.tmp.$$"
    if awk '
        BEGIN { in_api = 0 }
        /^[[:space:]]*\[[^]]+\][[:space:]]*$/ {
            in_api = ($0 ~ /^[[:space:]]*\[api\][[:space:]]*$/)
        }
        in_api && /^[[:space:]]*http_port[[:space:]]*=[[:space:]]*80([[:space:]]*(#.*)?)?$/ {
            sub(/80/, "8080")
        }
        { print }
    ' "$CONFIG" > "$TMPFILE" 2>/dev/null; then
        if mv "$TMPFILE" "$CONFIG" 2>/dev/null; then
            echo "  [MIGRATE] dcentrald API http_port 80 to 8080 in $CONFIG"
        else
            rm -f "$TMPFILE"
            echo "  [WARN] Could not migrate legacy dcentrald API port in $CONFIG"
        fi
    else
        rm -f "$TMPFILE"
        echo "  [WARN] Could not inspect dcentrald API port in $CONFIG"
    fi
}

bosminer_pic_bootstrap_enabled() {
    BOSMINER_PIC_BOOTSTRAP=0
    [ -f "$DEFAULT_COMPAT" ] && . "$DEFAULT_COMPAT"
    [ -f "$PERSISTENT_COMPAT" ] && . "$PERSISTENT_COMPAT"

    case "$BOSMINER_PIC_BOOTSTRAP" in
        1|true|TRUE|yes|YES|on|ON|enabled|ENABLED)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

compat_bootstrap_model_allowed() {
    BOSMINER_PIC_BOOTSTRAP_ALLOW_MODEL="am1-s9"
    [ -f "$DEFAULT_COMPAT" ] && . "$DEFAULT_COMPAT"
    [ -f "$PERSISTENT_COMPAT" ] && . "$PERSISTENT_COMPAT"

    [ "$BOSMINER_PIC_BOOTSTRAP_ALLOW_MODEL" = "am1-s9" ] || return 1
    [ -d /sys/class/uio ] || return 1

    UIO_COUNT=$(ls -d /sys/class/uio/uio* 2>/dev/null | wc -l)
    [ "$UIO_COUNT" -gt 0 ] || return 1
    [ "$UIO_COUNT" -le 14 ] || return 1

    for name_file in /sys/class/uio/*/name; do
        [ -f "$name_file" ] || continue
        if grep -q "board-control" "$name_file" 2>/dev/null; then
            return 1
        fi
    done

    return 0
}

# Safety: command fans to the home cap. Tach/RPM is the physical noise proof.
# PIC watchdog cuts voltage within 10-60s if dcentrald is dead.
# NEVER PWM 127 — home mining, noise is unacceptable.
am2_power_cut_proven() {
    [ -r /sys/class/gpio/gpio907/value ] && [ "$(cat /sys/class/gpio/gpio907/value 2>/dev/null)" = "0" ]
}

# AUDIT FIX (2026-05-29, feedback_am2_never_leave_unit_without_fan_manager.md):
# On AM2 (XIL Zynq) the fan PWM is held only while a process owns the uio16 mmap
# AND keeps re-commanding it. The old `$DAEMON --set-fan N` wrote the PWM once
# and EXITED, so the board's fan IP drifted the fans back to full speed — a home
# unit blasts and the documented AM2 safety remediation silently failed. The fix
# is a BACKGROUNDED persistent custodian (`dcentrald --hold-fan N`) that stays
# resident and re-asserts the PWM every ~5 s (mmap-hold alone is not enough). We
# kill any prior custodian first so only one owns the mmap. AM2 ONLY ($IS_AM2);
# S9/am1 fan registers hold via devmem so that branch is unchanged. PWM is only
# ever driven DOWN (<= 30).
am2_start_fan_custodian() {
    _pwm="$1"
    FAN_CUSTODY_RESULT=1
    if [ "$FAN_CUSTODY_AVAILABLE" -eq 1 ]; then
        if [ "$FAN_TRANSITION_LOCK_HELD" -eq 1 ]; then
            dcent_fan_custodian_start_locked "$DAEMON" "$_pwm" "$FANHOLD_PIDFILE" \
                "$FANHOLD_READYFILE" start-stop-daemon "$LOGFILE" 10
        else
            dcent_fan_custodian_start "$DAEMON" "$_pwm" "$FANHOLD_PIDFILE" \
                "$FANHOLD_READYFILE" start-stop-daemon "$LOGFILE" 10
        fi
        FAN_CUSTODY_RESULT=$?
    fi
    if [ "$FAN_CUSTODY_RESULT" -eq 0 ]; then
        echo "$(date): SAFETY: verified am2 fan custody at PWM $_pwm (pid $(cat "$FANHOLD_PIDFILE" 2>/dev/null))" >> "$LOGFILE"
        return 0
    fi
    echo "$(date): WARN: am2 fan custodian (--hold-fan $_pwm) failed readiness/custody validation" >> "$LOGFILE"
    return 1
}

am2_stop_fan_custodian() {
    [ "$FAN_CUSTODY_AVAILABLE" -eq 1 ] || return 1
    if [ "$FAN_TRANSITION_LOCK_HELD" -eq 1 ]; then
        dcent_fan_custodian_stop_locked "$DAEMON" "$FANHOLD_PIDFILE" \
            "$FANHOLD_READYFILE" 5 2
    else
        dcent_fan_custodian_stop "$DAEMON" "$FANHOLD_PIDFILE" "$FANHOLD_READYFILE" 5 2
    fi
}

fan_safety_override() {
    if [ "$IS_AM2" = "1" ]; then
        if [ -x "$DAEMON" ]; then
            FAN_PWM=30
            if am2_power_cut_proven; then
                FAN_PWM=10
            fi
            # Persistent custodian, NOT the exit-immediately --set-fan one-shot.
            am2_start_fan_custodian "$FAN_PWM"
            return $?
        else
            echo "$(date): WARN: am2 dcentrald binary absent at $DAEMON; fan cap not applied" >> $LOGFILE
            return 1
        fi
    elif command -v devmem > /dev/null 2>&1; then
        devmem $((FAN_BASE + 0x10)) 32 30 2>/dev/null || return 1
        devmem $((FAN_BASE + 0x14)) 32 30 2>/dev/null || return 1
        echo "$(date): SAFETY: fans commanded to PWM 30 (dcentrald exited)" >> $LOGFILE
        return 0
    fi
    return 1
}

FIRSTLIGHT_CENSUS_TOKEN=
FIRSTLIGHT_WORK_SECONDS=
validate_firstlight_request() {
    [ "${#1}" -eq 32 ] || return 1
    case "$1" in *[!0-9a-f]*) return 1 ;; esac
    case "$(cat /etc/dcentos/board_target 2>/dev/null)" in
        am1-s9|am1-s9i|am1-s9j|am2-s17p|am2-s17plus|am2-t17|am2-t17plus|am2-s19j) ;;
        *) return 1 ;;
    esac
}

validate_firstlight_seconds() {
    case "$1" in ''|*[!0-9]*|0*) return 1 ;; esac
    [ "$1" -ge 1 ] && [ "$1" -le 300 ]
}

firstlight_restore_locked() {
    [ "$#" -eq 1 ] && validate_firstlight_request "$1" || return 1
    dcent_validate_deploy_lock_handoff || return 1
    [ ! -e /run/dcentos-sysupgrade.lock ] && [ ! -L /run/dcentos-sysupgrade.lock ] || return 1
    [ ! -e /data/dcent/reset-settings ] && [ ! -L /data/dcent/reset-settings ] || return 1
    [ ! -e "$DEPLOY_MAINTENANCE_MARKER" ] && [ ! -L "$DEPLOY_MAINTENANCE_MARKER" ] || return 1
    [ "$(sh "$SESSION_LATCH_HELPER" status)" = clear ] || return 1
    [ -z "$(pidof dcentrald 2>/dev/null || true)" ] || return 1
    resolve_interrupted_persistent_deploy || return 1
    select_dcentrald_paths
    dcent_verify_resolved_deploy_selection "$DAEMON" "$CONFIG" \
        && dcent_verify_resolved_deploy_generation || return 1
    [ -r "$FIRSTLIGHT_CONFIG_HELPER" ] && [ ! -L "$FIRSTLIGHT_CONFIG_HELPER" ] \
        && . "$FIRSTLIGHT_CONFIG_HELPER" || return 1
    firstlight_restore_token=$1
    firstlight_retired=$DCENT_FIRSTLIGHT_BASE/retired-$1
    firstlight_retire_needed=0
    if [ -e "$DCENT_FIRSTLIGHT_STAGE" ] || [ -L "$DCENT_FIRSTLIGHT_STAGE" ]; then
        # Never merge/overwrite a foreign destination, including dangling links.
        [ ! -e "$firstlight_retired" ] && [ ! -L "$firstlight_retired" ] || return 1
        dcent_firstlight_capture_config "$1" work "$DAEMON" "$CONFIG" || return 1
        firstlight_retire_needed=1
    else
        # Resume after atomic retirement. The session record must still exist;
        # a bare empty directory cannot stand in for original-generation proof.
        DCENT_FIRSTLIGHT_STAGE=$firstlight_retired
        dcent_firstlight_identity "$DCENT_FIRSTLIGHT_STAGE/session" record >/dev/null || return 1
        dcent_firstlight_check_identity_record "$1" && dcent_firstlight_check_original || return 1
        [ "$DAEMON" = "$(dcent_firstlight_field daemon_path)" ] \
            && [ "$CONFIG" = "$(dcent_firstlight_field original_path)" ] || return 1
    fi
    firstlight_restore_record=$(dcent_firstlight_identity "$DCENT_FIRSTLIGHT_STAGE/session" record) || return 1
    for firstlight_entry in "$DCENT_FIRSTLIGHT_STAGE"/* "$DCENT_FIRSTLIGHT_STAGE"/.[!.]* "$DCENT_FIRSTLIGHT_STAGE"/..?*; do
        [ -e "$firstlight_entry" ] || [ -L "$firstlight_entry" ] || continue
        case "$firstlight_entry" in
            "$DCENT_FIRSTLIGHT_STAGE/session") ;;
            "$DCENT_FIRSTLIGHT_STAGE/census.toml"|"$DCENT_FIRSTLIGHT_STAGE/work.toml")
                firstlight_phase=${firstlight_entry##*/}
                firstlight_phase=${firstlight_phase%.toml}
                firstlight_observed=$(dcent_firstlight_identity "$firstlight_entry" phase) || return 1
                [ "$firstlight_observed" = "$(dcent_firstlight_field "$firstlight_phase")" ] || return 1
                ;;
            *) return 1 ;;
        esac
    done
    "$DAEMON" --check-firstlight-idle-config "$CONFIG" || return 1
    # The parser process is read-only, but its result is still a point-in-time
    # observation. Revalidate original bytes/inodes and the record afterwards.
    dcent_firstlight_check_original || return 1
    [ "$firstlight_restore_record" = "$(dcent_firstlight_identity "$DCENT_FIRSTLIGHT_STAGE/session" record)" ] || return 1
    if [ "$firstlight_retire_needed" -eq 1 ]; then
        mv "$DCENT_FIRSTLIGHT_STAGE" "$firstlight_retired" || return 1
        sync || return 1
        DCENT_FIRSTLIGHT_STAGE=$firstlight_retired
    fi
    # Only the two recognized phase files and exact bound session record can be
    # removed. Any interruption retains either a resumable record or no active
    # stage; the original mining-disabled config is never rewritten.
    for firstlight_phase in census work; do
        if [ -e "$DCENT_FIRSTLIGHT_STAGE/$firstlight_phase.toml" ]; then
            rm "$DCENT_FIRSTLIGHT_STAGE/$firstlight_phase.toml" || return 1
            sync || return 1
        fi
    done
    dcent_firstlight_check_original || return 1
    [ "$firstlight_restore_record" = "$(dcent_firstlight_identity "$DCENT_FIRSTLIGHT_STAGE/session" record)" ] || return 1
    [ "$(sh "$SESSION_LATCH_HELPER" status)" = clear ] \
        && [ -z "$(pidof dcentrald 2>/dev/null || true)" ] || return 1
    rm "$DCENT_FIRSTLIGHT_STAGE/session" || return 1
    rmdir "$DCENT_FIRSTLIGHT_STAGE" || return 1
    sync || return 1
    # exec preserves this shell PID, its compiled parent and the original
    # handoff descriptors. Ordinary start rechecks the same protected generation.
    exec "$0" start-locked
}

case "$1" in
    firstlight-restore)
        [ "$#" -eq 2 ] && validate_firstlight_request "$2" || exit 64
        [ -x "$DEPLOY_LOCK_HELPER" ] || exit 1
        exec "$DEPLOY_LOCK_HELPER" --handoff "$0" firstlight-restore-locked "$2"
        ;;
    firstlight-restore-locked)
        [ "$#" -eq 2 ] || exit 64
        firstlight_restore_locked "$2"
        exit $?
        ;;
    firstlight-work)
        [ "$#" -eq 3 ] && validate_firstlight_request "$2" \
            && validate_firstlight_seconds "$3" || exit 64
        [ -x "$DEPLOY_LOCK_HELPER" ] || exit 1
        exec "$DEPLOY_LOCK_HELPER" --handoff "$0" start-locked firstlight-work "$2" "$3"
        ;;
    firstlight-census)
        [ "$#" -eq 2 ] && validate_firstlight_request "$2" || exit 64
        [ -x "$DEPLOY_LOCK_HELPER" ] || exit 1
        exec "$DEPLOY_LOCK_HELPER" --handoff "$0" start-locked firstlight-census "$2"
        ;;
    deploy-recovery-capabilities)
        [ -r "$DEPLOY_RECOVERY_HELPER" ] \
            && [ ! -L "$DEPLOY_RECOVERY_HELPER" ] || exit 1
        . "$DEPLOY_RECOVERY_HELPER" || exit 1
        command -v dcent_deploy_print_capabilities >/dev/null 2>&1 || exit 1
        dcent_deploy_print_capabilities
        ;;

    start)
        if [ ! -x "$DEPLOY_LOCK_HELPER" ]; then
            echo "  [FAIL] dcentrald deploy/admission lock helper is missing"
            exit 1
        fi
        exec "$DEPLOY_LOCK_HELPER" --handoff "$0" start-locked
        ;;

    start-locked)
        if [ "$#" -ne 1 ]; then
            if [ "$#" -eq 3 ] && [ "$2" = firstlight-census ]; then
                validate_firstlight_request "$3" || exit 64
            elif [ "$#" -eq 4 ] && [ "$2" = firstlight-work ]; then
                validate_firstlight_request "$3" && validate_firstlight_seconds "$4" || exit 64
                FIRSTLIGHT_WORK_SECONDS=$4
            else
                exit 64
            fi
            FIRSTLIGHT_CENSUS_TOKEN=$3
        fi
        if ! dcent_validate_deploy_lock_handoff; then
            echo "  [FAIL] dcentrald deploy-lock handoff provenance is invalid"
            exit 1
        fi
        # A durable settings-reset intent survives worker death and /run loss.
        # Only the checked reset owner may consume it after mining-off defaults
        # are durable. Any unsafe entry also refuses ordinary startup.
        if [ -e /data/dcent/reset-settings ] || [ -L /data/dcent/reset-settings ]; then
            echo "  [FAIL] dcentrald admission blocked by /data/dcent/reset-settings; complete the checked reset"
            exit 1
        fi
        for firstlight_prepared in /data/.dcent-firstlight/prepared-* /data/.dcent-firstlight/retired-*; do
            [ ! -e "$firstlight_prepared" ] && [ ! -L "$firstlight_prepared" ] || {
                echo "  [FAIL] Resolve the retained firstlight session before startup"
                exit 1
            }
        done
        [ ! -e "$DEPLOY_MAINTENANCE_MARKER" ] \
            && [ ! -L "$DEPLOY_MAINTENANCE_MARKER" ] || {
            echo "  [FAIL] dcentrald admission blocked by active deploy maintenance"
            exit 1
        }
        if ! resolve_interrupted_persistent_deploy; then
            echo "  [FAIL] dcentrald admission blocked by unresolved persistent deploy"
            exit 1
        fi
        # Recovery may have restored or removed /data paths, so selection must
        # occur after the generation resolver completes.
        select_dcentrald_paths
        if ! dcent_verify_resolved_deploy_selection "$DAEMON" "$CONFIG" \
            || ! dcent_verify_resolved_deploy_generation; then
            echo "  [FAIL] Resolved persistent generation changed before admission"
            exit 1
        fi
        if [ -n "$FIRSTLIGHT_CENSUS_TOKEN" ]; then
            [ -r "$FIRSTLIGHT_CONFIG_HELPER" ] && [ ! -L "$FIRSTLIGHT_CONFIG_HELPER" ] \
                && . "$FIRSTLIGHT_CONFIG_HELPER" || exit 1
            FIRSTLIGHT_PHASE=census
            [ -z "$FIRSTLIGHT_WORK_SECONDS" ] || FIRSTLIGHT_PHASE=work
            dcent_firstlight_capture_config "$FIRSTLIGHT_CENSUS_TOKEN" "$FIRSTLIGHT_PHASE" \
                "$DAEMON" "$CONFIG" || {
                echo "  [FAIL] Firstlight stage/context is absent, stale or foreign"
                exit 1
            }
        elif [ -e /data/.dcent-firstlight/current ] || [ -L /data/.dcent-firstlight/current ]; then
            echo "  [FAIL] Resolve the retained firstlight session before normal start"
            exit 1
        fi
        if [ ! -x "$DAEMON" ]; then
            if [ -n "$FIRSTLIGHT_CENSUS_TOKEN" ]; then
                echo "  [FAIL] Firstlight census requires the installed daemon"
                exit 1
            fi
            echo "  [SKIP] dcentrald: binary not found at $DAEMON"
            exit 0
        fi
        dcent_acquire_fan_transition_lock || {
            echo "  [FAIL] Cannot serialize dcentrald owner admission"
            exit 1
        }

        if [ "$IS_AM2" = "1" ] \
            && { [ -e "$FANHOLD_PIDFILE" ] || [ -e "$FANHOLD_READYFILE" ] \
                || [ -e "${FANHOLD_READYFILE}.pending" ]; }; then
            am2_stop_fan_custodian || {
                echo "  [FAIL] Prior fan custodian cannot be verified and stopped"
                exit 1
            }
            echo "  [OK] exact prior am2 fan custodian stopped before owner admission"
        fi

        RUNNING_PIDS=$(pidof dcentrald 2>/dev/null || true)
        if [ -n "$RUNNING_PIDS" ]; then
            echo "  [FAIL] Refusing a second dcentrald owner (PID(s): $RUNNING_PIDS)"
            exit 1
        fi
        if [ ! -r "$SESSION_LATCH_HELPER" ] \
            || [ "$PROCESS_IDENTITY_AVAILABLE" -ne 1 ]; then
            echo "  [FAIL] Persistent session or exact-process identity helper is missing"
            fan_safety_override
            exit 1
        fi
        SESSION_TOKEN=$(/bin/sh "$SESSION_LATCH_HELPER" prepare 2>> "$LOGFILE") || {
            echo "  [FAIL] Hardware-session admission is blocked; see $LOGFILE"
            fan_safety_override
            exit 1
        }

        # W1.5 (2026-05-07): re-assert tight perms on /data/dcent/ + auth.json
        # before launching the daemon. dcentrald will also auto-correct via
        # verify_auth_file_perms() at startup, but doing it here closes the
        # window between mount(/data) and the first daemon write — and works
        # even if the daemon binary is being rolled back to a pre-W1.5 build.
        if [ -d /data/dcent ]; then
            chmod 0700 /data/dcent 2>/dev/null
        fi
        if [ -f /data/dcent/auth.json ]; then
            chmod 0600 /data/dcent/auth.json 2>/dev/null
        fi

        # Legacy compatibility mode only. Native DCENT_OS cold boot is the
        # default product path, so only wait for S15pic_boot when the explicit
        # compat bootstrap is enabled and the board is positively allowlisted.
        if bosminer_pic_bootstrap_enabled && compat_bootstrap_model_allowed && [ -x /usr/bin/bosminer ] && [ ! -f /tmp/pic_init_done ]; then
            echo "  [WAIT] dcentrald: waiting for legacy PIC bootstrap..."
            for i in $(seq 1 120); do
                [ -f /tmp/pic_init_done ] && break
                [ -f /tmp/pic_init_failed ] && break
                sleep 1
            done
            if [ -f /tmp/pic_init_done ]; then
                :
            elif [ -f /tmp/pic_init_failed ]; then
                echo "  [WARN] S15pic_boot did not confirm PIC init - starting dcentrald native path"
            else
                echo "  [WARN] PIC init flag not found after 120s - starting anyway"
            fi
        fi

        echo "Starting dcentrald mining daemon..."
        migrate_legacy_api_port
        echo "  Config: $CONFIG"
        echo "  API: REST/WebSocket :$API_PORT"
        echo "  Dashboard: HTTP :80 via S80dashboard"

        # Do not kill all Python interpreters here.
        # S81mcp starts immediately before us and must keep running.

        # NOTE (updated 2026-03-26): dcentrald uses devmem AXI IIC bypass for I2C.
        # It unbinds the kernel xiic-i2c driver internally on startup.
        # The kernel driver is NOT used - devmem provides direct register access
        # with proper clock timing (THIGH/TLOW=993 for 100kHz).

        # Kill any orphaned dcentrald from previous manual runs.
        # Expected exits are marked so the wrapper does not misclassify replacement
        # as a crash and blast the fans.
        # A live owner is refused above. Never kill-and-replace an orphan from
        # the start path: its hardware disposition is unknown.

        # Revalidate at the actual supervisor exec boundary. Fan/session
        # serialization, optional PIC bootstrap, and compatibility work above
        # can take minutes and must not widen config admission into a stale
        # point-in-time observation.
        select_dcentrald_paths
        if [ ! -x "$DAEMON" ] \
            || ! dcent_verify_resolved_deploy_selection "$DAEMON" "$CONFIG" \
            || ! dcent_verify_resolved_deploy_generation; then
            /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" deploy-generation-binding-changed \
                >> "$LOGFILE" 2>&1 || true
            fan_safety_override
            echo "  [FAIL] Resolved persistent generation changed at exec boundary"
            exit 1
        fi

        if [ -n "$FIRSTLIGHT_CENSUS_TOKEN" ]; then
            if ! dcent_firstlight_verify_config "$DAEMON" "$CONFIG"; then
                /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" firstlight-config-binding-changed \
                    >> "$LOGFILE" 2>&1 || true
                fan_safety_override
                echo "  [FAIL] Firstlight config/context changed at exec"
                exit 1
            fi
            CONFIG=$DCENT_FIRSTLIGHT_CONFIG
        fi
        rm -f "$PIDFILE"
        set -- "$DAEMON" --config "$CONFIG"
        if [ -n "$FIRSTLIGHT_CENSUS_TOKEN" ]; then
            # Firstlight census/work extends the common supervisor with
            # dedicated latch modes; the ordinary path below keeps the
            # canonical literal invocation the admission-policy gate pins.
            if [ -n "$FIRSTLIGHT_WORK_SECONDS" ]; then
                SUPERVISE_MODE=supervise-firstlight-work
                set -- "$@" --firstlight-work "$FIRSTLIGHT_CENSUS_TOKEN" \
                    --firstlight-seconds "$FIRSTLIGHT_WORK_SECONDS"
            else
                SUPERVISE_MODE=supervise-census
                set -- "$@" --firstlight-census "$FIRSTLIGHT_CENSUS_TOKEN"
            fi
            DCENTOS_SESSION_WRAPPER_PIDFILE="$PIDFILE" \
                /bin/sh "$SESSION_LATCH_HELPER" "$SUPERVISE_MODE" "$SESSION_TOKEN" \
                "$SCRIPT" "$LOGFILE" "$CHILD_PIDFILE" "$EXPECTFILE" \
                "$@" >>"$LOGFILE" 2>&1 &
        else
            DCENTOS_SESSION_WRAPPER_PIDFILE="$PIDFILE" \
                /bin/sh "$SESSION_LATCH_HELPER" supervise "$SESSION_TOKEN" \
                "$SCRIPT" "$LOGFILE" "$CHILD_PIDFILE" "$EXPECTFILE" \
                "$@" >>"$LOGFILE" 2>&1 &
        fi
        SUPERVISOR_PID=$!
        case "$SUPERVISOR_PID" in
            ''|*[!0-9]*)
                /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" supervisor-launch-failed \
                    >> "$LOGFILE" 2>&1 || true
                fan_safety_override
                echo "  [FAIL] dcentrald supervisor did not start"
                exit 1
                ;;
        esac
        if ! kill -0 "$SUPERVISOR_PID" 2>/dev/null; then
            /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" supervisor-launch-failed \
                >> "$LOGFILE" 2>&1 || true
            fan_safety_override
            echo "  [FAIL] dcentrald supervisor did not start"
            exit 1
        fi
        if [ -n "$FIRSTLIGHT_CENSUS_TOKEN" ]; then
            # Keep the kernel deploy lock until the one-shot supervisor ends.
            # Release only fan transition serialization, allowing its post-exit
            # safety owner to run after wait(2) proves the child is dead.
            dcent_release_fan_transition_lock
            wait "$SUPERVISOR_PID"
            CENSUS_RESULT=$?
            if [ "$CENSUS_RESULT" -eq 0 ]; then
                /bin/sh "$SESSION_LATCH_HELPER" status | grep -qx clear || CENSUS_RESULT=1
            fi
            # EOF on process exit also releases the handoff. No live mining
            # owner remains for this synchronous one-shot entry.
            exit "$CENSUS_RESULT"
        fi
        ADMITTED_CHILD=
        ADMISSION_WAIT=0
        while [ -z "$ADMITTED_CHILD" ]; do
            if ! kill -0 "$SUPERVISOR_PID" 2>/dev/null; then
                wait "$SUPERVISOR_PID" 2>/dev/null || true
                /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" supervisor-exited-before-admission \
                    >> "$LOGFILE" 2>&1 || true
                fan_safety_override
                echo "  [FAIL] dcentrald supervisor exited before child admission"
                exit 1
            fi
            CHILD_RECORD=$(cat "$CHILD_PIDFILE" 2>/dev/null || true)
            CHILD_PID=$(printf '%s\n' "$CHILD_RECORD" | awk 'NF == 4 {print $1}')
            CHILD_START=$(printf '%s\n' "$CHILD_RECORD" | awk 'NF == 4 {print $2}')
            CHILD_TOKEN=$(printf '%s\n' "$CHILD_RECORD" | awk 'NF == 4 {print $3}')
            case "$CHILD_PID:$CHILD_START" in
                *[!0-9:]*|:*|*:)
                    sleep 1
                    continue
                    ;;
            esac
            if [ "$CHILD_TOKEN" = "$SESSION_TOKEN" ] \
                && [ "$(sed 's/^[^)]*) //' "/proc/$CHILD_PID/stat" 2>/dev/null \
                    | awk '{print $20}')" = "$CHILD_START" ] \
                && [ "$(readlink "/proc/$CHILD_PID/exe" 2>/dev/null)" = "$DAEMON" ]; then
                ADMITTED_CHILD=$CHILD_PID
                break
            fi
            ADMISSION_WAIT=$((ADMISSION_WAIT + 1))
            if [ "$ADMISSION_WAIT" -eq 30 ]; then
                echo "  [WAIT] dcentrald child admission remains unproven; retaining the kernel deploy lock"
            fi
            sleep 1
        done
        while ! "$DEPLOY_LOCK_HELPER" --ready; do
            echo "  [WAIT] child admission is proven, but kernel deploy-lock handoff failed"
            sleep 1
        done
        case "${DCENTOS_DEPLOY_LOCK_READY_FD:-}" in
            ''|*[!0-9]*) ;;
            *) eval "exec ${DCENTOS_DEPLOY_LOCK_READY_FD}>&-" ;;
        esac
        case "${DCENTOS_DEPLOY_LOCK_FD:-}" in
            ''|*[!0-9]*) ;;
            *) eval "exec ${DCENTOS_DEPLOY_LOCK_FD}>&-" ;;
        esac
        unset DCENTOS_DEPLOY_LOCK_READY_FD DCENTOS_DEPLOY_LOCK_FD
        echo "  [OK] dcentrald started (REST/WebSocket :$API_PORT, CGMiner :4028)"
        echo "  Log: $LOGFILE"
        ;;

    stop)
        echo "Stopping dcentrald mining daemon..."
        # Send SIGTERM for graceful shutdown. dcentrald keeps PIC heartbeats alive
        # while disabling voltage, then cools fans. Full sequence takes ~12s:
        #   0.5s nonce drain + voltage disable + 2s heartbeat stop + 2s cap discharge
        #   + 5s fan cooldown = ~12s total
        # NEVER SIGKILL before voltage is off - kills heartbeat thread, PIC watchdog
        # fires, corrupts I2C bus, requires PSU power cycle.
        if [ "$PROCESS_IDENTITY_AVAILABLE" -ne 1 ]; then
            echo "  [FAIL] Exact-process identity helper is unavailable; refusing stop mutations"
            exit 1
        fi
        dcent_stop_managed_session "$CHILD_PIDFILE" "$EXPECTFILE" "$DAEMON" \
            "$PIDFILE" "$CRASH_LATCH_FILE" "$SCRIPT" "$LOGFILE" \
            30 5 30 || exit $?
        echo "  [OK] dcentrald stopped after exact owner-death and terminal-custody evidence"
        ;;

    restart)
        "$0" stop || exit $?
        sleep 2
        exec "$0" start
        ;;

    safety)
        dcent_acquire_fan_transition_lock || exit 1
        if [ -e "$FANHOLD_PIDFILE" ] || [ -e "$FANHOLD_READYFILE" ] \
            || [ -e "${FANHOLD_READYFILE}.pending" ]; then
            am2_stop_fan_custodian || exit 1
        fi
        if dcent_dcentrald_may_execute; then
            echo "$(date): SAFETY-FAIL: refusing a competing safety writer while dcentrald may execute" >> "$LOGFILE"
            exit 1
        fi
        # Power cut is load-bearing. A fan-only response must never be
        # promoted by the session latch as a successful terminal receipt.
        if [ ! -x "$DAEMON" ]; then
            echo "$(date): SAFETY-FAIL: $DAEMON absent; power cut unavailable" >> "$LOGFILE"
            exit 1
        fi
        "$DAEMON" --safe-off >> "$LOGFILE" 2>&1 || {
            fan_safety_override || true
            exit 1
        }
        fan_safety_override || exit 1
        ;;

    status)
        if start-stop-daemon -K -p "$PIDFILE" -t -q 2>/dev/null; then
            PID=$(cat "$PIDFILE" 2>/dev/null)
            echo "dcentrald is running (PID $PID)"
        else
            echo "dcentrald is not running"
        fi
        ;;

    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

exit 0
