Applying a preset was a conversation the app held with the module: snapshot runtime.ini, ask whether the service was running, read runtime.ini again, stage a candidate, commit it, verify it, then restart. Seven privileged round trips, each paying a libsu round trip and a full common.sh sourcing, and — worse — the rollback decisions lived in Kotlin. The app was deciding what the module's state was, which is exactly what the typed machine payload exists to prevent. zapret2/scripts/zapret-apply-preset.sh is now the single app-facing entry point for that mutation. It sources the lifecycle helpers once, inherits the Android lease through ZAPRET2_LIFECYCLE_TOKEN, and under that one lock it validates the request with the packaged name policy, qualifies the preset through the same compile-and-dry-run the preview machinery already owned, publishes the selection through runtime-config.sh --commit-candidate, and hands the replacement to zapret-start.sh --replace exactly as zapret-restart.sh does. Nothing here reimplements a transaction that already had an owner. Because the qualification happens before the commit, an incompatible preset is now refused with the live selection untouched instead of being written, failed and rolled back. A failed replacement restores the previous generation and says so, and a rollback that cannot restore it says that instead — the payload distinguishes the two, carrying the exact Z2_ERROR identity of whichever nested stage refused. A service the user had stopped is still only saved to, measured from the committed lifecycle receipt the way zapret-status.sh derives Z2_PROCESS. The app projects that payload and nothing more. It keeps the stepwise flow for one reason: a module generation installed before this entry point existed cannot grow it, so the same round trip answers with an unsupported sentinel and the old path runs. A payload that is truncated, inconsistent or written by a newer module is never guessed at — the repository reads the published selection, and accepts an application only when the live owner generation is the one this lease stamped, which is the same proof the restart path already required. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
505 lines
21 KiB
Shell
Executable file
505 lines
21 KiB
Shell
Executable file
#!/system/bin/sh
|
|
# One module-owned transaction that applies a preset selection end to end.
|
|
#
|
|
# Selecting a preset used to be orchestrated from the Android app as four to six
|
|
# privileged round trips: snapshot runtime.ini, observe the service, read/stage/
|
|
# commit runtime.ini, then restart, with the rollback decisions living in Kotlin.
|
|
# Every one of those trips paid a libsu round trip plus a full common.sh
|
|
# sourcing, which is the most expensive thing a script can do on Android.
|
|
#
|
|
# The module owns that mutation here. One entry point validates the request,
|
|
# persists the selection, runs the existing replacement transaction, and reports
|
|
# a single typed outcome. The app projects that payload; it never rebuilds the
|
|
# steps, and it never decides on its own what the module's state is.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ZAPRET_DIR="$(dirname "$SCRIPT_DIR")"
|
|
MODDIR="$(dirname "$ZAPRET_DIR")"
|
|
. "$SCRIPT_DIR/common.sh"
|
|
. "$SCRIPT_DIR/command-builder.sh"
|
|
|
|
set -f
|
|
|
|
APPLY_SCHEMA_VERSION=1
|
|
APPLY_OUTCOME=IO_FAILED
|
|
APPLY_ISSUE=NONE
|
|
APPLY_REQUESTED_PRESET=""
|
|
APPLY_PREVIOUS_PRESET=""
|
|
APPLY_CONFIG_COMMITTED=0
|
|
APPLY_SERVICE_WAS_RUNNING=0
|
|
APPLY_LOCK_TAKEN=0
|
|
APPLY_NONCE=""
|
|
RUNTIME_CANONICAL_TEXT=""
|
|
RUNTIME_NEXT_TEXT=""
|
|
RUNTIME_ACTIVE_PRESET_LINES=0
|
|
RUNTIME_PREVIOUS_DIGEST=""
|
|
RUNTIME_NEXT_DIGEST=""
|
|
RUNTIME_CANONICAL_DIGEST=""
|
|
APPLY_PUBLISHED_GENERATION=unknown
|
|
CHILD_ERROR_STATUS=""
|
|
CHILD_ERROR_DOMAIN=""
|
|
CHILD_ERROR_CODE=""
|
|
CHILD_ERROR_STAGE=""
|
|
CHILD_ERROR_DETAIL=""
|
|
|
|
log_msg() {
|
|
append_lifecycle_log "$(date '+%Y-%m-%d %H:%M:%S') [APPLY] $1"
|
|
}
|
|
|
|
# The payload is the whole report: an outcome the app maps onto one typed
|
|
# mutation result, plus the shared Z2_ERROR envelope carrying the exact failure
|
|
# identity of whichever stage refused. The terminal sentinel lets a strict
|
|
# caller reject truncated shell output instead of guessing.
|
|
emit_apply_machine() {
|
|
printf 'Z2_APPLY_SCHEMA=%s\n' "$APPLY_SCHEMA_VERSION"
|
|
printf 'Z2_APPLY_OUTCOME=%s\n' "$APPLY_OUTCOME"
|
|
printf 'Z2_APPLY_ISSUE=%s\n' "$APPLY_ISSUE"
|
|
printf 'Z2_APPLY_PRESET=%s\n' "$APPLY_REQUESTED_PRESET"
|
|
printf 'Z2_APPLY_PREVIOUS_PRESET=%s\n' "$APPLY_PREVIOUS_PRESET"
|
|
printf 'Z2_APPLY_CONFIG_COMMITTED=%s\n' "$APPLY_CONFIG_COMMITTED"
|
|
printf 'Z2_APPLY_SERVICE_WAS_RUNNING=%s\n' "$APPLY_SERVICE_WAS_RUNNING"
|
|
z2_error_emit_machine || return 1
|
|
printf 'Z2_APPLY_COMPLETE=1\n'
|
|
}
|
|
|
|
apply_release_lock() {
|
|
[ "$APPLY_LOCK_TAKEN" = 1 ] || return 0
|
|
release_lifecycle_lock || return 1
|
|
APPLY_LOCK_TAKEN=0
|
|
return 0
|
|
}
|
|
|
|
apply_report_failure() {
|
|
local domain="$1" code="$2" stage="$3" detail="$4" outcome="$5" issue="${6:-NONE}"
|
|
APPLY_OUTCOME="$outcome"
|
|
APPLY_ISSUE="$issue"
|
|
if ! apply_release_lock; then
|
|
detail="$detail; lifecycle ownership release failed"
|
|
fi
|
|
z2_error_set "$domain" "$code" "$stage" "$detail" ||
|
|
z2_error_set LIFECYCLE LIFECYCLE_FAILED APPLY "$detail" ||
|
|
z2_error_set LIFECYCLE LIFECYCLE_FAILED APPLY "preset application failed"
|
|
log_msg "ERROR: $detail"
|
|
emit_apply_machine
|
|
exit 1
|
|
}
|
|
|
|
# A committed transaction whose lock release fails is deliberately reported with
|
|
# an ERROR envelope on a committed outcome: the selection is live, but the
|
|
# lifecycle is now fenced, so no caller may read this as an ordinary success.
|
|
# The app never reaches it, because an inherited Android lease is released by
|
|
# the app that owns it and this release is then a no-op.
|
|
apply_report_success() {
|
|
APPLY_OUTCOME="$1"
|
|
APPLY_ISSUE=NONE
|
|
if ! apply_release_lock; then
|
|
z2_error_set LIFECYCLE LIFECYCLE_FAILED APPLY_LOCK_RELEASE \
|
|
"preset application committed but lifecycle ownership release failed" ||
|
|
z2_error_set LIFECYCLE LIFECYCLE_FAILED APPLY_LOCK_RELEASE \
|
|
"lifecycle ownership release failed"
|
|
emit_apply_machine
|
|
exit 1
|
|
fi
|
|
z2_error_clear
|
|
emit_apply_machine
|
|
exit 0
|
|
}
|
|
|
|
apply_interrupted() {
|
|
local signal="$1"
|
|
trap '' HUP INT TERM
|
|
if [ "$APPLY_CONFIG_COMMITTED" = 1 ]; then
|
|
if rollback_runtime_config; then
|
|
apply_report_failure LIFECYCLE LIFECYCLE_FAILED APPLY_SIGNAL \
|
|
"preset application interrupted by $signal; the previous selection was restored" \
|
|
RESTART_FAILED_ROLLED_BACK
|
|
fi
|
|
apply_report_failure LIFECYCLE LIFECYCLE_FAILED APPLY_SIGNAL \
|
|
"preset application interrupted by $signal and the previous selection could not be restored" \
|
|
ROLLBACK_FAILED
|
|
fi
|
|
apply_report_failure LIFECYCLE LIFECYCLE_FAILED APPLY_SIGNAL \
|
|
"preset application interrupted by $signal before any change" IO_FAILED
|
|
}
|
|
|
|
# Adopt the exact typed identity a nested module transaction published instead
|
|
# of flattening it into free text. The outcome says what this transaction did
|
|
# about the failure; the envelope says what actually refused.
|
|
parse_typed_error_envelope() {
|
|
local text="$1" line old_ifs
|
|
CHILD_ERROR_STATUS=""; CHILD_ERROR_DOMAIN=""; CHILD_ERROR_CODE=""
|
|
CHILD_ERROR_STAGE=""; CHILD_ERROR_DETAIL=""
|
|
old_ifs="$IFS"
|
|
IFS='
|
|
'
|
|
set -- $text
|
|
IFS="$old_ifs"
|
|
for line in "$@"; do
|
|
case "$line" in
|
|
Z2_ERROR_STATUS=*) CHILD_ERROR_STATUS="${line#Z2_ERROR_STATUS=}" ;;
|
|
Z2_ERROR_DOMAIN=*) CHILD_ERROR_DOMAIN="${line#Z2_ERROR_DOMAIN=}" ;;
|
|
Z2_ERROR_CODE=*) CHILD_ERROR_CODE="${line#Z2_ERROR_CODE=}" ;;
|
|
Z2_ERROR_STAGE=*) CHILD_ERROR_STAGE="${line#Z2_ERROR_STAGE=}" ;;
|
|
Z2_ERROR_DETAIL=*) CHILD_ERROR_DETAIL="${line#Z2_ERROR_DETAIL=}" ;;
|
|
esac
|
|
done
|
|
[ "$CHILD_ERROR_STATUS" = ERROR ] || return 1
|
|
z2_error_fields_are_valid ERROR "$CHILD_ERROR_DOMAIN" "$CHILD_ERROR_STAGE" \
|
|
"$CHILD_ERROR_CODE" "$CHILD_ERROR_DETAIL"
|
|
}
|
|
|
|
# Mirrors runtime-config.sh's canonical runtime identity: CR stripped from each
|
|
# line, trailing empty lines dropped, exactly one LF per surviving line. The
|
|
# commit boundary computes the same identity from the published file, so any
|
|
# divergence fails the compare-and-swap closed instead of publishing a wrong
|
|
# generation. tests/shell/preset-apply-transaction.sh pins the two together.
|
|
runtime_canonical_digest() {
|
|
local digest
|
|
command -v sha256sum >/dev/null 2>&1 || return 1
|
|
digest="$(printf '%s' "$1" | sha256sum 2>/dev/null)" || return 1
|
|
digest="${digest%% *}"
|
|
is_lower_sha256 "$digest" || return 1
|
|
RUNTIME_CANONICAL_DIGEST="$digest"
|
|
return 0
|
|
}
|
|
|
|
# One bounded pass produces both the canonical current content and the exact
|
|
# candidate content, so the previous generation is buffered for rollback before
|
|
# anything is staged and neither form needs a second read of the file.
|
|
read_runtime_generations() {
|
|
local path="$1" requested="$2" line stripped trimmed key section="" pending="" cr
|
|
RUNTIME_CANONICAL_TEXT=""
|
|
RUNTIME_NEXT_TEXT=""
|
|
RUNTIME_ACTIVE_PRESET_LINES=0
|
|
[ -f "$path" ] && [ ! -L "$path" ] && [ -r "$path" ] || return 1
|
|
path_uid_is_root "$path" && path_nlink_is_one "$path" || return 1
|
|
cr="$(printf '\r')"
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
stripped="${line%"$cr"}"
|
|
if [ -z "$stripped" ]; then
|
|
pending="$pending
|
|
"
|
|
continue
|
|
fi
|
|
RUNTIME_CANONICAL_TEXT="$RUNTIME_CANONICAL_TEXT$pending$stripped
|
|
"
|
|
trim_config_value_in_place "$stripped"
|
|
trimmed="$CONFIG_VALUE_TRIMMED"
|
|
case "$trimmed" in
|
|
"["*"]")
|
|
section="${trimmed#[}"
|
|
section="${section%]}"
|
|
;;
|
|
*=*)
|
|
if [ "$section" = core ]; then
|
|
trim_config_value_in_place "${trimmed%%=*}"
|
|
key="$CONFIG_VALUE_TRIMMED"
|
|
if [ "$key" = active_preset ]; then
|
|
RUNTIME_ACTIVE_PRESET_LINES=$((RUNTIME_ACTIVE_PRESET_LINES + 1))
|
|
RUNTIME_NEXT_TEXT="$RUNTIME_NEXT_TEXT${pending}active_preset=$requested
|
|
"
|
|
pending=""
|
|
continue
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
RUNTIME_NEXT_TEXT="$RUNTIME_NEXT_TEXT$pending$stripped
|
|
"
|
|
pending=""
|
|
done < "$path"
|
|
[ -n "$RUNTIME_CANONICAL_TEXT" ]
|
|
}
|
|
|
|
stage_runtime_candidate() {
|
|
local content="$1" path="$2"
|
|
case "$path" in "$RUNTIME_CONFIG.candidate.$$."*) ;; *) return 1 ;; esac
|
|
[ ! -e "$path" ] && [ ! -L "$path" ] || return 1
|
|
umask 077
|
|
printf '%s' "$content" > "$path" || { rm -f "$path" 2>/dev/null; return 1; }
|
|
chmod 0644 "$path" 2>/dev/null || { rm -f "$path" 2>/dev/null; return 1; }
|
|
return 0
|
|
}
|
|
|
|
# runtime-config.sh owns runtime.ini publication for every caller, including
|
|
# this one: the candidate identity policy, the stale-candidate sweep, the
|
|
# compare-and-swap against the expected content, the full core revalidation and
|
|
# the durability barrier live there and are not duplicated here.
|
|
commit_runtime_candidate() {
|
|
local content="$1" expected="$2" step="$3" candidate output rc=0
|
|
candidate="$RUNTIME_CONFIG.candidate.$$.$APPLY_NONCE$step"
|
|
stage_runtime_candidate "$content" "$candidate" || return 1
|
|
output="$(ZAPRET2_EMIT_STATUS_V6=0 sh "$SCRIPT_DIR/runtime-config.sh" \
|
|
--commit-candidate "$candidate" "$expected" "$RUNTIME_CONFIG" 2>&1)" || rc=$?
|
|
rm -f "$candidate" 2>/dev/null
|
|
[ "$rc" -eq 0 ] || {
|
|
parse_typed_error_envelope "$output" || {
|
|
CHILD_ERROR_DOMAIN=CONFIG
|
|
CHILD_ERROR_CODE=RUNTIME_COMMIT_FAILED
|
|
CHILD_ERROR_STAGE=RUNTIME_COMMIT
|
|
CHILD_ERROR_DETAIL="runtime.ini commit failed without a typed envelope"
|
|
}
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
# A failed commit is not automatically a clean refusal: the publication rename
|
|
# can succeed and a later step of the same commit still fail. Re-derive the
|
|
# published identity and answer from evidence instead of assuming either way.
|
|
# The buffered previous generation is the only copy rollback has, so it is
|
|
# restored around the probe rather than overwritten by it.
|
|
classify_published_runtime() {
|
|
local saved_canonical="$RUNTIME_CANONICAL_TEXT" saved_next="$RUNTIME_NEXT_TEXT"
|
|
local saved_lines="$RUNTIME_ACTIVE_PRESET_LINES"
|
|
APPLY_PUBLISHED_GENERATION=unknown
|
|
if read_runtime_generations "$RUNTIME_CONFIG" "$APPLY_REQUESTED_PRESET" &&
|
|
runtime_canonical_digest "$RUNTIME_CANONICAL_TEXT"; then
|
|
if [ "$RUNTIME_CANONICAL_DIGEST" = "$RUNTIME_PREVIOUS_DIGEST" ]; then
|
|
APPLY_PUBLISHED_GENERATION=previous
|
|
elif [ -n "$RUNTIME_NEXT_DIGEST" ] &&
|
|
[ "$RUNTIME_CANONICAL_DIGEST" = "$RUNTIME_NEXT_DIGEST" ]; then
|
|
APPLY_PUBLISHED_GENERATION=requested
|
|
fi
|
|
fi
|
|
RUNTIME_CANONICAL_TEXT="$saved_canonical"
|
|
RUNTIME_NEXT_TEXT="$saved_next"
|
|
RUNTIME_ACTIVE_PRESET_LINES="$saved_lines"
|
|
return 0
|
|
}
|
|
|
|
rollback_runtime_config() {
|
|
[ "$APPLY_CONFIG_COMMITTED" = 1 ] || return 0
|
|
[ -n "$RUNTIME_NEXT_DIGEST" ] && [ -n "$RUNTIME_CANONICAL_TEXT" ] || return 1
|
|
commit_runtime_candidate "$RUNTIME_CANONICAL_TEXT" "$RUNTIME_NEXT_DIGEST" 2 || return 1
|
|
APPLY_CONFIG_COMMITTED=0
|
|
return 0
|
|
}
|
|
|
|
# The same compatibility qualification the app used to request through
|
|
# command-builder.sh --preflight-preset-machine, run in this process against a
|
|
# disposable artifact. It happens before runtime.ini is touched, so an
|
|
# incompatible preset is refused with the live selection untouched instead of
|
|
# being written, failed and rolled back.
|
|
validate_requested_preset() {
|
|
local artifact rc=0
|
|
PRESET_VALIDATION_CODE=OK
|
|
ensure_state_tmp_dir || {
|
|
PRESET_VALIDATION_CODE=PRESET_UNREADABLE
|
|
return 1
|
|
}
|
|
artifact="$Z2_STATE_TMP/preset-apply.$$"
|
|
state_file_target_is_safe "$artifact" || {
|
|
PRESET_VALIDATION_CODE=PRESET_UNREADABLE
|
|
return 1
|
|
}
|
|
rm -f "$artifact" 2>/dev/null
|
|
if compile_preset_artifact "$PRESETS_DIR/$APPLY_REQUESTED_PRESET" \
|
|
"$APPLY_REQUESTED_PRESET" "$artifact" &&
|
|
run_compiled_artifact "$artifact" dry-run >/dev/null 2>&1; then
|
|
rm -f "$artifact" 2>/dev/null
|
|
return 0
|
|
fi
|
|
rc=1
|
|
[ "$PRESET_VALIDATION_CODE" != OK ] || PRESET_VALIDATION_CODE=NFQWS_DRY_RUN_FAILED
|
|
rm -f "$artifact" 2>/dev/null
|
|
return "$rc"
|
|
}
|
|
|
|
# The app's "was the service running" question, answered from the committed
|
|
# lifecycle receipt exactly as zapret-status.sh derives Z2_PROCESS: the last
|
|
# transaction published a healthy generation and its exact process identity is
|
|
# still live. Read-only and constant in the size of the package and the process
|
|
# table; this transaction never re-audits the firewall to answer it.
|
|
service_process_is_running() {
|
|
state_dir_is_secure || return 1
|
|
OBSERVER_STATE_DIR_VERIFIED=1
|
|
read_iptables_status >/dev/null 2>&1 || return 1
|
|
[ "$STATUS_FILE_STATUS" = ok ] || return 1
|
|
[ "$STATUS_FILE_OWNER_METADATA_VERIFIED" = 1 ] || return 1
|
|
[ "$STATUS_FILE_RULESET_VERIFIED" = 1 ] || return 1
|
|
[ "$STATUS_FILE_IPV4_ACTIVE" = 1 ] || return 1
|
|
[ "$STATUS_FILE_RULES_TOTAL" = "$((STATUS_FILE_IPV4_RULES + STATUS_FILE_IPV6_RULES))" ] || return 1
|
|
[ "$STATUS_FILE_RULES_TOTAL" = "$STATUS_FILE_RULES_EXPECTED" ] || return 1
|
|
verify_status_snapshot_pid
|
|
}
|
|
|
|
# zapret-start.sh --replace is the replacement transaction, exactly as
|
|
# zapret-restart.sh invokes it. It inherits this transaction's lifecycle
|
|
# ownership through ZAPRET2_LIFECYCLE_TOKEN and may never release it, so its
|
|
# failure paths keep converging on the clean stopped state while this
|
|
# transaction stays responsible for the configuration it published.
|
|
run_replace_transaction() {
|
|
local output rc=0
|
|
trap '' HUP INT TERM
|
|
output="$(ZAPRET2_EMIT_STATUS_V6=0 sh "$SCRIPT_DIR/zapret-start.sh" --replace 2>&1)" || rc=$?
|
|
trap 'apply_interrupted HUP' HUP
|
|
trap 'apply_interrupted INT' INT
|
|
trap 'apply_interrupted TERM' TERM
|
|
[ "$rc" -eq 0 ] && return 0
|
|
parse_typed_error_envelope "$output" || {
|
|
CHILD_ERROR_DOMAIN=LIFECYCLE
|
|
CHILD_ERROR_CODE=LIFECYCLE_FAILED
|
|
CHILD_ERROR_STAGE=APPLY_REPLACE
|
|
CHILD_ERROR_DETAIL="the replacement transaction failed without a typed envelope"
|
|
}
|
|
return 1
|
|
}
|
|
|
|
# The identity reported is the one that refused, captured before the rollback
|
|
# runs its own commit and overwrites the shared envelope scratch fields.
|
|
report_rollback_after_commit() {
|
|
local detail="$1" outcome="$2"
|
|
local domain="$CHILD_ERROR_DOMAIN" code="$CHILD_ERROR_CODE" stage="$CHILD_ERROR_STAGE"
|
|
if rollback_runtime_config; then
|
|
apply_report_failure "$domain" "$code" "$stage" \
|
|
"$detail; the previous selection was restored" "$outcome"
|
|
fi
|
|
apply_report_failure "$domain" "$code" "$stage" \
|
|
"$detail; the previous selection could not be restored: ${CHILD_ERROR_DETAIL:-unknown}" \
|
|
ROLLBACK_FAILED
|
|
}
|
|
|
|
main() {
|
|
local requested="${1:-}"
|
|
|
|
[ "$#" -eq 1 ] || {
|
|
z2_error_set CONFIG INVALID_ARGUMENTS APPLY_REQUEST \
|
|
"usage: zapret-apply-preset.sh PRESET_FILE_NAME"
|
|
APPLY_OUTCOME=REJECTED
|
|
APPLY_ISSUE=UNSAFE_PRESET_NAME
|
|
emit_apply_machine
|
|
exit 2
|
|
}
|
|
|
|
# The requested name is never echoed back before it passes the packaged
|
|
# name policy: an unsafe request must not put attacker-chosen bytes into a
|
|
# machine payload the app parses line by line.
|
|
is_safe_preset_file_name "$requested" || {
|
|
z2_error_set CONFIG UNSAFE_PRESET_NAME APPLY_REQUEST \
|
|
"the requested preset name is not a safe packaged preset file name"
|
|
APPLY_OUTCOME=REJECTED
|
|
APPLY_ISSUE=UNSAFE_PRESET_NAME
|
|
emit_apply_machine
|
|
exit 1
|
|
}
|
|
APPLY_REQUESTED_PRESET="$requested"
|
|
|
|
ensure_state_dir ||
|
|
apply_report_failure STATE STATE_UNAVAILABLE APPLY_STATE \
|
|
"insecure or unavailable zapret2 state directory: $STATE_DIR" IO_FAILED
|
|
APPLY_NONCE="$(proc_starttime "$$")" ||
|
|
apply_report_failure STATE STATE_UNAVAILABLE APPLY_STATE \
|
|
"the transaction identity could not be established" IO_FAILED
|
|
|
|
acquire_lifecycle_lock ||
|
|
apply_report_failure LIFECYCLE LIFECYCLE_BUSY APPLY_LOCK \
|
|
"zapret2 lifecycle is busy" BLOCKED
|
|
APPLY_LOCK_TAKEN=1
|
|
|
|
if ! audit_recovery_artifacts lifecycle; then
|
|
apply_report_failure LIFECYCLE RECOVERY_BLOCKED APPLY_RECOVERY \
|
|
"preset application blocked by recovery state: ${RECOVERY_ARTIFACT_DIAGNOSTIC:-unsafe recovery artifact}$(recovery_block_remedy)" \
|
|
BLOCKED
|
|
fi
|
|
if ! uninstall_tombstone_allows_start; then
|
|
apply_report_failure LIFECYCLE UNINSTALL_BLOCKED APPLY_UNINSTALL \
|
|
"preset application blocked by uninstall serialization: $UNINSTALL_TOMBSTONE_ERROR" \
|
|
BLOCKED
|
|
fi
|
|
if [ -e "$MODDIR/disable" ] || [ -L "$MODDIR/disable" ]; then
|
|
apply_report_failure LIFECYCLE MODULE_DISABLED APPLY_PREFLIGHT \
|
|
"preset application blocked because the module is disabled" BLOCKED
|
|
fi
|
|
if module_removal_pending; then
|
|
apply_report_failure LIFECYCLE MODULE_REMOVAL_PENDING APPLY_PREFLIGHT \
|
|
"preset application blocked because the root manager scheduled the module for removal" \
|
|
BLOCKED
|
|
fi
|
|
|
|
trap 'apply_interrupted HUP' HUP
|
|
trap 'apply_interrupted INT' INT
|
|
trap 'apply_interrupted TERM' TERM
|
|
prepare_lifecycle_log || LOG_READY=0
|
|
|
|
if ! load_effective_core_config_readonly >/dev/null 2>&1; then
|
|
runtime_config_error_code "$RUNTIME_CONFIG_ERROR"
|
|
apply_report_failure CONFIG "${RUNTIME_CONFIG_ERROR_CODE:-CONFIG_INVALID}" \
|
|
APPLY_CONFIG_READ \
|
|
"${RUNTIME_CONFIG_ERROR:-runtime.ini validation failed}" IO_FAILED
|
|
fi
|
|
APPLY_PREVIOUS_PRESET="$ACTIVE_PRESET"
|
|
|
|
read_runtime_generations "$RUNTIME_CONFIG" "$requested" ||
|
|
apply_report_failure CONFIG UNSAFE_RUNTIME_FILE APPLY_CONFIG_READ \
|
|
"runtime.ini could not be read as a safe root-owned regular file" IO_FAILED
|
|
[ "$RUNTIME_ACTIVE_PRESET_LINES" -eq 1 ] ||
|
|
apply_report_failure CONFIG CONFIG_INVALID APPLY_CONFIG_READ \
|
|
"runtime.ini [core] does not declare exactly one active_preset" IO_FAILED
|
|
runtime_canonical_digest "$RUNTIME_CANONICAL_TEXT" ||
|
|
apply_report_failure CONFIG RUNTIME_READ_FAILED APPLY_CONFIG_READ \
|
|
"runtime.ini content identity could not be computed" IO_FAILED
|
|
RUNTIME_PREVIOUS_DIGEST="$RUNTIME_CANONICAL_DIGEST"
|
|
runtime_canonical_digest "$RUNTIME_NEXT_TEXT" ||
|
|
apply_report_failure CONFIG RUNTIME_READ_FAILED APPLY_CONFIG_READ \
|
|
"the requested runtime.ini generation identity could not be computed" IO_FAILED
|
|
RUNTIME_NEXT_DIGEST="$RUNTIME_CANONICAL_DIGEST"
|
|
|
|
validate_requested_preset ||
|
|
apply_report_failure CONFIG "$PRESET_VALIDATION_CODE" APPLY_VALIDATE \
|
|
"the requested preset was refused by preset qualification: $PRESET_VALIDATION_CODE" \
|
|
REJECTED "$PRESET_VALIDATION_CODE"
|
|
|
|
# Measured before the write, exactly like the flow this replaces: a service
|
|
# the user had stopped must not be started by a configuration change.
|
|
if service_process_is_running; then APPLY_SERVICE_WAS_RUNNING=1; fi
|
|
|
|
log_msg "Applying preset $requested (previous: ${APPLY_PREVIOUS_PRESET:-unknown}, running: $APPLY_SERVICE_WAS_RUNNING)"
|
|
|
|
if commit_runtime_candidate "$RUNTIME_NEXT_TEXT" "$RUNTIME_PREVIOUS_DIGEST" 1; then
|
|
APPLY_CONFIG_COMMITTED=1
|
|
else
|
|
classify_published_runtime
|
|
case "$APPLY_PUBLISHED_GENERATION" in
|
|
previous)
|
|
apply_report_failure "$CHILD_ERROR_DOMAIN" "$CHILD_ERROR_CODE" \
|
|
"$CHILD_ERROR_STAGE" "$CHILD_ERROR_DETAIL" WRITE_FAILED
|
|
;;
|
|
requested)
|
|
APPLY_CONFIG_COMMITTED=1
|
|
report_rollback_after_commit \
|
|
"the selection was published but its commit could not be completed: $CHILD_ERROR_DETAIL" \
|
|
WRITE_FAILED_ROLLED_BACK
|
|
;;
|
|
*)
|
|
# Neither the previous nor the requested generation is on disk.
|
|
# Claiming the selection was left alone would be an assertion
|
|
# nothing here measured, so the report keeps the conservative
|
|
# side: something is published and this transaction cannot say
|
|
# what it is.
|
|
APPLY_CONFIG_COMMITTED=1
|
|
apply_report_failure "$CHILD_ERROR_DOMAIN" "$CHILD_ERROR_CODE" \
|
|
"$CHILD_ERROR_STAGE" \
|
|
"$CHILD_ERROR_DETAIL; the published runtime generation is unrecognized" \
|
|
ROLLBACK_FAILED
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if [ "$APPLY_SERVICE_WAS_RUNNING" = 0 ]; then
|
|
trap - HUP INT TERM
|
|
log_msg "Preset $requested saved; the service was not running so nothing was replaced"
|
|
apply_report_success SAVED
|
|
fi
|
|
|
|
if ! run_replace_transaction; then
|
|
report_rollback_after_commit "the replacement transaction failed: $CHILD_ERROR_DETAIL" \
|
|
RESTART_FAILED_ROLLED_BACK
|
|
fi
|
|
|
|
trap - HUP INT TERM
|
|
log_msg "Preset $requested applied and the replacement transaction committed"
|
|
apply_report_success APPLIED
|
|
}
|
|
|
|
main "$@"
|