From 728301c9a47b192361ef986a4d598b8853eb56be Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 17 Aug 2026 14:59:41 +0200 Subject: [PATCH] monitor: split 449 -> 215 + 138 + 136, partitioned by probe kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/monitor.sh was 199 lines over the cap. Its eleven _check_* probes move into two siblings: monitor_checks_health.sh (format indicators, battery, storage, the three enforcer daemons) and monitor_checks_policy.sh (hosts blocklist, DNS chain, launcher, companion app, boot persistence). A single monitor_checks.sh was tried first and left at 265 lines — still over, the net-zero outcome. Two libraries split the cluster at its own seam and land both comfortably under. The _MONITOR_* constants are partitioned by reader so no file assigns a global it never reads. The five read only by the policy probes move into that file; the ones monitor.sh reads itself or shares between both children stay put. shellcheck is clean on each file standing alone. Not a deployment concern: lib/monitor.sh is PC-side, consumed by run_phone.sh, and deploy.sh never references lib/ — so neither copy list changes. Verified three ways: test_monitor.sh reports 9 passed / 0 failed, the same count as before; verify_shell_split.sh confirms all 25 top-level functions moved verbatim; and run_phone.sh --help exits 0, proving the source chain resolves in the real consumer. Over-cap count 24 -> 23. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- .../contracts/monitor-split-2026-08-17.json | 17 ++ .../evidence/monitor-split-2026-08-17.json | 51 ++++ phone_focus_mode/lib/monitor.sh | 256 +----------------- phone_focus_mode/lib/monitor_checks_health.sh | 138 ++++++++++ phone_focus_mode/lib/monitor_checks_policy.sh | 136 ++++++++++ 5 files changed, 353 insertions(+), 245 deletions(-) create mode 100644 docs/superpowers/contracts/monitor-split-2026-08-17.json create mode 100644 docs/superpowers/evidence/monitor-split-2026-08-17.json create mode 100755 phone_focus_mode/lib/monitor_checks_health.sh create mode 100755 phone_focus_mode/lib/monitor_checks_policy.sh diff --git a/docs/superpowers/contracts/monitor-split-2026-08-17.json b/docs/superpowers/contracts/monitor-split-2026-08-17.json new file mode 100644 index 00000000..e4d5503e --- /dev/null +++ b/docs/superpowers/contracts/monitor-split-2026-08-17.json @@ -0,0 +1,17 @@ +{ + "title": "split lib/monitor.sh under the 250-line cap", + "objective": "Move monitor.sh's eleven _check_* probes into two sibling libraries so every resulting file is under 250 lines, with monitoring behaviour byte-identical.", + "acceptance_criteria": [ + "test_monitor.sh reports exactly 9 passed, 0 failed — the same count as before the split", + "verify_shell_split.sh reports all 25 top-level functions moved verbatim", + "run_phone.sh --help exits 0, proving the source chain resolves in the real consumer", + "shellcheck is clean on each of the three files checked standing alone, with no suppressions", + "check_file_length.sh --all drops from 24 over-cap files to 23, with no new file over the cap" + ], + "out_of_scope": [ + "deploy.sh copy lists: lib/monitor.sh is PC-side and deploy.sh never references lib/", + "Any behavioural change to the probes themselves", + "Running the probes against a real device" + ], + "verifier": "bash phone_focus_mode/lib/tests/test_monitor.sh + meta/scripts/verify_shell_split.sh + shellcheck" +} diff --git a/docs/superpowers/evidence/monitor-split-2026-08-17.json b/docs/superpowers/evidence/monitor-split-2026-08-17.json new file mode 100644 index 00000000..956bf81f --- /dev/null +++ b/docs/superpowers/evidence/monitor-split-2026-08-17.json @@ -0,0 +1,51 @@ +{ + "intent": "Bring phone_focus_mode/lib/monitor.sh under the 250-line cap by moving its health probes into two sibling libraries, without changing any monitoring behaviour.", + "scope": [ + "phone_focus_mode/lib/monitor.sh — 449 lines, now 215", + "phone_focus_mode/lib/monitor_checks_health.sh — new, 138 lines", + "phone_focus_mode/lib/monitor_checks_policy.sh — new, 136 lines", + "Non-goal: deploy.sh copy lists. lib/monitor.sh is a PC-side library consumed by run_phone.sh; deploy.sh never references lib/ and does not push it to the phone." + ], + "changes": [ + "Moved the six device-health probes (_check_format_indicators, _check_battery, _check_storage, _check_daemon, _check_hosts_daemon, _check_launcher_daemon) into monitor_checks_health.sh.", + "Moved the five enforcement-integrity probes (_check_hosts_integrity, _check_dns, _check_launcher, _check_companion_app, _check_boot_persistence) into monitor_checks_policy.sh.", + "Partitioned the _MONITOR_* constants by reader so no file assigns a global it never reads (SC2034). The five read only by the policy probes moved into that file; _MONITOR_REMOTE_DIR, _MONITOR_HOSTS_TARGET, _MONITOR_HOSTS_CANDIDATES and the two launcher constants stay in monitor.sh, which reads them or shares them across both children.", + "Placed the two source lines after the constant block they depend on, using the already-computed _MONITOR_LIB_DIR rather than recomputing the directory per source line.", + "A single-library split was tried first and rejected: it left monitor_checks.sh at 265 lines, still over the cap, which is the net-zero outcome the brief warns about." + ], + "verification": [ + { + "command": "bash phone_focus_mode/lib/tests/test_monitor.sh", + "result": "pass", + "evidence": "9 passed, 0 failed — identical to the pre-split baseline captured before any edit. This is the discriminating check: the count matches exactly." + }, + { + "command": "bash meta/scripts/verify_shell_split.sh HEAD phone_focus_mode/lib/monitor.sh phone_focus_mode/lib/monitor.sh phone_focus_mode/lib/monitor_checks_health.sh phone_focus_mode/lib/monitor_checks_policy.sh", + "result": "pass", + "evidence": "'IDENTICAL: all 25 top-level functions moved verbatim'. Note the old path must also be listed among the new paths for a partial split, since 14 functions stayed in monitor.sh; omitting it makes the tool report a false DIFFERENCE with an empty after-set." + }, + { + "command": "bash phone_focus_mode/run_phone.sh --help", + "result": "pass", + "evidence": "Exit 0 with the full usage block, proving the source chain through both new libraries resolves in the real consumer, not just under the test harness." + }, + { + "command": "shellcheck phone_focus_mode/lib/monitor.sh phone_focus_mode/lib/monitor_checks_health.sh phone_focus_mode/lib/monitor_checks_policy.sh", + "result": "pass", + "evidence": "All three clean, each checked standing alone (no -x), so the constant partition holds without a source directive." + }, + { + "command": "bash meta/scripts/check_file_length.sh --all", + "result": "fail-as-expected", + "evidence": "24 files over the cap before, 23 after; monitor.sh at 215 and both new libraries at 138 and 136 are all under. No new file was pushed over." + } + ], + "risks": [ + "monitor.sh now sources two siblings before defining the helpers they call (_mon_check, _trim_output). This is safe because nothing executes until monitor_collect_snapshot runs, but a future top-level call in either library would break at source time.", + "The test covers 9 assertions against helper functions, none of which are the moved _check_* probes; those are device-dependent and unexercised here. The verbatim hash check is what covers them." + ], + "rollback": [ + "git revert the commit; monitor.sh returns to 449 lines and the two libraries disappear. run_phone.sh needs no change because it only ever sourced monitor.sh.", + "After rollback, re-run bash phone_focus_mode/lib/tests/test_monitor.sh and confirm 9 passed, then bash phone_focus_mode/run_phone.sh --help and confirm exit 0." + ] +} diff --git a/phone_focus_mode/lib/monitor.sh b/phone_focus_mode/lib/monitor.sh index d6a9768b..45cc59a1 100755 --- a/phone_focus_mode/lib/monitor.sh +++ b/phone_focus_mode/lib/monitor.sh @@ -8,17 +8,23 @@ set -euo pipefail _MONITOR_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly _MONITOR_LIB_DIR +# Constants read here or by both probe libraries. Those read by exactly one of +# them live in that file instead: a file must not assign a global it never +# reads (SC2034), and shellcheck runs without -x so each file stands alone. readonly _MONITOR_REMOTE_DIR="/data/local/tmp/focus_mode" -readonly _MONITOR_HOSTS_CANONICAL="/data/local/tmp/focus_mode/hosts.canonical" -readonly _MONITOR_HOSTS_SHA_FILE="/data/local/tmp/focus_mode/hosts.sha256" readonly _MONITOR_HOSTS_TARGET="/system/etc/hosts" -readonly _MONITOR_BOOT_SCRIPT="/data/adb/service.d/99-focus-mode.sh" readonly _MONITOR_LAUNCHER_PACKAGE="com.qqlabs.minimalistlauncher" readonly _MONITOR_LAUNCHER_ACTIVITY_FILE="/data/local/tmp/focus_mode/minimalist_launcher.activity" -readonly _MONITOR_COMPANION_PACKAGE="com.kuhy.focusstatus" -readonly _MONITOR_DNS_CHAIN="FOCUS_DNS_BLOCK" readonly _MONITOR_HOSTS_CANDIDATES="/system/etc/hosts /etc/hosts /vendor/etc/hosts /system/system/etc/hosts" +# Sourced after the constants above, which the probes read. Both libraries call +# helpers defined below (_mon_check, _trim_output, _safe_adb_root_output); that +# is fine because nothing here runs until monitor_collect_snapshot is called. +# shellcheck source=monitor_checks_health.sh +. "${_MONITOR_LIB_DIR}/monitor_checks_health.sh" +# shellcheck source=monitor_checks_policy.sh +. "${_MONITOR_LIB_DIR}/monitor_checks_policy.sh" + _mon_escape_json() { local escaped="$1" @@ -145,256 +151,16 @@ monitor_print_format_warning() { _box "PHONE APPEARS TO HAVE BEEN WIPED" "${box_lines[@]}" } -_check_format_indicators() { - local outfile="$1" - local -a missing_indicators=() - local status="ok" - local message="All format indicators are present" - mapfile -t missing_indicators < <(monitor_check_format_indicators) - if (( ${#missing_indicators[@]} >= FORMAT_DETECTION_MIN_MISSING )); then - status="fatal" - message="Missing ${#missing_indicators[@]} format indicators: ${missing_indicators[*]}" - elif (( ${#missing_indicators[@]} > 0 )); then - status="warn" - message="Missing ${#missing_indicators[@]} format indicators: ${missing_indicators[*]}" - fi - _mon_check "format_indicators" "${status}" "FORMAT_INDICATORS" "${message}" "false" >>"${outfile}" -} -_check_battery() { - local outfile="$1" - local level="" - local health="" - local temp="" - local status="ok" - local message="" - level="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/level:/{print \$2; exit}'")")" - health="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/health:/{print \$2; exit}'")")" - temp="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/temperature:/{print \$2; exit}'")")" - if [[ ! "${level}" =~ ^[0-9]+$ ]]; then - status="warn" - message="Battery level unavailable" - elif (( level < BATTERY_WARN_BELOW )); then - status="warn" - message="Battery low: ${level}% (threshold ${BATTERY_WARN_BELOW}%)" - else - message="Battery level ${level}%, health ${health:-unknown}, temp ${temp:-unknown}" - fi - _mon_check "battery" "${status}" "dumpsys battery" "${message}" "false" >>"${outfile}" -} -_check_storage() { - local outfile="$1" - local free_kb="" - local free_mb=0 - local status="ok" - local message="" - free_kb="$(_trim_output "$(_safe_adb_root_output "df /sdcard 2>/dev/null | awk 'NR==2{print \$4; exit}'")")" - if [[ ! "${free_kb}" =~ ^[0-9]+$ ]]; then - free_kb="$(_trim_output "$(_safe_adb_root_output "df /storage/emulated/0 2>/dev/null | awk 'NR==2{print \$4; exit}'")")" - fi - if [[ "${free_kb}" =~ ^[0-9]+$ ]]; then - free_mb=$((free_kb / 1024)) - if (( free_mb < STORAGE_WARN_BELOW_MB )); then - status="warn" - message="Low storage: ${free_mb} MB free (threshold ${STORAGE_WARN_BELOW_MB} MB)" - else - message="Free storage: ${free_mb} MB" - fi - else - status="warn" - message="Free storage unavailable" - fi - _mon_check "storage" "${status}" "df /sdcard" "${message}" "false" >>"${outfile}" -} - -_check_daemon() { - local daemon_name="$1" - local script_name="$2" - local pidfile="$3" - local outfile="$4" - local pid="" - local pgrep_pid="" - - pid="$(_monitor_read_pidfile "${pidfile}")" - if [[ "${pid}" =~ ^[0-9]+$ ]] && _monitor_pid_matches_script "${pid}" "${script_name}"; then - _mon_check "${daemon_name}" "ok" "${pidfile}" "${daemon_name} running (PID ${pid})" "false" >>"${outfile}" - return - fi - - pgrep_pid="$(_trim_output "$(_safe_adb_root_output "pgrep -f '${script_name}' 2>/dev/null | head -1")")" - if [[ "${pgrep_pid}" =~ ^[0-9]+$ ]] && adb_root_shell "kill -0 ${pgrep_pid} >/dev/null 2>&1" >/dev/null 2>&1; then - _mon_check "${daemon_name}" "ok" "pgrep -f ${script_name}" "${daemon_name} running (PID ${pgrep_pid})" "false" >>"${outfile}" - return - fi - - _mon_check "${daemon_name}" "error" "${pidfile}" "${daemon_name} is NOT running" "true" >>"${outfile}" -} - -_check_hosts_daemon() { - local outfile="$1" - local resolved_target="" - - resolved_target="$(_monitor_resolve_hosts_target)" - if [[ -z "${resolved_target}" ]]; then - _mon_check "hosts_enforcer" "warn" "hosts target probe" \ - "No hosts target file exists on this ROM; hosts daemon check skipped" "false" >>"${outfile}" - return - fi - - _check_daemon "hosts_enforcer" "hosts_enforcer.sh" "${_MONITOR_REMOTE_DIR}/hosts_enforcer.pid" "${outfile}" -} - -_check_launcher_daemon() { - local outfile="$1" - local has_snapshot="no" - local launcher_installed="no" - - if adb_root_shell "test -s '${_MONITOR_LAUNCHER_ACTIVITY_FILE}'" >/dev/null 2>&1; then - has_snapshot="yes" - fi - - if adb_root_shell "pm path '${_MONITOR_LAUNCHER_PACKAGE}' >/dev/null 2>&1" >/dev/null 2>&1; then - launcher_installed="yes" - fi - - if [[ "${has_snapshot}" == "no" && "${launcher_installed}" == "no" ]]; then - _mon_check "launcher_enforcer" "warn" "launcher optional probe" \ - "Launcher enforcer check skipped (launcher not configured yet)" "false" >>"${outfile}" - return - fi - - _check_daemon "launcher_enforcer" "launcher_enforcer.sh" "${_MONITOR_REMOTE_DIR}/launcher_enforcer.pid" "${outfile}" -} - -_check_hosts_integrity() { - local outfile="$1" - local hosts_target="" - local expected_hash="" - local actual_hash="" - - hosts_target="$(_monitor_resolve_hosts_target)" - if [[ -z "${hosts_target}" ]]; then - _mon_check "hosts_integrity" "warn" "hosts target probe" \ - "No hosts file target exists on this ROM; hosts integrity check skipped" "false" >>"${outfile}" - return - fi - - if ! adb_root_shell "test -f ${_MONITOR_HOSTS_CANONICAL}" >/dev/null 2>&1; then - _mon_check "hosts_integrity" "fatal" "${_MONITOR_HOSTS_CANONICAL}" \ - "Canonical hosts file missing at ${_MONITOR_HOSTS_CANONICAL}" "true" >>"${outfile}" - return - fi - - expected_hash="$(_trim_output "$(_safe_adb_root_output "cat ${_MONITOR_HOSTS_SHA_FILE} 2>/dev/null")")" - actual_hash="$(_trim_output "$(_safe_adb_root_output "sha256sum ${hosts_target} 2>/dev/null | awk '{print \$1}'")")" - - if [[ -z "${expected_hash}" || -z "${actual_hash}" ]]; then - _mon_check "hosts_integrity" "error" "${hosts_target}" \ - "Could not read hosts integrity hashes" "true" >>"${outfile}" - elif [[ "${expected_hash}" == "${actual_hash}" ]]; then - _mon_check "hosts_integrity" "ok" "${hosts_target}" \ - "Hosts file matches canonical (${actual_hash:0:12}…)" "false" >>"${outfile}" - else - _mon_check "hosts_integrity" "error" "${hosts_target}" \ - "Hosts mismatch: active ${actual_hash:0:12}… != expected ${expected_hash:0:12}…" "true" >>"${outfile}" - fi -} - -_check_dns() { - local outfile="$1" - local private_dns_mode="" - local chain_present="no" - local status="ok" - local message="" - - private_dns_mode="$(_trim_output "$(_safe_adb_root_output "settings get global private_dns_mode 2>/dev/null")")" - if adb_root_shell "iptables -L ${_MONITOR_DNS_CHAIN} >/dev/null 2>&1 && ip6tables -L ${_MONITOR_DNS_CHAIN} >/dev/null 2>&1" >/dev/null 2>&1; then - chain_present="yes" - fi - - if [[ "${private_dns_mode}" == "off" || "${private_dns_mode}" == "null" || -z "${private_dns_mode}" ]]; then - if [[ "${chain_present}" == "yes" ]]; then - message="Private DNS disabled and DNS firewall chains present" - else - status="error" - message="Private DNS disabled, but DNS firewall chains are missing" - fi - else - status="error" - message="Private DNS is enabled (mode=${private_dns_mode})" - fi - - _mon_check "dns_enforcement" "${status}" "settings get global private_dns_mode" "${message}" "true" >>"${outfile}" -} - -_check_launcher() { - local outfile="$1" - local desired_activity="" - local actual_activity="" - local has_snapshot="no" - - if adb_root_shell "test -s '${_MONITOR_LAUNCHER_ACTIVITY_FILE}'" >/dev/null 2>&1; then - has_snapshot="yes" - fi - - if ! adb_root_shell "pm path '${_MONITOR_LAUNCHER_PACKAGE}' >/dev/null 2>&1" >/dev/null 2>&1; then - if [[ "${has_snapshot}" == "yes" ]]; then - _mon_check "launcher_state" "fatal" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ - "Minimalist launcher is not installed but snapshot metadata exists" "true" >>"${outfile}" - else - _mon_check "launcher_state" "warn" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ - "Minimalist launcher is not installed (optional until snapshot is configured)" "false" >>"${outfile}" - fi - return - fi - - desired_activity="$(_trim_output "$(_safe_adb_root_output "cat ${_MONITOR_LAUNCHER_ACTIVITY_FILE} 2>/dev/null")")" - actual_activity="$(_trim_output "$(_safe_adb_root_output "cmd package resolve-activity --brief -c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}'")")" - - if [[ -z "${desired_activity}" ]]; then - _mon_check "launcher_state" "warn" "cat ${_MONITOR_LAUNCHER_ACTIVITY_FILE}" \ - "Launcher snapshot metadata is missing or empty" "true" >>"${outfile}" - elif [[ -n "${actual_activity}" && "${desired_activity}" != "${actual_activity}" ]]; then - _mon_check "launcher_state" "error" "cmd package resolve-activity" \ - "Launcher default mismatch: expected ${desired_activity}, got ${actual_activity}" "true" >>"${outfile}" - else - _mon_check "launcher_state" "ok" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ - "Minimalist launcher installed and HOME activity aligned" "false" >>"${outfile}" - fi -} - -_check_companion_app() { - local outfile="$1" - - if adb_root_shell "pm list packages -e '${_MONITOR_COMPANION_PACKAGE}' 2>/dev/null | grep -q '${_MONITOR_COMPANION_PACKAGE}'" >/dev/null 2>&1; then - _mon_check "companion_app" "ok" "pm list packages -e ${_MONITOR_COMPANION_PACKAGE}" \ - "Focus companion app is installed" "false" >>"${outfile}" - else - _mon_check "companion_app" "warn" "pm list packages -e ${_MONITOR_COMPANION_PACKAGE}" \ - "Focus companion app is missing" "true" >>"${outfile}" - fi -} - -_check_boot_persistence() { - local outfile="$1" - - if adb_root_shell "test -x ${_MONITOR_BOOT_SCRIPT}" >/dev/null 2>&1; then - _mon_check "boot_persistence" "ok" "test -x ${_MONITOR_BOOT_SCRIPT}" \ - "Magisk boot script present and executable" "false" >>"${outfile}" - else - _mon_check "boot_persistence" "fatal" "test -x ${_MONITOR_BOOT_SCRIPT}" \ - "Magisk boot script missing or not executable" "true" >>"${outfile}" - fi -} monitor_collect_snapshot() { local snapshot_dir="$1" diff --git a/phone_focus_mode/lib/monitor_checks_health.sh b/phone_focus_mode/lib/monitor_checks_health.sh new file mode 100755 index 00000000..9c8ae015 --- /dev/null +++ b/phone_focus_mode/lib/monitor_checks_health.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# lib/monitor_checks_health.sh — device-health probes behind monitor.sh: +# format indicators, battery, storage, and the three enforcer daemons. +# Each _check_* appends one JSON object to the checks file it is given. +# +# Sourced by monitor.sh, which defines every _MONITOR_* constant these read +# and the _mon_check/_trim_output/_monitor_read_pidfile helpers they call. +# Nothing is assigned here, so there is no constant to keep in step. + +_check_format_indicators() { + local outfile="$1" + local -a missing_indicators=() + local status="ok" + local message="All format indicators are present" + + mapfile -t missing_indicators < <(monitor_check_format_indicators) + if (( ${#missing_indicators[@]} >= FORMAT_DETECTION_MIN_MISSING )); then + status="fatal" + message="Missing ${#missing_indicators[@]} format indicators: ${missing_indicators[*]}" + elif (( ${#missing_indicators[@]} > 0 )); then + status="warn" + message="Missing ${#missing_indicators[@]} format indicators: ${missing_indicators[*]}" + fi + + _mon_check "format_indicators" "${status}" "FORMAT_INDICATORS" "${message}" "false" >>"${outfile}" +} + +_check_battery() { + local outfile="$1" + local level="" + local health="" + local temp="" + local status="ok" + local message="" + + level="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/level:/{print \$2; exit}'")")" + health="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/health:/{print \$2; exit}'")")" + temp="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/temperature:/{print \$2; exit}'")")" + + if [[ ! "${level}" =~ ^[0-9]+$ ]]; then + status="warn" + message="Battery level unavailable" + elif (( level < BATTERY_WARN_BELOW )); then + status="warn" + message="Battery low: ${level}% (threshold ${BATTERY_WARN_BELOW}%)" + else + message="Battery level ${level}%, health ${health:-unknown}, temp ${temp:-unknown}" + fi + + _mon_check "battery" "${status}" "dumpsys battery" "${message}" "false" >>"${outfile}" +} + +_check_storage() { + local outfile="$1" + local free_kb="" + local free_mb=0 + local status="ok" + local message="" + + free_kb="$(_trim_output "$(_safe_adb_root_output "df /sdcard 2>/dev/null | awk 'NR==2{print \$4; exit}'")")" + if [[ ! "${free_kb}" =~ ^[0-9]+$ ]]; then + free_kb="$(_trim_output "$(_safe_adb_root_output "df /storage/emulated/0 2>/dev/null | awk 'NR==2{print \$4; exit}'")")" + fi + + if [[ "${free_kb}" =~ ^[0-9]+$ ]]; then + free_mb=$((free_kb / 1024)) + if (( free_mb < STORAGE_WARN_BELOW_MB )); then + status="warn" + message="Low storage: ${free_mb} MB free (threshold ${STORAGE_WARN_BELOW_MB} MB)" + else + message="Free storage: ${free_mb} MB" + fi + else + status="warn" + message="Free storage unavailable" + fi + + _mon_check "storage" "${status}" "df /sdcard" "${message}" "false" >>"${outfile}" +} + +_check_daemon() { + local daemon_name="$1" + local script_name="$2" + local pidfile="$3" + local outfile="$4" + local pid="" + local pgrep_pid="" + + pid="$(_monitor_read_pidfile "${pidfile}")" + if [[ "${pid}" =~ ^[0-9]+$ ]] && _monitor_pid_matches_script "${pid}" "${script_name}"; then + _mon_check "${daemon_name}" "ok" "${pidfile}" "${daemon_name} running (PID ${pid})" "false" >>"${outfile}" + return + fi + + pgrep_pid="$(_trim_output "$(_safe_adb_root_output "pgrep -f '${script_name}' 2>/dev/null | head -1")")" + if [[ "${pgrep_pid}" =~ ^[0-9]+$ ]] && adb_root_shell "kill -0 ${pgrep_pid} >/dev/null 2>&1" >/dev/null 2>&1; then + _mon_check "${daemon_name}" "ok" "pgrep -f ${script_name}" "${daemon_name} running (PID ${pgrep_pid})" "false" >>"${outfile}" + return + fi + + _mon_check "${daemon_name}" "error" "${pidfile}" "${daemon_name} is NOT running" "true" >>"${outfile}" +} + +_check_hosts_daemon() { + local outfile="$1" + local resolved_target="" + + resolved_target="$(_monitor_resolve_hosts_target)" + if [[ -z "${resolved_target}" ]]; then + _mon_check "hosts_enforcer" "warn" "hosts target probe" \ + "No hosts target file exists on this ROM; hosts daemon check skipped" "false" >>"${outfile}" + return + fi + + _check_daemon "hosts_enforcer" "hosts_enforcer.sh" "${_MONITOR_REMOTE_DIR}/hosts_enforcer.pid" "${outfile}" +} + +_check_launcher_daemon() { + local outfile="$1" + local has_snapshot="no" + local launcher_installed="no" + + if adb_root_shell "test -s '${_MONITOR_LAUNCHER_ACTIVITY_FILE}'" >/dev/null 2>&1; then + has_snapshot="yes" + fi + + if adb_root_shell "pm path '${_MONITOR_LAUNCHER_PACKAGE}' >/dev/null 2>&1" >/dev/null 2>&1; then + launcher_installed="yes" + fi + + if [[ "${has_snapshot}" == "no" && "${launcher_installed}" == "no" ]]; then + _mon_check "launcher_enforcer" "warn" "launcher optional probe" \ + "Launcher enforcer check skipped (launcher not configured yet)" "false" >>"${outfile}" + return + fi + + _check_daemon "launcher_enforcer" "launcher_enforcer.sh" "${_MONITOR_REMOTE_DIR}/launcher_enforcer.pid" "${outfile}" +} diff --git a/phone_focus_mode/lib/monitor_checks_policy.sh b/phone_focus_mode/lib/monitor_checks_policy.sh new file mode 100755 index 00000000..3bb30e6c --- /dev/null +++ b/phone_focus_mode/lib/monitor_checks_policy.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# lib/monitor_checks_policy.sh — enforcement-integrity probes behind monitor.sh: +# the hosts blocklist, DNS chain, launcher, companion app and boot persistence. +# Each _check_* appends one JSON object to the checks file it is given. +# +# Sourced by monitor.sh, which defines _MONITOR_LAUNCHER_PACKAGE and +# _MONITOR_LAUNCHER_ACTIVITY_FILE (shared with the health probes) plus the +# _mon_check/_trim_output/_safe_adb_root_output helpers these call. +# The constants below are read only here, so they are assigned only here. + +readonly _MONITOR_HOSTS_CANONICAL="/data/local/tmp/focus_mode/hosts.canonical" +readonly _MONITOR_HOSTS_SHA_FILE="/data/local/tmp/focus_mode/hosts.sha256" +readonly _MONITOR_BOOT_SCRIPT="/data/adb/service.d/99-focus-mode.sh" +readonly _MONITOR_COMPANION_PACKAGE="com.kuhy.focusstatus" +readonly _MONITOR_DNS_CHAIN="FOCUS_DNS_BLOCK" + +_check_hosts_integrity() { + local outfile="$1" + local hosts_target="" + local expected_hash="" + local actual_hash="" + + hosts_target="$(_monitor_resolve_hosts_target)" + if [[ -z "${hosts_target}" ]]; then + _mon_check "hosts_integrity" "warn" "hosts target probe" \ + "No hosts file target exists on this ROM; hosts integrity check skipped" "false" >>"${outfile}" + return + fi + + if ! adb_root_shell "test -f ${_MONITOR_HOSTS_CANONICAL}" >/dev/null 2>&1; then + _mon_check "hosts_integrity" "fatal" "${_MONITOR_HOSTS_CANONICAL}" \ + "Canonical hosts file missing at ${_MONITOR_HOSTS_CANONICAL}" "true" >>"${outfile}" + return + fi + + expected_hash="$(_trim_output "$(_safe_adb_root_output "cat ${_MONITOR_HOSTS_SHA_FILE} 2>/dev/null")")" + actual_hash="$(_trim_output "$(_safe_adb_root_output "sha256sum ${hosts_target} 2>/dev/null | awk '{print \$1}'")")" + + if [[ -z "${expected_hash}" || -z "${actual_hash}" ]]; then + _mon_check "hosts_integrity" "error" "${hosts_target}" \ + "Could not read hosts integrity hashes" "true" >>"${outfile}" + elif [[ "${expected_hash}" == "${actual_hash}" ]]; then + _mon_check "hosts_integrity" "ok" "${hosts_target}" \ + "Hosts file matches canonical (${actual_hash:0:12}…)" "false" >>"${outfile}" + else + _mon_check "hosts_integrity" "error" "${hosts_target}" \ + "Hosts mismatch: active ${actual_hash:0:12}… != expected ${expected_hash:0:12}…" "true" >>"${outfile}" + fi +} + +_check_dns() { + local outfile="$1" + local private_dns_mode="" + local chain_present="no" + local status="ok" + local message="" + + private_dns_mode="$(_trim_output "$(_safe_adb_root_output "settings get global private_dns_mode 2>/dev/null")")" + if adb_root_shell "iptables -L ${_MONITOR_DNS_CHAIN} >/dev/null 2>&1 && ip6tables -L ${_MONITOR_DNS_CHAIN} >/dev/null 2>&1" >/dev/null 2>&1; then + chain_present="yes" + fi + + if [[ "${private_dns_mode}" == "off" || "${private_dns_mode}" == "null" || -z "${private_dns_mode}" ]]; then + if [[ "${chain_present}" == "yes" ]]; then + message="Private DNS disabled and DNS firewall chains present" + else + status="error" + message="Private DNS disabled, but DNS firewall chains are missing" + fi + else + status="error" + message="Private DNS is enabled (mode=${private_dns_mode})" + fi + + _mon_check "dns_enforcement" "${status}" "settings get global private_dns_mode" "${message}" "true" >>"${outfile}" +} + +_check_launcher() { + local outfile="$1" + local desired_activity="" + local actual_activity="" + local has_snapshot="no" + + if adb_root_shell "test -s '${_MONITOR_LAUNCHER_ACTIVITY_FILE}'" >/dev/null 2>&1; then + has_snapshot="yes" + fi + + if ! adb_root_shell "pm path '${_MONITOR_LAUNCHER_PACKAGE}' >/dev/null 2>&1" >/dev/null 2>&1; then + if [[ "${has_snapshot}" == "yes" ]]; then + _mon_check "launcher_state" "fatal" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ + "Minimalist launcher is not installed but snapshot metadata exists" "true" >>"${outfile}" + else + _mon_check "launcher_state" "warn" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ + "Minimalist launcher is not installed (optional until snapshot is configured)" "false" >>"${outfile}" + fi + return + fi + + desired_activity="$(_trim_output "$(_safe_adb_root_output "cat ${_MONITOR_LAUNCHER_ACTIVITY_FILE} 2>/dev/null")")" + actual_activity="$(_trim_output "$(_safe_adb_root_output "cmd package resolve-activity --brief -c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}'")")" + + if [[ -z "${desired_activity}" ]]; then + _mon_check "launcher_state" "warn" "cat ${_MONITOR_LAUNCHER_ACTIVITY_FILE}" \ + "Launcher snapshot metadata is missing or empty" "true" >>"${outfile}" + elif [[ -n "${actual_activity}" && "${desired_activity}" != "${actual_activity}" ]]; then + _mon_check "launcher_state" "error" "cmd package resolve-activity" \ + "Launcher default mismatch: expected ${desired_activity}, got ${actual_activity}" "true" >>"${outfile}" + else + _mon_check "launcher_state" "ok" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ + "Minimalist launcher installed and HOME activity aligned" "false" >>"${outfile}" + fi +} + +_check_companion_app() { + local outfile="$1" + + if adb_root_shell "pm list packages -e '${_MONITOR_COMPANION_PACKAGE}' 2>/dev/null | grep -q '${_MONITOR_COMPANION_PACKAGE}'" >/dev/null 2>&1; then + _mon_check "companion_app" "ok" "pm list packages -e ${_MONITOR_COMPANION_PACKAGE}" \ + "Focus companion app is installed" "false" >>"${outfile}" + else + _mon_check "companion_app" "warn" "pm list packages -e ${_MONITOR_COMPANION_PACKAGE}" \ + "Focus companion app is missing" "true" >>"${outfile}" + fi +} + +_check_boot_persistence() { + local outfile="$1" + + if adb_root_shell "test -x ${_MONITOR_BOOT_SCRIPT}" >/dev/null 2>&1; then + _mon_check "boot_persistence" "ok" "test -x ${_MONITOR_BOOT_SCRIPT}" \ + "Magisk boot script present and executable" "false" >>"${outfile}" + else + _mon_check "boot_persistence" "fatal" "test -x ${_MONITOR_BOOT_SCRIPT}" \ + "Magisk boot script missing or not executable" "true" >>"${outfile}" + fi +}