From b83ce5d3c742aefb27fe7dcc111a52fe62ed060a Mon Sep 17 00:00:00 2001 From: Krzysztof Rudnicki Date: Mon, 3 Aug 2026 18:34:49 +0200 Subject: [PATCH] Fail the build on any shellcheck finding; fix the 222 the old gate hid The shellcheck hook ran with --severity=warning, so info- and style-level findings never failed a commit. 222 had accumulated across 94 of 244 shell files. They were not all style noise -- six were bugs: - Five scripts in single_use/misc/testsAndMisc-bash/ sourced $SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists at that path, so all five failed at runtime. - setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage() (which ends in `exit 0`) and then `exit 1` for an unknown command. The exit 1 was unreachable, so a typo'd subcommand reported SUCCESS. - Six i3blocks scripts had a `# shellcheck source=` directive missing the scripts/periodic_background/ path segment. The stale path had silently disabled source-following for all of them. - diagnose_arch_performance.sh word-split a newline-joined file list; when the find came back empty, awk got no file operands, fell through to reading stdin, and the diagnostic hung. - lib/common.sh's handle_arg_help_or_unknown ended in an unreachable `return 0` while documenting "Returns: 0 if handled" -- a contract no code path could satisfy. - deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top level, and config.sh can repoint it at the on-device path /data/local/tmp/focus_mode. Renamed to DEPLOY_DIR. The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc sets external-sources + source-path=SCRIPTDIR so sourced files are actually followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs. 17 `# shellcheck disable=` directives remain, each with a written reason, and only where the finding is provably wrong: injection-test payloads that must not expand, literal source text being grepped for, POSIX sh scripts on the phone where arrays do not exist, and source paths that only exist at runtime. Everything else was fixed in code. Verified in a CLEAN worktree, not just my own tree. config.sh sources the gitignored config_secrets.sh, which exists locally but not in a fresh checkout; a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a clean worktree, exactly as CI does -- failed on it. The gate is verified to bite: a file whose only finding is SC2016 (info) fails the hook, while the old --severity=warning gate exits 0 on it. third_party/ is excluded -- vendored upstream, matching the prettier hook. Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing SC2034 disable that hides a real bug -- its `patterns` array is never used, so the checker detects nothing. Confirmed unchanged by this commit. Co-Authored-By: Claude Opus 5 (1M context) --- .pre-commit-config.yaml | 14 +- .shellcheckrc | 20 + .../shellcheck-strict-gate-2026-08-03.json | 18 + .../shellcheck-strict-gate-2026-08-03.json | 69 + linux_configuration/scripts/lib/common.sh | 14 +- .../check_and_enable_services.sh | 22 +- .../digital_wellbeing/music_parallelism.sh | 595 +++--- .../i3blocks/activitywatch_status.sh | 88 +- .../i3-configuration/i3blocks/bluetooth.sh | 84 +- .../i3-configuration/i3blocks/ethernet.sh | 92 +- .../i3-configuration/i3blocks/gpu_monitor.sh | 111 +- .../i3-configuration/i3blocks/warp_status.sh | 80 +- .../i3-configuration/i3blocks/wifi_monitor.sh | 142 +- .../utils/android_guardian/service.sh | 6 +- .../single_use/features/setup_dns_blocker.sh | 40 +- .../features/setup_kcd2_dice_solver.sh | 49 +- .../features/setup_personal_website.sh | 55 +- .../fixes/diagnose_arch_performance.sh | 22 +- .../testsAndMisc-bash/fix_thorium_unity.sh | 4 +- .../misc/testsAndMisc-bash/fix_unity.sh | 390 ++-- .../testsAndMisc-bash/get_rnnoise_model.sh | 188 +- .../install_ffmpeg_with_arnndn.sh | 200 +- .../testsAndMisc-bash/install_unity_mcp.sh | 236 +-- .../scripts/single_use/utils/analyze_repo.sh | 26 +- .../utils/download_exercism_bulk.sh | 52 +- .../utils/generate_study_materials.sh | 112 +- .../single_use/utils/install_exercism.sh | 461 ++--- .../utils/install_plagiarism_tools.sh | 127 +- .../scripts/single_use/utils/lookup_docs.sh | 1516 +++++++-------- .../single_use/utils/update_android_hosts.sh | 6 +- .../scripts/single_use/utils/upgrade.sh | 14 +- .../tests/test_android_guardian_service.sh | 45 +- .../test_hosts_guard_pacman_integration.sh | 5 +- .../tests/test_pacman_wrapper_security.sh | 141 +- .../tests/test_shutdown_timer_monitor.sh | 109 +- ...t_usage_monitoring_installer_efficiency.sh | 36 +- meta/lint_python.sh | 301 +-- meta/run.sh | 197 +- meta/scripts/check_polling_antipatterns.sh | 154 +- meta/scripts/disk_cleanup_check.sh | 317 ++-- phone_focus_mode/config.sh | 5 + phone_focus_mode/deploy.sh | 1277 +++++++------ phone_focus_mode/dns_enforcer.sh | 302 +-- phone_focus_mode/focus_ctl.sh | 1671 +++++++++-------- phone_focus_mode/focus_daemon.sh | 739 ++++---- phone_focus_mode/focus_status_app/build.sh | 69 +- phone_focus_mode/lib/tests/test_adb_common.sh | 213 ++- .../lib/tests/test_tether_enforcer.sh | 88 +- 48 files changed, 5441 insertions(+), 5081 deletions(-) create mode 100644 .shellcheckrc create mode 100644 docs/superpowers/contracts/shellcheck-strict-gate-2026-08-03.json create mode 100644 docs/superpowers/evidence/shellcheck-strict-gate-2026-08-03.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90586284..ebedeffc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -412,13 +412,25 @@ repos: hooks: - id: shellcheck name: shellcheck - entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck --severity=warning' -- + # NO --severity filter: every finding shellcheck reports, down to info + # and style, fails the commit. The previous --severity=warning quietly + # accepted 222 findings across 94 of 244 shell files -- and among them + # were real defects, not noise: two scripts that reported SUCCESS on an + # unknown command, five that sourced a path off by one directory (so + # they failed at runtime), a stale `source=` directive that had silently + # disabled following for six i3blocks scripts, and a diagnostic that + # would hang on stdin when its file list came back empty. + # Settings live in .shellcheckrc (external-sources, source-path). + entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck' -- language: system types: [shell] # shellcheck only understands sh/bash/dash/ksh; it cannot parse zsh # syntax (e.g. extended globs like (#s)). zsh files are checked by the # zsh-syntax hook below instead. exclude_types: [zsh] + # Vendored upstream code: we do not own it and patches here would be + # lost on the next vendor sync. Matches the prettier hook's exclusion. + exclude: ^third_party/ # =========================================================================== # ZSH SYNTAX - zsh -n parse check (shellcheck has no zsh mode) diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 00000000..69bebaf2 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,20 @@ +# Repo-wide shellcheck configuration. +# +# The pre-commit hook runs shellcheck with NO --severity filter, so every +# finding shellcheck reports - including info- and style-level - fails the +# commit. Previously the hook gated at --severity=warning, which silently +# accepted 222 findings across 94 of 244 shell files. +# +# Follow `source`d files so SC1091 can be satisfied by pointing at the real +# file rather than silenced. Where a script sources a path that only exists at +# runtime (e.g. /usr/local/bin/pacman_lock_lib.sh, installed by its installer), +# the call site carries an explicit `# shellcheck source=` directive +# naming the in-repo original, so the sourced code is genuinely checked. +external-sources=true + +# Resolve `source` targets relative to the sourcing script's own directory. +# Nearly every site here is `source "$SCRIPT_DIR/../../lib/common.sh"`, and +# shellcheck cannot expand $SCRIPT_DIR - but with SCRIPTDIR on the search path a +# `# shellcheck source=../../lib/common.sh` directive at the call site resolves +# to the real file, so the sourced code is actually analysed instead of skipped. +source-path=SCRIPTDIR diff --git a/docs/superpowers/contracts/shellcheck-strict-gate-2026-08-03.json b/docs/superpowers/contracts/shellcheck-strict-gate-2026-08-03.json new file mode 100644 index 00000000..21c7c363 --- /dev/null +++ b/docs/superpowers/contracts/shellcheck-strict-gate-2026-08-03.json @@ -0,0 +1,18 @@ +{ + "title": "Gate shellcheck on every finding, and fix all 222 that the old gate hid", + "objective": "The pre-commit shellcheck hook ran with --severity=warning, so info- and style-level findings never failed a commit. 222 of them had accumulated across 94 of 244 shell files, and several were real defects rather than style noise. Make the hook fail on ANY shellcheck finding and drive the repo to zero, fixing the underlying code rather than suppressing it.", + "acceptance_criteria": [ + "shellcheck reports zero findings, at full strictness, across every tracked first-party .sh file", + "The pre-commit hook runs shellcheck with no --severity filter", + "The hook demonstrably FAILS on an info-level-only finding and passes on the clean tree", + "Sourced files are genuinely followed (external-sources + source-path) rather than SC1091 being silenced", + "Every remaining `# shellcheck disable=` carries a written reason at the call site", + "Behaviour is preserved: scripts that were runnable before still run, and tests that covered them still pass" + ], + "out_of_scope": [ + "third_party/ — vendored upstream code we do not own; excluded from the hook, matching the existing prettier exclusion", + "The pre-existing SC2034 disable in check_polling_antipatterns.sh, which hides a real bug (its `patterns` array is never used, so the checker checks nothing). Reported, not fixed — it is a behavioural change beyond a lint sweep.", + "linux_configuration/scripts/single_use/fresh-install/main.sh — another session's uncommitted work, deliberately left unstaged" + ], + "verifier": "pre-commit run shellcheck --all-files (exit 0); the same hook against a deliberately planted info-level canary (exit 1); per-file `bash -n`/`sh -n`; and re-running the affected test suites" +} diff --git a/docs/superpowers/evidence/shellcheck-strict-gate-2026-08-03.json b/docs/superpowers/evidence/shellcheck-strict-gate-2026-08-03.json new file mode 100644 index 00000000..4a556356 --- /dev/null +++ b/docs/superpowers/evidence/shellcheck-strict-gate-2026-08-03.json @@ -0,0 +1,69 @@ +{ + "intent": "Make the shellcheck pre-commit hook fail on every finding, and clear all 222 findings the previous --severity=warning gate had been accepting silently.", + "scope": [ + ".pre-commit-config.yaml (hook strictness + third_party exclusion), .shellcheckrc (new)", + "45 shell files across linux_configuration/, phone_focus_mode/, meta/", + "NON-GOAL: third_party/ vendored code", + "NON-GOAL: linux_configuration/scripts/single_use/fresh-install/main.sh (another session's uncommitted work, left unstaged)" + ], + "changes": [ + "Added .shellcheckrc with external-sources=true and source-path=SCRIPTDIR. This RESOLVES sourced files instead of silencing SC1091: 81 -> 13 findings with no suppression, and it made shellcheck analyse the sourced code, which is what surfaced several of the bugs below.", + "Removed --severity=warning from the hook so info- and style-level findings fail the commit; excluded third_party/ (vendored, matching the prettier hook).", + "REAL BUG: five scripts in single_use/misc/testsAndMisc-bash/ sourced $SCRIPT_DIR/../../lib/common.sh, one directory too shallow. The file does not exist at that path, so every one of them failed at runtime. Corrected to ../../../lib/common.sh.", + "REAL BUG: setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage() (which ends in `exit 0`) and then `exit 1` on an unknown command. The exit 1 was unreachable, so a typo'd subcommand reported SUCCESS. usage() now takes an exit status; both verified to exit 1.", + "REAL BUG: six i3blocks scripts carried a `# shellcheck source=linux_configuration/i3-configuration/...` directive missing the scripts/periodic_background/ segment. The stale path silently disabled source-following. Corrected to a script-relative `source=persist_common.sh`.", + "REAL BUG: diagnose_arch_performance.sh word-split a newline-joined `gov_files` string; when the find returned nothing, awk was left with no file operands and fell through to reading stdin, hanging the diagnostic. Now a mapfile array with an explicit empty guard.", + "REAL BUG: lib/common.sh's handle_arg_help_or_unknown ended with an unreachable `return 0` while its doc promised 'Returns: 0 if handled' — a contract no code path could satisfy. Dead line removed, doc corrected.", + "phone_focus_mode/deploy.sh: renamed its SCRIPT_DIR to DEPLOY_DIR. config.sh is sourced at top level and reassigns SCRIPT_DIR, and can repoint it at the on-device path /data/local/tmp/focus_mode — after which every later path in deploy.sh would resolve against the wrong tree.", + "Mechanical but genuine fixes elsewhere: printf format strings (SC2059) with colour variables moved to %s arguments — which required switching the colour definitions to ANSI-C $'...' quoting, since printf does not interpret backslash escapes in arguments; `A && B || C` rewritten as if/then/else; ls-parsing replaced with find/glob arrays; unquoted expansions quoted; repeated appends grouped." + ], + "verification": [ + { + "command": "shellcheck (full strictness) over every tracked first-party *.sh, in a CLEAN worktree", + "result": "pass", + "evidence": "222 findings in 94 files -> 0 findings in 0 files. Verified with `git worktree add --detach` on HEAD rather than the working tree: config.sh sources the gitignored config_secrets.sh, which exists locally but NOT in a clean checkout, so a working-tree-only check passed while ci_mirror.sh (which lints HEAD in a clean worktree) failed." + }, + { + "command": "pre-commit run shellcheck --all-files", + "result": "pass", + "evidence": "exit 0" + }, + { + "command": "pre-commit run shellcheck --files ", + "result": "pass", + "evidence": "exit 1 on a file whose ONLY finding is SC2016 (info). The old --severity=warning gate exits 0 on the same file - this is the regression that let 222 findings accumulate." + }, + { + "command": "bash phone_focus_mode/lib/tests/test_tether_enforcer.sh", + "result": "pass", + "evidence": "17 passed, 0 failed after rewriting all 17 assertions from `cond && ok || no` to if/then/else" + }, + { + "command": "bash phone_focus_mode/lib/tests/test_adb_common.sh", + "result": "pass", + "evidence": "13 passed, 0 failed" + }, + { + "command": "bash linux_configuration/tests/test_shutdown_timer_monitor.sh", + "result": "pass", + "evidence": "regression checks passed" + }, + { + "command": "behavioural spot-checks", + "result": "pass", + "evidence": "setup_personal_website.sh bogus -> exit 1 (was 0), --help -> 0; disk_cleanup_check.sh output byte-identical with ANSI escapes present and zero literal \\033 leaked; build.sh picks the same build-tools dir as the old ls pipeline; every touched file passes bash -n / sh -n" + } + ], + "risks": [ + "The gate is now strict for everyone: any new shell code must be shellcheck-clean at info level. That is the intent, but it will fail commits that previously passed.", + "16 `# shellcheck disable=` directives were added where the finding is provably a false positive - injection-test payloads that must not expand, literal source text being grepped for, POSIX sh scripts on the phone where arrays do not exist, and runtime-only source paths. Each carries a written reason. They are the only suppressions; everything else was fixed in code.", + "A comment line beginning '# shellcheck ' is parsed as a directive - one such comment broke a file mid-sweep and had to be reworded. Worth knowing when writing these reasons.", + "Verifying a lint gate against the WORKING TREE is not the same as verifying it against a clean checkout. config.sh sources a gitignored secrets file that exists locally; shellcheck resolved it here and reported nothing, while ci_mirror.sh - which checks HEAD out into a clean worktree, exactly as CI does - failed. Any future gate work must be checked the ci_mirror way.", + "check_polling_antipatterns.sh has a pre-existing SC2034 disable hiding a real bug: its `patterns` array is never used, so the checker detects nothing. Confirmed against the pre-change version - behaviour is unchanged by this commit. Left for a separate fix." + ], + "rollback": [ + "Restore --severity=warning in the shellcheck hook entry and delete .shellcheckrc to return to the old gate", + "git revert the commit to restore all 45 files; the code fixes are independent of the hook change and can be kept separately", + "After rollback, validate: pre-commit run shellcheck --all-files still passes, and the affected test suites still pass" + ] +} diff --git a/linux_configuration/scripts/lib/common.sh b/linux_configuration/scripts/lib/common.sh index 6099ec6f..9e1c8647 100755 --- a/linux_configuration/scripts/lib/common.sh +++ b/linux_configuration/scripts/lib/common.sh @@ -116,8 +116,11 @@ parse_interactive_args() { # Handle common argument patterns for scripts with custom usage functions # Usage: handle_arg_help_or_unknown "$1" usage_function err_function -# Returns: 0 if argument was handled (caller should continue), 1 if not our concern -# Exits: on -h/--help (exit 0) or unknown arg starting with - (exit 2) +# Returns: 1 if the argument is not ours, so the caller handles it. +# Exits: on -h/--help (exit 0) or unknown arg starting with - (exit 2). +# There is deliberately no success return: every other branch exits. The old +# trailing `return 0` was unreachable, and the doc promised a 0 that no code +# path could ever produce. handle_arg_help_or_unknown() { local arg="$1" local usage_fn="${2:-usage}" @@ -137,7 +140,6 @@ handle_arg_help_or_unknown() { return 1 # Not a flag, let caller handle it ;; esac - return 0 } # Initialize a setup script with common boilerplate @@ -186,7 +188,7 @@ is_focus_app_running() { if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then local regex wid printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}" - regex="${regex%|}" # strip trailing | + regex="${regex%|}" # strip trailing | while IFS= read -r wid; do [[ -n $wid ]] || continue echo "focus app" @@ -199,7 +201,7 @@ is_focus_app_running() { for app in "${FOCUS_APPS_PROCESSES[@]}"; do for comm in /proc/[0-9]*/comm; do [[ -r $comm ]] || continue - read -r _proc_comm < "$comm" 2>/dev/null || continue + read -r _proc_comm <"$comm" 2>/dev/null || continue if [[ $_proc_comm == *"$app"* ]]; then echo "$_proc_comm" return 0 @@ -450,7 +452,7 @@ get_second() { # Get Unix timestamp from boot (uptime in seconds) # Usage: boot_seconds=$(get_uptime_seconds) get_uptime_seconds() { - read -r uptime_with_fraction _ < /proc/uptime + read -r uptime_with_fraction _ /dev/null || true +mkdir -p "$LOG_DIR" 2>/dev/null || true export LOG_FILE="$LOG_DIR/music-parallelism.log" CHECK_INTERVAL=15 FAST_CHECK_INTERVAL=5 @@ -33,60 +36,60 @@ ENFORCEMENT_COOLDOWN=20 PROC_ROOT="${PROC_ROOT:-/proc}" MUSIC_PROCESS_NAMES=( - "youtube-music" - "spotify" - "tidal" - "deezer" - "amazon music" + "youtube-music" + "spotify" + "tidal" + "deezer" + "amazon music" ) # Override focus apps with extended list for this script FOCUS_APPS_WINDOWS=( - # IDEs and code editors - match window titles - "Visual Studio Code" - "VSCodium" - "Cursor" - "IntelliJ IDEA" - "PyCharm" - "WebStorm" - "CLion" - "Rider" - "Sublime Text" - "Atom" - "Neovide" - # Gaming - "Steam" - # Creative apps - "Blender" - "Godot" - "Unity" - "Unreal Editor" + # IDEs and code editors - match window titles + "Visual Studio Code" + "VSCodium" + "Cursor" + "IntelliJ IDEA" + "PyCharm" + "WebStorm" + "CLion" + "Rider" + "Sublime Text" + "Atom" + "Neovide" + # Gaming + "Steam" + # Creative apps + "Blender" + "Godot" + "Unity" + "Unreal Editor" ) # Music streaming services - browser tabs or electron apps # These will be killed when focus apps are detected MUSIC_SERVICES=( - # YouTube Music specific patterns (NOT regular YouTube) - "music.youtube.com" - "youtube-music" # Electron app - "YouTube Music" # Window title - # Spotify - "spotify" - "Spotify" - # Tidal - "tidal" - "TIDAL" - # Deezer - "deezer" - # Amazon Music - "Amazon Music" - "amazon music" - # Apple Music (web) - "music.apple.com" - # SoundCloud - "soundcloud.com" - # Pandora - "pandora.com" + # YouTube Music specific patterns (NOT regular YouTube) + "music.youtube.com" + "youtube-music" # Electron app + "YouTube Music" # Window title + # Spotify + "spotify" + "Spotify" + # Tidal + "tidal" + "TIDAL" + # Deezer + "deezer" + # Amazon Music + "Amazon Music" + "amazon music" + # Apple Music (web) + "music.apple.com" + # SoundCloud + "soundcloud.com" + # Pandora + "pandora.com" ) readonly MUSIC_WINDOWS_PATTERN='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com' @@ -97,313 +100,313 @@ MUSIC_FOUND_PROCESS=0 MUSIC_FOUND_WINDOW=0 wait_seconds() { - local timeout_s=$1 - local start_ts end_ts elapsed_s remaining_s + local timeout_s=$1 + local start_ts end_ts elapsed_s remaining_s - if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then - printf '%s\n' "$timeout_s" >> "$MUSIC_PARALLELISM_TEST_WAIT_LOG" - if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then - exit 99 - fi - return 0 - fi + if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then + printf '%s\n' "$timeout_s" >>"$MUSIC_PARALLELISM_TEST_WAIT_LOG" + if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then + exit 99 + fi + return 0 + fi - printf -v start_ts '%(%s)T' -1 - IFS= read -r -t "$timeout_s" || true - printf -v end_ts '%(%s)T' -1 + printf -v start_ts '%(%s)T' -1 + IFS= read -r -t "$timeout_s" || true + printf -v end_ts '%(%s)T' -1 - elapsed_s=$((end_ts - start_ts)) - if (( elapsed_s < timeout_s )); then - remaining_s=$((timeout_s - elapsed_s)) - sleep "$remaining_s" - fi + elapsed_s=$((end_ts - start_ts)) + if ((elapsed_s < timeout_s)); then + remaining_s=$((timeout_s - elapsed_s)) + sleep "$remaining_s" + fi } contains_music_process() { - local comm_file comm_lower token_lower + local comm_file comm_lower token_lower - for comm_file in "$PROC_ROOT"/[0-9]*/comm; do - [[ -r $comm_file ]] || continue - read -r comm_lower < "$comm_file" || continue - comm_lower=${comm_lower,,} + for comm_file in "$PROC_ROOT"/[0-9]*/comm; do + [[ -r $comm_file ]] || continue + read -r comm_lower <"$comm_file" || continue + comm_lower=${comm_lower,,} - for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do - if [[ $comm_lower == *"${token_lower,,}"* ]]; then - return 0 - fi - done - done + for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do + if [[ $comm_lower == *"${token_lower,,}"* ]]; then + return 0 + fi + done + done - return 1 + return 1 } # Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls) find_music_services() { - local found_services=() - MUSIC_FOUND_PROCESS=0 - MUSIC_FOUND_WINDOW=0 + local found_services=() + MUSIC_FOUND_PROCESS=0 + MUSIC_FOUND_WINDOW=0 - # Check processes using /proc (fork-free) - if contains_music_process; then - MUSIC_FOUND_PROCESS=1 - found_services+=("music process") - fi + # Check processes using /proc (fork-free) + if contains_music_process; then + MUSIC_FOUND_PROCESS=1 + found_services+=("music process") + fi - # Check windows (use optimized is_focus_app_running logic: single xdotool regex call) - if command -v xdotool &> /dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then - if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &> /dev/null 2>&1; then - MUSIC_FOUND_WINDOW=1 - found_services+=("music service (window)") - fi - fi + # Check windows (use optimized is_focus_app_running logic: single xdotool regex call) + if command -v xdotool &>/dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then + if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &>/dev/null 2>&1; then + MUSIC_FOUND_WINDOW=1 + found_services+=("music service (window)") + fi + fi - if [[ ${#found_services[@]} -gt 0 ]]; then - printf '%s\n' "${found_services[@]}" - return 0 - fi - return 1 + if [[ ${#found_services[@]} -gt 0 ]]; then + printf '%s\n' "${found_services[@]}" + return 0 + fi + return 1 } # Kill music services kill_music_services() { - local use_cached_detection="${1:-0}" - local killed=false - local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com' - local should_check_windows=1 - local should_check_processes=1 + local use_cached_detection="${1:-0}" + local killed=false + local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com' + local should_check_windows=1 + local should_check_processes=1 - if [[ $use_cached_detection -eq 1 ]]; then - should_check_windows=$MUSIC_FOUND_WINDOW - should_check_processes=$MUSIC_FOUND_PROCESS - fi + if [[ $use_cached_detection -eq 1 ]]; then + should_check_windows=$MUSIC_FOUND_WINDOW + should_check_processes=$MUSIC_FOUND_PROCESS + fi - # Close browser tabs for web-based music services via one xdotool search - if [[ $should_check_windows -eq 1 ]] && command -v xdotool &> /dev/null; then - local windows wid - windows=$(xdotool search --name "$window_pattern" 2> /dev/null || true) - for wid in $windows; do - [[ -n $wid ]] || continue - xdotool windowclose "$wid" 2> /dev/null || true - killed=true - done - fi + # Close browser tabs for web-based music services via one xdotool search + if [[ $should_check_windows -eq 1 ]] && command -v xdotool &>/dev/null; then + local windows wid + windows=$(xdotool search --name "$window_pattern" 2>/dev/null || true) + for wid in $windows; do + [[ -n $wid ]] || continue + xdotool windowclose "$wid" 2>/dev/null || true + killed=true + done + fi - # Kill app processes with /proc scan + builtin kill (fork-free in hot path) - if [[ $should_check_processes -eq 1 ]]; then - local comm_file pid comm_lower token_lower - for comm_file in "$PROC_ROOT"/[0-9]*/comm; do - [[ -r $comm_file ]] || continue - read -r comm_lower < "$comm_file" || continue - comm_lower=${comm_lower,,} - pid=${comm_file#"$PROC_ROOT"/} - pid=${pid%%/*} + # Kill app processes with /proc scan + builtin kill (fork-free in hot path) + if [[ $should_check_processes -eq 1 ]]; then + local comm_file pid comm_lower token_lower + for comm_file in "$PROC_ROOT"/[0-9]*/comm; do + [[ -r $comm_file ]] || continue + read -r comm_lower <"$comm_file" || continue + comm_lower=${comm_lower,,} + pid=${comm_file#"$PROC_ROOT"/} + pid=${pid%%/*} - for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do - if [[ $comm_lower == *"${token_lower,,}"* ]]; then - if [[ $PROC_ROOT != "/proc" ]]; then - # Test mode (fake proc tree): mark as killed without signaling host PIDs. - killed=true - elif kill -9 "$pid" 2> /dev/null; then - killed=true - fi - break - fi - done - done - fi + for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do + if [[ $comm_lower == *"${token_lower,,}"* ]]; then + if [[ $PROC_ROOT != "/proc" ]]; then + # Test mode (fake proc tree): mark as killed without signaling host PIDs. + killed=true + elif kill -9 "$pid" 2>/dev/null; then + killed=true + fi + break + fi + done + done + fi - if $killed; then - return 0 - fi - return 1 + if $killed; then + return 0 + fi + return 1 } # Send notification to user notify_user() { - local focus_app="$1" - local message="Music stopped - focus mode active ($focus_app detected)" + local focus_app="$1" + local message="Music stopped - focus mode active ($focus_app detected)" - # Try to send desktop notification - if command -v notify-send &> /dev/null; then - notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2> /dev/null || true - fi + # Try to send desktop notification + if command -v notify-send &>/dev/null; then + notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2>/dev/null || true + fi - log_message "$message" + log_message "$message" } # Instant monitoring loop - uses polling at high frequency ONLY when focus app is detected # When focus app active: checks every 0.5s. When idle: checks every 3s. Reduces fork overhead. # OPTIMIZATION: Single batched pgrep call instead of multiple separate calls instant_monitor_loop() { - local next_enforcement_ts=0 - local current_ts=0 - local focus_app="" - local sleep_interval="$IDLE_CHECK_INTERVAL" + local next_enforcement_ts=0 + local current_ts=0 + local focus_app="" + local sleep_interval="$IDLE_CHECK_INTERVAL" - log_message "=== Music Parallelism INSTANT Monitor Started ===" - log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" - log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" - log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${ACTIVE_NO_MUSIC_INTERVAL}s stable-focus, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown" + log_message "=== Music Parallelism INSTANT Monitor Started ===" + log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" + log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" + log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${ACTIVE_NO_MUSIC_INTERVAL}s stable-focus, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown" - while true; do - if focus_app=$(is_focus_app_running 2> /dev/null); then - current_ts=$(get_timestamp) - if (( current_ts >= next_enforcement_ts )); then - if find_music_services > /dev/null 2>&1; then - if kill_music_services 1; then - notify_user "$focus_app" - log_message "INSTANT KILL: Music services terminated" - sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL" - fi - else - sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" - fi - next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN)) - else - sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" - fi - else - next_enforcement_ts=0 - sleep_interval="$IDLE_CHECK_INTERVAL" - fi + while true; do + if focus_app=$(is_focus_app_running 2>/dev/null); then + current_ts=$(get_timestamp) + if ((current_ts >= next_enforcement_ts)); then + if find_music_services >/dev/null 2>&1; then + if kill_music_services 1; then + notify_user "$focus_app" + log_message "INSTANT KILL: Music services terminated" + sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL" + fi + else + sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" + fi + next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN)) + else + sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" + fi + else + next_enforcement_ts=0 + sleep_interval="$IDLE_CHECK_INTERVAL" + fi - wait_seconds "$sleep_interval" - done + wait_seconds "$sleep_interval" + done } # Main monitoring loop monitor_loop() { - log_message "=== Music Parallelism Monitor Started ===" - log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" - log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" - log_message "Music services monitored: ${MUSIC_SERVICES[*]}" - log_message "Check interval: ${CHECK_INTERVAL}s" + log_message "=== Music Parallelism Monitor Started ===" + log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" + log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" + log_message "Music services monitored: ${MUSIC_SERVICES[*]}" + log_message "Check interval: ${CHECK_INTERVAL}s" - while true; do - # Check if a focus app is running - local focus_app - if focus_app=$(is_focus_app_running); then - # Focus app detected, check for music services - local music_services - if music_services=$(find_music_services); then - log_message "Conflict detected: Focus app '$focus_app' running with music services" - log_message "Active music services: $music_services" + while true; do + # Check if a focus app is running + local focus_app + if focus_app=$(is_focus_app_running); then + # Focus app detected, check for music services + local music_services + if music_services=$(find_music_services); then + log_message "Conflict detected: Focus app '$focus_app' running with music services" + log_message "Active music services: $music_services" - # Kill the music services - if kill_music_services 1; then - notify_user "$focus_app" - fi - fi - fi + # Kill the music services + if kill_music_services 1; then + notify_user "$focus_app" + fi + fi + fi - wait_seconds "$CHECK_INTERVAL" - done + wait_seconds "$CHECK_INTERVAL" + done } # Show status show_status() { - echo "Music Parallelism Monitor Status" - echo "=================================" - echo "" + echo "Music Parallelism Monitor Status" + echo "=================================" + echo "" - echo "Focus Applications (window-based detection):" - local focus_running=false + echo "Focus Applications (window-based detection):" + local focus_running=false - # Check windows (OPTIMIZED: single xdotool call with combined regex) - if command -v xdotool &> /dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then - local regex - printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}" - regex="${regex%|}" # strip trailing | - if xdotool search --name "$regex" &> /dev/null 2>&1; then - echo " ✓ Focus window detected" - focus_running=true - fi - fi + # Check windows (OPTIMIZED: single xdotool call with combined regex) + if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then + local regex + printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}" + regex="${regex%|}" # strip trailing | + if xdotool search --name "$regex" &>/dev/null 2>&1; then + echo " ✓ Focus window detected" + focus_running=true + fi + fi - # Check processes using shared /proc-based helper (fork-free) - if is_focus_app_running > /dev/null 2>&1; then - echo " ✓ Focus process running" - focus_running=true - fi + # Check processes using shared /proc-based helper (fork-free) + if is_focus_app_running >/dev/null 2>&1; then + echo " ✓ Focus process running" + focus_running=true + fi - if ! $focus_running; then - echo " (none detected)" - fi + if ! $focus_running; then + echo " (none detected)" + fi - echo "" - echo "Music Services:" - local music_running=false - if music_services=$(find_music_services 2> /dev/null); then - echo "$music_services" | while read -r svc; do - echo " ♪ $svc (RUNNING)" - done - music_running=true - fi - if ! $music_running; then - echo " (none detected)" - fi + echo "" + echo "Music Services:" + local music_running=false + if music_services=$(find_music_services 2>/dev/null); then + echo "$music_services" | while read -r svc; do + echo " ♪ $svc (RUNNING)" + done + music_running=true + fi + if ! $music_running; then + echo " (none detected)" + fi - echo "" - if $focus_running && $music_running; then - echo "⚠️ CONFLICT: Focus app and music running together!" - echo " Music would be killed in monitoring mode." - elif $focus_running; then - echo "✓ Focus mode active (no music playing)" - elif $music_running; then - echo "✓ Music playing (no focus app detected - this is fine)" - else - echo "✓ Idle (nothing detected)" - fi + echo "" + if $focus_running && $music_running; then + echo "⚠️ CONFLICT: Focus app and music running together!" + echo " Music would be killed in monitoring mode." + elif $focus_running; then + echo "✓ Focus mode active (no music playing)" + elif $music_running; then + echo "✓ Music playing (no focus app detected - this is fine)" + else + echo "✓ Idle (nothing detected)" + fi } # Show usage show_usage() { - echo "Music Parallelism Prevention Script" - echo "====================================" - echo "" - echo "Usage: $0 [command]" - echo "" - echo "Commands:" - echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)" - echo " instant - Instant monitoring (${FAST_CHECK_INTERVAL}s active / ${IDLE_CHECK_INTERVAL}s idle)" - echo " status - Show current status of focus apps and music services" - echo " kill - Immediately kill all music services" - echo " help - Show this help message" - echo "" - echo "Description:" - echo " This script prevents multitasking between focus work and music." - echo " When a focus application (VS Code, Steam, etc.) is detected" - echo " alongside a music streaming service, the music is stopped." - echo "" - echo " Music is allowed when no focus apps are running." - echo "" + echo "Music Parallelism Prevention Script" + echo "====================================" + echo "" + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)" + echo " instant - Instant monitoring (${FAST_CHECK_INTERVAL}s active / ${IDLE_CHECK_INTERVAL}s idle)" + echo " status - Show current status of focus apps and music services" + echo " kill - Immediately kill all music services" + echo " help - Show this help message" + echo "" + echo "Description:" + echo " This script prevents multitasking between focus work and music." + echo " When a focus application (VS Code, Steam, etc.) is detected" + echo " alongside a music streaming service, the music is stopped." + echo "" + echo " Music is allowed when no focus apps are running." + echo "" } # Main case "${1:-instant}" in - monitor | start | run) - monitor_loop - ;; - instant | fast) - instant_monitor_loop - ;; - status) - show_status - ;; - kill) - log_message "Manual kill requested" - if kill_music_services; then - echo "Music services killed" - else - echo "No music services found to kill" - fi - ;; - help | -h | --help) - show_usage - ;; - *) - echo "Unknown command: $1" - show_usage - exit 1 - ;; +monitor | start | run) + monitor_loop + ;; +instant | fast) + instant_monitor_loop + ;; +status) + show_status + ;; +kill) + log_message "Manual kill requested" + if kill_music_services; then + echo "Music services killed" + else + echo "No music services found to kill" + fi + ;; +help | -h | --help) + show_usage + ;; +*) + echo "Unknown command: $1" + show_usage + exit 1 + ;; esac diff --git a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/activitywatch_status.sh b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/activitywatch_status.sh index 431bb55d..c234f650 100755 --- a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/activitywatch_status.sh +++ b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/activitywatch_status.sh @@ -5,58 +5,58 @@ set -euo pipefail SCRIPT_DIR=${BASH_SOURCE[0]%/*} [[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.' -# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh +# shellcheck source=persist_common.sh source "$SCRIPT_DIR/persist_common.sh" check_installed() { - command -v aw-qt > /dev/null 2>&1 || command -v aw-server > /dev/null 2>&1 + command -v aw-qt >/dev/null 2>&1 || command -v aw-server >/dev/null 2>&1 } check_running() { - local proc_file proc_name - for proc_file in /proc/[0-9]*/comm; do - [[ -r $proc_file ]] || continue - read -r proc_name < "$proc_file" || continue - case $proc_name in - aw-qt | aw-server) - return 0 - ;; - esac - done - return 1 + local proc_file proc_name + for proc_file in /proc/[0-9]*/comm; do + [[ -r $proc_file ]] || continue + read -r proc_name <"$proc_file" || continue + case $proc_name in + aw-qt | aw-server) + return 0 + ;; + esac + done + return 1 } emit() { - local state - if ! check_installed; then - state='uninstalled' - elif check_running; then - state='on' - else - state='off' - fi + local state + if ! check_installed; then + state='uninstalled' + elif check_running; then + state='on' + else + state='off' + fi - if ! i3blocks_update_if_changed_key "activitywatch_state" "$state"; then - return 0 - fi + if ! i3blocks_update_if_changed_key "activitywatch_state" "$state"; then + return 0 + fi - if [[ $state == 'uninstalled' ]]; then - echo "AW uninstalled" - echo - echo "#FF0000" - elif [[ $state == 'on' ]]; then - echo "AW on" - echo - echo "#00FF00" - else - echo "AW off" - echo - echo "#FF0000" - fi + if [[ $state == 'uninstalled' ]]; then + echo "AW uninstalled" + echo + echo "#FF0000" + elif [[ $state == 'on' ]]; then + echo "AW on" + echo + echo "#00FF00" + else + echo "AW off" + echo + echo "#FF0000" + fi } is_persist_mode() { - [[ ${BLOCK_INTERVAL:-} == "persist" ]] + [[ ${BLOCK_INTERVAL:-} == "persist" ]] } HEARTBEAT_INTERVAL_S=60 @@ -64,10 +64,10 @@ HEARTBEAT_INTERVAL_S=60 emit if is_persist_mode; then - # Intentionally calm heartbeat in persist mode: process-table event streams can - # be extremely noisy and cause unnecessary churn. - while true; do - i3blocks_wait_seconds "$HEARTBEAT_INTERVAL_S" - emit - done + # Intentionally calm heartbeat in persist mode: process-table event streams can + # be extremely noisy and cause unnecessary churn. + while true; do + i3blocks_wait_seconds "$HEARTBEAT_INTERVAL_S" + emit + done fi diff --git a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/bluetooth.sh b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/bluetooth.sh index 8b3538b5..e8aa9ee4 100755 --- a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/bluetooth.sh +++ b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/bluetooth.sh @@ -5,55 +5,55 @@ set -euo pipefail SCRIPT_DIR=${BASH_SOURCE[0]%/*} [[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.' -# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh +# shellcheck source=persist_common.sh source "$SCRIPT_DIR/persist_common.sh" get_bluetooth_info() { - local info - info=$(bluetoothctl info 2> /dev/null) || info='' - printf '%s\n' "$info" + local info + info=$(bluetoothctl info 2>/dev/null) || info='' + printf '%s\n' "$info" } emit() { - local bluetooth_info connected device line state_key - bluetooth_info=$(get_bluetooth_info) + local bluetooth_info connected device line state_key + bluetooth_info=$(get_bluetooth_info) - connected='no' - device='' - while IFS= read -r line; do - case $line in - *'Connected: yes') - connected='yes' - ;; - *'Alias: '*) - device=${line#*Alias: } - ;; - esac - done <<< "$bluetooth_info" + connected='no' + device='' + while IFS= read -r line; do + case $line in + *'Connected: yes') + connected='yes' + ;; + *'Alias: '*) + device=${line#*Alias: } + ;; + esac + done <<<"$bluetooth_info" - state_key="$connected|$device" - if ! i3blocks_update_if_changed_key "bluetooth_state" "$state_key"; then - return 0 - fi + state_key="$connected|$device" + if ! i3blocks_update_if_changed_key "bluetooth_state" "$state_key"; then + return 0 + fi - if [[ $connected == yes && -n $device ]]; then - echo " $device" - echo - echo "#50FA7B" - else - echo " Disconnected" - fi + if [[ $connected == yes && -n $device ]]; then + echo " $device" + echo + echo "#50FA7B" + else + echo " Disconnected" + fi } is_persist_mode() { - [[ ${BLOCK_INTERVAL:-} == "persist" ]] + [[ ${BLOCK_INTERVAL:-} == "persist" ]] } emit_throttled() { - if ! i3blocks_should_emit_by_interval_key "bluetooth_emit" "$EMIT_MIN_INTERVAL_S"; then - return 0 - fi - emit + if ! i3blocks_should_emit_by_interval_key "bluetooth_emit" "$EMIT_MIN_INTERVAL_S"; then + return 0 + fi + emit } EMIT_MIN_INTERVAL_S=2 @@ -61,12 +61,12 @@ EMIT_MIN_INTERVAL_S=2 emit if is_persist_mode; then - # React to BlueZ D-Bus signals instead of polling. - if command -v dbus-monitor > /dev/null 2>&1; then - dbus-monitor --system "type='signal',sender='org.bluez'" 2> /dev/null | - while read -r line; do - [[ $line == *"PropertiesChanged"* ]] || continue - emit_throttled - done - fi + # React to BlueZ D-Bus signals instead of polling. + if command -v dbus-monitor >/dev/null 2>&1; then + dbus-monitor --system "type='signal',sender='org.bluez'" 2>/dev/null | + while read -r line; do + [[ $line == *"PropertiesChanged"* ]] || continue + emit_throttled + done + fi fi diff --git a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/ethernet.sh b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/ethernet.sh index 91232ede..6c7f4417 100755 --- a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/ethernet.sh +++ b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/ethernet.sh @@ -5,62 +5,62 @@ set -euo pipefail SCRIPT_DIR=${BASH_SOURCE[0]%/*} [[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.' -# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh +# shellcheck source=persist_common.sh source "$SCRIPT_DIR/persist_common.sh" find_ethernet_interface() { - local iface_path iface - for iface_path in /sys/class/net/*; do - iface=${iface_path##*/} - [[ $iface == lo ]] && continue - [[ -d ${iface_path}/wireless ]] && continue - [[ -r ${iface_path}/operstate ]] || continue - printf '%s\n' "$iface" - return 0 - done - return 1 + local iface_path iface + for iface_path in /sys/class/net/*; do + iface=${iface_path##*/} + [[ $iface == lo ]] && continue + [[ -d ${iface_path}/wireless ]] && continue + [[ -r ${iface_path}/operstate ]] || continue + printf '%s\n' "$iface" + return 0 + done + return 1 } emit() { - local iface state addr_output output_line - iface=$(find_ethernet_interface) || { - output_line=' down' - if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then - printf '%s\n' "$output_line" - fi - return 0 - } + local iface state addr_output output_line + iface=$(find_ethernet_interface) || { + output_line=' down' + if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then + printf '%s\n' "$output_line" + fi + return 0 + } - read -r state < "/sys/class/net/${iface}/operstate" - if [[ $state != up ]]; then - output_line=' down' - if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then - printf '%s\n' "$output_line" - fi - return 0 - fi + read -r state <"/sys/class/net/${iface}/operstate" + if [[ $state != up ]]; then + output_line=' down' + if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then + printf '%s\n' "$output_line" + fi + return 0 + fi - addr_output=$(ip -o -4 addr show dev "$iface" scope global 2> /dev/null) || addr_output='' - if [[ $addr_output =~ ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+) ]]; then - output_line=" ${BASH_REMATCH[1]}" - else - output_line=' down' - fi + addr_output=$(ip -o -4 addr show dev "$iface" scope global 2>/dev/null) || addr_output='' + if [[ $addr_output =~ ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+) ]]; then + output_line=" ${BASH_REMATCH[1]}" + else + output_line=' down' + fi - if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then - printf '%s\n' "$output_line" - fi + if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then + printf '%s\n' "$output_line" + fi } is_persist_mode() { - [[ ${BLOCK_INTERVAL:-} == "persist" ]] + [[ ${BLOCK_INTERVAL:-} == "persist" ]] } emit_throttled() { - if ! i3blocks_should_emit_by_interval_key "ethernet_emit" "$EMIT_MIN_INTERVAL_S"; then - return 0 - fi - emit + if ! i3blocks_should_emit_by_interval_key "ethernet_emit" "$EMIT_MIN_INTERVAL_S"; then + return 0 + fi + emit } EMIT_MIN_INTERVAL_S=2 @@ -68,9 +68,9 @@ EMIT_MIN_INTERVAL_S=2 emit if is_persist_mode; then - ip monitor link address route 2> /dev/null | - while read -r line; do - [[ $line == *"eth"* || $line == *"en"* || $line == *"inet "* ]] || continue - emit_throttled - done + ip monitor link address route 2>/dev/null | + while read -r line; do + [[ $line == *"eth"* || $line == *"en"* || $line == *"inet "* ]] || continue + emit_throttled + done fi diff --git a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/gpu_monitor.sh b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/gpu_monitor.sh index a46ccbc7..dda7ac4e 100755 --- a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/gpu_monitor.sh +++ b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/gpu_monitor.sh @@ -14,81 +14,80 @@ set -u SCRIPT_DIR=${BASH_SOURCE[0]%/*} [[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.' -# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh +# shellcheck source=persist_common.sh source "$SCRIPT_DIR/persist_common.sh" # Nerd Font glyph: display / desktop icon (U+F108). ICON=$'\uf108' emit() { - local temp=$1 load=$2 color - if [[ $load == 'N/A' ]]; then - color='#FFFFFF' - elif ((load < 50)); then - color='#50FA7B' - elif ((load < 75)); then - color='#F1FA8C' - else - color='#FF5555' - fi - printf '%s %s°C, %s%%\n' \ - "$color" "$ICON" "$temp" "$load" + local temp=$1 load=$2 color + if [[ $load == 'N/A' ]]; then + color='#FFFFFF' + elif ((load < 50)); then + color='#50FA7B' + elif ((load < 75)); then + color='#F1FA8C' + else + color='#FF5555' + fi + printf '%s %s°C, %s%%\n' \ + "$color" "$ICON" "$temp" "$load" } emit_if_changed() { - local temp=$1 load=$2 - if ! i3blocks_update_if_changed_key "gpu_metric" "$temp|$load"; then - return 0 - fi - emit "$temp" "$load" + local temp=$1 load=$2 + if ! i3blocks_update_if_changed_key "gpu_metric" "$temp|$load"; then + return 0 + fi + emit "$temp" "$load" } - # Prefer NVIDIA if present (persist via --loop). -if command -v nvidia-smi > /dev/null 2>&1; then - # One child process for the lifetime of i3blocks; emits CSV every 5s. - nvidia-smi \ - --query-gpu=temperature.gpu,utilization.gpu \ - --format=csv,noheader,nounits \ - --loop=5 2> /dev/null | - while IFS=',' read -r temp load; do - # Strip leading/trailing whitespace using parameter expansion. - temp=${temp## } - temp=${temp%% } - load=${load## } - load=${load%% } - [[ -z $temp || -z $load ]] && continue - emit_if_changed "$temp" "$load" - done - exit 0 +if command -v nvidia-smi >/dev/null 2>&1; then + # One child process for the lifetime of i3blocks; emits CSV every 5s. + nvidia-smi \ + --query-gpu=temperature.gpu,utilization.gpu \ + --format=csv,noheader,nounits \ + --loop=5 2>/dev/null | + while IFS=',' read -r temp load; do + # Strip leading/trailing whitespace using parameter expansion. + temp=${temp## } + temp=${temp%% } + load=${load## } + load=${load%% } + [[ -z $temp || -z $load ]] && continue + emit_if_changed "$temp" "$load" + done + exit 0 fi # AMD fallback: read sysfs directly; emit once (i3blocks restarts on exit). amdgpu='' for d in /sys/class/hwmon/hwmon*/; do - [[ -r ${d}name ]] || continue - read -r n < "${d}name" - [[ $n == amdgpu ]] && { - amdgpu=$d - break - } + [[ -r ${d}name ]] || continue + read -r n <"${d}name" + [[ $n == amdgpu ]] && { + amdgpu=$d + break + } done if [[ -n $amdgpu ]]; then - temp='N/A' - if [[ -r ${amdgpu}temp1_input ]]; then - read -r milli < "${amdgpu}temp1_input" - temp=$((milli / 1000)) - fi - load='N/A' - # drm card matching the amdgpu hwmon exposes gpu_busy_percent. - for card in /sys/class/drm/card*/device/gpu_busy_percent; do - [[ -r $card ]] && { - read -r load < "$card" - break - } - done - emit_if_changed "$temp" "$load" - exit 0 + temp='N/A' + if [[ -r ${amdgpu}temp1_input ]]; then + read -r milli <"${amdgpu}temp1_input" + temp=$((milli / 1000)) + fi + load='N/A' + # drm card matching the amdgpu hwmon exposes gpu_busy_percent. + for card in /sys/class/drm/card*/device/gpu_busy_percent; do + [[ -r $card ]] && { + read -r load <"$card" + break + } + done + emit_if_changed "$temp" "$load" + exit 0 fi printf '%s No supported GPU\n' "$ICON" diff --git a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/warp_status.sh b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/warp_status.sh index 7becf3a7..de816468 100755 --- a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/warp_status.sh +++ b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/warp_status.sh @@ -5,67 +5,67 @@ set -euo pipefail SCRIPT_DIR=${BASH_SOURCE[0]%/*} [[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.' -# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh +# shellcheck source=persist_common.sh source "$SCRIPT_DIR/persist_common.sh" -if ! command -v warp-cli > /dev/null 2>&1; then - echo " N/A" - exit 0 +if ! command -v warp-cli >/dev/null 2>&1; then + echo " N/A" + exit 0 fi is_persist_mode() { - [[ ${BLOCK_INTERVAL:-} == "persist" ]] + [[ ${BLOCK_INTERVAL:-} == "persist" ]] } WARP_POLL_INTERVAL_S=120 read_status() { - local status line - status='' - while IFS= read -r line; do - case $line in - 'Status update: '*) - status=${line#Status update: } - ;; - esac - done < <(warp-cli status 2> /dev/null) - printf '%s\n' "$status" + local status line + status='' + while IFS= read -r line; do + case $line in + 'Status update: '*) + status=${line#Status update: } + ;; + esac + done < <(warp-cli status 2>/dev/null) + printf '%s\n' "$status" } emit_status() { - local status=$1 - if [[ $status == Connected ]]; then - echo "🔒 !!! WARP CONNECTED !!!" - echo - echo "#FFFF00" - elif [[ $status == Disconnected ]]; then - echo "WARP disconnected" - echo - echo "#00FF00" - else - echo "⚠️ ! WARP unknown !" - echo - echo "#FF0000" - fi + local status=$1 + if [[ $status == Connected ]]; then + echo "🔒 !!! WARP CONNECTED !!!" + echo + echo "#FFFF00" + elif [[ $status == Disconnected ]]; then + echo "WARP disconnected" + echo + echo "#00FF00" + else + echo "⚠️ ! WARP unknown !" + echo + echo "#FF0000" + fi } emit_if_changed() { - local status=$1 - if ! i3blocks_update_if_changed_key "warp_status" "$status"; then - return 0 - fi - emit_status "$status" + local status=$1 + if ! i3blocks_update_if_changed_key "warp_status" "$status"; then + return 0 + fi + emit_status "$status" } current_status=$(read_status) emit_status "$current_status" if is_persist_mode; then - i3blocks_update_if_changed_key "warp_status" "$current_status" >/dev/null || true + i3blocks_update_if_changed_key "warp_status" "$current_status" >/dev/null || true fi if is_persist_mode; then - while true; do - i3blocks_wait_seconds "$WARP_POLL_INTERVAL_S" - current_status=$(read_status) - emit_if_changed "$current_status" - done + while true; do + i3blocks_wait_seconds "$WARP_POLL_INTERVAL_S" + current_status=$(read_status) + emit_if_changed "$current_status" + done fi diff --git a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/wifi_monitor.sh b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/wifi_monitor.sh index 009ebe0f..d8027c0d 100755 --- a/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/wifi_monitor.sh +++ b/linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/wifi_monitor.sh @@ -5,90 +5,90 @@ set -euo pipefail SCRIPT_DIR=${BASH_SOURCE[0]%/*} [[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.' -# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh +# shellcheck source=persist_common.sh source "$SCRIPT_DIR/persist_common.sh" find_wifi_interface() { - local line - while IFS= read -r line; do - case $line in - *'Interface '*) - printf '%s\n' "${line##*Interface }" - return 0 - ;; - esac - done < <(iw dev 2> /dev/null) - return 1 + local line + while IFS= read -r line; do + case $line in + *'Interface '*) + printf '%s\n' "${line##*Interface }" + return 0 + ;; + esac + done < <(iw dev 2>/dev/null) + return 1 } emit() { - local wifi_interface ssid signal ip_address line fields i output_line + local wifi_interface ssid signal ip_address line fields i output_line - wifi_interface=$(find_wifi_interface) || { - output_line=' down' - if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then - echo "$output_line" - fi - return 0 - } + wifi_interface=$(find_wifi_interface) || { + output_line=' down' + if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then + echo "$output_line" + fi + return 0 + } - ssid='' - signal='' - while IFS= read -r line; do - case $line in - 'SSID: '*) - ssid=${line#SSID: } - ;; - 'signal: '*) - signal=${line#signal: } - signal=${signal% dBm} - ;; - 'Not connected.'*) - ssid='' - ;; - esac - done < <(iw dev "$wifi_interface" link 2> /dev/null) + ssid='' + signal='' + while IFS= read -r line; do + case $line in + 'SSID: '*) + ssid=${line#SSID: } + ;; + 'signal: '*) + signal=${line#signal: } + signal=${signal% dBm} + ;; + 'Not connected.'*) + ssid='' + ;; + esac + done < <(iw dev "$wifi_interface" link 2>/dev/null) - if [[ -z $ssid ]]; then - output_line=' down' - if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then - echo "$output_line" - fi - return 0 - fi + if [[ -z $ssid ]]; then + output_line=' down' + if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then + echo "$output_line" + fi + return 0 + fi - ip_address='' - while IFS= read -r line; do - [[ $line == *' inet '* ]] || continue - read -r -a fields <<< "$line" - for ((i = 0; i < ${#fields[@]}; i++)); do - if [[ ${fields[i]} == inet && $((i + 1)) -lt ${#fields[@]} ]]; then - ip_address=${fields[i + 1]%%/*} - break 2 - fi - done - done < <(ip -o -4 addr show dev "$wifi_interface" scope global 2> /dev/null) + ip_address='' + while IFS= read -r line; do + [[ $line == *' inet '* ]] || continue + read -r -a fields <<<"$line" + for ((i = 0; i < ${#fields[@]}; i++)); do + if [[ ${fields[i]} == inet && $((i + 1)) -lt ${#fields[@]} ]]; then + ip_address=${fields[i + 1]%%/*} + break 2 + fi + done + done < <(ip -o -4 addr show dev "$wifi_interface" scope global 2>/dev/null) - if [[ -n $ip_address ]]; then - output_line=" $ssid ($signal dBm) $ip_address" - else - output_line=" $ssid ($signal dBm)" - fi + if [[ -n $ip_address ]]; then + output_line=" $ssid ($signal dBm) $ip_address" + else + output_line=" $ssid ($signal dBm)" + fi - if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then - echo "$output_line" - fi + if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then + echo "$output_line" + fi } is_persist_mode() { - [[ ${BLOCK_INTERVAL:-} == "persist" ]] + [[ ${BLOCK_INTERVAL:-} == "persist" ]] } emit_throttled() { - if ! i3blocks_should_emit_by_interval_key "wifi_emit" "$EMIT_MIN_INTERVAL_S"; then - return 0 - fi - emit + if ! i3blocks_should_emit_by_interval_key "wifi_emit" "$EMIT_MIN_INTERVAL_S"; then + return 0 + fi + emit } EMIT_MIN_INTERVAL_S=2 @@ -96,9 +96,9 @@ EMIT_MIN_INTERVAL_S=2 emit if is_persist_mode; then - ip monitor link address route 2> /dev/null | - while read -r line; do - [[ $line == *"wlan"* || $line == *"wl"* || $line == *"inet "* ]] || continue - emit_throttled - done + ip monitor link address route 2>/dev/null | + while read -r line; do + [[ $line == *"wlan"* || $line == *"wl"* || $line == *"inet "* ]] || continue + emit_throttled + done fi diff --git a/linux_configuration/scripts/periodic_background/utils/android_guardian/service.sh b/linux_configuration/scripts/periodic_background/utils/android_guardian/service.sh index f89cf5cf..dcbc298c 100755 --- a/linux_configuration/scripts/periodic_background/utils/android_guardian/service.sh +++ b/linux_configuration/scripts/periodic_background/utils/android_guardian/service.sh @@ -100,7 +100,11 @@ $installed_packages package:$package "*) log "Blocked app detected: $package - Uninstalling..." - pm uninstall "$package" 2>/dev/null && log "Uninstalled: $package" || log "Failed to uninstall: $package" + if pm uninstall "$package" 2>/dev/null; then + log "Uninstalled: $package" + else + log "Failed to uninstall: $package" + fi ;; esac done <"$BLOCKED_APPS_FILE" diff --git a/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh b/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh index 7d9f535d..78400a45 100755 --- a/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh +++ b/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh @@ -45,7 +45,7 @@ readonly WATCHDOG_TIMER="/etc/systemd/system/dnsmasq-watchdog.timer" # Optional DHCP-server mode (for routers that cannot advertise a custom DNS). readonly DHCP_CONF="/etc/dnsmasq.d/lan-dhcp.conf" readonly DHCP_LEASE="12h" -readonly DHCP_START_HOST="10" # .10 .. .150 mirrors a common router default range +readonly DHCP_START_HOST="10" # .10 .. .150 mirrors a common router default range readonly DHCP_END_HOST="150" # Feed generator (chattr +i but still executable) and firewall owner script. readonly GEN="${SCRIPT_DIR}/../../periodic_background/hosts/generate_hosts_file.sh" @@ -442,10 +442,21 @@ cmd_status() { detect_lan echo "=== LAN DNS blocker status ===" - is_service_active dnsmasq && status_line 0 "dnsmasq: active" || status_line 1 "dnsmasq: NOT active" - is_service_enabled dnsmasq && status_line 0 "dnsmasq: enabled at boot" || status_line 1 "dnsmasq: NOT enabled" - [[ -f $DNSMASQ_DROPIN ]] && status_line 0 "Restart=always drop-in present" || status_line 1 "Restart drop-in missing" - + if is_service_active dnsmasq; then + status_line 0 "dnsmasq: active" + else + status_line 1 "dnsmasq: NOT active" + fi + if is_service_enabled dnsmasq; then + status_line 0 "dnsmasq: enabled at boot" + else + status_line 1 "dnsmasq: NOT enabled" + fi + if [[ -f $DNSMASQ_DROPIN ]]; then + status_line 0 "Restart=always drop-in present" + else + status_line 1 "Restart drop-in missing" + fi if [[ -f $FEED ]]; then status_line 0 "blocklist feed: $(wc -l <"$FEED") lines, updated $(date -r "$FEED" '+%Y-%m-%d %H:%M')" else @@ -472,10 +483,11 @@ cmd_status() { status_line 0 "firewall: no default-drop ruleset loaded (DNS/DHCP not filtered)" fi - systemctl is-active dns-blocklist-refresh.timer &>/dev/null && - status_line 0 "refresh timer: active (next $(systemctl show -p NextElapseUSecRealtime --value dns-blocklist-refresh.timer 2>/dev/null))" || + if systemctl is-active dns-blocklist-refresh.timer &>/dev/null; then + status_line 0 "refresh timer: active (next $(systemctl show -p NextElapseUSecRealtime --value dns-blocklist-refresh.timer 2>/dev/null))" + else status_line 1 "refresh timer: NOT active" - + fi if [[ -f $DHCP_CONF ]]; then if ss -ulnp 2>/dev/null | grep -q ':67 '; then status_line 0 "DHCP mode: ON (serving leases; PC is the LAN DHCP server)" @@ -492,12 +504,16 @@ cmd_status() { local blocked passthru blocked="$(dig +short +time=2 +tries=1 @"${LAN_IP}" youtube.com 2>/dev/null | head -1)" passthru="$(dig +short +time=2 +tries=1 @"${LAN_IP}" example.com 2>/dev/null | head -1)" - [[ $blocked == "0.0.0.0" ]] && - status_line 0 "youtube.com -> ${blocked} (BLOCKED, correct)" || + if [[ $blocked == "0.0.0.0" ]]; then + status_line 0 "youtube.com -> ${blocked} (BLOCKED, correct)" + else status_line 1 "youtube.com -> ${blocked:-} (expected 0.0.0.0)" - [[ -n $passthru && $passthru != "0.0.0.0" ]] && - status_line 0 "example.com -> ${passthru} (passthrough, correct)" || + fi + if [[ -n $passthru && $passthru != "0.0.0.0" ]]; then + status_line 0 "example.com -> ${passthru} (passthrough, correct)" + else status_line 1 "example.com -> ${passthru:-} (expected a real IP)" + fi else log_warn "install 'dig' (bind) to run the live resolution test." fi diff --git a/linux_configuration/scripts/single_use/features/setup_kcd2_dice_solver.sh b/linux_configuration/scripts/single_use/features/setup_kcd2_dice_solver.sh index e911730f..ae8c3ee2 100755 --- a/linux_configuration/scripts/single_use/features/setup_kcd2_dice_solver.sh +++ b/linux_configuration/scripts/single_use/features/setup_kcd2_dice_solver.sh @@ -67,9 +67,12 @@ die() { exit 1 } +# Takes the exit status to leave with, so the error path can print the same +# help and still fail. Hardcoding `exit 0` made the `exit 1` after the +# unknown-command branch unreachable -- the script reported SUCCESS on a typo. usage() { grep -E '^#( |$)' "$0" | sed -E 's/^# ?//' - exit 0 + exit "${1:-0}" } # --- Phase 1: preflight ----------------------------------------------------- @@ -394,13 +397,21 @@ check_http() { status_cmd() { print_setup_header "KCD2 dice solver status" - has_cmd node && status_line 0 "node present" || status_line 1 "node missing" - has_cmd pnpm && status_line 0 "pnpm present" || status_line 1 "pnpm missing" - - [[ -f "${DICE_SRC}/dist/index.html" ]] && - status_line 0 "build present (dist/index.html)" || + if has_cmd node; then + status_line 0 "node present" + else + status_line 1 "node missing" + fi + if has_cmd pnpm; then + status_line 0 "pnpm present" + else + status_line 1 "pnpm missing" + fi + if [[ -f "${DICE_SRC}/dist/index.html" ]]; then + status_line 0 "build present (dist/index.html)" + else status_line 1 "build missing — run setup" - + fi if docker ps --format '{{.Names}}' | grep -qx "$DICE_CONTAINER"; then status_line 0 "${DICE_CONTAINER} container running" local pid listeners @@ -418,14 +429,16 @@ status_cmd() { status_line "$(check_http "http://127.0.0.1:${DICE_PORT}/" "$DICE_DOMAIN")" \ "local static server (127.0.0.1:${DICE_PORT}, Host: ${DICE_DOMAIN})" - [[ -f $DICE_SNIPPET ]] && - status_line 0 "Caddy snippet present" || + if [[ -f $DICE_SNIPPET ]]; then + status_line 0 "Caddy snippet present" + else status_line 1 "Caddy snippet missing" - - getent hosts "$DICE_DOMAIN" >/dev/null && - status_line 0 "${DICE_DOMAIN} resolves" || + fi + if getent hosts "$DICE_DOMAIN" >/dev/null; then + status_line 0 "${DICE_DOMAIN} resolves" + else status_line 1 "${DICE_DOMAIN} does not resolve" - + fi status_line "$(check_http "https://${DICE_DOMAIN}/")" \ "external https://${DICE_DOMAIN}/" @@ -438,10 +451,11 @@ status_cmd() { status_line 1 "post-commit hook missing — re-run setup" fi - [[ -f $UNIT_FILE ]] && - status_line 0 "${UNIT_NAME} installed" || + if [[ -f $UNIT_FILE ]]; then + status_line 0 "${UNIT_NAME} installed" + else status_line 1 "${UNIT_NAME} missing — re-run setup" - + fi if systemctl --user is-failed --quiet "$UNIT_NAME"; then status_line 1 "last auto-rebuild FAILED — journalctl --user -u ${UNIT_NAME}" else @@ -475,8 +489,7 @@ main() { ;; *) log_error "Unknown command: $cmd" - usage - exit 1 + usage 1 ;; esac } diff --git a/linux_configuration/scripts/single_use/features/setup_personal_website.sh b/linux_configuration/scripts/single_use/features/setup_personal_website.sh index 5d17a5ca..780f4f09 100755 --- a/linux_configuration/scripts/single_use/features/setup_personal_website.sh +++ b/linux_configuration/scripts/single_use/features/setup_personal_website.sh @@ -66,9 +66,13 @@ die() { exit 1 } +# Takes the exit status to leave with, so the error path can print the same help +# and still fail. It used to hardcode `exit 0`, which made the `exit 1` after the +# unknown-command branch unreachable -- so `setup_personal_website.sh bogus` +# printed an error and then reported SUCCESS to its caller. usage() { grep -E '^#( |$)' "$0" | sed -E 's/^# ?//' - exit 0 + exit "${1:-0}" } # --- Phase 1: preflight ----------------------------------------------------- @@ -320,13 +324,21 @@ check_http() { status_cmd() { print_setup_header "Personal website status" - has_cmd node && status_line 0 "node present" || status_line 1 "node missing" - has_cmd pnpm && status_line 0 "pnpm present" || status_line 1 "pnpm missing" - - [[ -f "${WEBSITE_SRC}/dist/index.html" ]] && - status_line 0 "build present (dist/index.html)" || + if has_cmd node; then + status_line 0 "node present" + else + status_line 1 "node missing" + fi + if has_cmd pnpm; then + status_line 0 "pnpm present" + else + status_line 1 "pnpm missing" + fi + if [[ -f "${WEBSITE_SRC}/dist/index.html" ]]; then + status_line 0 "build present (dist/index.html)" + else status_line 1 "build missing — run setup" - + fi if docker ps --format '{{.Names}}' | grep -qx personal-website; then status_line 0 "personal-website container running" else @@ -336,20 +348,26 @@ status_cmd() { status_line "$(check_http "http://127.0.0.1:${WEBSITE_PORT}/")" \ "local static server (127.0.0.1:${WEBSITE_PORT})" - [[ -f $WEBSITE_SNIPPET ]] && - status_line 0 "Caddy website snippet present" || + if [[ -f $WEBSITE_SNIPPET ]]; then + status_line 0 "Caddy website snippet present" + else status_line 1 "Caddy website snippet missing" - grep -qE '^[[:space:]]*import[[:space:]]+/etc/caddy/sites' "$CADDYFILE" 2>/dev/null && - status_line 0 "root Caddyfile uses snippet imports" || + fi + if grep -qE '^[[:space:]]*import[[:space:]]+/etc/caddy/sites' "$CADDYFILE" 2>/dev/null; then + status_line 0 "root Caddyfile uses snippet imports" + else status_line 1 "root Caddyfile not migrated to imports" - - getent hosts "$WEBSITE_DOMAIN" >/dev/null && - status_line 0 "${WEBSITE_DOMAIN} resolves" || + fi + if getent hosts "$WEBSITE_DOMAIN" >/dev/null; then + status_line 0 "${WEBSITE_DOMAIN} resolves" + else status_line 1 "${WEBSITE_DOMAIN} does not resolve" - getent hosts "$GITEA_DOMAIN" >/dev/null && - status_line 0 "${GITEA_DOMAIN} resolves" || + fi + if getent hosts "$GITEA_DOMAIN" >/dev/null; then + status_line 0 "${GITEA_DOMAIN} resolves" + else status_line 1 "${GITEA_DOMAIN} does not resolve" - + fi status_line "$(check_http "https://${WEBSITE_DOMAIN}/")" \ "external https://${WEBSITE_DOMAIN}/" status_line "$(check_http "https://${GITEA_DOMAIN}/")" \ @@ -370,8 +388,7 @@ main() { ;; *) log_error "Unknown command: $cmd" - usage - exit 1 + usage 1 ;; esac } diff --git a/linux_configuration/scripts/single_use/fixes/diagnose_arch_performance.sh b/linux_configuration/scripts/single_use/fixes/diagnose_arch_performance.sh index 433988e4..bce8ba15 100755 --- a/linux_configuration/scripts/single_use/fixes/diagnose_arch_performance.sh +++ b/linux_configuration/scripts/single_use/fixes/diagnose_arch_performance.sh @@ -145,19 +145,23 @@ collect_basics() { } check_cpu_governor() { - local gov_files - gov_files=$(find /sys/devices/system/cpu -maxdepth 3 -name scaling_governor 2>/dev/null || true) + # An ARRAY, not a newline-joined string: the awk/grep calls below need one + # argument per file. With the old unquoted "$gov_files" an empty result + # also left awk with NO file operands, so it fell through to reading stdin + # and the diagnostic hung instead of reporting 'not found'. + local -a gov_files=() + mapfile -t gov_files < <(find /sys/devices/system/cpu -maxdepth 3 -name scaling_governor 2>/dev/null || true) - if [[ -z $gov_files ]]; then + if ((${#gov_files[@]} == 0)); then add_action "CPU governor files not found (may be unsupported on this platform)." return fi local summary - summary=$(awk '{count[$1]++} END {for (g in count) printf "%s:%d ", g, count[g]}' $gov_files 2>/dev/null || true) + summary=$(awk '{count[$1]++} END {for (g in count) printf "%s:%d ", g, count[g]}' "${gov_files[@]}" 2>/dev/null || true) echo "CPU governor summary: ${summary:-unknown}" >>"$REPORT_FILE" - if grep -q '^powersave$' $gov_files 2>/dev/null; then + if grep -q '^powersave$' "${gov_files[@]}" 2>/dev/null; then add_finding "CPU governor includes 'powersave' on one or more cores; this can make high-end hardware feel slow." fi } @@ -261,9 +265,11 @@ check_gpu_state() { util=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null | head -n 1 | xargs || true) power=$(nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits 2>/dev/null | head -n 1 | xargs || true) - echo "NVIDIA pstate: ${pstate:-unknown}" >>"$REPORT_FILE" - echo "NVIDIA util: ${util:-unknown}%" >>"$REPORT_FILE" - echo "NVIDIA power: ${power:-unknown}W" >>"$REPORT_FILE" + { + echo "NVIDIA pstate: ${pstate:-unknown}" + echo "NVIDIA util: ${util:-unknown}%" + echo "NVIDIA power: ${power:-unknown}W" + } >>"$REPORT_FILE" if [[ ${pstate:-} == "P0" && ${util:-100} -le 5 ]]; then add_finding "NVIDIA GPU is in P0 high-performance state while mostly idle; this can increase heat and trigger thermal limits." diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_thorium_unity.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_thorium_unity.sh index 6849d709..94ac4108 100755 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_thorium_unity.sh +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_thorium_unity.sh @@ -20,8 +20,8 @@ IFS=$'\n\t' # Source common library for log functions SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -# shellcheck source=../../lib/common.sh -source "$SCRIPT_DIR/../../lib/common.sh" +# shellcheck source=../../../lib/common.sh +source "$SCRIPT_DIR/../../../lib/common.sh" DO_POLICY=false SET_DEFAULT=false diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_unity.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_unity.sh index c6208cb0..d26c59a7 100755 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_unity.sh +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/fix_unity.sh @@ -26,11 +26,11 @@ SCRIPT_NAME="$(basename "$0")" # Source common library for log functions SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -# shellcheck source=../../lib/common.sh -source "$SCRIPT_DIR/../../lib/common.sh" +# shellcheck source=../../../lib/common.sh +source "$SCRIPT_DIR/../../../lib/common.sh" usage() { - cat << EOF + cat < /dev/null 2>&1; then - return 1 - fi + if ! command -v "$1" >/dev/null 2>&1; then + return 1 + fi } ensure_deps_arch() { - # Best-effort install for Arch-based systems - if [[ $AUTO_INSTALL != true ]]; then - log_warn "Skipping package installation (use -y to auto-install)." - return 0 - fi - if ! require_cmd pacman; then - log_warn "Not an Arch-based system (pacman not found). Skipping auto-install." - return 0 - fi - local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk) - log_info "Installing/ensuring packages: ${pkgs[*]}" - if ! require_cmd sudo; then - log_warn "sudo not found; attempting pacman directly (may fail)." - sudo_cmd="" - else - sudo_cmd="sudo" - fi - # Use --needed to avoid reinstalling - set +e - $sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}" - local rc=$? - set -e - if [[ $rc -ne 0 ]]; then - log_warn "Package install may have failed or been partial. Continuing anyway." - else - log_ok "Dependencies installed/verified." - fi + # Best-effort install for Arch-based systems + if [[ $AUTO_INSTALL != true ]]; then + log_warn "Skipping package installation (use -y to auto-install)." + return 0 + fi + if ! require_cmd pacman; then + log_warn "Not an Arch-based system (pacman not found). Skipping auto-install." + return 0 + fi + local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk) + log_info "Installing/ensuring packages: ${pkgs[*]}" + if ! require_cmd sudo; then + log_warn "sudo not found; attempting pacman directly (may fail)." + sudo_cmd="" + else + sudo_cmd="sudo" + fi + # Use --needed to avoid reinstalling + set +e + $sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}" + local rc=$? + set -e + if [[ $rc -ne 0 ]]; then + log_warn "Package install may have failed or been partial. Continuing anyway." + else + log_ok "Dependencies installed/verified." + fi } desktop_dir="$HOME/.local/share/applications" mkdir -p "$desktop_dir" detect_unityhub() { - # Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD - local install_type="UNKNOWN" exec_cmd="" + # Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD + local install_type="UNKNOWN" exec_cmd="" - # 1) Flatpak - if command -v flatpak > /dev/null 2>&1; then - if flatpak info com.unity.UnityHub > /dev/null 2>&1; then - install_type="FLATPAK" - exec_cmd="flatpak run com.unity.UnityHub %U" - echo "$install_type|$exec_cmd" - return 0 - fi - fi + # 1) Flatpak + if command -v flatpak >/dev/null 2>&1; then + if flatpak info com.unity.UnityHub >/dev/null 2>&1; then + install_type="FLATPAK" + exec_cmd="flatpak run com.unity.UnityHub %U" + echo "$install_type|$exec_cmd" + return 0 + fi + fi - # 2) Native binary in PATH - if command -v unityhub > /dev/null 2>&1; then - local path - path="$(command -v unityhub)" - install_type="NATIVE" - exec_cmd="$path %U" - echo "$install_type|$exec_cmd" - return 0 - fi + # 2) Native binary in PATH + if command -v unityhub >/dev/null 2>&1; then + local path + path="$(command -v unityhub)" + install_type="NATIVE" + exec_cmd="$path %U" + echo "$install_type|$exec_cmd" + return 0 + fi - # 3) Search desktop files for Unity Hub Exec - local search_dirs=( - "$HOME/.local/share/applications" - "/usr/share/applications" - "/var/lib/flatpak/exports/share/applications" - "$HOME/.local/share/flatpak/exports/share/applications" - ) - local found_exec="" - for d in "${search_dirs[@]}"; do - [[ -d $d ]] || continue - # prefer official naming when present - local f - for f in "$d"/*.desktop; do - [[ -e $f ]] || continue - if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2> /dev/null || - grep -qiE 'Exec=.*unityhub' "$f" 2> /dev/null; then - local exec_line - exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')" - if [[ -n $exec_line ]]; then - found_exec="$exec_line" - break 2 - fi - fi - done - done + # 3) Search desktop files for Unity Hub Exec + local search_dirs=( + "$HOME/.local/share/applications" + "/usr/share/applications" + "/var/lib/flatpak/exports/share/applications" + "$HOME/.local/share/flatpak/exports/share/applications" + ) + local found_exec="" + for d in "${search_dirs[@]}"; do + [[ -d $d ]] || continue + # prefer official naming when present + local f + for f in "$d"/*.desktop; do + [[ -e $f ]] || continue + if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2>/dev/null || + grep -qiE 'Exec=.*unityhub' "$f" 2>/dev/null; then + local exec_line + exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')" + if [[ -n $exec_line ]]; then + found_exec="$exec_line" + break 2 + fi + fi + done + done - if [[ -n $found_exec ]]; then - # Normalize: ensure %U present - if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then - found_exec+=" %U" - fi - if [[ $found_exec == flatpak* ]]; then - install_type="FLATPAK" - elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then - install_type="APPIMAGE" - else - install_type="NATIVE" - fi - echo "$install_type|$found_exec" - return 0 - fi + if [[ -n $found_exec ]]; then + # Normalize: ensure %U present + if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then + found_exec+=" %U" + fi + if [[ $found_exec == flatpak* ]]; then + install_type="FLATPAK" + elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then + install_type="APPIMAGE" + else + install_type="NATIVE" + fi + echo "$install_type|$found_exec" + return 0 + fi - # 4) Try common AppImage locations - local ai_candidates=( - "$HOME/Applications/UnityHub*.AppImage" - "$HOME/.local/bin/UnityHub*.AppImage" - "/opt/UnityHub*/UnityHub*.AppImage" - ) - local ai - for ai in "${ai_candidates[@]}"; do - for p in $ai; do - if [[ -f $p && -x $p ]]; then - install_type="APPIMAGE" - exec_cmd="$p %U" - echo "$install_type|$exec_cmd" - return 0 - fi - done - done + # 4) Try common AppImage locations + local ai_candidates=( + "$HOME/Applications/UnityHub*.AppImage" + "$HOME/.local/bin/UnityHub*.AppImage" + "/opt/UnityHub*/UnityHub*.AppImage" + ) + local ai + for ai in "${ai_candidates[@]}"; do + for p in $ai; do + if [[ -f $p && -x $p ]]; then + install_type="APPIMAGE" + exec_cmd="$p %U" + echo "$install_type|$exec_cmd" + return 0 + fi + done + done - echo "$install_type|$exec_cmd" + echo "$install_type|$exec_cmd" } create_handler_desktop() { - local exec_cmd="$1" - local dest="$desktop_dir/unityhub-url-handler.desktop" - log_info "Writing handler desktop entry: $dest" - cat > "$dest" << DESK + local exec_cmd="$1" + local dest="$desktop_dir/unityhub-url-handler.desktop" + log_info "Writing handler desktop entry: $dest" + cat >"$dest" < /dev/null 2>&1; then - update-desktop-database "$desktop_dir" || true - else - log_warn "update-desktop-database not found (install desktop-file-utils)." - fi + local desktop_file="$1" + # Update desktop database if available + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "$desktop_dir" || true + else + log_warn "update-desktop-database not found (install desktop-file-utils)." + fi - # Register as default handler for both schemes - if command -v xdg-mime > /dev/null 2>&1; then - xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true - xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true - else - log_error "xdg-mime not found (install xdg-utils)." - return 1 - fi - log_ok "MIME handler registered for unityhub:// (and unity://)." + # Register as default handler for both schemes + if command -v xdg-mime >/dev/null 2>&1; then + xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true + xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true + else + log_error "xdg-mime not found (install xdg-utils)." + return 1 + fi + log_ok "MIME handler registered for unityhub:// (and unity://)." } verify_registration() { - local expected cur1 cur2 - expected="$(basename "$1")" - cur1="$(xdg-mime query default x-scheme-handler/unityhub 2> /dev/null || true)" - cur2="$(xdg-mime query default x-scheme-handler/unity 2> /dev/null || true)" - log_info "Current handler (unityhub): ${cur1:-}" - log_info "Current handler (unity): ${cur2:-}" - if [[ $cur1 == "$expected" ]]; then - log_ok "unityhub scheme correctly set to $expected" - else - log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})." - fi + local expected cur1 cur2 + expected="$(basename "$1")" + cur1="$(xdg-mime query default x-scheme-handler/unityhub 2>/dev/null || true)" + cur2="$(xdg-mime query default x-scheme-handler/unity 2>/dev/null || true)" + log_info "Current handler (unityhub): ${cur1:-}" + log_info "Current handler (unity): ${cur2:-}" + if [[ $cur1 == "$expected" ]]; then + log_ok "unityhub scheme correctly set to $expected" + else + log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})." + fi } maybe_test_open() { - if [[ $RUN_TEST == true ]]; then - log_info "Opening test link: unityhub://v1/editor-signin" - if command -v xdg-open > /dev/null 2>&1; then - xdg-open 'unityhub://v1/editor-signin' > /dev/null 2>&1 || true - log_ok "Test link invoked. Check if Unity Hub launches or focuses." - else - log_warn "xdg-open not found; cannot run test automatically." - fi - else - log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'" - fi + if [[ $RUN_TEST == true ]]; then + log_info "Opening test link: unityhub://v1/editor-signin" + if command -v xdg-open >/dev/null 2>&1; then + xdg-open 'unityhub://v1/editor-signin' >/dev/null 2>&1 || true + log_ok "Test link invoked. Check if Unity Hub launches or focuses." + else + log_warn "xdg-open not found; cannot run test automatically." + fi + else + log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'" + fi } main() { - log_info "Ensuring required tools (optional)." - ensure_deps_arch + log_info "Ensuring required tools (optional)." + ensure_deps_arch - log_info "Detecting Unity Hub installation..." - IFS='|' read -r install_type exec_cmd < <(detect_unityhub) - log_info "Detected type: $install_type" - if [[ -z ${exec_cmd:-} ]]; then - log_warn "Could not find Unity Hub executable automatically." - log_warn "- If using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'" - log_warn "- If native (AUR): ensure 'unityhub' is in PATH" - log_warn "- If AppImage: place it in ~/Applications and make it executable" - log_error "Aborting—no Exec command available to create handler." - exit 2 - fi - log_info "Using Exec: $exec_cmd" + log_info "Detecting Unity Hub installation..." + IFS='|' read -r install_type exec_cmd < <(detect_unityhub) + log_info "Detected type: $install_type" + if [[ -z ${exec_cmd:-} ]]; then + log_warn "Could not find Unity Hub executable automatically." + log_warn "- If using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'" + log_warn "- If native (AUR): ensure 'unityhub' is in PATH" + log_warn "- If AppImage: place it in ~/Applications and make it executable" + log_error "Aborting—no Exec command available to create handler." + exit 2 + fi + log_info "Using Exec: $exec_cmd" - local desktop_file - desktop_file="$(create_handler_desktop "$exec_cmd")" + local desktop_file + desktop_file="$(create_handler_desktop "$exec_cmd")" - register_mime_handler "$desktop_file" - verify_registration "$desktop_file" + register_mime_handler "$desktop_file" + verify_registration "$desktop_file" - cat << 'NOTE' + cat <<'NOTE' --- Next steps: - Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub. @@ -294,9 +294,9 @@ Next steps: --- NOTE - maybe_test_open + maybe_test_open - log_ok "Done. If login still fails, check the Hub's logs and share the outputs of:\n which unityhub || true\n flatpak info com.unity.UnityHub 2>/dev/null | sed -n '1,5p' || true\n xdg-mime query default x-scheme-handler/unityhub\n grep -R \"x-scheme-handler/unityhub\" ~/.local/share/applications /usr/share/applications 2>/dev/null | head -n 10" + log_ok "Done. If login still fails, check the Hub's logs and share the outputs of:\n which unityhub || true\n flatpak info com.unity.UnityHub 2>/dev/null | sed -n '1,5p' || true\n xdg-mime query default x-scheme-handler/unityhub\n grep -R \"x-scheme-handler/unityhub\" ~/.local/share/applications /usr/share/applications 2>/dev/null | head -n 10" } main "$@" diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/get_rnnoise_model.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/get_rnnoise_model.sh index bf03a32e..d0652a37 100755 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/get_rnnoise_model.sh +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/get_rnnoise_model.sh @@ -13,21 +13,21 @@ set -euo pipefail # Source common library for shared functions SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -# shellcheck source=../../lib/common.sh -source "$SCRIPT_DIR/../../lib/common.sh" +# shellcheck source=../../../lib/common.sh +source "$SCRIPT_DIR/../../../lib/common.sh" YES=false while [[ $# -gt 0 ]]; do - case "$1" in - -y | --yes) - YES=true - shift - ;; - *) - echo "Unknown option: $1" >&2 - exit 2 - ;; - esac + case "$1" in + -y | --yes) + YES=true + shift + ;; + *) + echo "Unknown option: $1" >&2 + exit 2 + ;; + esac done RN_TARGET_DIR=${RN_TARGET_DIR:-"$(dirname "$0")/models"} @@ -37,125 +37,125 @@ mkdir -p "$RN_TARGET_DIR" dest="$RN_TARGET_DIR/$RN_TARGET_NAME" if [[ -f $dest ]]; then - echo "Model already exists at: $dest" - exit 0 + echo "Model already exists at: $dest" + exit 0 fi if ! $YES; then - if ! ask_yes_no "Download RNNoise model to $dest?"; then - echo "Aborted." - exit 1 - fi + if ! ask_yes_no "Download RNNoise model to $dest?"; then + echo "Aborted." + exit 1 + fi fi if ! has_cmd curl && ! has_cmd wget; then - echo "Error: Need curl or wget to download RNNoise model." >&2 - exit 3 + echo "Error: Need curl or wget to download RNNoise model." >&2 + exit 3 fi # Helper: try to download a URL to destination, exit 0 on success # Usage: try_download_model URL DEST try_download_model() { - local url="$1" - local dest="$2" - local tmp - tmp=$(mktemp) - echo "Attempting to download RNNoise model from: $url" >&2 - if has_cmd curl; then - curl -fsSL "$url" -o "$tmp" 2> /dev/null || true - else - wget -qO "$tmp" "$url" 2> /dev/null || true - fi - if [[ -s $tmp ]]; then - mv "$tmp" "$dest" - echo "Saved RNNoise model to: $dest" >&2 - exit 0 - fi - rm -f "$tmp" || true + local url="$1" + local dest="$2" + local tmp + tmp=$(mktemp) + echo "Attempting to download RNNoise model from: $url" >&2 + if has_cmd curl; then + curl -fsSL "$url" -o "$tmp" 2>/dev/null || true + else + wget -qO "$tmp" "$url" 2>/dev/null || true + fi + if [[ -s $tmp ]]; then + mv "$tmp" "$dest" + echo "Saved RNNoise model to: $dest" >&2 + exit 0 + fi + rm -f "$tmp" || true } # Priority 1: explicit URL if [[ -n ${RN_URL:-} ]]; then - echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2 - try_download_model "$RN_URL" "$dest" - echo "Warning: RN_URL download failed; continuing to fallback sources." >&2 + echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2 + try_download_model "$RN_URL" "$dest" + echo "Warning: RN_URL download failed; continuing to fallback sources." >&2 fi # Priority 2: rnnoise-nu known models (GregorR) NU_URLS=( - "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/sh.rnnn" - "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/lq.rnnn" - "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/mp.rnnn" - "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/bd.rnnn" - "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/sh.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/lq.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/mp.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/bd.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn" ) for u in "${NU_URLS[@]}"; do - try_download_model "$u" "$dest" + try_download_model "$u" "$dest" done # Priority 2b: arnndn-models fallback (richardpl) RNNDN_URLS=( - "https://raw.githubusercontent.com/richardpl/arnndn-models/master/sh.rnnn" + "https://raw.githubusercontent.com/richardpl/arnndn-models/master/sh.rnnn" ) for u in "${RNNDN_URLS[@]}"; do - try_download_model "$u" "$dest" + try_download_model "$u" "$dest" done # Priority 3: repo archives (rnnoise-nu and arnndn-models) ARCHIVES=( - "https://github.com/GregorR/rnnoise-nu/archive/refs/heads/master.zip" - "https://github.com/richardpl/arnndn-models/archive/refs/heads/master.zip" + "https://github.com/GregorR/rnnoise-nu/archive/refs/heads/master.zip" + "https://github.com/richardpl/arnndn-models/archive/refs/heads/master.zip" ) for aurl in "${ARCHIVES[@]}"; do - echo "Attempting to download archive: $aurl" >&2 - tmpdir=$(mktemp -d) - archive="$tmpdir/models.zip" - set +e - if has_cmd curl; then - curl -fL "$aurl" -o "$archive" - else - wget -O "$archive" "$aurl" - fi - status=$? - set -e - if [[ $status -ne 0 ]]; then - rm -rf "$tmpdir" || true - continue - fi - if has_cmd bsdtar; then - bsdtar -xf "$archive" -C "$tmpdir" - elif has_cmd unzip; then - unzip -q "$archive" -d "$tmpdir" - else - echo "Warning: Need bsdtar or unzip to extract archive; skipping archive method." >&2 - rm -rf "$tmpdir" || true - continue - fi - mapfile -t nnfiles < <(bash -lc 'shopt -s globstar nullglob; for f in '"$tmpdir"'/**/*.rnnn '"$tmpdir"'/**/*.nn; do [[ -f "$f" ]] && echo "$f"; done') - if [[ ${#nnfiles[@]} -gt 0 ]]; then - cp -f "${nnfiles[0]}" "$dest" - echo "Saved RNNoise model to: $dest (from archive)" >&2 - rm -rf "$tmpdir" || true - exit 0 - fi - rm -rf "$tmpdir" || true + echo "Attempting to download archive: $aurl" >&2 + tmpdir=$(mktemp -d) + archive="$tmpdir/models.zip" + set +e + if has_cmd curl; then + curl -fL "$aurl" -o "$archive" + else + wget -O "$archive" "$aurl" + fi + status=$? + set -e + if [[ $status -ne 0 ]]; then + rm -rf "$tmpdir" || true + continue + fi + if has_cmd bsdtar; then + bsdtar -xf "$archive" -C "$tmpdir" + elif has_cmd unzip; then + unzip -q "$archive" -d "$tmpdir" + else + echo "Warning: Need bsdtar or unzip to extract archive; skipping archive method." >&2 + rm -rf "$tmpdir" || true + continue + fi + mapfile -t nnfiles < <(bash -lc 'shopt -s globstar nullglob; for f in '"$tmpdir"'/**/*.rnnn '"$tmpdir"'/**/*.nn; do [[ -f "$f" ]] && echo "$f"; done') + if [[ ${#nnfiles[@]} -gt 0 ]]; then + cp -f "${nnfiles[0]}" "$dest" + echo "Saved RNNoise model to: $dest (from archive)" >&2 + rm -rf "$tmpdir" || true + exit 0 + fi + rm -rf "$tmpdir" || true done # Priority 4: Arch-based AUR packages and search only .nn/.rnnn if has_cmd yay; then - echo "Attempting to install AUR packages that may include RNNoise models..." >&2 - set +e - yay -S --noconfirm denoiseit-git 2> /dev/null - yay -S --noconfirm speech-denoiser-git 2> /dev/null - set -e - mapfile -t found < <(bash -lc 'shopt -s globstar nullglob; for f in /usr/share/**/*.nn /usr/share/**/*.rnnn /usr/local/share/**/*.nn /usr/local/share/**/*.rnnn; do [[ -f "$f" ]] && echo "$f"; done' 2> /dev/null || true) - if [[ ${#found[@]} -gt 0 ]]; then - echo "Found candidate models:" >&2 - printf ' %s\n' "${found[@]}" >&2 - cp -f "${found[0]}" "$dest" - echo "Copied model to: $dest" >&2 - exit 0 - fi + echo "Attempting to install AUR packages that may include RNNoise models..." >&2 + set +e + yay -S --noconfirm denoiseit-git 2>/dev/null + yay -S --noconfirm speech-denoiser-git 2>/dev/null + set -e + mapfile -t found < <(bash -lc 'shopt -s globstar nullglob; for f in /usr/share/**/*.nn /usr/share/**/*.rnnn /usr/local/share/**/*.nn /usr/local/share/**/*.rnnn; do [[ -f "$f" ]] && echo "$f"; done' 2>/dev/null || true) + if [[ ${#found[@]} -gt 0 ]]; then + echo "Found candidate models:" >&2 + printf ' %s\n' "${found[@]}" >&2 + cp -f "${found[0]}" "$dest" + echo "Copied model to: $dest" >&2 + exit 0 + fi fi echo "Error: Could not obtain an RNNoise model automatically." >&2 diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh index 9ebc86a9..92580bba 100755 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh @@ -9,117 +9,117 @@ set -euo pipefail # Source common library for shared functions SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -# shellcheck source=../../lib/common.sh -source "$SCRIPT_DIR/../../lib/common.sh" +# shellcheck source=../../../lib/common.sh +source "$SCRIPT_DIR/../../../lib/common.sh" print_info() { - echo "[info] $*" + echo "[info] $*" } detect_distro() { - if [[ -f /etc/os-release ]]; then - . /etc/os-release - echo "${ID:-unknown}" - else - echo "unknown" - fi + if [[ -f /etc/os-release ]]; then + . /etc/os-release + echo "${ID:-unknown}" + else + echo "unknown" + fi } main() { - local distro - distro=$(detect_distro) - print_info "Detected distro: $distro" + local distro + distro=$(detect_distro) + print_info "Detected distro: $distro" - if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then - print_info "Your ffmpeg already supports arnndn." - else - case "$distro" in - ubuntu | debian) - print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source." - echo "Options:" - echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg" - echo " - source build (recommended for latest): run this script to build from source" - ;; - arch | manjaro | endeavouros) - print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg" - ;; - fedora) - print_info "On Fedora, try: sudo dnf install ffmpeg" - ;; - *) - print_info "Distro not recognized; will offer source build." - ;; - esac - fi + if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then + print_info "Your ffmpeg already supports arnndn." + else + case "$distro" in + ubuntu | debian) + print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source." + echo "Options:" + echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg" + echo " - source build (recommended for latest): run this script to build from source" + ;; + arch | manjaro | endeavouros) + print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg" + ;; + fedora) + print_info "On Fedora, try: sudo dnf install ffmpeg" + ;; + *) + print_info "Distro not recognized; will offer source build." + ;; + esac + fi - if ask_yes_no "Build FFmpeg from source with rnnoise/arnndn support now?"; then - echo "This will clone FFmpeg and build locally under ./ffmpeg-build. Continue?" - if ! ask_yes_no "Proceed"; then - exit 0 - fi - set -x - mkdir -p ffmpeg-build && cd ffmpeg-build - # Prepare repository - if [[ -d FFmpeg ]]; then - if [[ -d FFmpeg/.git ]]; then - if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then - set +e - git -C FFmpeg fetch --all --tags --prune - git -C FFmpeg pull --rebase --ff-only || true - set -e - else - if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then - rm -rf FFmpeg - else - echo "Keeping existing FFmpeg directory as-is." - fi - fi - else - if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then - rm -rf FFmpeg - else - echo "Cannot proceed with a non-git FFmpeg directory present. Aborting." - exit 4 - fi - fi - fi - # Dependencies - if [[ $distro == "ubuntu" || $distro == "debian" ]]; then - sudo apt update - sudo apt install -y git build-essential yasm nasm pkg-config libx264-dev libx265-dev libvpx-dev libopus-dev libfdk-aac-dev libmp3lame-dev libvorbis-dev libass-dev libfreetype6-dev libgnutls28-dev libaom-dev libdav1d-dev libxvidcore-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxcb-shape0-dev libdrm-dev libvulkan-dev libva-dev libvdpau-dev librtmp-dev libunistring-dev libgnutls28-dev libchromaprint-dev libbluray-dev librubberband-dev libspeex-dev libsoxr-dev libvmaf-dev libzimg-dev libsvtav1-dev libtheora-dev libwebp-dev libopenal-dev libjack-jackd2-dev libpulse-dev librnnoise-dev - elif [[ $distro == "arch" || $distro == "manjaro" || $distro == "endeavouros" ]]; then - sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise - elif [[ $distro == "fedora" ]]; then - sudo dnf install -y git make gcc yasm nasm pkgconf-pkg-config rnnoise-devel libX11-devel libXext-devel libXfixes-devel libXv-devel libXrandr-devel libXi-devel libXtst-devel libXinerama-devel freetype-devel fontconfig-devel libass-devel libvpx-devel libaom-devel libdav1d-devel zimg-devel rubberband-devel soxr-devel libvorbis-devel opus-devel lame-devel - else - echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2 - fi - if [[ ! -d FFmpeg/.git ]]; then - git clone https://github.com/FFmpeg/FFmpeg.git --depth=1 - fi - cd FFmpeg - RN_FLAG="" - # Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported - if ./configure --help | grep -q "librnnoise"; then - RN_FLAG="--enable-librnnoise" - else - echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2 - fi + if ask_yes_no "Build FFmpeg from source with rnnoise/arnndn support now?"; then + echo "This will clone FFmpeg and build locally under ./ffmpeg-build. Continue?" + if ! ask_yes_no "Proceed"; then + exit 0 + fi + set -x + mkdir -p ffmpeg-build && cd ffmpeg-build + # Prepare repository + if [[ -d FFmpeg ]]; then + if [[ -d FFmpeg/.git ]]; then + if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then + set +e + git -C FFmpeg fetch --all --tags --prune + git -C FFmpeg pull --rebase --ff-only || true + set -e + else + if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then + rm -rf FFmpeg + else + echo "Keeping existing FFmpeg directory as-is." + fi + fi + else + if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then + rm -rf FFmpeg + else + echo "Cannot proceed with a non-git FFmpeg directory present. Aborting." + exit 4 + fi + fi + fi + # Dependencies + if [[ $distro == "ubuntu" || $distro == "debian" ]]; then + sudo apt update + sudo apt install -y git build-essential yasm nasm pkg-config libx264-dev libx265-dev libvpx-dev libopus-dev libfdk-aac-dev libmp3lame-dev libvorbis-dev libass-dev libfreetype6-dev libgnutls28-dev libaom-dev libdav1d-dev libxvidcore-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxcb-shape0-dev libdrm-dev libvulkan-dev libva-dev libvdpau-dev librtmp-dev libunistring-dev libgnutls28-dev libchromaprint-dev libbluray-dev librubberband-dev libspeex-dev libsoxr-dev libvmaf-dev libzimg-dev libsvtav1-dev libtheora-dev libwebp-dev libopenal-dev libjack-jackd2-dev libpulse-dev librnnoise-dev + elif [[ $distro == "arch" || $distro == "manjaro" || $distro == "endeavouros" ]]; then + sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise + elif [[ $distro == "fedora" ]]; then + sudo dnf install -y git make gcc yasm nasm pkgconf-pkg-config rnnoise-devel libX11-devel libXext-devel libXfixes-devel libXv-devel libXrandr-devel libXi-devel libXtst-devel libXinerama-devel freetype-devel fontconfig-devel libass-devel libvpx-devel libaom-devel libdav1d-devel zimg-devel rubberband-devel soxr-devel libvorbis-devel opus-devel lame-devel + else + echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2 + fi + if [[ ! -d FFmpeg/.git ]]; then + git clone https://github.com/FFmpeg/FFmpeg.git --depth=1 + fi + cd FFmpeg + RN_FLAG="" + # Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported + if ./configure --help | grep -q "librnnoise"; then + RN_FLAG="--enable-librnnoise" + else + echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2 + fi - ./configure \ - --enable-gpl --enable-nonfree \ - --enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \ - --enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \ - --enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \ - --enable-libdav1d --enable-libaom --enable-libsvtav1 \ - ${RN_FLAG} \ - --enable-ffplay --enable-ffprobe - make -j"$(nproc)" - echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide." - set +x - else - echo "Skipped building from source." - fi + ./configure \ + --enable-gpl --enable-nonfree \ + --enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \ + --enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \ + --enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \ + --enable-libdav1d --enable-libaom --enable-libsvtav1 \ + ${RN_FLAG} \ + --enable-ffplay --enable-ffprobe + make -j"$(nproc)" + echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide." + set +x + else + echo "Skipped building from source." + fi } main "$@" diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_unity_mcp.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_unity_mcp.sh index d786c910..2054ca69 100755 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_unity_mcp.sh +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/install_unity_mcp.sh @@ -4,8 +4,8 @@ set -euo pipefail # Source common library for shared functions SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -# shellcheck source=../../lib/common.sh -source "$SCRIPT_DIR/../../lib/common.sh" +# shellcheck source=../../../lib/common.sh +source "$SCRIPT_DIR/../../../lib/common.sh" SCRIPT_NAME="$(basename "$0")" @@ -15,158 +15,158 @@ BLUE="\033[34m" RESET="\033[0m" info() { - printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*" + printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*" } warn() { - printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2 + printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2 } error() { - printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2 + printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2 } ensure_pacman_packages() { - install_missing_pacman_packages python git curl jq code + install_missing_pacman_packages python git curl jq code } install_uv() { - if command -v uv > /dev/null 2>&1; then - info "uv is already installed." - return - fi + if command -v uv >/dev/null 2>&1; then + info "uv is already installed." + return + fi - info "Installing uv toolchain manager via official installer." - curl -LsSf https://astral.sh/uv/install.sh | sh + info "Installing uv toolchain manager via official installer." + curl -LsSf https://astral.sh/uv/install.sh | sh - local local_bin="$HOME/.local/bin" - if [[ :$PATH: != *":$local_bin:"* ]]; then - warn "Adding $local_bin to PATH in ~/.profile and ~/.zshrc. Open a new shell to apply." - printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.profile" - printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.zshrc" - fi + local local_bin="$HOME/.local/bin" + if [[ :$PATH: != *":$local_bin:"* ]]; then + warn "Adding $local_bin to PATH in ~/.profile and ~/.zshrc. Open a new shell to apply." + printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >>"$HOME/.profile" + printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >>"$HOME/.zshrc" + fi } ensure_unity_hub() { - if command -v unityhub > /dev/null 2>&1; then - info "Unity Hub already installed." - return - fi + if command -v unityhub >/dev/null 2>&1; then + info "Unity Hub already installed." + return + fi - if command -v yay > /dev/null 2>&1; then - info "Installing Unity Hub from AUR using yay." - yay -S --needed --noconfirm unityhub - elif command -v flatpak > /dev/null 2>&1; then - warn "Unity Hub not found. Attempting Flatpak installation." - flatpak install -y com.unity.UnityHub || warn "Flatpak installation failed. Install Unity Hub manually via https://unity.com/download" - else - warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download." - fi + if command -v yay >/dev/null 2>&1; then + info "Installing Unity Hub from AUR using yay." + yay -S --needed --noconfirm unityhub + elif command -v flatpak >/dev/null 2>&1; then + warn "Unity Hub not found. Attempting Flatpak installation." + flatpak install -y com.unity.UnityHub || warn "Flatpak installation failed. Install Unity Hub manually via https://unity.com/download" + else + warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download." + fi } sync_unity_mcp_repo() { - local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" - local unity_mcp_root="$data_home/UnityMCP" - local repo_dir="$unity_mcp_root/unity-mcp-repo" - local server_link="$unity_mcp_root/UnityMcpServer" - local candidates=( - "UnityMcpServer" - "UnityMcpBridge/UnityMcpServer" - "UnityMcpBridge/UnityMcpServer~" - ) - local server_subdir="" + local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" + local unity_mcp_root="$data_home/UnityMCP" + local repo_dir="$unity_mcp_root/unity-mcp-repo" + local server_link="$unity_mcp_root/UnityMcpServer" + local candidates=( + "UnityMcpServer" + "UnityMcpBridge/UnityMcpServer" + "UnityMcpBridge/UnityMcpServer~" + ) + local server_subdir="" - mkdir -p "$unity_mcp_root" + mkdir -p "$unity_mcp_root" - if [[ -d "$repo_dir/.git" ]]; then - info "Updating existing unity-mcp repository." - git -C "$repo_dir" pull --ff-only - else - info "Cloning unity-mcp repository." - rm -rf "$repo_dir" - git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir" - fi + if [[ -d "$repo_dir/.git" ]]; then + info "Updating existing unity-mcp repository." + git -C "$repo_dir" pull --ff-only + else + info "Cloning unity-mcp repository." + rm -rf "$repo_dir" + git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir" + fi - for candidate in "${candidates[@]}"; do - if [[ -d "$repo_dir/$candidate/src" ]]; then - server_subdir="$candidate" - break - fi - done + for candidate in "${candidates[@]}"; do + if [[ -d "$repo_dir/$candidate/src" ]]; then + server_subdir="$candidate" + break + fi + done - if [[ -z $server_subdir ]]; then - error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}" - error "Repository layout may have changed. Inspect $repo_dir for the new server location." - exit 1 - fi + if [[ -z $server_subdir ]]; then + error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}" + error "Repository layout may have changed. Inspect $repo_dir for the new server location." + exit 1 + fi - ln -sfn "$repo_dir/$server_subdir" "$server_link" - info "UnityMcpServer synchronized at $server_link (source: $server_subdir)" + ln -sfn "$repo_dir/$server_subdir" "$server_link" + info "UnityMcpServer synchronized at $server_link (source: $server_subdir)" } configure_vscode_mcp() { - local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" - local server_src="$data_home/UnityMCP/UnityMcpServer/src" - local mcp_config_dir="$HOME/.config/Code/User" - local mcp_config="$mcp_config_dir/mcp.json" - local tmp + local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" + local server_src="$data_home/UnityMCP/UnityMcpServer/src" + local mcp_config_dir="$HOME/.config/Code/User" + local mcp_config="$mcp_config_dir/mcp.json" + local tmp - if [[ ! -d $server_src ]]; then - error "Server source directory $server_src is missing." - exit 1 - fi + if [[ ! -d $server_src ]]; then + error "Server source directory $server_src is missing." + exit 1 + fi - mkdir -p "$mcp_config_dir" + mkdir -p "$mcp_config_dir" - if [[ ! -f $mcp_config ]]; then - info "Creating new VS Code MCP configuration at $mcp_config" - echo '{}' > "$mcp_config" - else - info "Updating existing VS Code MCP configuration at $mcp_config" - fi + if [[ ! -f $mcp_config ]]; then + info "Creating new VS Code MCP configuration at $mcp_config" + echo '{}' >"$mcp_config" + else + info "Updating existing VS Code MCP configuration at $mcp_config" + fi - tmp="$(mktemp)" + tmp="$(mktemp)" - if ! jq '.' "$mcp_config" > /dev/null 2>&1; then - error "Existing $mcp_config is not valid JSON. Please fix it before running this script again." - exit 1 - fi + if ! jq '.' "$mcp_config" >/dev/null 2>&1; then + error "Existing $mcp_config is not valid JSON. Please fix it before running this script again." + exit 1 + fi - jq \ - --arg path "$server_src" \ - '(.servers //= {}) | + jq \ + --arg path "$server_src" \ + '(.servers //= {}) | .servers.unityMCP = { command: "uv", args: ["--directory", $path, "run", "server.py"], type: "stdio" }' \ - "$mcp_config" > "$tmp" + "$mcp_config" >"$tmp" - mv "$tmp" "$mcp_config" - info "VS Code MCP server configuration updated for UnityMCP." + mv "$tmp" "$mcp_config" + info "VS Code MCP server configuration updated for UnityMCP." } verify_python_version() { - require_command python "python" - local version - version="$( - python - << 'PY' + require_command python "python" + local version + version="$( + python - <<'PY' import sys print("%d.%d.%d" % sys.version_info[:3]) PY - )" - local major minor - IFS='.' read -r major minor _ <<< "$version" - if ((major < 3 || (major == 3 && minor < 12))); then - error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing." - exit 1 - fi - info "Python version $version satisfies requirement (>= 3.12)." + )" + local major minor + IFS='.' read -r major minor _ <<<"$version" + if ((major < 3 || (major == 3 && minor < 12))); then + error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing." + exit 1 + fi + info "Python version $version satisfies requirement (>= 3.12)." } print_next_steps() { - cat << 'EOT' + cat <<'EOT' Next steps: 1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer. @@ -190,22 +190,22 @@ EOT } main() { - if [[ ! -f /etc/arch-release ]]; then - error "This script is intended for Arch Linux." - exit 1 - fi + if [[ ! -f /etc/arch-release ]]; then + error "This script is intended for Arch Linux." + exit 1 + fi - info "Ensuring base dependencies are installed." - require_command sudo "sudo" - require_command pacman "pacman" - ensure_pacman_packages - verify_python_version - install_uv - ensure_unity_hub - sync_unity_mcp_repo - configure_vscode_mcp - print_next_steps - info "Setup complete. Follow the next steps above to finish configuration inside Unity." + info "Ensuring base dependencies are installed." + require_command sudo "sudo" + require_command pacman "pacman" + ensure_pacman_packages + verify_python_version + install_uv + ensure_unity_hub + sync_unity_mcp_repo + configure_vscode_mcp + print_next_steps + info "Setup complete. Follow the next steps above to finish configuration inside Unity." } main "$@" diff --git a/linux_configuration/scripts/single_use/utils/analyze_repo.sh b/linux_configuration/scripts/single_use/utils/analyze_repo.sh index ee7bcc23..5e79b6f4 100755 --- a/linux_configuration/scripts/single_use/utils/analyze_repo.sh +++ b/linux_configuration/scripts/single_use/utils/analyze_repo.sh @@ -113,7 +113,7 @@ find_files() { # Not a git repo - fall back to manual exclusion local find_args=() for i in "${!patterns[@]}"; do - if [ $i -eq 0 ]; then + if [ "$i" -eq 0 ]; then find_args+=(-name "${patterns[$i]}") else find_args+=(-o -name "${patterns[$i]}") @@ -125,7 +125,7 @@ find_files() { # No filtering - find all files local find_args=() for i in "${!patterns[@]}"; do - if [ $i -eq 0 ]; then + if [ "$i" -eq 0 ]; then find_args+=(-name "${patterns[$i]}") else find_args+=(-o -name "${patterns[$i]}") @@ -302,7 +302,7 @@ if [ "$RESPECT_GITIGNORE" = true ] && is_git_repo; then } | sort -u | wc -l) echo "Files: $FILE_COUNT (respecting .gitignore)" elif [ "$RESPECT_GITIGNORE" = true ]; then - echo "Files: $(find . -type f 2>/dev/null | grep -Ev "/($EXCLUDE_DIRS)/" | wc -l) (excluding common dirs)" + echo "Files: $(find . -type f 2>/dev/null | grep -cEv "/($EXCLUDE_DIRS)/") (excluding common dirs)" else echo "Files: $(find . -type f | wc -l)" fi @@ -336,7 +336,7 @@ print_header "STEP 4: Fast Keyword Analysis (Code vs Comments)" fast_count() { local top_n="${1:-50}" if command -v counts &>/dev/null; then - counts 2>/dev/null | head -$((top_n + 1)) | tail -$top_n + counts 2>/dev/null | head -n "$((top_n + 1))" | tail -n "$top_n" else python3 "$SCRIPT_DIR/fast_count.py" "$top_n" fi @@ -387,14 +387,14 @@ HAS_GO=false HAS_RUST=false HAS_JAVA=false -((${LANG_FILES[c]} + ${LANG_FILES[cpp]} + ${LANG_FILES[h]} > 0)) && HAS_C_FAMILY=true -((${LANG_FILES[python]} > 0)) && HAS_PYTHON=true -((${LANG_FILES[javascript]} + ${LANG_FILES[typescript]} > 0)) && HAS_JS_FAMILY=true -((${LANG_FILES[shell]} > 0)) && HAS_SHELL=true -((${LANG_FILES[ruby]} > 0)) && HAS_RUBY=true -((${LANG_FILES[go]} > 0)) && HAS_GO=true -((${LANG_FILES[rust]} > 0)) && HAS_RUST=true -((${LANG_FILES[java]} > 0)) && HAS_JAVA=true +((LANG_FILES[c] + LANG_FILES[cpp] + LANG_FILES[h] > 0)) && HAS_C_FAMILY=true +((LANG_FILES[python] > 0)) && HAS_PYTHON=true +((LANG_FILES[javascript] + LANG_FILES[typescript] > 0)) && HAS_JS_FAMILY=true +((LANG_FILES[shell] > 0)) && HAS_SHELL=true +((LANG_FILES[ruby] > 0)) && HAS_RUBY=true +((LANG_FILES[go] > 0)) && HAS_GO=true +((LANG_FILES[rust] > 0)) && HAS_RUST=true +((LANG_FILES[java] > 0)) && HAS_JAVA=true #------------------------------------------------------------------------------ # Language-specific keyword definitions @@ -735,7 +735,7 @@ if [ -f "$RESULTS_DIR/tags" ]; then print_subheader "Symbol Types Distribution" # Fast: extract single-letter kind code after ;" and count - grep -aoP ';"\t\K[a-z]' "$RESULTS_DIR/tags" 2>/dev/null | fast_count 20 | while read count kind; do + grep -aoP ';"\t\K[a-z]' "$RESULTS_DIR/tags" 2>/dev/null | fast_count 20 | while read -r count kind; do case $kind in f) echo "$count functions" ;; v) echo "$count variables" ;; diff --git a/linux_configuration/scripts/single_use/utils/download_exercism_bulk.sh b/linux_configuration/scripts/single_use/utils/download_exercism_bulk.sh index 3ee31129..b8aa1ea6 100755 --- a/linux_configuration/scripts/single_use/utils/download_exercism_bulk.sh +++ b/linux_configuration/scripts/single_use/utils/download_exercism_bulk.sh @@ -32,14 +32,14 @@ cd "$TRACKS_DIR" # Tracks to download (add/remove as needed) declare -A TRACKS=( - ["python"]="https://github.com/exercism/python.git" - ["c"]="https://github.com/exercism/c.git" - ["cpp"]="https://github.com/exercism/cpp.git" - ["javascript"]="https://github.com/exercism/javascript.git" - ["typescript"]="https://github.com/exercism/typescript.git" - ["rust"]="https://github.com/exercism/rust.git" - ["go"]="https://github.com/exercism/go.git" - ["bash"]="https://github.com/exercism/bash.git" + ["python"]="https://github.com/exercism/python.git" + ["c"]="https://github.com/exercism/c.git" + ["cpp"]="https://github.com/exercism/cpp.git" + ["javascript"]="https://github.com/exercism/javascript.git" + ["typescript"]="https://github.com/exercism/typescript.git" + ["rust"]="https://github.com/exercism/rust.git" + ["go"]="https://github.com/exercism/go.git" + ["bash"]="https://github.com/exercism/bash.git" ) # Optional tracks (uncomment to include) @@ -52,22 +52,22 @@ echo "Downloading ${#TRACKS[@]} tracks to: $TRACKS_DIR" echo "" for track in "${!TRACKS[@]}"; do - url="${TRACKS[$track]}" + url="${TRACKS[$track]}" - if [[ -d $track ]]; then - info "Updating $track..." - (cd "$track" && git pull --quiet) && success "$track updated" - else - info "Cloning $track..." - git clone --depth 1 "$url" && success "$track cloned" - fi + if [[ -d $track ]]; then + info "Updating $track..." + (cd "$track" && git pull --quiet) && success "$track updated" + else + info "Cloning $track..." + git clone --depth 1 "$url" && success "$track cloned" + fi - # Show exercise count - if [[ -d "$track/exercises/practice" ]]; then - count=$(ls "$track/exercises/practice" | wc -l) - echo " → $count practice exercises available" - fi - echo "" + # Show exercise count + if [[ -d "$track/exercises/practice" ]]; then + count=$(find "$track/exercises/practice" -mindepth 1 -maxdepth 1 | wc -l) + echo " → $count practice exercises available" + fi + echo "" done echo "==============================================" @@ -99,8 +99,8 @@ echo "==============================================" echo "" echo "Track summary:" for track in "${!TRACKS[@]}"; do - if [[ -d "$track/exercises/practice" ]]; then - count=$(ls "$track/exercises/practice" 2> /dev/null | wc -l) - printf " %-15s %3d exercises\n" "$track" "$count" - fi + if [[ -d "$track/exercises/practice" ]]; then + count=$(find "$track/exercises/practice" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l) + printf " %-15s %3d exercises\n" "$track" "$count" + fi done | sort diff --git a/linux_configuration/scripts/single_use/utils/generate_study_materials.sh b/linux_configuration/scripts/single_use/utils/generate_study_materials.sh index 1a1b4b07..f05ef508 100755 --- a/linux_configuration/scripts/single_use/utils/generate_study_materials.sh +++ b/linux_configuration/scripts/single_use/utils/generate_study_materials.sh @@ -498,8 +498,10 @@ if [ -d "$PER_LANG_DIR" ]; then } # Process keywords by language - echo "## Language Keywords" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" + { + echo "## Language Keywords" + echo "" + } >>"$DOCS_FILE" for keyword_file in "$PER_LANG_DIR"/keywords_*.txt; do [ ! -f "$keyword_file" ] && continue @@ -523,12 +525,14 @@ if [ -d "$PER_LANG_DIR" ]; then *) display_lang="$lang" ;; esac - echo "### $display_lang Keywords" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" - echo "| Keyword | Count | Documentation |" >>"$DOCS_FILE" - echo "|---------|-------|---------------|" >>"$DOCS_FILE" + { + echo "### $display_lang Keywords" + echo "" + echo "| Keyword | Count | Documentation |" + echo "|---------|-------|---------------|" + } >>"$DOCS_FILE" - head -$TOP_N "$keyword_file" | while read -r count term; do + head -n "$TOP_N" "$keyword_file" | while read -r count term; do [ -z "$term" ] && continue [[ $term =~ ^[#] ]] && continue # Skip comment lines url=$(get_doc_url "$term" "$doc_lang") @@ -538,8 +542,10 @@ if [ -d "$PER_LANG_DIR" ]; then done # Process functions by language - echo "## Function/Method Calls" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" + { + echo "## Function/Method Calls" + echo "" + } >>"$DOCS_FILE" for func_file in "$PER_LANG_DIR"/functions_*.txt; do [ ! -f "$func_file" ] && continue @@ -561,12 +567,14 @@ if [ -d "$PER_LANG_DIR" ]; then *) display_lang="$lang" ;; esac - echo "### $display_lang Functions" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" - echo "| Function | Count | Documentation |" >>"$DOCS_FILE" - echo "|----------|-------|---------------|" >>"$DOCS_FILE" + { + echo "### $display_lang Functions" + echo "" + echo "| Function | Count | Documentation |" + echo "|----------|-------|---------------|" + } >>"$DOCS_FILE" - head -$TOP_N "$func_file" | while read -r count term; do + head -n "$TOP_N" "$func_file" | while read -r count term; do [ -z "$term" ] && continue [[ $term =~ ^(if|for|while|switch|catch|elif)$ ]] && continue url=$(get_doc_url "$term" "$doc_lang") @@ -576,8 +584,10 @@ if [ -d "$PER_LANG_DIR" ]; then done # Process imports by language - echo "## Imports/Includes" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" + { + echo "## Imports/Includes" + echo "" + } >>"$DOCS_FILE" for import_file in "$PER_LANG_DIR"/imports_*.txt; do [ ! -f "$import_file" ] && continue @@ -599,10 +609,12 @@ if [ -d "$PER_LANG_DIR" ]; then *) display_lang="$lang" ;; esac - echo "### $display_lang" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" - echo "| Import | Count | Documentation |" >>"$DOCS_FILE" - echo "|--------|-------|---------------|" >>"$DOCS_FILE" + { + echo "### $display_lang" + echo "" + echo "| Import | Count | Documentation |" + echo "|--------|-------|---------------|" + } >>"$DOCS_FILE" head -20 "$import_file" | while read -r count import; do [ -z "$import" ] && continue @@ -613,7 +625,7 @@ if [ -d "$PER_LANG_DIR" ]; then module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//') url=$(get_doc_url "$module" "$doc_lang") fi - import_escaped=$(echo "$import" | sed 's/|/\\|/g') + import_escaped="${import//|/\\|}" echo "| \`$import_escaped\` | $count | [docs]($url) |" >>"$DOCS_FILE" done echo "" >>"$DOCS_FILE" @@ -624,12 +636,14 @@ else echo -e "${YELLOW}No per-language files found, using combined analysis${NC}" if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then - echo "## Language Keywords" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" - echo "| Keyword | Count | Documentation |" >>"$DOCS_FILE" - echo "|---------|-------|---------------|" >>"$DOCS_FILE" + { + echo "## Language Keywords" + echo "" + echo "| Keyword | Count | Documentation |" + echo "|---------|-------|---------------|" + } >>"$DOCS_FILE" - head -$TOP_N "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do + head -n "$TOP_N" "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do [ -z "$term" ] && continue url=$(get_doc_url "$term" "$PRIMARY_LANG") echo "| \`$term\` | $count | [docs]($url) |" >>"$DOCS_FILE" @@ -638,12 +652,14 @@ else fi if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then - echo "## Function/Method Calls" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" - echo "| Function | Count | Documentation |" >>"$DOCS_FILE" - echo "|----------|-------|---------------|" >>"$DOCS_FILE" + { + echo "## Function/Method Calls" + echo "" + echo "| Function | Count | Documentation |" + echo "|----------|-------|---------------|" + } >>"$DOCS_FILE" - head -$TOP_N "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do + head -n "$TOP_N" "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do [ -z "$term" ] && continue [[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue url=$(get_doc_url "$term" "$PRIMARY_LANG") @@ -653,25 +669,29 @@ else fi if [ -f "$RESULTS_DIR/grep_imports.txt" ]; then - echo "## Imports/Includes" >>"$DOCS_FILE" - echo "" >>"$DOCS_FILE" - echo "| Import | Count | Documentation |" >>"$DOCS_FILE" - echo "|--------|-------|---------------|" >>"$DOCS_FILE" + { + echo "## Imports/Includes" + echo "" + echo "| Import | Count | Documentation |" + echo "|--------|-------|---------------|" + } >>"$DOCS_FILE" head -20 "$RESULTS_DIR/grep_imports.txt" | while read -r count import; do [ -z "$import" ] && continue module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//') url=$(get_doc_url "$module" "$PRIMARY_LANG") - import_escaped=$(echo "$import" | sed 's/|/\\|/g') + import_escaped="${import//|/\\|}" echo "| \`$import_escaped\` | $count | [docs]($url) |" >>"$DOCS_FILE" done echo "" >>"$DOCS_FILE" fi fi -echo "" >>"$DOCS_FILE" -echo "---" >>"$DOCS_FILE" -echo "*Generated by analyze_repo.sh + generate_study_materials.sh*" >>"$DOCS_FILE" +{ + echo "" + echo "---" + echo "*Generated by analyze_repo.sh + generate_study_materials.sh*" +} >>"$DOCS_FILE" echo -e "${GREEN}Created: $DOCS_FILE${NC}" #============================================================================== @@ -694,7 +714,7 @@ EOF # Generate cards for top keywords if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then echo "# Keywords" >>"$ANKI_FILE" - head -$TOP_N "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do + head -n "$TOP_N" "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do [ -z "$term" ] && continue url=$(get_doc_url "$term" "$PRIMARY_LANG") @@ -730,9 +750,11 @@ fi # Generate cards for top functions if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then - echo "" >>"$ANKI_FILE" - echo "# Functions" >>"$ANKI_FILE" - head -$TOP_N "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do + { + echo "" + echo "# Functions" + } >>"$ANKI_FILE" + head -n "$TOP_N" "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do [ -z "$term" ] && continue [[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue url=$(get_doc_url "$term" "$PRIMARY_LANG") @@ -780,7 +802,7 @@ generate_keywords_with_docs() { local keywords_file="$RESULTS_DIR/grep_keywords.txt" [ ! -f "$keywords_file" ] && echo "No keywords found" && return - head -$TOP_N "$keywords_file" | grep -v '^#' | while read -r line; do + head -n "$TOP_N" "$keywords_file" | grep -v '^#' | while read -r line; do local count count=$(echo "$line" | awk '{print $1}') local keyword @@ -797,7 +819,7 @@ generate_functions_with_docs() { local functions_file="$RESULTS_DIR/grep_function_calls.txt" [ ! -f "$functions_file" ] && echo "No functions found" && return - head -$TOP_N "$functions_file" | grep -v '^#' | while read -r line; do + head -n "$TOP_N" "$functions_file" | grep -v '^#' | while read -r line; do local count count=$(echo "$line" | awk '{print $1}') local func diff --git a/linux_configuration/scripts/single_use/utils/install_exercism.sh b/linux_configuration/scripts/single_use/utils/install_exercism.sh index 078a108f..09f97973 100755 --- a/linux_configuration/scripts/single_use/utils/install_exercism.sh +++ b/linux_configuration/scripts/single_use/utils/install_exercism.sh @@ -34,286 +34,297 @@ echo "" # Install Exercism CLI install_exercism_cli() { - if command -v exercism &> /dev/null; then - local version - version=$(exercism version 2> /dev/null | head -1) - success "Exercism CLI already installed: $version" - return 0 - fi + if command -v exercism &>/dev/null; then + local version + version=$(exercism version 2>/dev/null | head -1) + success "Exercism CLI already installed: $version" + return 0 + fi - echo "Installing Exercism CLI..." + echo "Installing Exercism CLI..." - # Try package managers first - if command -v pacman &> /dev/null; then - # Check AUR - if command -v yay &> /dev/null; then - yay -S --noconfirm exercism-bin - success "Exercism CLI installed via AUR" - return 0 - elif command -v paru &> /dev/null; then - paru -S --noconfirm exercism-bin - success "Exercism CLI installed via AUR" - return 0 - fi - elif command -v brew &> /dev/null; then - brew install exercism - success "Exercism CLI installed via Homebrew" - return 0 - fi + # Try package managers first + if command -v pacman &>/dev/null; then + # Check AUR + if command -v yay &>/dev/null; then + yay -S --noconfirm exercism-bin + success "Exercism CLI installed via AUR" + return 0 + elif command -v paru &>/dev/null; then + paru -S --noconfirm exercism-bin + success "Exercism CLI installed via AUR" + return 0 + fi + elif command -v brew &>/dev/null; then + brew install exercism + success "Exercism CLI installed via Homebrew" + return 0 + fi - # Manual installation from GitHub releases - info "Installing from GitHub releases..." + # Manual installation from GitHub releases + info "Installing from GitHub releases..." - local arch - case "$(uname -m)" in - x86_64) arch="x86_64" ;; - aarch64 | arm64) arch="arm64" ;; - armv7l) arch="armv7" ;; - i686) arch="i386" ;; - *) - error "Unsupported architecture: $(uname -m)" - return 1 - ;; - esac + local arch + case "$(uname -m)" in + x86_64) arch="x86_64" ;; + aarch64 | arm64) arch="arm64" ;; + armv7l) arch="armv7" ;; + i686) arch="i386" ;; + *) + error "Unsupported architecture: $(uname -m)" + return 1 + ;; + esac - local os="linux" - [[ "$(uname -s)" == "Darwin" ]] && os="darwin" + local os="linux" + [[ "$(uname -s)" == "Darwin" ]] && os="darwin" - # Get latest release - local latest_url="https://api.github.com/repos/exercism/cli/releases/latest" - local download_url + # Get latest release + local latest_url="https://api.github.com/repos/exercism/cli/releases/latest" + local download_url - download_url=$(curl -fsSL "$latest_url" | grep "browser_download_url.*${os}-${arch}" | head -1 | cut -d '"' -f 4) + download_url=$(curl -fsSL "$latest_url" | grep "browser_download_url.*${os}-${arch}" | head -1 | cut -d '"' -f 4) - if [[ -z $download_url ]]; then - error "Could not find download URL for your system" - echo "Please install manually from: https://exercism.org/docs/using/solving-exercises/working-locally" - return 1 - fi + if [[ -z $download_url ]]; then + error "Could not find download URL for your system" + echo "Please install manually from: https://exercism.org/docs/using/solving-exercises/working-locally" + return 1 + fi - echo "Downloading from: $download_url" - local temp_dir - temp_dir=$(mktemp -d) + echo "Downloading from: $download_url" + local temp_dir + temp_dir=$(mktemp -d) - curl -fL --progress-bar "$download_url" -o "$temp_dir/exercism.tar.gz" - tar -xzf "$temp_dir/exercism.tar.gz" -C "$temp_dir" + curl -fL --progress-bar "$download_url" -o "$temp_dir/exercism.tar.gz" + tar -xzf "$temp_dir/exercism.tar.gz" -C "$temp_dir" - # Install to ~/.local/bin - mkdir -p "$HOME/.local/bin" - mv "$temp_dir/exercism" "$HOME/.local/bin/" - chmod +x "$HOME/.local/bin/exercism" + # Install to ~/.local/bin + mkdir -p "$HOME/.local/bin" + mv "$temp_dir/exercism" "$HOME/.local/bin/" + chmod +x "$HOME/.local/bin/exercism" - rm -rf "$temp_dir" + rm -rf "$temp_dir" - success "Exercism CLI installed to ~/.local/bin/exercism" + success "Exercism CLI installed to ~/.local/bin/exercism" - # Check PATH - if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - warn "Add ~/.local/bin to your PATH:" - echo ' export PATH="$HOME/.local/bin:$PATH"' - fi + # Check PATH + if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + warn "Add ~/.local/bin to your PATH:" + # Printing a line for the USER to paste into their shell rc: $HOME and + # $PATH must appear literally, not be resolved to this machine's values. + # shellcheck disable=SC2016 + echo ' export PATH="$HOME/.local/bin:$PATH"' + fi } # Configure exercism workspace configure_exercism() { - echo "" - echo "=== Configuring Exercism ===" + echo "" + echo "=== Configuring Exercism ===" - mkdir -p "$EXERCISM_DIR" + mkdir -p "$EXERCISM_DIR" - # Check if already configured - if exercism configure 2>&1 | grep -q "workspace"; then - success "Exercism already configured" - else - # Set workspace directory - exercism configure --workspace="$EXERCISM_DIR" - success "Workspace set to: $EXERCISM_DIR" - fi + # Check if already configured + if exercism configure 2>&1 | grep -q "workspace"; then + success "Exercism already configured" + else + # Set workspace directory + exercism configure --workspace="$EXERCISM_DIR" + success "Workspace set to: $EXERCISM_DIR" + fi - echo "" - info "To fully configure Exercism with your account:" - echo " 1. Create free account at https://exercism.org" - echo " 2. Go to https://exercism.org/settings/api_cli" - echo " 3. Copy your API token" - echo " 4. Run: exercism configure --token=YOUR_TOKEN" - echo "" + echo "" + info "To fully configure Exercism with your account:" + echo " 1. Create free account at https://exercism.org" + echo " 2. Go to https://exercism.org/settings/api_cli" + echo " 3. Copy your API token" + echo " 4. Run: exercism configure --token=YOUR_TOKEN" + echo "" } # Install test runners for languages install_test_runners() { - echo "" - echo "=== Installing Test Runners ===" - echo "" + echo "" + echo "=== Installing Test Runners ===" + echo "" - # Python - pytest - if command -v python3 &> /dev/null; then - if python3 -c "import pytest" 2> /dev/null; then - success "Python: pytest already installed" - else - info "Installing pytest for Python exercises..." - pip3 install --user pytest 2> /dev/null && success "Python: pytest installed" || warn "Python: install pytest manually" - fi - fi + # Python - pytest + if command -v python3 &>/dev/null; then + if python3 -c "import pytest" 2>/dev/null; then + success "Python: pytest already installed" + else + info "Installing pytest for Python exercises..." + if pip3 install --user pytest 2>/dev/null; then + success "Python: pytest installed" + else + warn "Python: install pytest manually" + fi + fi + fi - # JavaScript/TypeScript - Node.js + npm - if command -v node &> /dev/null; then - success "JavaScript/TypeScript: Node.js available ($(node --version))" - info " Tests run with: npm test (or jest)" - else - warn "JavaScript/TypeScript: Install Node.js for JS/TS exercises" - fi + # JavaScript/TypeScript - Node.js + npm + if command -v node &>/dev/null; then + success "JavaScript/TypeScript: Node.js available ($(node --version))" + info " Tests run with: npm test (or jest)" + else + warn "JavaScript/TypeScript: Install Node.js for JS/TS exercises" + fi - # C - gcc + criterion/cmocka - if command -v gcc &> /dev/null; then - success "C: gcc available" - info " Some C exercises use Unity test framework (included in exercise)" - else - warn "C: Install gcc for C exercises" - fi + # C - gcc + criterion/cmocka + if command -v gcc &>/dev/null; then + success "C: gcc available" + info " Some C exercises use Unity test framework (included in exercise)" + else + warn "C: Install gcc for C exercises" + fi - # C++ - g++ + Catch2/doctest - if command -v g++ &> /dev/null; then - success "C++: g++ available" - info " C++ exercises use Catch2 (header-only, included in exercise)" - else - warn "C++: Install g++ for C++ exercises" - fi + # C++ - g++ + Catch2/doctest + if command -v g++ &>/dev/null; then + success "C++: g++ available" + info " C++ exercises use Catch2 (header-only, included in exercise)" + else + warn "C++: Install g++ for C++ exercises" + fi - # Rust - if command -v cargo &> /dev/null; then - success "Rust: cargo available (tests with: cargo test)" - fi + # Rust + if command -v cargo &>/dev/null; then + success "Rust: cargo available (tests with: cargo test)" + fi - # Go - if command -v go &> /dev/null; then - success "Go: go available (tests with: go test)" - fi + # Go + if command -v go &>/dev/null; then + success "Go: go available (tests with: go test)" + fi } # Download exercises for a track (language) download_track() { - local track="$1" - local count="${2:-10}" + local track="$1" + local count="${2:-10}" - echo "" - info "Downloading $count exercises for $track track..." + echo "" + info "Downloading $count exercises for $track track..." - # Get list of exercises - local exercises - exercises=$(curl -fsSL "https://exercism.org/api/v2/tracks/${track}/exercises" 2> /dev/null | - grep -oP '"slug":"\K[^"]+' | head -n "$count") + # Get list of exercises + local exercises + exercises=$(curl -fsSL "https://exercism.org/api/v2/tracks/${track}/exercises" 2>/dev/null | + grep -oP '"slug":"\K[^"]+' | head -n "$count") - if [[ -z $exercises ]]; then - warn "Could not fetch exercise list for $track" - return 1 - fi + if [[ -z $exercises ]]; then + warn "Could not fetch exercise list for $track" + return 1 + fi - local downloaded=0 - for exercise in $exercises; do - local exercise_dir="$EXERCISM_DIR/$track/$exercise" - if [[ -d $exercise_dir ]]; then - echo " [exists] $exercise" - else - if exercism download --track="$track" --exercise="$exercise" 2> /dev/null; then - echo " [downloaded] $exercise" - ((downloaded++)) - else - echo " [failed] $exercise (may require auth)" - fi - fi - done + local downloaded=0 + for exercise in $exercises; do + local exercise_dir="$EXERCISM_DIR/$track/$exercise" + if [[ -d $exercise_dir ]]; then + echo " [exists] $exercise" + else + if exercism download --track="$track" --exercise="$exercise" 2>/dev/null; then + echo " [downloaded] $exercise" + ((downloaded++)) + else + echo " [failed] $exercise (may require auth)" + fi + fi + done - success "Downloaded $downloaded new exercises for $track" + success "Downloaded $downloaded new exercises for $track" } # Show available tracks and usage show_usage() { - echo "" - echo "==============================================" - echo " Exercism Usage Guide" - echo "==============================================" - echo "" - echo -e "${CYAN}Download exercises:${NC}" - echo " exercism download --track=python --exercise=hello-world" - echo " exercism download --track=javascript --exercise=two-fer" - echo " exercism download --track=c --exercise=isogram" - echo "" - echo -e "${CYAN}Run tests locally:${NC}" - echo " Python: cd ~/exercism/python/hello-world && pytest" - echo " JavaScript: cd ~/exercism/javascript/hello-world && npm test" - echo " TypeScript: cd ~/exercism/typescript/hello-world && npm test" - echo " C: cd ~/exercism/c/hello-world && make test" - echo " C++: cd ~/exercism/cpp/hello-world && make" - echo " Rust: cd ~/exercism/rust/hello-world && cargo test" - echo " Go: cd ~/exercism/go/hello-world && go test" - echo "" - echo -e "${CYAN}Submit solution (when online):${NC}" - echo " exercism submit solution.py" - echo "" - echo -e "${CYAN}Popular tracks:${NC}" - echo " python, javascript, typescript, c, cpp, rust, go, java, ruby" - echo " bash, elixir, haskell, kotlin, swift, csharp, php, sql" - echo "" - echo -e "${CYAN}Batch download (requires API token):${NC}" - echo " # Download first 20 Python exercises:" - echo ' for ex in $(exercism download --track=python 2>&1 | head -20); do' - echo ' exercism download --track=python --exercise=$ex' - echo " done" - echo "" - echo "Exercises are in: $EXERCISM_DIR" - echo "" - echo "==============================================" + echo "" + echo "==============================================" + echo " Exercism Usage Guide" + echo "==============================================" + echo "" + echo -e "${CYAN}Download exercises:${NC}" + echo " exercism download --track=python --exercise=hello-world" + echo " exercism download --track=javascript --exercise=two-fer" + echo " exercism download --track=c --exercise=isogram" + echo "" + echo -e "${CYAN}Run tests locally:${NC}" + echo " Python: cd ~/exercism/python/hello-world && pytest" + echo " JavaScript: cd ~/exercism/javascript/hello-world && npm test" + echo " TypeScript: cd ~/exercism/typescript/hello-world && npm test" + echo " C: cd ~/exercism/c/hello-world && make test" + echo " C++: cd ~/exercism/cpp/hello-world && make" + echo " Rust: cd ~/exercism/rust/hello-world && cargo test" + echo " Go: cd ~/exercism/go/hello-world && go test" + echo "" + echo -e "${CYAN}Submit solution (when online):${NC}" + echo " exercism submit solution.py" + echo "" + echo -e "${CYAN}Popular tracks:${NC}" + echo " python, javascript, typescript, c, cpp, rust, go, java, ruby" + echo " bash, elixir, haskell, kotlin, swift, csharp, php, sql" + echo "" + echo -e "${CYAN}Batch download (requires API token):${NC}" + echo " # Download first 20 Python exercises:" + # Example commands printed for the user to copy; $(...) and $ex must stay + # literal rather than execute here. + # shellcheck disable=SC2016 + echo ' for ex in $(exercism download --track=python 2>&1 | head -20); do' + # shellcheck disable=SC2016 + echo ' exercism download --track=python --exercise=$ex' + echo " done" + echo "" + echo "Exercises are in: $EXERCISM_DIR" + echo "" + echo "==============================================" } # Main main() { - # Step 1: Install CLI - echo "" - echo "=== Step 1: Installing Exercism CLI ===" - install_exercism_cli + # Step 1: Install CLI + echo "" + echo "=== Step 1: Installing Exercism CLI ===" + install_exercism_cli - # Step 2: Configure - configure_exercism + # Step 2: Configure + configure_exercism - # Step 3: Install test runners - install_test_runners + # Step 3: Install test runners + install_test_runners - # Step 4: Download sample exercises - echo "" - echo "=== Step 4: Downloading Sample Exercises ===" - echo "" - echo "Downloading a few starter exercises for common languages..." - echo "(Full download requires API token from exercism.org)" - echo "" + # Step 4: Download sample exercises + echo "" + echo "=== Step 4: Downloading Sample Exercises ===" + echo "" + echo "Downloading a few starter exercises for common languages..." + echo "(Full download requires API token from exercism.org)" + echo "" - # Try to download hello-world for each track - local tracks=("python" "javascript" "typescript" "c" "cpp") + # Try to download hello-world for each track + local tracks=("python" "javascript" "typescript" "c" "cpp") - for track in "${tracks[@]}"; do - local exercise_dir="$EXERCISM_DIR/$track/hello-world" - if [[ -d $exercise_dir ]]; then - echo " [$track] hello-world already exists" - else - if exercism download --track="$track" --exercise="hello-world" 2> /dev/null; then - success "[$track] hello-world downloaded" - else - warn "[$track] hello-world requires authentication" - fi - fi - done + for track in "${tracks[@]}"; do + local exercise_dir="$EXERCISM_DIR/$track/hello-world" + if [[ -d $exercise_dir ]]; then + echo " [$track] hello-world already exists" + else + if exercism download --track="$track" --exercise="hello-world" 2>/dev/null; then + success "[$track] hello-world downloaded" + else + warn "[$track] hello-world requires authentication" + fi + fi + done - # Show usage - show_usage + # Show usage + show_usage - echo "" - success "Installation complete!" - echo "" - echo "Next steps:" - echo " 1. Sign up at https://exercism.org (free)" - echo " 2. Get your token from https://exercism.org/settings/api_cli" - echo " 3. Run: exercism configure --token=YOUR_TOKEN" - echo " 4. Download exercises and code offline!" - echo "" + echo "" + success "Installation complete!" + echo "" + echo "Next steps:" + echo " 1. Sign up at https://exercism.org (free)" + echo " 2. Get your token from https://exercism.org/settings/api_cli" + echo " 3. Run: exercism configure --token=YOUR_TOKEN" + echo " 4. Download exercises and code offline!" + echo "" } main "$@" diff --git a/linux_configuration/scripts/single_use/utils/install_plagiarism_tools.sh b/linux_configuration/scripts/single_use/utils/install_plagiarism_tools.sh index 1de0b64a..ed8b4572 100755 --- a/linux_configuration/scripts/single_use/utils/install_plagiarism_tools.sh +++ b/linux_configuration/scripts/single_use/utils/install_plagiarism_tools.sh @@ -39,40 +39,43 @@ echo "" echo "=== 1. Installing Python NLP-based Plagiarism Tools ===" # Check for Python 3 -if ! command -v python3 &> /dev/null; then - error "Python 3 is required but not installed." - exit 1 +if ! command -v python3 &>/dev/null; then + error "Python 3 is required but not installed." + exit 1 fi # Create virtual environment if [ ! -d "$VENV_DIR" ]; then - echo "Creating Python virtual environment..." - python3 -m venv "$VENV_DIR" - success "Virtual environment created at $VENV_DIR" + echo "Creating Python virtual environment..." + python3 -m venv "$VENV_DIR" + success "Virtual environment created at $VENV_DIR" else - warn "Virtual environment already exists at $VENV_DIR" + warn "Virtual environment already exists at $VENV_DIR" fi # Activate and install packages +# Created at runtime by `python -m venv` above, so there is nothing on disk +# for the linter to follow at check time. +# shellcheck disable=SC1091 source "$VENV_DIR/bin/activate" echo "Installing Python packages for text similarity detection..." pip install --upgrade pip pip install --progress-bar on \ - scikit-learn \ - nltk \ - spacy \ - gensim \ - numpy \ - pandas \ - python-docx \ - PyPDF2 \ - beautifulsoup4 \ - lxml \ - textdistance \ - fuzzywuzzy \ - python-Levenshtein + scikit-learn \ + nltk \ + spacy \ + gensim \ + numpy \ + pandas \ + python-docx \ + PyPDF2 \ + beautifulsoup4 \ + lxml \ + textdistance \ + fuzzywuzzy \ + python-Levenshtein success "Python NLP packages installed" @@ -90,11 +93,11 @@ success "NLTK data downloaded" # Download spaCy English model (small) echo "Downloading spaCy English model..." -python3 -m spacy download en_core_web_sm 2> /dev/null || warn "spaCy model download may need manual install: python -m spacy download en_core_web_sm" +python3 -m spacy download en_core_web_sm 2>/dev/null || warn "spaCy model download may need manual install: python -m spacy download en_core_web_sm" success "spaCy model installed" # Create a simple plagiarism checker script -cat > "$INSTALL_DIR/check_plagiarism.py" << 'PYEOF' +cat >"$INSTALL_DIR/check_plagiarism.py" <<'PYEOF' #!/usr/bin/env python3 """ Simple Text Plagiarism Checker @@ -325,7 +328,7 @@ success "Created plagiarism checker script at $INSTALL_DIR/check_plagiarism.py" # Create convenience wrapper mkdir -p "$HOME/.local/bin" -cat > "$HOME/.local/bin/plagcheck" << WRAPEOF +cat >"$HOME/.local/bin/plagcheck" < /dev/null; then - # Clone a text-based similarity tool - git clone --depth 1 https://github.com/Zedeldi/sherlock-py.git "$SHERLOCK_DIR" 2> /dev/null || { - warn "Could not clone sherlock-py, trying alternative..." - # Alternative: Create a simple n-gram based sherlock - mkdir -p "$SHERLOCK_DIR" - cat > "$SHERLOCK_DIR/sherlock.py" << 'SHERLOCKEOF' + # There are several Sherlock implementations; using a popular Python one + if command -v git &>/dev/null; then + # Clone a text-based similarity tool + git clone --depth 1 https://github.com/Zedeldi/sherlock-py.git "$SHERLOCK_DIR" 2>/dev/null || { + warn "Could not clone sherlock-py, trying alternative..." + # Alternative: Create a simple n-gram based sherlock + mkdir -p "$SHERLOCK_DIR" + cat >"$SHERLOCK_DIR/sherlock.py" <<'SHERLOCKEOF' #!/usr/bin/env python3 """ Sherlock - Simple text plagiarism detector using n-gram fingerprinting. @@ -443,14 +446,14 @@ def main(): if __name__ == '__main__': main() SHERLOCKEOF - chmod +x "$SHERLOCK_DIR/sherlock.py" - } - success "Sherlock installed at $SHERLOCK_DIR" - else - warn "Git not available, skipping Sherlock installation" - fi + chmod +x "$SHERLOCK_DIR/sherlock.py" + } + success "Sherlock installed at $SHERLOCK_DIR" + else + warn "Git not available, skipping Sherlock installation" + fi else - warn "Sherlock already installed at $SHERLOCK_DIR" + warn "Sherlock already installed at $SHERLOCK_DIR" fi # ------------------------------------------------------------------------------ @@ -459,17 +462,17 @@ fi echo "" echo "=== 3. Checking for Ferret (Java-based plagiarism tool) ===" -if command -v java &> /dev/null; then - FERRET_DIR="$INSTALL_DIR/ferret" - if [ ! -d "$FERRET_DIR" ]; then - mkdir -p "$FERRET_DIR" - echo "Ferret is a Java-based tool from University of Hertfordshire." - echo "Download manually from: https://homepages.herts.ac.uk/~comqcln/Ferret/" - echo "Place JAR file in: $FERRET_DIR" - warn "Ferret requires manual download (academic license)" - fi +if command -v java &>/dev/null; then + FERRET_DIR="$INSTALL_DIR/ferret" + if [ ! -d "$FERRET_DIR" ]; then + mkdir -p "$FERRET_DIR" + echo "Ferret is a Java-based tool from University of Hertfordshire." + echo "Download manually from: https://homepages.herts.ac.uk/~comqcln/Ferret/" + echo "Place JAR file in: $FERRET_DIR" + warn "Ferret requires manual download (academic license)" + fi else - warn "Java not installed, skipping Ferret" + warn "Java not installed, skipping Ferret" fi # ------------------------------------------------------------------------------ @@ -478,16 +481,16 @@ fi echo "" echo "=== 4. WCopyfind Information (Windows tool, needs Wine) ===" -if command -v wine &> /dev/null; then - echo "Wine is available. WCopyfind can be run via Wine." - echo "Download from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/" - echo "Run with: wine /path/to/WCopyfind.exe" - warn "WCopyfind requires manual download" +if command -v wine &>/dev/null; then + echo "Wine is available. WCopyfind can be run via Wine." + echo "Download from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/" + echo "Run with: wine /path/to/WCopyfind.exe" + warn "WCopyfind requires manual download" else - echo "Wine not installed. To use WCopyfind:" - echo " 1. Install wine: sudo apt install wine (or equivalent)" - echo " 2. Download WCopyfind from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/" - warn "WCopyfind skipped (Wine not available)" + echo "Wine not installed. To use WCopyfind:" + echo " 1. Install wine: sudo apt install wine (or equivalent)" + echo " 2. Download WCopyfind from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/" + warn "WCopyfind skipped (Wine not available)" fi # ------------------------------------------------------------------------------ @@ -521,12 +524,18 @@ echo " plagcheck thesis_v1.pdf thesis_v2.pdf --detailed" echo " plagcheck --dir ./student_papers/ --threshold 0.4" echo "" echo "Note: Ensure ~/.local/bin is in your PATH:" +# Printed for the user to paste into their shell rc: $HOME and $PATH must +# stay literal rather than resolve to this machine's values. +# shellcheck disable=SC2016 echo ' export PATH="$HOME/.local/bin:$PATH"' echo "" echo "==============================================" # Add to PATH reminder if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - warn "Add ~/.local/bin to your PATH by adding this to ~/.bashrc or ~/.zshrc:" - echo ' export PATH="$HOME/.local/bin:$PATH"' + warn "Add ~/.local/bin to your PATH by adding this to ~/.bashrc or ~/.zshrc:" + # Printed for the user to paste into their shell rc: $HOME and $PATH must + # stay literal rather than resolve to this machine's values. + # shellcheck disable=SC2016 + echo ' export PATH="$HOME/.local/bin:$PATH"' fi diff --git a/linux_configuration/scripts/single_use/utils/lookup_docs.sh b/linux_configuration/scripts/single_use/utils/lookup_docs.sh index 80a2df9c..30b0133e 100755 --- a/linux_configuration/scripts/single_use/utils/lookup_docs.sh +++ b/linux_configuration/scripts/single_use/utils/lookup_docs.sh @@ -20,400 +20,400 @@ INDEX_DIR="$DOCS_DIR/.index" # Colors - only use if stdout is a terminal if [ -t 1 ]; then - RED='\033[0;31m' - GREEN='\033[0;32m' - BLUE='\033[0;34m' - YELLOW='\033[1;33m' - CYAN='\033[0;36m' - NC='\033[0m' + RED='\033[0;31m' + GREEN='\033[0;32m' + BLUE='\033[0;34m' + YELLOW='\033[1;33m' + CYAN='\033[0;36m' + NC='\033[0m' else - RED='' - GREEN='' - BLUE='' - YELLOW='' - CYAN='' - NC='' + RED='' + GREEN='' + BLUE='' + YELLOW='' + CYAN='' + NC='' fi #============================================================================== # Python-specific lookup #============================================================================== lookup_python() { - local term="$1" - local in_module="$2" # Optional: look for term within this module - local doc_dir="$DOCS_DIR/python" - local result="" - local desc="" + local term="$1" + local in_module="$2" # Optional: look for term within this module + local doc_dir="$DOCS_DIR/python" + local result="" + local desc="" - # Normalize term (preserve case for True/False/None) - local term_lower - term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') + # Normalize term (preserve case for True/False/None) + local term_lower + term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') - # If looking for a term within a specific module - if [ -n "$in_module" ]; then - local module_lower - module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]') + # If looking for a term within a specific module + if [ -n "$in_module" ]; then + local module_lower + module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]') - if [ -f "$doc_dir/library/${module_lower}.html" ]; then - # Find anchor for the specific item in the module - local anchor - anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2> /dev/null | head -1 | sed 's/id="//;s/"//') + if [ -f "$doc_dir/library/${module_lower}.html" ]; then + # Find anchor for the specific item in the module + local anchor + anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2>/dev/null | head -1 | sed 's/id="//;s/"//') - if [ -n "$anchor" ]; then - result="$doc_dir/library/${module_lower}.html#$anchor" - desc="Python: $in_module.$term" - else - # Just link to the module - result="$doc_dir/library/${module_lower}.html" - desc="Python: $term in module $in_module" - fi - echo "$result|$desc" - return 0 - fi - fi + if [ -n "$anchor" ]; then + result="$doc_dir/library/${module_lower}.html#$anchor" + desc="Python: $in_module.$term" + else + # Just link to the module + result="$doc_dir/library/${module_lower}.html" + desc="Python: $term in module $in_module" + fi + echo "$result|$desc" + return 0 + fi + fi - #-------------------------------------------------------------------------- - # PRIORITY 1: Python keywords - map to exact documentation locations - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- + # PRIORITY 1: Python keywords - map to exact documentation locations + #-------------------------------------------------------------------------- - # Compound statements (reference/compound_stmts.html) - case "$term_lower" in - if | elif | else) - result="$doc_dir/reference/compound_stmts.html#if" - desc="Python: if statement" - ;; - for) - result="$doc_dir/reference/compound_stmts.html#for" - desc="Python: for statement" - ;; - while) - result="$doc_dir/reference/compound_stmts.html#while" - desc="Python: while statement" - ;; - def) - result="$doc_dir/reference/compound_stmts.html#def" - desc="Python: function definition" - ;; - class) - result="$doc_dir/reference/compound_stmts.html#class" - desc="Python: class definition" - ;; - try | except | finally) - result="$doc_dir/reference/compound_stmts.html#try" - desc="Python: try statement" - ;; - with) - result="$doc_dir/reference/compound_stmts.html#with" - desc="Python: with statement" - ;; - async) - result="$doc_dir/reference/compound_stmts.html#async" - desc="Python: async definition" - ;; - match | case) - result="$doc_dir/reference/compound_stmts.html#match" - desc="Python: match statement" - ;; - esac + # Compound statements (reference/compound_stmts.html) + case "$term_lower" in + if | elif | else) + result="$doc_dir/reference/compound_stmts.html#if" + desc="Python: if statement" + ;; + for) + result="$doc_dir/reference/compound_stmts.html#for" + desc="Python: for statement" + ;; + while) + result="$doc_dir/reference/compound_stmts.html#while" + desc="Python: while statement" + ;; + def) + result="$doc_dir/reference/compound_stmts.html#def" + desc="Python: function definition" + ;; + class) + result="$doc_dir/reference/compound_stmts.html#class" + desc="Python: class definition" + ;; + try | except | finally) + result="$doc_dir/reference/compound_stmts.html#try" + desc="Python: try statement" + ;; + with) + result="$doc_dir/reference/compound_stmts.html#with" + desc="Python: with statement" + ;; + async) + result="$doc_dir/reference/compound_stmts.html#async" + desc="Python: async definition" + ;; + match | case) + result="$doc_dir/reference/compound_stmts.html#match" + desc="Python: match statement" + ;; + esac - # Simple statements (reference/simple_stmts.html) - if [ -z "$result" ]; then - case "$term_lower" in - return) - result="$doc_dir/reference/simple_stmts.html#return" - desc="Python: return statement" - ;; - pass) - result="$doc_dir/reference/simple_stmts.html#pass" - desc="Python: pass statement" - ;; - break) - result="$doc_dir/reference/simple_stmts.html#break" - desc="Python: break statement" - ;; - continue) - result="$doc_dir/reference/simple_stmts.html#continue" - desc="Python: continue statement" - ;; - import | from) - result="$doc_dir/reference/simple_stmts.html#import" - desc="Python: import statement" - ;; - raise) - result="$doc_dir/reference/simple_stmts.html#raise" - desc="Python: raise statement" - ;; - assert) - result="$doc_dir/reference/simple_stmts.html#assert" - desc="Python: assert statement" - ;; - yield) - result="$doc_dir/reference/simple_stmts.html#yield" - desc="Python: yield expression" - ;; - del) - result="$doc_dir/reference/simple_stmts.html#del" - desc="Python: del statement" - ;; - global) - result="$doc_dir/reference/simple_stmts.html#global" - desc="Python: global statement" - ;; - nonlocal) - result="$doc_dir/reference/simple_stmts.html#nonlocal" - desc="Python: nonlocal statement" - ;; - type) - result="$doc_dir/reference/simple_stmts.html#type" - desc="Python: type alias statement" - ;; - esac - fi + # Simple statements (reference/simple_stmts.html) + if [ -z "$result" ]; then + case "$term_lower" in + return) + result="$doc_dir/reference/simple_stmts.html#return" + desc="Python: return statement" + ;; + pass) + result="$doc_dir/reference/simple_stmts.html#pass" + desc="Python: pass statement" + ;; + break) + result="$doc_dir/reference/simple_stmts.html#break" + desc="Python: break statement" + ;; + continue) + result="$doc_dir/reference/simple_stmts.html#continue" + desc="Python: continue statement" + ;; + import | from) + result="$doc_dir/reference/simple_stmts.html#import" + desc="Python: import statement" + ;; + raise) + result="$doc_dir/reference/simple_stmts.html#raise" + desc="Python: raise statement" + ;; + assert) + result="$doc_dir/reference/simple_stmts.html#assert" + desc="Python: assert statement" + ;; + yield) + result="$doc_dir/reference/simple_stmts.html#yield" + desc="Python: yield expression" + ;; + del) + result="$doc_dir/reference/simple_stmts.html#del" + desc="Python: del statement" + ;; + global) + result="$doc_dir/reference/simple_stmts.html#global" + desc="Python: global statement" + ;; + nonlocal) + result="$doc_dir/reference/simple_stmts.html#nonlocal" + desc="Python: nonlocal statement" + ;; + type) + result="$doc_dir/reference/simple_stmts.html#type" + desc="Python: type alias statement" + ;; + esac + fi - # Expressions/operators (reference/expressions.html) - if [ -z "$result" ]; then - case "$term_lower" in - and) - result="$doc_dir/reference/expressions.html#and" - desc="Python: and operator" - ;; - or) - result="$doc_dir/reference/expressions.html#or" - desc="Python: or operator" - ;; - not) - result="$doc_dir/reference/expressions.html#not" - desc="Python: not operator" - ;; - in) - result="$doc_dir/reference/expressions.html#in" - desc="Python: in operator" - ;; - is) - result="$doc_dir/reference/expressions.html#is" - desc="Python: is operator" - ;; - lambda) - result="$doc_dir/reference/expressions.html#lambda" - desc="Python: lambda expression" - ;; - await) - result="$doc_dir/reference/expressions.html#await" - desc="Python: await expression" - ;; - esac - fi + # Expressions/operators (reference/expressions.html) + if [ -z "$result" ]; then + case "$term_lower" in + and) + result="$doc_dir/reference/expressions.html#and" + desc="Python: and operator" + ;; + or) + result="$doc_dir/reference/expressions.html#or" + desc="Python: or operator" + ;; + not) + result="$doc_dir/reference/expressions.html#not" + desc="Python: not operator" + ;; + in) + result="$doc_dir/reference/expressions.html#in" + desc="Python: in operator" + ;; + is) + result="$doc_dir/reference/expressions.html#is" + desc="Python: is operator" + ;; + lambda) + result="$doc_dir/reference/expressions.html#lambda" + desc="Python: lambda expression" + ;; + await) + result="$doc_dir/reference/expressions.html#await" + desc="Python: await expression" + ;; + esac + fi - # Built-in constants (library/constants.html) - case-sensitive! - if [ -z "$result" ]; then - case "$term" in - True | False) - result="$doc_dir/library/constants.html#$term" - desc="Python: $term constant" - ;; - None) - result="$doc_dir/library/constants.html#None" - desc="Python: None constant" - ;; - Ellipsis) - result="$doc_dir/library/constants.html#Ellipsis" - desc="Python: Ellipsis constant" - ;; - NotImplemented) - result="$doc_dir/library/constants.html#NotImplemented" - desc="Python: NotImplemented constant" - ;; - esac - fi + # Built-in constants (library/constants.html) - case-sensitive! + if [ -z "$result" ]; then + case "$term" in + True | False) + result="$doc_dir/library/constants.html#$term" + desc="Python: $term constant" + ;; + None) + result="$doc_dir/library/constants.html#None" + desc="Python: None constant" + ;; + Ellipsis) + result="$doc_dir/library/constants.html#Ellipsis" + desc="Python: Ellipsis constant" + ;; + NotImplemented) + result="$doc_dir/library/constants.html#NotImplemented" + desc="Python: NotImplemented constant" + ;; + esac + fi - # Verify file exists for keyword lookups - if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then - result="" - desc="" - fi + # Verify file exists for keyword lookups + if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then + result="" + desc="" + fi - #-------------------------------------------------------------------------- - # PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.) - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then - result="$doc_dir/library/${term_lower}.html" - desc="Python module: $term" - fi + #-------------------------------------------------------------------------- + # PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.) + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then + result="$doc_dir/library/${term_lower}.html" + desc="Python module: $term" + fi - #-------------------------------------------------------------------------- - # PRIORITY 3: Built-in functions (library/functions.html) - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then - if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2> /dev/null; then - result="$doc_dir/library/functions.html#$term_lower" - desc="Python built-in function: $term" - fi - fi + #-------------------------------------------------------------------------- + # PRIORITY 3: Built-in functions (library/functions.html) + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then + if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2>/dev/null; then + result="$doc_dir/library/functions.html#$term_lower" + desc="Python built-in function: $term" + fi + fi - #-------------------------------------------------------------------------- - # PRIORITY 4: Built-in types (library/stdtypes.html) - #-------------------------------------------------------------------------- - if [ -z "$result" ]; then - case "$term_lower" in - str | string) - result="$doc_dir/library/stdtypes.html#str" - desc="Python: str type" - ;; - int | integer) - result="$doc_dir/library/stdtypes.html#int" - desc="Python: int type" - ;; - float) - result="$doc_dir/library/stdtypes.html#float" - desc="Python: float type" - ;; - list) - result="$doc_dir/library/stdtypes.html#list" - desc="Python: list type" - ;; - dict | dictionary) - result="$doc_dir/library/stdtypes.html#dict" - desc="Python: dict type" - ;; - set) - result="$doc_dir/library/stdtypes.html#set" - desc="Python: set type" - ;; - tuple) - result="$doc_dir/library/stdtypes.html#tuple" - desc="Python: tuple type" - ;; - bool | boolean) - result="$doc_dir/library/stdtypes.html#boolean-values" - desc="Python: bool type" - ;; - bytes) - result="$doc_dir/library/stdtypes.html#bytes" - desc="Python: bytes type" - ;; - esac - fi + #-------------------------------------------------------------------------- + # PRIORITY 4: Built-in types (library/stdtypes.html) + #-------------------------------------------------------------------------- + if [ -z "$result" ]; then + case "$term_lower" in + str | string) + result="$doc_dir/library/stdtypes.html#str" + desc="Python: str type" + ;; + int | integer) + result="$doc_dir/library/stdtypes.html#int" + desc="Python: int type" + ;; + float) + result="$doc_dir/library/stdtypes.html#float" + desc="Python: float type" + ;; + list) + result="$doc_dir/library/stdtypes.html#list" + desc="Python: list type" + ;; + dict | dictionary) + result="$doc_dir/library/stdtypes.html#dict" + desc="Python: dict type" + ;; + set) + result="$doc_dir/library/stdtypes.html#set" + desc="Python: set type" + ;; + tuple) + result="$doc_dir/library/stdtypes.html#tuple" + desc="Python: tuple type" + ;; + bool | boolean) + result="$doc_dir/library/stdtypes.html#boolean-values" + desc="Python: bool type" + ;; + bytes) + result="$doc_dir/library/stdtypes.html#bytes" + desc="Python: bytes type" + ;; + esac + fi - #-------------------------------------------------------------------------- - # PRIORITY 5: Check for class/function in module docs (exact id match) - #-------------------------------------------------------------------------- - if [ -z "$result" ]; then - local found_in - # Look for exact id match first - found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2> /dev/null | head -1) - if [ -n "$found_in" ]; then - result="$found_in#$term" - local module - module=$(basename "$found_in" .html) - desc="Python: $term in module $module" - fi - fi + #-------------------------------------------------------------------------- + # PRIORITY 5: Check for class/function in module docs (exact id match) + #-------------------------------------------------------------------------- + if [ -z "$result" ]; then + local found_in + # Look for exact id match first + found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2>/dev/null | head -1) + if [ -n "$found_in" ]; then + result="$found_in#$term" + local module + module=$(basename "$found_in" .html) + desc="Python: $term in module $module" + fi + fi - #-------------------------------------------------------------------------- - # PRIORITY 6: Search in index - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then - local index_match - index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2> /dev/null | head -1) - if [ -n "$index_match" ]; then - result=$(echo "$index_match" | cut -d' ' -f2-) - desc="Python: $term (from index)" - fi - fi + #-------------------------------------------------------------------------- + # PRIORITY 6: Search in index + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then + local index_match + index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2>/dev/null | head -1) + if [ -n "$index_match" ]; then + result=$(echo "$index_match" | cut -d' ' -f2-) + desc="Python: $term (from index)" + fi + fi - # NO full-text search fallback - it produces garbage results - # If we can't find a specific doc, return nothing (will fall back to online) + # NO full-text search fallback - it produces garbage results + # If we can't find a specific doc, return nothing (will fall back to online) - if [ -n "$result" ]; then - echo "$result|$desc" - fi + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # C/C++ specific lookup #============================================================================== lookup_cpp() { - local term="$1" - local doc_dir="$DOCS_DIR/c_cpp" - local result="" - local desc="" + local term="$1" + local doc_dir="$DOCS_DIR/c_cpp" + local result="" + local desc="" - # Resolve symlink if present (system package installs to c_cpp/system/) - [ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system" + # Resolve symlink if present (system package installs to c_cpp/system/) + [ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system" - # Common C headers - case "$term" in - stdio.h | stdio) - [ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html" - [ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html" - desc="C standard I/O header" - ;; - stdlib.h | stdlib) - [ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html" - [ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html" - desc="C standard library header" - ;; - string.h | cstring) - [ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html" - desc="C string handling header" - ;; - math.h | cmath) - [ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html" - desc="C math header" - ;; - esac + # Common C headers + case "$term" in + stdio.h | stdio) + [ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html" + [ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html" + desc="C standard I/O header" + ;; + stdlib.h | stdlib) + [ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html" + [ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html" + desc="C standard library header" + ;; + string.h | cstring) + [ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html" + desc="C string handling header" + ;; + math.h | cmath) + [ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html" + desc="C math header" + ;; + esac - # C++ STL containers - case "$term" in - vector) - [ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html" - [ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html" - desc="C++ std::vector container" - ;; - map) - [ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html" - desc="C++ std::map container" - ;; - string) - [ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html" - desc="C++ std::string" - ;; - iostream) - [ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html" - desc="C++ iostream header" - ;; - esac + # C++ STL containers + case "$term" in + vector) + [ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html" + [ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html" + desc="C++ std::vector container" + ;; + map) + [ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html" + desc="C++ std::map container" + ;; + string) + [ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html" + desc="C++ std::string" + ;; + iostream) + [ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html" + desc="C++ iostream header" + ;; + esac - # C keywords - case "$term" in - if | else | for | while | do | switch | case | break | continue | return | goto) - [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" - [ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html" - desc="C/C++ keyword: $term" - ;; - int | char | float | double | void | long | short | unsigned | signed) - [ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html" - desc="C/C++ type: $term" - ;; - struct | union | enum | typedef) - [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" - desc="C/C++ keyword: $term" - ;; - esac + # C keywords + case "$term" in + if | else | for | while | do | switch | case | break | continue | return | goto) + [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" + [ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html" + desc="C/C++ keyword: $term" + ;; + int | char | float | double | void | long | short | unsigned | signed) + [ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html" + desc="C/C++ type: $term" + ;; + struct | union | enum | typedef) + [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" + desc="C/C++ keyword: $term" + ;; + esac - # Search in files if not found (use -L to follow symlinks) - if [ -z "$result" ]; then - local found - found=$(find -L "$doc_dir" -name "*${term}*" -type f 2> /dev/null | head -1) - if [ -n "$found" ]; then - result="$found" - desc="C/C++: $term" - fi - fi + # Search in files if not found (use -L to follow symlinks) + if [ -z "$result" ]; then + local found + found=$(find -L "$doc_dir" -name "*${term}*" -type f 2>/dev/null | head -1) + if [ -n "$found" ]; then + result="$found" + desc="C/C++: $term" + fi + fi - if [ -n "$result" ]; then - echo "$result|$desc" - fi + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== @@ -421,387 +421,387 @@ lookup_cpp() { # Searches the cloned MDN content repository #============================================================================== lookup_js() { - local term="$1" - local mdn_dir="$DOCS_DIR/mdn-content/files/en-us" + local term="$1" + local mdn_dir="$DOCS_DIR/mdn-content/files/en-us" - # Normalize term for searching - local term_lower - term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') + # Normalize term for searching + local term_lower + term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') - # Handle common statement aliases (MDN uses if...else, try...catch, etc.) - local statement_aliases=( - "if:if...else" - "else:if...else" - "try:try...catch" - "catch:try...catch" - "finally:try...catch" - "do:do...while" - "while:while" - "for:for" - "switch:switch" - "case:switch" - "default:switch" - ) + # Handle common statement aliases (MDN uses if...else, try...catch, etc.) + local statement_aliases=( + "if:if...else" + "else:if...else" + "try:try...catch" + "catch:try...catch" + "finally:try...catch" + "do:do...while" + "while:while" + "for:for" + "switch:switch" + "case:switch" + "default:switch" + ) - for alias in "${statement_aliases[@]}"; do - local key="${alias%%:*}" - local value="${alias##*:}" - if [ "$term_lower" = "$key" ]; then - local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value" - if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$stmt_dir/index.md|${title:-$term}" - return 0 - fi - fi - done + for alias in "${statement_aliases[@]}"; do + local key="${alias%%:*}" + local value="${alias##*:}" + if [ "$term_lower" = "$key" ]; then + local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value" + if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$stmt_dir/index.md|${title:-$term}" + return 0 + fi + fi + done - # Handle boolean/null literals - case "$term_lower" in - true | false) - local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean" - if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then - echo "$bool_dir/index.md|Boolean ($term)" - return 0 - fi - ;; - null) - local null_dir="$mdn_dir/web/javascript/reference/operators/null" - if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$null_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$null_dir/index.md|${title:-null}" - return 0 - fi - ;; - undefined) - local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined" - if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$undef_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$undef_dir/index.md|${title:-undefined}" - return 0 - fi - ;; - esac + # Handle boolean/null literals + case "$term_lower" in + true | false) + local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean" + if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then + echo "$bool_dir/index.md|Boolean ($term)" + return 0 + fi + ;; + null) + local null_dir="$mdn_dir/web/javascript/reference/operators/null" + if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$null_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$null_dir/index.md|${title:-null}" + return 0 + fi + ;; + undefined) + local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined" + if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$undef_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$undef_dir/index.md|${title:-undefined}" + return 0 + fi + ;; + esac - # Search JavaScript reference directory structure (priority order) - local search_dirs=( - "$mdn_dir/web/javascript/reference/statements" - "$mdn_dir/web/javascript/reference/operators" - "$mdn_dir/web/javascript/reference/global_objects" - "$mdn_dir/web/javascript/reference/functions" - "$mdn_dir/web/javascript/reference/classes" - ) + # Search JavaScript reference directory structure (priority order) + local search_dirs=( + "$mdn_dir/web/javascript/reference/statements" + "$mdn_dir/web/javascript/reference/operators" + "$mdn_dir/web/javascript/reference/global_objects" + "$mdn_dir/web/javascript/reference/functions" + "$mdn_dir/web/javascript/reference/classes" + ) - for search_dir in "${search_dirs[@]}"; do - if [ -d "$search_dir" ]; then - # Look for exact directory match (MDN uses directories with index.md) - local found_dir - found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2> /dev/null | head -1) - if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found_dir/index.md|${title:-$term}" - return 0 - fi - fi - done + for search_dir in "${search_dirs[@]}"; do + if [ -d "$search_dir" ]; then + # Look for exact directory match (MDN uses directories with index.md) + local found_dir + found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found_dir/index.md|${title:-$term}" + return 0 + fi + fi + done - # Search Web APIs - prioritize *_api directories for common terms - if [ -d "$mdn_dir/web/api" ]; then - # First try _api directory (e.g., fetch_api, console_api) - local api_dir="$mdn_dir/web/api/${term_lower}_api" - if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$api_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$api_dir/index.md|${title:-$term API}" - return 0 - fi + # Search Web APIs - prioritize *_api directories for common terms + if [ -d "$mdn_dir/web/api" ]; then + # First try _api directory (e.g., fetch_api, console_api) + local api_dir="$mdn_dir/web/api/${term_lower}_api" + if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$api_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$api_dir/index.md|${title:-$term API}" + return 0 + fi - # Then try exact top-level API interface (e.g., Console, Document, Element) - local found - found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2> /dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi + # Then try exact top-level API interface (e.g., Console, Document, Element) + local found + found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi - # Try window/ for global functions like alert, confirm, etc. - local window_method="$mdn_dir/web/api/window/${term_lower}" - if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$window_method/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$window_method/index.md|${title:-Window.$term()}" - return 0 - fi + # Try window/ for global functions like alert, confirm, etc. + local window_method="$mdn_dir/web/api/window/${term_lower}" + if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$window_method/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$window_method/index.md|${title:-Window.$term()}" + return 0 + fi - # Search nested API methods - found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2> /dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - fi + # Search nested API methods + found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi - # Now try partial matches in Global Objects (e.g., Array.from, Object.keys) - if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then - local found - found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2> /dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - fi + # Now try partial matches in Global Objects (e.g., Array.from, Object.keys) + if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then + local found + found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi - # Glossary as last resort - if [ -d "$mdn_dir/glossary" ]; then - local found - found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2> /dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - fi + # Glossary as last resort + if [ -d "$mdn_dir/glossary" ]; then + local found + found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi - return 1 + return 1 } #============================================================================== # Rust specific lookup #============================================================================== lookup_rust() { - local term="$1" - local result="" - local desc="" + local term="$1" + local result="" + local desc="" - if command -v rustup &> /dev/null; then - # Use rustup doc to get path - local rust_doc_path - rust_doc_path=$(rustup doc --path 2> /dev/null | head -1 | xargs dirname 2> /dev/null) + if command -v rustup &>/dev/null; then + # Use rustup doc to get path + local rust_doc_path + rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) - # Search in std docs - if [ -d "$rust_doc_path/std" ]; then - local found - found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2> /dev/null | head -1) - if [ -n "$found" ]; then - result="$found" - desc="Rust: $term" - fi - fi - fi + # Search in std docs + if [ -d "$rust_doc_path/std" ]; then + local found + found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2>/dev/null | head -1) + if [ -n "$found" ]; then + result="$found" + desc="Rust: $term" + fi + fi + fi - if [ -n "$result" ]; then - echo "$result|$desc" - fi + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # Go specific lookup #============================================================================== lookup_go() { - local term="$1" - local result="" - local desc="" + local term="$1" + local result="" + local desc="" - if command -v go &> /dev/null; then - # Check if it's a stdlib package - if go doc "$term" &> /dev/null; then - result="go doc $term" - desc="Go package: $term (use 'go doc $term' to view)" - fi - fi + if command -v go &>/dev/null; then + # Check if it's a stdlib package + if go doc "$term" &>/dev/null; then + result="go doc $term" + desc="Go package: $term (use 'go doc $term' to view)" + fi + fi - if [ -n "$result" ]; then - echo "$result|$desc" - fi + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # Shell specific lookup #============================================================================== lookup_shell() { - local term="$1" - local doc_dir="$DOCS_DIR/shell" - local result="" - local desc="" + local term="$1" + local doc_dir="$DOCS_DIR/shell" + local result="" + local desc="" - # Check bash builtins - if [ -f "$doc_dir/bash_builtins.txt" ]; then - if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2> /dev/null; then - result="$doc_dir/bash_builtins.txt" - desc="Bash builtin: $term" - fi - fi + # Check bash builtins + if [ -f "$doc_dir/bash_builtins.txt" ]; then + if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2>/dev/null; then + result="$doc_dir/bash_builtins.txt" + desc="Bash builtin: $term" + fi + fi - # Check common commands - if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then - if grep -q "^$term" "$doc_dir/common_commands.txt" 2> /dev/null; then - local cmd_desc - cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1) - result="$doc_dir/common_commands.txt" - desc="Shell command: $cmd_desc" - fi - fi + # Check common commands + if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then + if grep -q "^$term" "$doc_dir/common_commands.txt" 2>/dev/null; then + local cmd_desc + cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1) + result="$doc_dir/common_commands.txt" + desc="Shell command: $cmd_desc" + fi + fi - # Try man page - if [ -z "$result" ]; then - local man_path - man_path=$(man -w "$term" 2> /dev/null) - if [ -n "$man_path" ]; then - result="man $term" - desc="Manual page: $term (use 'man $term' to view)" - fi - fi + # Try man page + if [ -z "$result" ]; then + local man_path + man_path=$(man -w "$term" 2>/dev/null) + if [ -n "$man_path" ]; then + result="man $term" + desc="Manual page: $term (use 'man $term' to view)" + fi + fi - if [ -n "$result" ]; then - echo "$result|$desc" - fi + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # Generic lookup (searches all languages) #============================================================================== lookup_all() { - local term="$1" + local term="$1" - # Try each language - for lang in python cpp js rust go shell; do - local result - result=$(lookup_$lang "$term" 2> /dev/null) - if [ -n "$result" ]; then - echo "$lang: $result" - fi - done + # Try each language + for lang in python cpp js rust go shell; do + local result + result=$(lookup_$lang "$term" 2>/dev/null) + if [ -n "$result" ]; then + echo "$lang: $result" + fi + done } #============================================================================== # Parse Python import and lookup the actual imported item #============================================================================== parse_python_import() { - local import_line="$1" + local import_line="$1" - # Handle "from X import Y" format - if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then - local module="${BASH_REMATCH[1]}" - local items="${BASH_REMATCH[2]}" + # Handle "from X import Y" format + if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then + local module="${BASH_REMATCH[1]}" + local items="${BASH_REMATCH[2]}" - # Clean up items (remove parentheses, commas, etc.) - items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}') + # Clean up items (remove parentheses, commas, etc.) + items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}') - # Output: module and first imported item - echo "$module|$items" - return 0 - fi + # Output: module and first imported item + echo "$module|$items" + return 0 + fi - # Handle "import X" format - if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then - local module="${BASH_REMATCH[1]}" - echo "$module|" - return 0 - fi + # Handle "import X" format + if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then + local module="${BASH_REMATCH[1]}" + echo "$module|" + return 0 + fi - return 1 + return 1 } #============================================================================== # Smart lookup for imports #============================================================================== lookup_import() { - local import_line="$1" - local lang="$2" + local import_line="$1" + local lang="$2" - case "$lang" in - python) - local parsed - parsed=$(parse_python_import "$import_line") - if [ -n "$parsed" ]; then - local module item - module=$(echo "$parsed" | cut -d'|' -f1) - item=$(echo "$parsed" | cut -d'|' -f2) + case "$lang" in + python) + local parsed + parsed=$(parse_python_import "$import_line") + if [ -n "$parsed" ]; then + local module item + module=$(echo "$parsed" | cut -d'|' -f1) + item=$(echo "$parsed" | cut -d'|' -f2) - # For "from X import Y", look up Y within module X's documentation - if [ -n "$item" ] && [ -n "$module" ]; then - local result - # Pass both item and module to lookup_python - result=$(lookup_python "$item" "$module") - if [ -n "$result" ]; then - echo "$result" - return 0 - fi - fi + # For "from X import Y", look up Y within module X's documentation + if [ -n "$item" ] && [ -n "$module" ]; then + local result + # Pass both item and module to lookup_python + result=$(lookup_python "$item" "$module") + if [ -n "$result" ]; then + echo "$result" + return 0 + fi + fi - # Fall back to module documentation - lookup_python "$module" - fi - ;; + # Fall back to module documentation + lookup_python "$module" + fi + ;; - c_cpp) - # Extract header name from #include
or #include "header" - local header - header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//') - lookup_cpp "$header" - ;; + c_cpp) + # Extract header name from #include
or #include "header" + local header + header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//') + lookup_cpp "$header" + ;; - javascript | typescript) - # Extract module from import/require - local module="" - # Match: from "module" or from 'module' - module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+") - if [ -z "$module" ]; then - # Match: require("module") or require('module') - module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+") - fi - [ -n "$module" ] && lookup_js "$module" - ;; + javascript | typescript) + # Extract module from import/require + local module="" + # Match: from "module" or from 'module' + module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+") + if [ -z "$module" ]; then + # Match: require("module") or require('module') + module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+") + fi + [ -n "$module" ] && lookup_js "$module" + ;; - *) - echo "Unknown language: $lang" - ;; - esac + *) + echo "Unknown language: $lang" + ;; + esac } #============================================================================== # Extract documentation content #============================================================================== extract_doc_content() { - local file="$1" - local term="$2" - local max_lines="${3:-20}" + local file="$1" + local term="$2" + local max_lines="${3:-20}" - if [[ $file == *.html ]]; then - # Extract text from HTML, find section about term - if command -v html2text &> /dev/null; then - html2text "$file" 2> /dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" - elif command -v lynx &> /dev/null; then - lynx -dump -nolist "$file" 2> /dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" - else - # Basic extraction - sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines" - fi - elif [[ $file == *.json ]]; then - # Pretty print JSON section - grep -A5 "\"$term\"" "$file" 2> /dev/null - else - # Plain text - grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines" - fi + if [[ $file == *.html ]]; then + # Extract text from HTML, find section about term + if command -v html2text &>/dev/null; then + html2text "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" + elif command -v lynx &>/dev/null; then + lynx -dump -nolist "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" + else + # Basic extraction + sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines" + fi + elif [[ $file == *.json ]]; then + # Pretty print JSON section + grep -A5 "\"$term\"" "$file" 2>/dev/null + else + # Plain text + grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines" + fi } #============================================================================== # Main #============================================================================== usage() { - cat << EOF + cat < [language] [options] Search offline documentation for a term. @@ -824,119 +824,119 @@ EOF } main() { - if [ $# -eq 0 ]; then - usage - exit 0 - fi + if [ $# -eq 0 ]; then + usage + exit 0 + fi - local term="" - local lang="" - local action="lookup" - local open_file=false - local extract=false + local term="" + local lang="" + local action="lookup" + local open_file=false + local extract=false - while [ $# -gt 0 ]; do - case "$1" in - --open) - open_file=true - shift - ;; - --extract) - extract=true - shift - ;; - --import) - action="import" - shift - term="$1" - shift - ;; - --batch) - action="batch" - shift - term="$1" # This is the file - shift - ;; - --help | -h) - usage - exit 0 - ;; - python | cpp | c_cpp | c | js | javascript | ts | typescript | tsx | jsx | rust | go | shell | bash | all) - lang="$1" - shift - ;; - *) - if [ -z "$term" ]; then - term="$1" - fi - shift - ;; - esac - done + while [ $# -gt 0 ]; do + case "$1" in + --open) + open_file=true + shift + ;; + --extract) + extract=true + shift + ;; + --import) + action="import" + shift + term="$1" + shift + ;; + --batch) + action="batch" + shift + term="$1" # This is the file + shift + ;; + --help | -h) + usage + exit 0 + ;; + python | cpp | c_cpp | c | js | javascript | ts | typescript | tsx | jsx | rust | go | shell | bash | all) + lang="$1" + shift + ;; + *) + if [ -z "$term" ]; then + term="$1" + fi + shift + ;; + esac + done - # Normalize language - case "$lang" in - c) lang="cpp" ;; - javascript | js | typescript | ts | jsx | tsx) lang="js" ;; - bash) lang="shell" ;; - "") lang="all" ;; - esac + # Normalize language + case "$lang" in + c) lang="cpp" ;; + javascript | js | typescript | ts | jsx | tsx) lang="js" ;; + bash) lang="shell" ;; + "") lang="all" ;; + esac - case "$action" in - lookup) - if [ "$lang" = "all" ]; then - lookup_all "$term" - else - result=$(lookup_$lang "$term" 2> /dev/null) - if [ -n "$result" ]; then - local file desc - file=$(echo "$result" | cut -d'|' -f1) - desc=$(echo "$result" | cut -d'|' -f2) + case "$action" in + lookup) + if [ "$lang" = "all" ]; then + lookup_all "$term" + else + result=$("lookup_$lang" "$term" 2>/dev/null) + if [ -n "$result" ]; then + local file desc + file=$(echo "$result" | cut -d'|' -f1) + desc=$(echo "$result" | cut -d'|' -f2) - echo -e "${GREEN}Found:${NC} $desc" - echo -e "${BLUE}File:${NC} $file" + echo -e "${GREEN}Found:${NC} $desc" + echo -e "${BLUE}File:${NC} $file" - if $extract; then - echo "" - echo -e "${YELLOW}--- Content ---${NC}" - extract_doc_content "$file" "$term" - fi + if $extract; then + echo "" + echo -e "${YELLOW}--- Content ---${NC}" + extract_doc_content "$file" "$term" + fi - if $open_file && [ -f "$file" ]; then - xdg-open "$file" 2> /dev/null & - fi - else - echo -e "${RED}Not found:${NC} $term in $lang documentation" - fi - fi - ;; + if $open_file && [ -f "$file" ]; then + xdg-open "$file" 2>/dev/null & + fi + else + echo -e "${RED}Not found:${NC} $term in $lang documentation" + fi + fi + ;; - import) - result=$(lookup_import "$term" "$lang") - if [ -n "$result" ]; then - echo -e "${GREEN}Import lookup:${NC} $term" - echo "$result" - else - echo -e "${RED}Could not parse import:${NC} $term" - fi - ;; + import) + result=$(lookup_import "$term" "$lang") + if [ -n "$result" ]; then + echo -e "${GREEN}Import lookup:${NC} $term" + echo "$result" + else + echo -e "${RED}Could not parse import:${NC} $term" + fi + ;; - batch) - if [ ! -f "$term" ]; then - echo "File not found: $term" - exit 1 - fi + batch) + if [ ! -f "$term" ]; then + echo "File not found: $term" + exit 1 + fi - while IFS= read -r line || [ -n "$line" ]; do - [ -z "$line" ] && continue - [[ $line =~ ^# ]] && continue + while IFS= read -r line || [ -n "$line" ]; do + [ -z "$line" ] && continue + [[ $line =~ ^# ]] && continue - echo -e "${CYAN}Looking up:${NC} $line" - lookup_import "$line" "$lang" - echo "" - done < "$term" - ;; - esac + echo -e "${CYAN}Looking up:${NC} $line" + lookup_import "$line" "$lang" + echo "" + done <"$term" + ;; + esac } main "$@" diff --git a/linux_configuration/scripts/single_use/utils/update_android_hosts.sh b/linux_configuration/scripts/single_use/utils/update_android_hosts.sh index a94f1a0e..42ff34dd 100755 --- a/linux_configuration/scripts/single_use/utils/update_android_hosts.sh +++ b/linux_configuration/scripts/single_use/utils/update_android_hosts.sh @@ -25,7 +25,11 @@ ensure_adb_installed() { if command -v pacman &>/dev/null; then sudo pacman -S --noconfirm android-tools || die "Failed to install android-tools" elif command -v apt-get &>/dev/null; then - sudo apt-get update && sudo apt-get install -y adb || die "Failed to install adb" + # `apt-get update && apt-get install ... || die` would also die when the + # update succeeded but the install failed to REPORT - and, worse, skipped + # die entirely if update itself failed. Check each step. + sudo apt-get update || die "Failed to update apt package lists" + sudo apt-get install -y adb || die "Failed to install adb" elif command -v dnf &>/dev/null; then sudo dnf install -y android-tools || die "Failed to install android-tools" else diff --git a/linux_configuration/scripts/single_use/utils/upgrade.sh b/linux_configuration/scripts/single_use/utils/upgrade.sh index 0a0fba94..d3cc41a0 100755 --- a/linux_configuration/scripts/single_use/utils/upgrade.sh +++ b/linux_configuration/scripts/single_use/utils/upgrade.sh @@ -42,10 +42,12 @@ fix_cloudflare_key() { if [[ ${expired:-0} -gt 0 ]]; then log "Refreshing expired Cloudflare WARP GPG key..." - curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg \ - | gpg --yes --dearmor -o "$keyring" 2>/dev/null \ - && log "Cloudflare key refreshed." \ - || log "WARNING: Could not refresh Cloudflare key (network issue?). Skipping." + if curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | + gpg --yes --dearmor -o "$keyring" 2>/dev/null; then + log "Cloudflare key refreshed." + else + log "WARNING: Could not refresh Cloudflare key (network issue?). Skipping." + fi fi } @@ -66,8 +68,8 @@ fix_wine_legacy_key() { # Export key to modern location gpg --no-default-keyring --keyring "$legacy_keyring" \ - --export "$wine_key_id" \ - | gpg --yes --dearmor -o "$modern_keyring" 2>/dev/null + --export "$wine_key_id" | + gpg --yes --dearmor -o "$modern_keyring" 2>/dev/null # Remove from legacy keyring (suppress the deprecation warning) apt-key del "$wine_key_id" >/dev/null 2>&1 || true diff --git a/linux_configuration/tests/test_android_guardian_service.sh b/linux_configuration/tests/test_android_guardian_service.sh index 41e1fc18..c018bd71 100755 --- a/linux_configuration/tests/test_android_guardian_service.sh +++ b/linux_configuration/tests/test_android_guardian_service.sh @@ -8,22 +8,22 @@ REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) TARGET_SCRIPT="$REPO_DIR/scripts/periodic_background/utils/android_guardian/service.sh" fail() { - printf 'FAIL: %s\n' "$1" >&2 - exit 1 + printf 'FAIL: %s\n' "$1" >&2 + exit 1 } assert_equals() { - local expected="$1" - local actual="$2" - local context="$3" - if [[ "$expected" != "$actual" ]]; then - fail "$context (expected: '$expected', actual: '$actual')" - fi + local expected="$1" + local actual="$2" + local context="$3" + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected: '$expected', actual: '$actual')" + fi } TMP_DIR=$(mktemp -d) cleanup() { - rm -rf "$TMP_DIR" + rm -rf "$TMP_DIR" } trap cleanup EXIT @@ -34,12 +34,12 @@ cp "$TARGET_SCRIPT" "$WORKTREE/scripts/periodic_background/utils/android_guardia printf 'Checking skip-main avoids boot side effects...\n' SKIP_MAIN_GUARDIAN_DIR="$TMP_DIR/skip-main-guardian" ANDROID_GUARDIAN_SKIP_MAIN=1 \ -ANDROID_GUARDIAN_DIR="$SKIP_MAIN_GUARDIAN_DIR" \ -ANDROID_GUARDIAN_MODULE_DIR="$TMP_DIR/skip-main-module" \ -sh "$TARGET_SCRIPT" + ANDROID_GUARDIAN_DIR="$SKIP_MAIN_GUARDIAN_DIR" \ + ANDROID_GUARDIAN_MODULE_DIR="$TMP_DIR/skip-main-module" \ + sh "$TARGET_SCRIPT" -[[ ! -d "$SKIP_MAIN_GUARDIAN_DIR" ]] \ - || fail 'skip-main should prevent guardian boot setup side effects' +[[ ! -d "$SKIP_MAIN_GUARDIAN_DIR" ]] || + fail 'skip-main should prevent guardian boot setup side effects' printf 'Checking guardian loop cadence constants...\n' hosts_ticks=$(grep '^HOSTS_CHECK_EVERY_TICKS=' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" | cut -d= -f2) @@ -51,19 +51,22 @@ assert_equals '12' "$apps_ticks" 'blocked-app scan should run every 12 ticks' assert_equals '5' "$sleep_seconds" 'guardian loop should keep the 5 second base sleep' printf 'Checking guardian loop protects module every tick...\n' -grep -q 'protect_module' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" \ - || fail 'guardian loop must always protect the module each tick' +grep -q 'protect_module' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" || + fail 'guardian loop must always protect the module each tick' printf 'Checking blocked-app scans are cached per pass...\n' -grep -q 'installed_packages=$(pm list packages 2>/dev/null)' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" \ - || fail 'guardian service should cache installed packages once per blocked-app scan' +# Literal search pattern: the $ must reach grep as text, not be expanded +# by this shell. +# shellcheck disable=SC2016 +grep -q 'installed_packages=$(pm list packages 2>/dev/null)' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" || + fail 'guardian service should cache installed packages once per blocked-app scan' if grep -q 'pm list packages 2>/dev/null | grep -q' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh"; then - fail 'guardian service should not re-run pm list packages through grep for every blocked app' + fail 'guardian service should not re-run pm list packages through grep for every blocked app' fi printf 'Checking guardian loop uses skip-main guard for tests...\n' -grep -q 'ANDROID_GUARDIAN_SKIP_MAIN' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" \ - || fail 'guardian service should support skip-main testing' +grep -q 'ANDROID_GUARDIAN_SKIP_MAIN' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" || + fail 'guardian service should support skip-main testing' printf 'android_guardian service regression checks passed.\n' diff --git a/linux_configuration/tests/test_hosts_guard_pacman_integration.sh b/linux_configuration/tests/test_hosts_guard_pacman_integration.sh index fb010796..09e10b1b 100755 --- a/linux_configuration/tests/test_hosts_guard_pacman_integration.sh +++ b/linux_configuration/tests/test_hosts_guard_pacman_integration.sh @@ -60,7 +60,7 @@ assert_order() { exit 1 fi - if (( first_line < second_line )); then + if ((first_line < second_line)); then echo "PASS: $message" else echo "FAIL: $message" @@ -87,6 +87,9 @@ assert_order "$WRAPPER_FILE" 'if ! check_and_handle_db_lock "$@"; then' 'if shou assert_contains "$WRAPPER_FILE" 'manual_guard_lib_fallback=1' \ "wrapper tracks whether manual hosts guard fallback was used" +# Literal source text being searched for; expanding $SCRIPT_DIR / +# $LOCK_LIB_DEST here would search for this machine's values instead. +# shellcheck disable=SC2016 assert_contains "$INSTALLER_FILE" 'install -m 755 "$SCRIPT_DIR/pacman-hooks/hosts-guard-common.sh" /usr/local/share/hosts-guard/' \ "installer deploys shared hosts guard hook helpers" diff --git a/linux_configuration/tests/test_pacman_wrapper_security.sh b/linux_configuration/tests/test_pacman_wrapper_security.sh index 530b4352..50ebb15b 100755 --- a/linux_configuration/tests/test_pacman_wrapper_security.sh +++ b/linux_configuration/tests/test_pacman_wrapper_security.sh @@ -13,209 +13,212 @@ echo "" # Test 1: Check wrapper syntax echo "[TEST 1] Checking wrapper script syntax..." if bash -n "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ Wrapper script syntax is valid" + echo "✓ Wrapper script syntax is valid" else - echo "✗ Wrapper script has syntax errors" - exit 1 + echo "✗ Wrapper script has syntax errors" + exit 1 fi # Test 2: Check installer syntax echo "[TEST 2] Checking installer script syntax..." if bash -n "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer script syntax is valid" + echo "✓ Installer script syntax is valid" else - echo "✗ Installer script has syntax errors" - exit 1 + echo "✗ Installer script has syntax errors" + exit 1 fi # Test 3: Check VirtualBox enforcement script syntax echo "[TEST 3] Checking VirtualBox enforcement script syntax..." if bash -n "$VBOX_DIR/enforce_vbox_hosts.sh"; then - echo "✓ VirtualBox enforcement script syntax is valid" + echo "✓ VirtualBox enforcement script syntax is valid" else - echo "✗ VirtualBox enforcement script has syntax errors" - exit 1 + echo "✗ VirtualBox enforcement script has syntax errors" + exit 1 fi # Test 4: Verify integrity check function exists echo "[TEST 4] Verifying integrity check function exists in wrapper..." if grep -q "verify_policy_integrity()" "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ Integrity verification function found" + echo "✓ Integrity verification function found" else - echo "✗ Integrity verification function not found" - exit 1 + echo "✗ Integrity verification function not found" + exit 1 fi # Test 5: Verify hardcoded VirtualBox cleanup function exists echo "[TEST 5] Verifying hardcoded VirtualBox cleanup function exists..." if grep -q "auto_remove_virtualbox_vms()" "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ Hardcoded VirtualBox cleanup function found" + echo "✓ Hardcoded VirtualBox cleanup function found" else - echo "✗ Hardcoded VirtualBox cleanup function not found" - exit 1 + echo "✗ Hardcoded VirtualBox cleanup function not found" + exit 1 fi # Test 6: Verify VirtualBox cleanup uses VBoxManage directly echo "[TEST 6] Verifying VirtualBox cleanup uses VBoxManage directly..." if grep -q "VBoxManage" "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ VirtualBox cleanup logic found" + echo "✓ VirtualBox cleanup logic found" else - echo "✗ VirtualBox cleanup logic not found" - exit 1 + echo "✗ VirtualBox cleanup logic not found" + exit 1 fi # Test 7: Verify integrity check is called early in execution echo "[TEST 7] Verifying integrity check is called before operations..." if grep -B 2 -A 2 "verify_policy_integrity" "$WRAPPER_DIR/pacman_wrapper.sh" | grep -q "CRITICAL"; then - echo "✓ Integrity check is called early in execution" + echo "✓ Integrity check is called early in execution" else - echo "✗ Integrity check not found in early execution" - exit 1 + echo "✗ Integrity check not found in early execution" + exit 1 fi # Test 8: Verify installer creates integrity file echo "[TEST 8] Verifying installer creates integrity checksums..." if grep -q "INTEGRITY_FILE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer references integrity file" + echo "✓ Installer references integrity file" else - echo "✗ Installer does not create integrity file" - exit 1 + echo "✗ Installer does not create integrity file" + exit 1 fi # Test 9: Verify installer uses chattr to make files immutable echo "[TEST 9] Verifying installer makes policy files immutable..." if grep -q "chattr +i" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer sets immutable attributes" + echo "✓ Installer sets immutable attributes" else - echo "✗ Installer does not set immutable attributes" - exit 1 + echo "✗ Installer does not set immutable attributes" + exit 1 fi # Test 10: Verify VirtualBox cleanup enforcement is integrated echo "[TEST 10] Verifying VirtualBox cleanup is integrated into wrapper..." if grep -q "auto_remove_virtualbox_vms" "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ VirtualBox cleanup integration found" + echo "✓ VirtualBox cleanup integration found" else - echo "✗ VirtualBox cleanup integration not found" - exit 1 + echo "✗ VirtualBox cleanup integration not found" + exit 1 fi # Test 11: Verify VirtualBox script can show help echo "[TEST 11] Testing VirtualBox enforcement script help..." # Run without invoking sudo by setting EUID check (or just check for the help text in the file) if grep -q "VirtualBox /etc/hosts Enforcement Tool" "$VBOX_DIR/enforce_vbox_hosts.sh"; then - echo "✓ VirtualBox enforcement script has help text" + echo "✓ VirtualBox enforcement script has help text" else - echo "✗ VirtualBox enforcement script help text not found" - exit 1 + echo "✗ VirtualBox enforcement script help text not found" + exit 1 fi # Test 12: Verify installer installs VirtualBox enforcement script echo "[TEST 12] Verifying installer handles VirtualBox enforcement script..." if grep -q "VBOX_ENFORCE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer includes VirtualBox enforcement script" + echo "✓ Installer includes VirtualBox enforcement script" else - echo "✗ Installer does not include VirtualBox enforcement script" - exit 1 + echo "✗ Installer does not include VirtualBox enforcement script" + exit 1 fi # Test 13: Verify makepkg capped wrapper script syntax echo "[TEST 13] Checking makepkg capped wrapper syntax..." if bash -n "$WRAPPER_DIR/makepkg_capped.sh"; then - echo "✓ makepkg capped wrapper syntax is valid" + echo "✓ makepkg capped wrapper syntax is valid" else - echo "✗ makepkg capped wrapper has syntax errors" - exit 1 + echo "✗ makepkg capped wrapper has syntax errors" + exit 1 fi # Test 14: Verify pacman wrapper exposes makepkg capped command echo "[TEST 14] Verifying pacman wrapper supports --makepkg-capped..." if grep -q -- "--makepkg-capped" "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ pacman wrapper makepkg capped command found" + echo "✓ pacman wrapper makepkg capped command found" else - echo "✗ pacman wrapper makepkg capped command missing" - exit 1 + echo "✗ pacman wrapper makepkg capped command missing" + exit 1 fi # Test 15: Verify installer deploys makepkg capped wrapper echo "[TEST 15] Verifying installer deploys makepkg capped wrapper..." if grep -q "MAKEPKG_CAPPED" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer includes makepkg capped deployment" + echo "✓ Installer includes makepkg capped deployment" else - echo "✗ Installer does not include makepkg capped deployment" - exit 1 + echo "✗ Installer does not include makepkg capped deployment" + exit 1 fi # Test 16: Verify mkpkg helper script syntax echo "[TEST 16] Checking mkpkg helper script syntax..." if bash -n "$WRAPPER_DIR/mkpkg.sh"; then - echo "✓ mkpkg helper script syntax is valid" + echo "✓ mkpkg helper script syntax is valid" else - echo "✗ mkpkg helper script has syntax errors" - exit 1 + echo "✗ mkpkg helper script has syntax errors" + exit 1 fi # Test 17: Verify installer deploys mkpkg helper echo "[TEST 17] Verifying installer deploys mkpkg helper..." if grep -q "MKPKG" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer includes mkpkg helper deployment" + echo "✓ Installer includes mkpkg helper deployment" else - echo "✗ Installer does not include mkpkg helper deployment" - exit 1 + echo "✗ Installer does not include mkpkg helper deployment" + exit 1 fi # Test 18: Verify installer runs in strict mode echo "[TEST 18] Verifying installer uses strict shell mode..." if grep -q "set -euo pipefail" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer strict mode enabled" + echo "✓ Installer strict mode enabled" else - echo "✗ Installer strict mode not enabled" - exit 1 + echo "✗ Installer strict mode not enabled" + exit 1 fi # Test 19: Verify installer handles immutable files during updates echo "[TEST 19] Verifying installer unlocks immutable files before copy/write..." if grep -q "unlock_immutable_file_if_needed" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Installer immutable-file handling found" + echo "✓ Installer immutable-file handling found" else - echo "✗ Installer immutable-file handling missing" - exit 1 + echo "✗ Installer immutable-file handling missing" + exit 1 fi # Test: shared lock library syntax echo "[TEST] Checking shared lock library syntax..." if bash -n "$WRAPPER_DIR/pacman_lock_lib.sh"; then - echo "✓ pacman_lock_lib.sh syntax is valid" + echo "✓ pacman_lock_lib.sh syntax is valid" else - echo "✗ pacman_lock_lib.sh has syntax errors" - exit 1 + echo "✗ pacman_lock_lib.sh has syntax errors" + exit 1 fi # Test: wrapper sources the shared lock library echo "[TEST] Verifying wrapper sources the shared lock library..." if grep -q 'source .*pacman_lock_lib.sh' "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ Wrapper sources pacman_lock_lib.sh" + echo "✓ Wrapper sources pacman_lock_lib.sh" else - echo "✗ Wrapper does not source pacman_lock_lib.sh" - exit 1 + echo "✗ Wrapper does not source pacman_lock_lib.sh" + exit 1 fi # Test: the lock library is covered by the integrity manifest echo "[TEST] Verifying installer checksums the lock library..." +# Literal source text being searched for; expanding $SCRIPT_DIR / +# $LOCK_LIB_DEST here would search for this machine's values instead. +# shellcheck disable=SC2016 if grep -qF 'sha256sum "$LOCK_LIB_DEST"' "$WRAPPER_DIR/install_pacman_wrapper.sh"; then - echo "✓ Lock library is included in the integrity manifest" + echo "✓ Lock library is included in the integrity manifest" else - echo "✗ Lock library not checksummed by installer" - exit 1 + echo "✗ Lock library not checksummed by installer" + exit 1 fi # Test: lib is sourced AFTER integrity verification (tamper caught before source) echo "[TEST] Verifying lock library is sourced after integrity check..." if awk '/verify_policy_integrity/{v=NR} /source .*pacman_lock_lib.sh/{s=NR} END{exit !(v && s && s>v)}' "$WRAPPER_DIR/pacman_wrapper.sh"; then - echo "✓ Lock library sourced after verify_policy_integrity" + echo "✓ Lock library sourced after verify_policy_integrity" else - echo "✗ Lock library not sourced after integrity verification" - exit 1 + echo "✗ Lock library not sourced after integrity verification" + exit 1 fi echo "" diff --git a/linux_configuration/tests/test_shutdown_timer_monitor.sh b/linux_configuration/tests/test_shutdown_timer_monitor.sh index e255c1b9..0ca43c4d 100755 --- a/linux_configuration/tests/test_shutdown_timer_monitor.sh +++ b/linux_configuration/tests/test_shutdown_timer_monitor.sh @@ -9,22 +9,22 @@ TARGET_SCRIPT="$REPO_DIR/scripts/periodic_background/system-maintenance/bin/shut SETUP_SCRIPT="$REPO_DIR/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh" fail() { - printf 'FAIL: %s\n' "$1" >&2 - exit 1 + printf 'FAIL: %s\n' "$1" >&2 + exit 1 } assert_equals() { - local expected="$1" - local actual="$2" - local context="$3" - if [[ "$expected" != "$actual" ]]; then - fail "$context (expected: '$expected', actual: '$actual')" - fi + local expected="$1" + local actual="$2" + local context="$3" + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected: '$expected', actual: '$actual')" + fi } TMP_DIR=$(mktemp -d) cleanup() { - rm -rf "$TMP_DIR" + rm -rf "$TMP_DIR" } trap cleanup EXIT @@ -81,42 +81,46 @@ EOF chmod +x "$BIN_DIR/tee" run_case() { - local expected_mode="$1" - local busctl_present="$2" - local sleep_log="$TMP_DIR/sleep.log" - local mode_file="$TMP_DIR/mode.log" + local expected_mode="$1" + local busctl_present="$2" + local sleep_log="$TMP_DIR/sleep.log" + local mode_file="$TMP_DIR/mode.log" - : >"$sleep_log" - : >"$mode_file" - if [[ $busctl_present -eq 0 ]]; then - mv "$BIN_DIR/busctl" "$BIN_DIR/busctl.off" - fi + : >"$sleep_log" + : >"$mode_file" + if [[ $busctl_present -eq 0 ]]; then + mv "$BIN_DIR/busctl" "$BIN_DIR/busctl.off" + fi - mode=$(env -i PATH="$BIN_DIR" SLEEP_LOG="$sleep_log" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \ - "source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \ + mode=$(env -i PATH="$BIN_DIR" SLEEP_LOG="$sleep_log" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \ + "source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \ timer_needs_restoration() { return 1; }; \ restore_timer() { :; }; \ monitor_with_dbus() { printf 'dbus'; }; \ monitor_with_polling() { printf 'polling'; }; \ start_monitoring") - assert_equals "$expected_mode" "$mode" 'shutdown timer monitor should choose the expected dispatcher' + assert_equals "$expected_mode" "$mode" 'shutdown timer monitor should choose the expected dispatcher' - if [[ -f "$BIN_DIR/busctl.off" ]]; then - mv "$BIN_DIR/busctl.off" "$BIN_DIR/busctl" - fi + if [[ -f "$BIN_DIR/busctl.off" ]]; then + mv "$BIN_DIR/busctl.off" "$BIN_DIR/busctl" + fi } run_dbus_throttle_case() { - local ts_sequence="$1" - local expected_calls="$2" - local check_interval="${3:-30}" - local calls - local counter_file="$TMP_DIR/timer_checks.log" + local ts_sequence="$1" + local expected_calls="$2" + local check_interval="${3:-30}" + local calls + local counter_file="$TMP_DIR/timer_checks.log" - : >"$counter_file" + : >"$counter_file" - calls=$(env -i PATH="$BIN_DIR" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 MOCK_BUSCTL_LINES="day-specific-shutdown.timer\nday-specific-shutdown.timer\nday-specific-shutdown.timer" MOCK_TS_SEQUENCE="$ts_sequence" COUNTER_FILE="$counter_file" TEST_CHECK_INTERVAL="$check_interval" /bin/bash -c ' + # The `bash -c` body below is a script for a CHILD shell - $1, + # $TEST_CHECK_INTERVAL, $COUNTER_FILE and ${mock_ts[...]} must reach it + # UNEXPANDED and be evaluated there. Single quotes are load-bearing. + # shellcheck disable=SC2016 + calls=$(env -i PATH="$BIN_DIR" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 MOCK_BUSCTL_LINES="day-specific-shutdown.timer\nday-specific-shutdown.timer\nday-specific-shutdown.timer" MOCK_TS_SEQUENCE="$ts_sequence" COUNTER_FILE="$counter_file" TEST_CHECK_INTERVAL="$check_interval" /bin/bash -c ' source "$1" CHECK_INTERVAL="$TEST_CHECK_INTERVAL" timer_needs_restoration() { printf "x\n" >> "$COUNTER_FILE"; return 1; } @@ -142,7 +146,7 @@ run_dbus_throttle_case() { printf "%s" "$timer_checks" ' _ "$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh") - assert_equals "$expected_calls" "$calls" 'monitor_with_dbus should throttle repeated relevant events' + assert_equals "$expected_calls" "$calls" 'monitor_with_dbus should throttle repeated relevant events' } printf 'Checking D-Bus path is preferred when busctl exists...\n' @@ -159,7 +163,7 @@ run_dbus_throttle_case '100 101 102' '3' '0' printf 'Checking wait helper enforces delay even with /dev/null stdin...\n' wait_elapsed=$(env -i PATH="/usr/bin:/bin" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \ - "source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \ + "source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \ start=\$(printf '%(%s)T' -1); \ wait_seconds 1; \ end=\$(printf '%(%s)T' -1); \ @@ -167,25 +171,32 @@ wait_elapsed=$(env -i PATH="/usr/bin:/bin" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /b assert_equals '1' "$wait_elapsed" 'wait_seconds should not return immediately on /dev/null stdin' printf 'Checking installer template stays in sync with the event-driven monitor...\n' -grep -Fq 'monitor_with_dbus()' "$SETUP_SCRIPT" \ - || fail 'setup_midnight_shutdown.sh should install the D-Bus monitor helper' -grep -Fq 'start_monitoring()' "$SETUP_SCRIPT" \ - || fail 'setup_midnight_shutdown.sh should install the start_monitoring dispatcher' -grep -Fq 'if command -v busctl &>/dev/null; then' "$SETUP_SCRIPT" \ - || fail 'setup_midnight_shutdown.sh should prefer busctl when available' -grep -Fq 'current_epoch now_ts' "$SETUP_SCRIPT" \ - || fail 'setup_midnight_shutdown.sh should use out-var epoch helper in D-Bus throttling path' +grep -Fq 'monitor_with_dbus()' "$SETUP_SCRIPT" || + fail 'setup_midnight_shutdown.sh should install the D-Bus monitor helper' +grep -Fq 'start_monitoring()' "$SETUP_SCRIPT" || + fail 'setup_midnight_shutdown.sh should install the start_monitoring dispatcher' +grep -Fq 'if command -v busctl &>/dev/null; then' "$SETUP_SCRIPT" || + fail 'setup_midnight_shutdown.sh should prefer busctl when available' +grep -Fq 'current_epoch now_ts' "$SETUP_SCRIPT" || + fail 'setup_midnight_shutdown.sh should use out-var epoch helper in D-Bus throttling path' +# Searching for this literal source text; expanding it would run +# current_epoch and grep for its output instead. +# shellcheck disable=SC2016 if grep -Fq 'now_ts=$(current_epoch)' "$SETUP_SCRIPT"; then - fail 'setup_midnight_shutdown.sh should avoid subshell epoch capture in D-Bus path' + fail 'setup_midnight_shutdown.sh should avoid subshell epoch capture in D-Bus path' fi +# Literal source text, as above. +# shellcheck disable=SC2016 if grep -Fq 'now_ts=$(current_epoch)' "$TARGET_SCRIPT"; then - fail 'runtime shutdown monitor should avoid subshell epoch capture in D-Bus path' + fail 'runtime shutdown monitor should avoid subshell epoch capture in D-Bus path' fi -grep -Fq 'OnUnitActiveSec=300' "$SETUP_SCRIPT" \ - || fail 'setup_midnight_shutdown.sh should run watchdog timer at 300s cadence' -grep -Fq 'wait_seconds()' "$SETUP_SCRIPT" \ - || fail 'setup_midnight_shutdown.sh should install builtin wait helper in polling fallback' -grep -Fq 'wait_seconds "$CHECK_INTERVAL"' "$TARGET_SCRIPT" \ - || fail 'runtime shutdown monitor polling fallback should use builtin wait helper' +grep -Fq 'OnUnitActiveSec=300' "$SETUP_SCRIPT" || + fail 'setup_midnight_shutdown.sh should run watchdog timer at 300s cadence' +grep -Fq 'wait_seconds()' "$SETUP_SCRIPT" || + fail 'setup_midnight_shutdown.sh should install builtin wait helper in polling fallback' +# Literal source text, as above. +# shellcheck disable=SC2016 +grep -Fq 'wait_seconds "$CHECK_INTERVAL"' "$TARGET_SCRIPT" || + fail 'runtime shutdown monitor polling fallback should use builtin wait helper' printf 'shutdown-timer-monitor.sh regression checks passed.\n' diff --git a/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh b/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh index 044ecfb7..41a841be 100755 --- a/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh +++ b/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh @@ -8,12 +8,12 @@ REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) INSTALLER="$REPO_DIR/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh" fail() { - printf 'FAIL: %s\n' "$1" >&2 - exit 1 + printf 'FAIL: %s\n' "$1" >&2 + exit 1 } logger_template=$( - awk ' + awk ' /cat > "\$HOME\/.local\/bin\/nvidia-pmon-logger\.sh" << '\''SCRIPT'\''/ {capture=1; next} capture && /^SCRIPT$/ {capture=0; exit} capture {print} @@ -23,30 +23,36 @@ logger_template=$( [[ -n $logger_template ]] || fail 'could not extract nvidia-pmon-logger template from installer' printf 'Checking pmon logger template avoids read -t busy-loop pattern...\n' -! grep -q 'read -r -t' <<< "$logger_template" \ - || fail 'logger template must not use read -t as sleep surrogate' +! grep -q 'read -r -t' <<<"$logger_template" || + fail 'logger template must not use read -t as sleep surrogate' # The template used to poll with `sleep 60` until the day changed. It now # sleeps exactly once, until the next midnight, and blocks on `wait` for the # pmon child — strictly fewer wakeups. Assert that property rather than the # old literal, so an accidental return to polling still fails the test. printf 'Checking pmon logger template sleeps until rollover rather than polling...\n' -grep -q 'sleep "\$(seconds_until_next_day)"' <<< "$logger_template" \ - || fail 'logger template must sleep until the next day boundary' +# Literal search pattern: the $ must reach grep as text, not be expanded +# by this shell. +# shellcheck disable=SC2016 +grep -q 'sleep "\$(seconds_until_next_day)"' <<<"$logger_template" || + fail 'logger template must sleep until the next day boundary' -grep -q 'seconds_until_next_day()' <<< "$logger_template" \ - || fail 'logger template must define seconds_until_next_day' +grep -q 'seconds_until_next_day()' <<<"$logger_template" || + fail 'logger template must define seconds_until_next_day' printf 'Checking pmon logger template blocks on wait instead of spinning...\n' -grep -qE '^\s*wait "\$pmon_pid"' <<< "$logger_template" \ - || fail 'logger template must block on wait for the pmon child' +# Literal search pattern: the $ must reach grep as text, not be expanded +# by this shell. +# shellcheck disable=SC2016 +grep -qE '^\s*wait "\$pmon_pid"' <<<"$logger_template" || + fail 'logger template must block on wait for the pmon child' printf 'Checking pmon logger template uses fork-free date builtin...\n' -grep -q "printf '%(%Y%m%d)T' -1" <<< "$logger_template" \ - || fail 'logger template must use bash printf time builtin for current day' +grep -q "printf '%(%Y%m%d)T' -1" <<<"$logger_template" || + fail 'logger template must use bash printf time builtin for current day' printf 'Checking pmon logger template avoids external date command...\n' -! grep -q 'date +%Y%m%d' <<< "$logger_template" \ - || fail 'logger template must not call external date command in hot path' +! grep -q 'date +%Y%m%d' <<<"$logger_template" || + fail 'logger template must not call external date command in hot path' printf 'Usage monitoring installer efficiency tests passed.\n' diff --git a/meta/lint_python.sh b/meta/lint_python.sh index 0ff30c03..f9c63411 100755 --- a/meta/lint_python.sh +++ b/meta/lint_python.sh @@ -26,78 +26,85 @@ BOLD='\033[1m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="${SCRIPT_DIR}" PYTHON_PATHS=( - "PYTHON" - "articles" - "poker-modifier-app" - "tests" + "PYTHON" + "articles" + "poker-modifier-app" + "tests" ) EXCLUDE_PATHS=( - ".venv" - "__pycache__" - ".git" - "Bash/ffmpeg-build" - ".pytest_cache" - ".ruff_cache" - ".mypy_cache" + ".venv" + "__pycache__" + ".git" + "Bash/ffmpeg-build" + ".pytest_cache" + ".ruff_cache" + ".mypy_cache" ) # Build exclude pattern for find EXCLUDE_PATTERN="" for path in "${EXCLUDE_PATHS[@]}"; do - EXCLUDE_PATTERN="${EXCLUDE_PATTERN} -path '*/${path}/*' -prune -o" + EXCLUDE_PATTERN="${EXCLUDE_PATTERN} -path '*/${path}/*' -prune -o" done # Parse arguments FIX_MODE=false QUICK_MODE=false REPORT_MODE=false +# Kept as BOTH a string and an array on purpose: run_tool() takes a single +# command STRING (it evals it), so the string form has to stay, while the +# handful of direct invocations below need proper argument splitting that +# only an array gives -- an unquoted "${TARGET_FILES}" there would also glob. TARGET_FILES="" +TARGET_FILES_ARR=() while [[ $# -gt 0 ]]; do - case $1 in - --fix|-f) - FIX_MODE=true - shift - ;; - --quick|-q) - QUICK_MODE=true - shift - ;; - --report|-r) - REPORT_MODE=true - shift - ;; - --help|-h) - echo "Usage: $0 [OPTIONS] [FILES...]" - echo "" - echo "Options:" - echo " --fix, -f Auto-fix issues where possible" - echo " --quick, -q Quick mode (ruff + mypy only)" - echo " --report, -r Generate detailed reports to ./lint-reports/" - echo " --help, -h Show this help message" - echo "" - echo "Examples:" - echo " $0 # Lint all Python files" - echo " $0 --fix # Lint and auto-fix" - echo " $0 PYTHON/ # Lint specific directory" - echo " $0 --quick --fix # Quick lint with auto-fix" - exit 0 - ;; - *) - TARGET_FILES="${TARGET_FILES} $1" - shift - ;; - esac + case $1 in + --fix | -f) + FIX_MODE=true + shift + ;; + --quick | -q) + QUICK_MODE=true + shift + ;; + --report | -r) + REPORT_MODE=true + shift + ;; + --help | -h) + echo "Usage: $0 [OPTIONS] [FILES...]" + echo "" + echo "Options:" + echo " --fix, -f Auto-fix issues where possible" + echo " --quick, -q Quick mode (ruff + mypy only)" + echo " --report, -r Generate detailed reports to ./lint-reports/" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 # Lint all Python files" + echo " $0 --fix # Lint and auto-fix" + echo " $0 PYTHON/ # Lint specific directory" + echo " $0 --quick --fix # Quick lint with auto-fix" + exit 0 + ;; + *) + TARGET_FILES="${TARGET_FILES} $1" + TARGET_FILES_ARR+=("$1") + shift + ;; + esac done # If no target specified, use default paths if [[ -z "${TARGET_FILES}" ]]; then - TARGET_FILES="${PYTHON_PATHS[*]}" + TARGET_FILES="${PYTHON_PATHS[*]}" + TARGET_FILES_ARR=("${PYTHON_PATHS[@]}") fi # Create reports directory if needed if [[ "${REPORT_MODE}" == true ]]; then - mkdir -p "${PROJECT_ROOT}/lint-reports" + mkdir -p "${PROJECT_ROOT}/lint-reports" fi # Track overall status @@ -109,70 +116,70 @@ FAILED_TOOLS=() # ============================================================================== print_header() { - echo "" - echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}" - echo -e "${BOLD}${BLUE} $1${NC}" - echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD}${BLUE} $1${NC}" + echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}" } print_subheader() { - echo "" - echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}" - echo -e "${CYAN} $1${NC}" - echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}" + echo "" + echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}" + echo -e "${CYAN} $1${NC}" + echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}" } print_success() { - echo -e "${GREEN}✓${NC} $1" + echo -e "${GREEN}✓${NC} $1" } print_warning() { - echo -e "${YELLOW}⚠${NC} $1" + echo -e "${YELLOW}⚠${NC} $1" } print_error() { - echo -e "${RED}✗${NC} $1" + echo -e "${RED}✗${NC} $1" } print_info() { - echo -e "${BLUE}ℹ${NC} $1" + echo -e "${BLUE}ℹ${NC} $1" } run_tool() { - local tool_name="$1" - local tool_cmd="$2" - local report_file="${PROJECT_ROOT}/lint-reports/${tool_name}.txt" + local tool_name="$1" + local tool_cmd="$2" + local report_file="${PROJECT_ROOT}/lint-reports/${tool_name}.txt" - print_subheader "Running ${tool_name}..." + print_subheader "Running ${tool_name}..." - if [[ "${REPORT_MODE}" == true ]]; then - if eval "${tool_cmd}" 2>&1 | tee "${report_file}"; then - print_success "${tool_name} passed" - return 0 - else - print_error "${tool_name} found issues (see ${report_file})" - FAILED_TOOLS+=("${tool_name}") - return 1 - fi - else - if eval "${tool_cmd}"; then - print_success "${tool_name} passed" - return 0 - else - print_error "${tool_name} found issues" - FAILED_TOOLS+=("${tool_name}") - return 1 - fi - fi + if [[ "${REPORT_MODE}" == true ]]; then + if eval "${tool_cmd}" 2>&1 | tee "${report_file}"; then + print_success "${tool_name} passed" + return 0 + else + print_error "${tool_name} found issues (see ${report_file})" + FAILED_TOOLS+=("${tool_name}") + return 1 + fi + else + if eval "${tool_cmd}"; then + print_success "${tool_name} passed" + return 0 + else + print_error "${tool_name} found issues" + FAILED_TOOLS+=("${tool_name}") + return 1 + fi + fi } check_tool() { - if command -v "$1" &> /dev/null; then - return 0 - else - print_warning "$1 not found, skipping..." - return 1 - fi + if command -v "$1" &>/dev/null; then + return 0 + else + print_warning "$1 not found, skipping..." + return 1 + fi } # ============================================================================== @@ -192,135 +199,135 @@ cd "${PROJECT_ROOT}" # RUFF - Primary linter and formatter # ============================================================================== if check_tool ruff; then - if [[ "${FIX_MODE}" == true ]]; then - run_tool "ruff-lint" "ruff check --fix --show-fixes ${TARGET_FILES}" || OVERALL_STATUS=1 - run_tool "ruff-format" "ruff format ${TARGET_FILES}" || OVERALL_STATUS=1 - else - run_tool "ruff-lint" "ruff check ${TARGET_FILES}" || OVERALL_STATUS=1 - run_tool "ruff-format-check" "ruff format --check ${TARGET_FILES}" || OVERALL_STATUS=1 - fi + if [[ "${FIX_MODE}" == true ]]; then + run_tool "ruff-lint" "ruff check --fix --show-fixes ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "ruff-format" "ruff format ${TARGET_FILES}" || OVERALL_STATUS=1 + else + run_tool "ruff-lint" "ruff check ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "ruff-format-check" "ruff format --check ${TARGET_FILES}" || OVERALL_STATUS=1 + fi fi # ============================================================================== # MYPY - Static type checking # ============================================================================== if check_tool mypy; then - run_tool "mypy" "mypy --strict --ignore-missing-imports ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "mypy" "mypy --strict --ignore-missing-imports ${TARGET_FILES}" || OVERALL_STATUS=1 fi # Quick mode exits here if [[ "${QUICK_MODE}" == true ]]; then - print_header "Quick Lint Complete" - if [[ ${#FAILED_TOOLS[@]} -gt 0 ]]; then - print_error "Failed tools: ${FAILED_TOOLS[*]}" - exit 1 - else - print_success "All quick checks passed!" - exit 0 - fi + print_header "Quick Lint Complete" + if [[ ${#FAILED_TOOLS[@]} -gt 0 ]]; then + print_error "Failed tools: ${FAILED_TOOLS[*]}" + exit 1 + else + print_success "All quick checks passed!" + exit 0 + fi fi # ============================================================================== # PYLINT - Comprehensive linting # ============================================================================== if check_tool pylint; then - run_tool "pylint" "pylint --rcfile=pyproject.toml --jobs=0 --fail-under=10 ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "pylint" "pylint --rcfile=pyproject.toml --jobs=0 --fail-under=10 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # BANDIT - Security linting # ============================================================================== if check_tool bandit; then - run_tool "bandit" "bandit -c pyproject.toml -r ${TARGET_FILES} --severity-level low --confidence-level low" || OVERALL_STATUS=1 + run_tool "bandit" "bandit -c pyproject.toml -r ${TARGET_FILES} --severity-level low --confidence-level low" || OVERALL_STATUS=1 fi # ============================================================================== # VULTURE - Dead code detection # ============================================================================== if check_tool vulture; then - run_tool "vulture" "vulture --min-confidence 80 ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "vulture" "vulture --min-confidence 80 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # FLAKE8 - Traditional linter # ============================================================================== if check_tool flake8; then - run_tool "flake8" "flake8 --max-line-length=88 --extend-ignore=E203,W503 --max-complexity=10 ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "flake8" "flake8 --max-line-length=88 --extend-ignore=E203,W503 --max-complexity=10 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # PYCODESTYLE - PEP 8 style checker # ============================================================================== if check_tool pycodestyle; then - run_tool "pycodestyle" "pycodestyle --max-line-length=88 --ignore=E203,W503 ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "pycodestyle" "pycodestyle --max-line-length=88 --ignore=E203,W503 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # PYDOCSTYLE - Docstring style checker # ============================================================================== if check_tool pydocstyle; then - run_tool "pydocstyle" "pydocstyle --convention=google ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "pydocstyle" "pydocstyle --convention=google ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # RADON - Complexity metrics # ============================================================================== if check_tool radon; then - print_subheader "Running radon (complexity analysis)..." - echo "" - echo -e "${MAGENTA}Cyclomatic Complexity:${NC}" - radon cc -a -s ${TARGET_FILES} || true - echo "" - echo -e "${MAGENTA}Maintainability Index:${NC}" - radon mi -s ${TARGET_FILES} || true + print_subheader "Running radon (complexity analysis)..." + echo "" + echo -e "${MAGENTA}Cyclomatic Complexity:${NC}" + radon cc -a -s "${TARGET_FILES_ARR[@]}" || true + echo "" + echo -e "${MAGENTA}Maintainability Index:${NC}" + radon mi -s "${TARGET_FILES_ARR[@]}" || true - if [[ "${REPORT_MODE}" == true ]]; then - radon cc -a -s ${TARGET_FILES} > "${PROJECT_ROOT}/lint-reports/radon-cc.txt" 2>&1 || true - radon mi -s ${TARGET_FILES} > "${PROJECT_ROOT}/lint-reports/radon-mi.txt" 2>&1 || true - fi + if [[ "${REPORT_MODE}" == true ]]; then + radon cc -a -s "${TARGET_FILES_ARR[@]}" >"${PROJECT_ROOT}/lint-reports/radon-cc.txt" 2>&1 || true + radon mi -s "${TARGET_FILES_ARR[@]}" >"${PROJECT_ROOT}/lint-reports/radon-mi.txt" 2>&1 || true + fi fi # ============================================================================== # INTERROGATE - Docstring coverage # ============================================================================== if check_tool interrogate; then - run_tool "interrogate" "interrogate -v --fail-under=0 ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "interrogate" "interrogate -v --fail-under=0 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # PYRIGHT - Microsoft's type checker (optional, very strict) # ============================================================================== if check_tool pyright; then - run_tool "pyright" "pyright ${TARGET_FILES}" || OVERALL_STATUS=1 + run_tool "pyright" "pyright ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # AUTOFLAKE - Unused imports/variables (fix mode only) # ============================================================================== if [[ "${FIX_MODE}" == true ]] && check_tool autoflake; then - print_subheader "Running autoflake (removing unused imports)..." - find ${TARGET_FILES} -name "*.py" -type f -exec autoflake --in-place --remove-all-unused-imports --remove-unused-variables {} \; - print_success "autoflake completed" + print_subheader "Running autoflake (removing unused imports)..." + find "${TARGET_FILES_ARR[@]}" -name "*.py" -type f -exec autoflake --in-place --remove-all-unused-imports --remove-unused-variables {} \; + print_success "autoflake completed" fi # ============================================================================== # PYUPGRADE - Upgrade Python syntax (fix mode only) # ============================================================================== if [[ "${FIX_MODE}" == true ]] && check_tool pyupgrade; then - print_subheader "Running pyupgrade (upgrading syntax to Python 3.10+)..." - find ${TARGET_FILES} -name "*.py" -type f -exec pyupgrade --py310-plus {} \; - print_success "pyupgrade completed" + print_subheader "Running pyupgrade (upgrading syntax to Python 3.10+)..." + find "${TARGET_FILES_ARR[@]}" -name "*.py" -type f -exec pyupgrade --py310-plus {} \; + print_success "pyupgrade completed" fi # ============================================================================== # CODESPELL - Spell checking # ============================================================================== if check_tool codespell; then - if [[ "${FIX_MODE}" == true ]]; then - run_tool "codespell" "codespell -w --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1 - else - run_tool "codespell" "codespell --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1 - fi + if [[ "${FIX_MODE}" == true ]]; then + run_tool "codespell" "codespell -w --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1 + else + run_tool "codespell" "codespell --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1 + fi fi # ============================================================================== @@ -330,17 +337,17 @@ print_header "Linting Summary" echo "" if [[ ${OVERALL_STATUS} -ne 0 ]]; then - print_error "The following tools reported issues:" - for tool in "${FAILED_TOOLS[@]}"; do - echo " - ${tool}" - done - echo "" - if [[ "${REPORT_MODE}" == true ]]; then - print_info "Detailed reports saved to: ${PROJECT_ROOT}/lint-reports/" - fi - print_info "Run with --fix to auto-fix issues where possible" - exit ${OVERALL_STATUS} + print_error "The following tools reported issues:" + for tool in "${FAILED_TOOLS[@]}"; do + echo " - ${tool}" + done + echo "" + if [[ "${REPORT_MODE}" == true ]]; then + print_info "Detailed reports saved to: ${PROJECT_ROOT}/lint-reports/" + fi + print_info "Run with --fix to auto-fix issues where possible" + exit ${OVERALL_STATUS} else - print_success "All linting checks passed!" - exit 0 + print_success "All linting checks passed!" + exit 0 fi diff --git a/meta/run.sh b/meta/run.sh index ec5d880d..72d5b4c1 100755 --- a/meta/run.sh +++ b/meta/run.sh @@ -17,131 +17,134 @@ REPORT_SCRIPT="$SCRIPT_DIR/linux_configuration/scripts/periodic_background/syste ARTIFACT_INIT_SCRIPT="$SCRIPT_DIR/meta/scripts/init_agent_artifacts.sh" if [[ ! -f "$REPORT_SCRIPT" ]]; then - echo "Error: usage_report.py not found at: $REPORT_SCRIPT" >&2 - exit 1 + echo "Error: usage_report.py not found at: $REPORT_SCRIPT" >&2 + exit 1 fi if [[ ! -f "$ARTIFACT_INIT_SCRIPT" ]]; then - echo "Error: init_agent_artifacts.sh not found at: $ARTIFACT_INIT_SCRIPT" >&2 - exit 1 + echo "Error: init_agent_artifacts.sh not found at: $ARTIFACT_INIT_SCRIPT" >&2 + exit 1 fi # Profiling mode: trace fork-heavy scripts over time profile_polling_scripts() { - local duration="${1:-60}" - echo "=== Polling Script Profiler (${duration}s) ===" >&2 - echo "Tracing fork/exec calls in shell scripts..." >&2 - echo "" >&2 + local duration="${1:-60}" + echo "=== Polling Script Profiler (${duration}s) ===" >&2 + echo "Tracing fork/exec calls in shell scripts..." >&2 + echo "" >&2 - # Find common polling script processes and trace them - local trace_file="/tmp/polling_trace_$$.txt" + # Find common polling script processes and trace them + local trace_file="/tmp/polling_trace_$$.txt" - # Use perf/strace to capture system calls - ( - timeout "$duration" strace -f -e trace=clone,execve -c -p $$ 2>&1 || true - ) > "$trace_file" 2>&1 + # Use perf/strace to capture system calls + ( + timeout "$duration" strace -f -e trace=clone,execve -c -p $$ 2>&1 || true + ) >"$trace_file" 2>&1 - echo "Trace completed. Analyzing results:" >&2 - echo "" >&2 + echo "Trace completed. Analyzing results:" >&2 + echo "" >&2 - # Show fork/exec heavy processes - if ! grep -e "execve" -e "clone" "$trace_file" | head -20; then - : - fi + # Show fork/exec heavy processes + if ! grep -e "execve" -e "clone" "$trace_file" | head -20; then + : + fi - rm -f "$trace_file" + rm -f "$trace_file" } # Diagnostic mode: find inefficient patterns in shell scripts diagnose_polling_scripts() { - echo "=== Shell Script Efficiency Audit ===" >&2 - echo "" >&2 + echo "=== Shell Script Efficiency Audit ===" >&2 + echo "" >&2 - local issues_found=0 + local issues_found=0 - # Check for common anti-patterns - echo "Checking for anti-patterns in shell scripts..." >&2 - echo "" >&2 + # Check for common anti-patterns + echo "Checking for anti-patterns in shell scripts..." >&2 + echo "" >&2 - # Pattern 1: while true with sleep (no event-driven check) - echo "1. Polling loops (while true + sleep):" >&2 - set +e - grep -r "while true\|while :" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \ - | grep -v "Binary" | grep -v ".git" | head -5 - set -e - issues_found=$((issues_found + 1)) - echo "" >&2 + # Pattern 1: while true with sleep (no event-driven check) + echo "1. Polling loops (while true + sleep):" >&2 + set +e + grep -r "while true\|while :" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null | + grep -v "Binary" | grep -v ".git" | head -5 + set -e + issues_found=$((issues_found + 1)) + echo "" >&2 - # Pattern 2: $(date +...) calls in loops (fork-heavy) - echo "2. Excessive date calls (each forks a process):" >&2 - set +e - grep -r '\$(date' --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \ - | grep -v "Binary" | grep -v ".git" | head -5 - set -e - issues_found=$((issues_found + 1)) - echo "" >&2 + # Pattern 2: $(date +...) calls in loops (fork-heavy) + echo "2. Excessive date calls (each forks a process):" >&2 + set +e + # Literal search pattern: the $ must reach grep as text, not be expanded + # by this shell. + # shellcheck disable=SC2016 + grep -r '\$(date' --include="*.sh" "$SCRIPT_DIR" 2>/dev/null | + grep -v "Binary" | grep -v ".git" | head -5 + set -e + issues_found=$((issues_found + 1)) + echo "" >&2 - # Pattern 3: pgrep/xdotool in loops - echo "3. Process inspection in loops (pgrep, xdotool):" >&2 - set +e - grep -r "while.*pgrep\|while.*xdotool\|pgrep.*while" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \ - | grep -v "Binary" | grep -v ".git" | head -5 - set -e - issues_found=$((issues_found + 1)) - echo "" >&2 + # Pattern 3: pgrep/xdotool in loops + echo "3. Process inspection in loops (pgrep, xdotool):" >&2 + set +e + grep -r "while.*pgrep\|while.*xdotool\|pgrep.*while" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null | + grep -v "Binary" | grep -v ".git" | head -5 + set -e + issues_found=$((issues_found + 1)) + echo "" >&2 - # Pattern 4: pipes in hot paths - echo "4. Heavy pipes in polling scripts (| awk, | grep, | tr):" >&2 - set +e - while_true_file_list="$(mktemp)" - heavy_pipe_matches="$(mktemp)" - grep -r "while true" --include="*.sh" "$SCRIPT_DIR" > "$while_true_file_list" 2>/dev/null - if [ -s "$while_true_file_list" ]; then - xargs grep -l -e " | awk" -e " | grep" -e " | tr" < "$while_true_file_list" > "$heavy_pipe_matches" 2>/dev/null - head -5 "$heavy_pipe_matches" - fi - rm -f "$while_true_file_list" "$heavy_pipe_matches" - set -e - issues_found=$((issues_found + 1)) - echo "" >&2 + # Pattern 4: pipes in hot paths + echo "4. Heavy pipes in polling scripts (| awk, | grep, | tr):" >&2 + set +e + while_true_file_list="$(mktemp)" + heavy_pipe_matches="$(mktemp)" + grep -r "while true" --include="*.sh" "$SCRIPT_DIR" >"$while_true_file_list" 2>/dev/null + if [ -s "$while_true_file_list" ]; then + xargs grep -l -e " | awk" -e " | grep" -e " | tr" <"$while_true_file_list" >"$heavy_pipe_matches" 2>/dev/null + head -5 "$heavy_pipe_matches" + fi + rm -f "$while_true_file_list" "$heavy_pipe_matches" + set -e + issues_found=$((issues_found + 1)) + echo "" >&2 - # Pattern 5: sleep with very short intervals - echo "5. Aggressive polling (sleep < 1s):" >&2 - set +e - grep -rE "sleep 0\.[0-9]|sleep 0[^0-9]" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \ - | grep -v "Binary" | grep -v ".git" | head -5 - set -e - issues_found=$((issues_found + 1)) - echo "" >&2 + # Pattern 5: sleep with very short intervals + echo "5. Aggressive polling (sleep < 1s):" >&2 + set +e + grep -rE "sleep 0\.[0-9]|sleep 0[^0-9]" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null | + grep -v "Binary" | grep -v ".git" | head -5 + set -e + issues_found=$((issues_found + 1)) + echo "" >&2 - echo "=== Recommendations ===" >&2 - echo "1. Replace 'while true + sleep' with event-driven I/O (inotifywait, read -t, etc.)" >&2 - echo "2. Use /proc and /sys instead of forking date, sensors, acpi, etc." >&2 - echo "3. Cache frequently accessed values (e.g., in /tmp state files)" >&2 - echo "4. Use bash builtins: printf %()T instead of date, \${var//} instead of tr, etc." >&2 - echo "5. Use i3blocks interval=persist + event loop instead of polling mode" >&2 - echo "6. Increase polling intervals: 1s → 5s → 10s where acceptable" >&2 + echo "=== Recommendations ===" >&2 + echo "1. Replace 'while true + sleep' with event-driven I/O (inotifywait, read -t, etc.)" >&2 + echo "2. Use /proc and /sys instead of forking date, sensors, acpi, etc." >&2 + echo "3. Cache frequently accessed values (e.g., in /tmp state files)" >&2 + echo "4. Use bash builtins: printf %()T instead of date, \${var//} instead of tr, etc." >&2 + echo "5. Use i3blocks interval=persist + event loop instead of polling mode" >&2 + echo "6. Increase polling intervals: 1s → 5s → 10s where acceptable" >&2 } # Handle special modes case "${1:-}" in - --profile) - profile_polling_scripts "${2:-60}" - exit 0 - ;; - --diagnose) - diagnose_polling_scripts - exit 0 - ;; - --init-artifacts) - shift - exec "$ARTIFACT_INIT_SCRIPT" "$@" - ;; - --help) - grep '^# Usage:' "$0" | sed 's/^# //' | head -1 - grep '^# ' "$0" | sed 's/^# / /' - exit 0 - ;; +--profile) + profile_polling_scripts "${2:-60}" + exit 0 + ;; +--diagnose) + diagnose_polling_scripts + exit 0 + ;; +--init-artifacts) + shift + exec "$ARTIFACT_INIT_SCRIPT" "$@" + ;; +--help) + grep '^# Usage:' "$0" | sed 's/^# //' | head -1 + grep '^# ' "$0" | sed 's/^# / /' + exit 0 + ;; esac # Default: run usage_report.py with all remaining args diff --git a/meta/scripts/check_polling_antipatterns.sh b/meta/scripts/check_polling_antipatterns.sh index ac4f02ed..ee08078b 100755 --- a/meta/scripts/check_polling_antipatterns.sh +++ b/meta/scripts/check_polling_antipatterns.sh @@ -11,48 +11,50 @@ errors=0 files_checked=0 # Patterns to detect (these are red flags for fork storms) -# shellcheck disable=SC2034 +# These are regex patterns held literally -- the $( and ` inside them are +# part of what we search FOR, so they must not be expanded here. +# shellcheck disable=SC2034,SC2016 declare -A patterns=( - ['while_true_with_sleep']='while\s+(true|:).*sleep' - ['date_in_loop']='while.*date|for.*date' - ['pgrep_in_loop']='while.*pgrep|for.*pgrep' - ['xdotool_in_loop']='while.*xdotool|for.*xdotool' - ['pipe_fork_chain']='|.*|.*|.*|' - ['subshell_date']='$\(date\s+\+' - ['backtick_date']='`date\s+\+' - ['excessive_fork_chain']='(\$\(.*\|.*\|.*\)|`.*\|.*\|`)' - ['sleep_less_than_one']='sleep\s+0\.[0-9]' + ['while_true_with_sleep']='while\s+(true|:).*sleep' + ['date_in_loop']='while.*date|for.*date' + ['pgrep_in_loop']='while.*pgrep|for.*pgrep' + ['xdotool_in_loop']='while.*xdotool|for.*xdotool' + ['pipe_fork_chain']='|.*|.*|.*|' + ['subshell_date']='$\(date\s+\+' + ['backtick_date']='`date\s+\+' + ['excessive_fork_chain']='(\$\(.*\|.*\|.*\)|`.*\|.*\|`)' + ['sleep_less_than_one']='sleep\s+0\.[0-9]' ) usage() { - echo "Usage: $(basename "$0") [files...]" - echo "" - echo "Checks shell scripts for polling anti-patterns that cause fork storms." - echo "Exit code 0 = no issues, 1 = issues found" - echo "" - echo "Patterns detected:" - echo " - while true + sleep (should use event-driven I/O)" - echo " - \$(date +...) in loops (forks process)" - echo " - pgrep/xdotool in loops (forks process)" - echo " - Multiple piped commands (each | forks)" - echo " - Aggressive polling (sleep < 1s)" - exit 0 + echo "Usage: $(basename "$0") [files...]" + echo "" + echo "Checks shell scripts for polling anti-patterns that cause fork storms." + echo "Exit code 0 = no issues, 1 = issues found" + echo "" + echo "Patterns detected:" + echo " - while true + sleep (should use event-driven I/O)" + echo " - \$(date +...) in loops (forks process)" + echo " - pgrep/xdotool in loops (forks process)" + echo " - Multiple piped commands (each | forks)" + echo " - Aggressive polling (sleep < 1s)" + exit 0 } check_file() { - local file="$1" + local file="$1" - # Skip non-shell files - if ! grep -qE '^#!.*/(ba)?sh' "$file" 2>/dev/null; then - return 0 - fi + # Skip non-shell files + if ! grep -qE '^#!.*/(ba)?sh' "$file" 2>/dev/null; then + return 0 + fi - files_checked=$((files_checked + 1)) + files_checked=$((files_checked + 1)) - # Single-pass awk analysis: C code, no bash loops, no per-line subshell forks. - # '\'' embeds a literal single quote inside a bash single-quoted string. - local findings - findings=$(awk ' + # Single-pass awk analysis: C code, no bash loops, no per-line subshell forks. + # '\'' embeds a literal single quote inside a bash single-quoted string. + local findings + findings=$(awk ' /^[[:space:]]*([a-zA-Z0-9_]*_(loop|daemon)|poll[a-zA-Z0-9_]*)[[:space:]]*\(\)/ { in_poll=1 } /^[[:space:]]*\}/ { in_poll=0 } in_poll { @@ -77,58 +79,58 @@ check_file() { } ' "$file") - if [[ -n "$findings" ]]; then - echo " $file:" - while IFS= read -r finding; do - echo " Line $finding" - done <<< "$findings" - errors=$((errors + 1)) - return 1 - fi - return 0 + if [[ -n "$findings" ]]; then + echo " $file:" + while IFS= read -r finding; do + echo " Line $finding" + done <<<"$findings" + errors=$((errors + 1)) + return 1 + fi + return 0 } provide_suggestions() { - echo "" - echo "📋 Optimization Tips:" - echo "" - echo "Replace polling loops with:" - echo " • inotifywait/fanotify for file system events" - echo " • timerfd for interval-based tasks" - echo " • select/poll for I/O multiplexing" - echo " • systemd timers for scheduled tasks" - echo " • dbus signals for system events" - echo "" - echo "Reduce fork overhead:" - echo " • Cache \$(date +%s) in variable, update periodically" - echo " • Use /proc filesystem instead of pgrep" - echo " • Consolidate commands with && instead of separate invocations" - echo "" + echo "" + echo "📋 Optimization Tips:" + echo "" + echo "Replace polling loops with:" + echo " • inotifywait/fanotify for file system events" + echo " • timerfd for interval-based tasks" + echo " • select/poll for I/O multiplexing" + echo " • systemd timers for scheduled tasks" + echo " • dbus signals for system events" + echo "" + echo "Reduce fork overhead:" + echo " • Cache \$(date +%s) in variable, update periodically" + echo " • Use /proc filesystem instead of pgrep" + echo " • Consolidate commands with && instead of separate invocations" + echo "" } main() { - # Show usage if requested - [[ "$#" -eq 0 || "$1" == "-h" || "$1" == "--help" ]] && usage + # Show usage if requested + [[ "$#" -eq 0 || "$1" == "-h" || "$1" == "--help" ]] && usage - # Check all provided files - for file in "$@"; do - check_file "$file" || true - done + # Check all provided files + for file in "$@"; do + check_file "$file" || true + done - # Report results - if [[ $files_checked -eq 0 ]]; then - echo "ℹ️ No shell scripts to check" - exit 0 - fi + # Report results + if [[ $files_checked -eq 0 ]]; then + echo "ℹ️ No shell scripts to check" + exit 0 + fi - if [[ $errors -gt 0 ]]; then - echo "" - echo "❌ Found $errors issue(s) in shell scripts" - provide_suggestions - exit 1 - else - echo "✓ No polling anti-patterns detected in $files_checked file(s)" - exit 0 - fi + if [[ $errors -gt 0 ]]; then + echo "" + echo "❌ Found $errors issue(s) in shell scripts" + provide_suggestions + exit 1 + else + echo "✓ No polling anti-patterns detected in $files_checked file(s)" + exit 0 + fi } main "$@" diff --git a/meta/scripts/disk_cleanup_check.sh b/meta/scripts/disk_cleanup_check.sh index e8682274..49beab66 100755 --- a/meta/scripts/disk_cleanup_check.sh +++ b/meta/scripts/disk_cleanup_check.sh @@ -7,16 +7,16 @@ # # Safe by default: nothing is deleted without --clean AND user confirmation. -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -RESET='\033[0m' +RED=$'\033[0;31m' +GREEN=$'\033[0;32m' +YELLOW=$'\033[1;33m' +CYAN=$'\033[0;36m' +BOLD=$'\033[1m' +RESET=$'\033[0m' CLEAN=false if [[ "${1:-}" == "--clean" ]]; then - CLEAN=true + CLEAN=true fi TOTAL_RECLAIMABLE=0 @@ -24,66 +24,66 @@ TOTAL_RECLAIMABLE=0 # ───────────────────── helpers ───────────────────── human_readable() { - local kb=$1 - if (( kb >= 1048576 )); then - printf "%.1f GB" "$(echo "scale=1; $kb / 1048576" | bc)" - elif (( kb >= 1024 )); then - printf "%.1f MB" "$(echo "scale=1; $kb / 1024" | bc)" - else - printf "%d KB" "$kb" - fi + local kb=$1 + if ((kb >= 1048576)); then + printf "%.1f GB" "$(echo "scale=1; $kb / 1048576" | bc)" + elif ((kb >= 1024)); then + printf "%.1f MB" "$(echo "scale=1; $kb / 1024" | bc)" + else + printf "%d KB" "$kb" + fi } dir_size_kb() { - local dir="$1" - if [[ -d "$dir" ]]; then - du -sk "$dir" 2>/dev/null | awk '{print $1}' - else - echo 0 - fi + local dir="$1" + if [[ -d "$dir" ]]; then + du -sk "$dir" 2>/dev/null | awk '{print $1}' + else + echo 0 + fi } report() { - local label="$1" size_kb="$2" detail="${3:-}" - if (( size_kb > 0 )); then - local hr - hr=$(human_readable "$size_kb") - printf "${YELLOW}%-40s${RESET} %10s" "$label" "$hr" - if [[ -n "$detail" ]]; then - printf " ${CYAN}(%s)${RESET}" "$detail" - fi - printf "\n" - TOTAL_RECLAIMABLE=$(( TOTAL_RECLAIMABLE + size_kb )) - fi + local label="$1" size_kb="$2" detail="${3:-}" + if ((size_kb > 0)); then + local hr + hr=$(human_readable "$size_kb") + printf "${YELLOW}%-40s${RESET} %10s" "$label" "$hr" + if [[ -n "$detail" ]]; then + printf " ${CYAN}(%s)${RESET}" "$detail" + fi + printf "\n" + TOTAL_RECLAIMABLE=$((TOTAL_RECLAIMABLE + size_kb)) + fi } # Returns 0 if user confirms, 1 otherwise. Always 1 in dry-run. confirm() { - local prompt="$1" - if $CLEAN; then - printf "${BOLD} → %s [y/N]: ${RESET}" "$prompt" - read -r ans - if [[ "$ans" =~ ^[Yy]$ ]]; then - return 0 - fi - fi - return 1 + local prompt="$1" + if $CLEAN; then + printf "${BOLD} → %s [y/N]: ${RESET}" "$prompt" + read -r ans + if [[ "$ans" =~ ^[Yy]$ ]]; then + return 0 + fi + fi + return 1 } # Safe wrapper: confirm + action, never fails under errexit try_clean() { - local prompt="$1" - shift - if confirm "$prompt"; then - "$@" - printf "${GREEN} ✓ Done${RESET}\n" - fi - return 0 + local prompt="$1" + shift + if confirm "$prompt"; then + "$@" + printf "%s ✓ Done%s\n" "${GREEN}" "${RESET}" + fi + return 0 } # ───────────────────── checks ───────────────────── -printf "\n${BOLD}=== Disk Cleanup Analysis ===${RESET}\n\n" +printf "\n%s=== Disk Cleanup Analysis ===%s\n\n" "${BOLD}" "${RESET}" printf "${BOLD}%-40s %10s${RESET}\n" "Category" "Reclaimable" printf '%s\n' "$(printf '%.0s─' {1..60})" @@ -91,48 +91,48 @@ printf '%s\n' "$(printf '%.0s─' {1..60})" trash_dir="$HOME/.local/share/Trash" size=$(dir_size_kb "$trash_dir") report "Trash" "$size" "empty trash" -if (( size > 0 )); then - try_clean "Empty trash?" \ - rm -rf "${trash_dir}/files" "${trash_dir}/info" "${trash_dir}/expunged" +if ((size > 0)); then + try_clean "Empty trash?" \ + rm -rf "${trash_dir}/files" "${trash_dir}/info" "${trash_dir}/expunged" fi # 2. Pacman cache pacman_cache_size=$(dir_size_kb "/var/cache/pacman/pkg") -if (( pacman_cache_size > 0 )); then - if command -v paccache &>/dev/null; then - pkg_count=$(ls /var/cache/pacman/pkg/ 2>/dev/null | wc -l) - report "Pacman cache ($pkg_count pkgs)" "$pacman_cache_size" \ - "sudo paccache -rk2" - try_clean "Clean pacman cache (keep last 2 versions)?" \ - sudo paccache -rk2 - else - report "Pacman cache (install pacman-contrib)" "$pacman_cache_size" \ - "pacman -S pacman-contrib && paccache -rk2" - fi +if ((pacman_cache_size > 0)); then + if command -v paccache &>/dev/null; then + pkg_count=$(find /var/cache/pacman/pkg/ -maxdepth 1 -type f -name '*.pkg.tar*' 2>/dev/null | wc -l) + report "Pacman cache ($pkg_count pkgs)" "$pacman_cache_size" \ + "sudo paccache -rk2" + try_clean "Clean pacman cache (keep last 2 versions)?" \ + sudo paccache -rk2 + else + report "Pacman cache (install pacman-contrib)" "$pacman_cache_size" \ + "pacman -S pacman-contrib && paccache -rk2" + fi fi # 3. Yay build cache yay_cache="$HOME/.cache/yay" size=$(dir_size_kb "$yay_cache") report "Yay AUR build cache" "$size" "rm -rf ~/.cache/yay" -if (( size > 0 )); then - try_clean "Clear yay build cache?" rm -rf "${yay_cache:?}" +if ((size > 0)); then + try_clean "Clear yay build cache?" rm -rf "${yay_cache:?}" fi # 4. Pip cache pip_cache="$HOME/.cache/pip" size=$(dir_size_kb "$pip_cache") report "Pip cache" "$size" "pip cache purge" -if (( size > 0 )); then - try_clean "Clear pip cache?" rm -rf "${pip_cache:?}" +if ((size > 0)); then + try_clean "Clear pip cache?" rm -rf "${pip_cache:?}" fi # 5. uv cache uv_cache="$HOME/.cache/uv" size=$(dir_size_kb "$uv_cache") report "uv cache" "$size" "uv cache clean" -if (( size > 0 )); then - try_clean "Clear uv cache?" rm -rf "${uv_cache:?}" +if ((size > 0)); then + try_clean "Clear uv cache?" rm -rf "${uv_cache:?}" fi # 6. HuggingFace cache @@ -149,160 +149,159 @@ report "Suno AI cache" "$size" "manual review recommended" go_cache="$HOME/.cache/go-build" size=$(dir_size_kb "$go_cache") report "Go build cache" "$size" "go clean -cache" -if (( size > 0 )); then - try_clean "Clear Go build cache?" rm -rf "${go_cache:?}" +if ((size > 0)); then + try_clean "Clear Go build cache?" rm -rf "${go_cache:?}" fi # 9. Yarn cache yarn_cache="$HOME/.cache/yarn" size=$(dir_size_kb "$yarn_cache") report "Yarn cache" "$size" "yarn cache clean" -if (( size > 0 )); then - try_clean "Clear yarn cache?" rm -rf "${yarn_cache:?}" +if ((size > 0)); then + try_clean "Clear yarn cache?" rm -rf "${yarn_cache:?}" fi # 10. npm cache npm_cache="$HOME/.npm/_cacache" size=$(dir_size_kb "$npm_cache") report "npm cache" "$size" "npm cache clean --force" -if (( size > 0 )); then - try_clean "Clear npm cache?" rm -rf "${npm_cache:?}" +if ((size > 0)); then + try_clean "Clear npm cache?" rm -rf "${npm_cache:?}" fi # 11. Electron cache electron_cache="$HOME/.cache/electron" size=$(dir_size_kb "$electron_cache") report "Electron cache" "$size" -if (( size > 0 )); then - try_clean "Clear Electron cache?" rm -rf "${electron_cache:?}" +if ((size > 0)); then + try_clean "Clear Electron cache?" rm -rf "${electron_cache:?}" fi # 12. Thumbnails thumb_cache="$HOME/.cache/thumbnails" size=$(dir_size_kb "$thumb_cache") report "Thumbnails cache" "$size" -if (( size > 0 )); then - try_clean "Clear thumbnails?" rm -rf "${thumb_cache:?}" +if ((size > 0)); then + try_clean "Clear thumbnails?" rm -rf "${thumb_cache:?}" fi # 13. vscode-cpptools cache cpptools_cache="$HOME/.cache/vscode-cpptools" size=$(dir_size_kb "$cpptools_cache") -if (( size > 102400 )); then - report "VS Code cpptools cache" "$size" - try_clean "Clear cpptools cache?" rm -rf "${cpptools_cache:?}" +if ((size > 102400)); then + report "VS Code cpptools cache" "$size" + try_clean "Clear cpptools cache?" rm -rf "${cpptools_cache:?}" fi # 14. Bazel cache bazel_cache="$HOME/.cache/bazel" size=$(dir_size_kb "$bazel_cache") report "Bazel cache" "$size" -if (( size > 0 )); then - try_clean "Clear bazel cache?" rm -rf "${bazel_cache:?}" +if ((size > 0)); then + try_clean "Clear bazel cache?" rm -rf "${bazel_cache:?}" fi # 15. Browser caches (report only) for browser_cache in \ - "$HOME/.cache/thorium" \ - "$HOME/.cache/google-chrome" \ - "$HOME/.cache/mozilla" \ - "$HOME/.cache/BraveSoftware"; do - if [[ -d "$browser_cache" ]]; then - name=$(basename "$browser_cache") - size=$(dir_size_kb "$browser_cache") - if (( size > 102400 )); then - report "Browser cache: $name" "$size" "close browser first" - fi - fi + "$HOME/.cache/thorium" \ + "$HOME/.cache/google-chrome" \ + "$HOME/.cache/mozilla" \ + "$HOME/.cache/BraveSoftware"; do + if [[ -d "$browser_cache" ]]; then + name=$(basename "$browser_cache") + size=$(dir_size_kb "$browser_cache") + if ((size > 102400)); then + report "Browser cache: $name" "$size" "close browser first" + fi + fi done # 16. Docker if command -v docker &>/dev/null; then - docker_dir="/var/lib/docker" - size=$(dir_size_kb "$docker_dir" || echo 0) - if (( size > 1048576 )); then - report "Docker (images+build cache)" "$size" "docker system prune -a" - try_clean "Docker system prune (removes all unused)?" \ - docker system prune -af --volumes - fi + docker_dir="/var/lib/docker" + size=$(dir_size_kb "$docker_dir" || echo 0) + if ((size > 1048576)); then + report "Docker (images+build cache)" "$size" "docker system prune -a" + try_clean "Docker system prune (removes all unused)?" \ + docker system prune -af --volumes + fi fi # 17. Orphan packages orphans=$(pacman -Qdtq 2>/dev/null || true) orphan_count=0 if [[ -n "$orphans" ]]; then - orphan_count=$(echo "$orphans" | wc -l) + orphan_count=$(echo "$orphans" | wc -l) fi -if (( orphan_count > 0 )); then - orphan_size=$(echo "$orphans" | xargs pacman -Qi 2>/dev/null \ - | awk '/Installed Size/{ +if ((orphan_count > 0)); then + orphan_size=$(echo "$orphans" | xargs pacman -Qi 2>/dev/null | + awk '/Installed Size/{ size=$4; unit=$5; if (unit ~ /GiB/) total += size*1048576; else if (unit ~ /MiB/) total += size*1024; else if (unit ~ /KiB/) total += size; } END{printf "%d", total}' || echo 0) - report "Orphan packages ($orphan_count)" "$orphan_size" \ - "sudo pacman -Rns \$(pacman -Qdtq)" - if confirm "Remove $orphan_count orphan packages?"; then - # shellcheck disable=SC2046 - sudo pacman -Rns $(pacman -Qdtq) --noconfirm - printf "${GREEN} ✓ Orphans removed${RESET}\n" - fi + report "Orphan packages ($orphan_count)" "$orphan_size" \ + "sudo pacman -Rns \$(pacman -Qdtq)" + if confirm "Remove $orphan_count orphan packages?"; then + # shellcheck disable=SC2046 + sudo pacman -Rns $(pacman -Qdtq) --noconfirm + printf "%s ✓ Orphans removed%s\n" "${GREEN}" "${RESET}" + fi fi # 18. Journal logs — vacuum to 200M journal_line=$(journalctl --disk-usage 2>&1 || true) journal_kb=0 if [[ "$journal_line" =~ ([0-9.]+)G ]]; then - journal_kb=$(echo "${BASH_REMATCH[1]} * 1048576 / 1" | bc) + journal_kb=$(echo "${BASH_REMATCH[1]} * 1048576 / 1" | bc) elif [[ "$journal_line" =~ ([0-9.]+)M ]]; then - journal_kb=$(echo "${BASH_REMATCH[1]} * 1024 / 1" | bc) + journal_kb=$(echo "${BASH_REMATCH[1]} * 1024 / 1" | bc) fi -if (( journal_kb > 204800 )); then - excess=$(( journal_kb - 204800 )) - report "Journal logs (excess over 200M)" "$excess" "journalctl --vacuum-size=200M" - try_clean "Vacuum journal to 200M?" \ - sudo journalctl --vacuum-size=200M +if ((journal_kb > 204800)); then + excess=$((journal_kb - 204800)) + report "Journal logs (excess over 200M)" "$excess" "journalctl --vacuum-size=200M" + try_clean "Vacuum journal to 200M?" \ + sudo journalctl --vacuum-size=200M fi # 19. AUR source dirs — old archives (>30 days) aur_dir="$HOME/aur" if [[ -d "$aur_dir" ]]; then - old_archives_kb=$(find "$aur_dir" \ - \( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \ - -o -name "*.zip" -o -name "*.tar.bz2" \) \ - -mtime +30 -printf '%k\n' 2>/dev/null \ - | awk '{t+=$1} END{print t+0}') - if (( old_archives_kb > 0 )); then - report "AUR old archives (>30d)" "$old_archives_kb" \ - "find ~/aur '*.pkg.tar.zst' -mtime +30 -delete" - if confirm "Delete old AUR archives (>30 days)?"; then - find "$aur_dir" \ - \( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \ - -o -name "*.zip" -o -name "*.tar.bz2" \) \ - -mtime +30 -delete - printf "${GREEN} ✓ Old AUR archives deleted${RESET}\n" - fi - fi + old_archives_kb=$(find "$aur_dir" \ + \( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \ + -o -name "*.zip" -o -name "*.tar.bz2" \) \ + -mtime +30 -printf '%k\n' 2>/dev/null | + awk '{t+=$1} END{print t+0}') + if ((old_archives_kb > 0)); then + report "AUR old archives (>30d)" "$old_archives_kb" \ + "find ~/aur '*.pkg.tar.zst' -mtime +30 -delete" + if confirm "Delete old AUR archives (>30 days)?"; then + find "$aur_dir" \ + \( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \ + -o -name "*.zip" -o -name "*.tar.bz2" \) \ + -mtime +30 -delete + printf "%s ✓ Old AUR archives deleted%s\n" "${GREEN}" "${RESET}" + fi + fi fi # ───────────────────── report-only items ───────────────────── -printf "\n${BOLD}--- Report-only (manual review needed) ---${RESET}\n" - +printf "\n%s--- Report-only (manual review needed) ---%s\n" "${BOLD}" "${RESET}" for manual_entry in \ - "$HOME/Downloads/too_big:~/Downloads/too_big" \ - "$HOME/Downloads:~/Downloads total" \ - "$HOME/inne:~/inne" \ - "/Games:/Games — still playing?"; do - dir="${manual_entry%%:*}" - label="${manual_entry#*:}" - size=$(dir_size_kb "$dir") - if (( size > 0 )); then - hr=$(human_readable "$size") - printf "${RED}%-40s %10s${RESET} ${CYAN}(review manually)${RESET}\n" \ - "$label" "$hr" - fi + "$HOME/Downloads/too_big:~/Downloads/too_big" \ + "$HOME/Downloads:~/Downloads total" \ + "$HOME/inne:~/inne" \ + "/Games:/Games — still playing?"; do + dir="${manual_entry%%:*}" + label="${manual_entry#*:}" + size=$(dir_size_kb "$dir") + if ((size > 0)); then + hr=$(human_readable "$size") + printf "${RED}%-40s %10s${RESET} ${CYAN}(review manually)${RESET}\n" \ + "$label" "$hr" + fi done # ───────────────────── summary ───────────────────── @@ -311,20 +310,20 @@ printf "\n%s\n" "$(printf '%.0s─' {1..60})" total_hr=$(human_readable "$TOTAL_RECLAIMABLE") printf "${BOLD}Total auto-reclaimable: ${GREEN}%s${RESET}\n" "$total_hr" -read -r used_kb total_kb used_pct <<< "$(df -k / | awk 'NR==2{print $3, $2, $5}')" +read -r used_kb total_kb used_pct <<<"$(df -k / | awk 'NR==2{print $3, $2, $5}')" used_pct="${used_pct%\%}" printf "${BOLD}Current usage: %s / %s (%s%%)${RESET}\n" \ - "$(human_readable "$used_kb")" "$(human_readable "$total_kb")" "$used_pct" + "$(human_readable "$used_kb")" "$(human_readable "$total_kb")" "$used_pct" -if (( TOTAL_RECLAIMABLE > 0 )); then - new_used=$(( used_kb - TOTAL_RECLAIMABLE )) - if (( new_used < 0 )); then new_used=0; fi - new_pct=$(( new_used * 100 / total_kb )) - printf "${BOLD}After cleanup: %s / %s (%d%%)${RESET}\n" \ - "$(human_readable "$new_used")" "$(human_readable "$total_kb")" "$new_pct" +if ((TOTAL_RECLAIMABLE > 0)); then + new_used=$((used_kb - TOTAL_RECLAIMABLE)) + if ((new_used < 0)); then new_used=0; fi + new_pct=$((new_used * 100 / total_kb)) + printf "${BOLD}After cleanup: %s / %s (%d%%)${RESET}\n" \ + "$(human_readable "$new_used")" "$(human_readable "$total_kb")" "$new_pct" fi if ! $CLEAN; then - printf "\n${CYAN}Run with --clean to interactively clean each category.${RESET}\n" + printf "\n%sRun with --clean to interactively clean each category.%s\n" "${CYAN}" "${RESET}" fi printf "\n" diff --git a/phone_focus_mode/config.sh b/phone_focus_mode/config.sh index ad78c748..b2b0fb03 100755 --- a/phone_focus_mode/config.sh +++ b/phone_focus_mode/config.sh @@ -15,6 +15,11 @@ SCRIPT_DIR="${FOCUS_MODE_SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" if [ ! -f "$SCRIPT_DIR/config_secrets.sh" ] && [ -f "/data/local/tmp/focus_mode/config_secrets.sh" ]; then SCRIPT_DIR="/data/local/tmp/focus_mode" fi +# config_secrets.sh is deliberately untracked (it holds secrets, see +# .gitignore) and is created per-machine / pushed to the phone at deploy +# time. It does not exist in a clean checkout, so the linter cannot follow +# it there -- which is exactly what CI lints. +# shellcheck disable=SC1091 . "$SCRIPT_DIR/config_secrets.sh" # --- Radius in meters --- diff --git a/phone_focus_mode/deploy.sh b/phone_focus_mode/deploy.sh index 6d9bc1d4..64461ef9 100755 --- a/phone_focus_mode/deploy.sh +++ b/phone_focus_mode/deploy.sh @@ -17,12 +17,16 @@ set -euo pipefail PHONE_IP="${1:-}" ACTION="${2:---deploy}" REMOTE_DIR="/data/local/tmp/focus_mode" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -NEEDS_GPS_FETCH=0 # set to 1 by check_coords when local coords are placeholder +# Deliberately NOT named SCRIPT_DIR: config.sh (sourced below, at top +# level) assigns SCRIPT_DIR itself and may repoint it at the on-device +# path /data/local/tmp/focus_mode. Every path here must stay anchored to +# the checkout deploy.sh runs from. +DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" +NEEDS_GPS_FETCH=0 # set to 1 by check_coords when local coords are placeholder # Source shared config constants (BROWSER_PACKAGES, REMOTE_DIR, etc.) # shellcheck source=config.sh -. "$SCRIPT_DIR/config.sh" +. "$DEPLOY_DIR/config.sh" ADB_TARGET=() @@ -30,130 +34,130 @@ ADB_TARGET=() # When ADB_SERIAL is set, deploy.sh uses that target directly and preserves # the existing PHONE_IP workflow when ADB_SERIAL is unset. if [[ -n "${ADB_SERIAL:-}" ]]; then - ADB_TARGET=(-s "${ADB_SERIAL}") - if [[ -z "${PHONE_IP}" || "${PHONE_IP}" == --* ]]; then - ACTION="${PHONE_IP:---deploy}" - PHONE_IP="" - fi + ADB_TARGET=(-s "${ADB_SERIAL}") + if [[ -z "${PHONE_IP}" || "${PHONE_IP}" == --* ]]; then + ACTION="${PHONE_IP:---deploy}" + PHONE_IP="" + fi fi adb_cmd() { - adb "${ADB_TARGET[@]}" "$@" + adb "${ADB_TARGET[@]}" "$@" } usage() { - echo "Usage: $0 [action]" - echo " or: ADB_SERIAL= $0 [action]" - echo "" - echo "Actions:" - echo " (none) Full deploy" - echo " --status Show daemon status and current mode" - echo " --log Tail the daemon log" - echo " --stop Stop daemon (re-enables all apps)" - echo " --start Start daemon" - echo " --restart Restart daemon" - echo " --enable Force focus mode on" - echo " --disable Force focus mode off" - echo " --list List all third-party apps and whitelist status" - echo " --pull-log Download log file locally" - echo " --find-pkg Show installed packages matching a filter (e.g. --find-pkg pomodoro)" - echo " --capture-coords Capture current GPS as home location (run after WiFi setup)" - echo " --hosts-status Show hosts enforcer status on the phone" - echo " --hosts-log Show hosts enforcer log on the phone" - echo " --launcher-status Show launcher enforcer status on the phone" - echo " --launcher-log Show launcher enforcer log on the phone" - echo " --snapshot-launcher Snapshot installed Minimalist Phone APK + default HOME" - echo " --install-aurora Download & install Aurora Store (open-source Play Store alt)" - echo "" - echo "Examples:" - echo " $0 192.168.1.42" - echo " $0 192.168.1.42 --status" - echo " $0 192.168.1.42 --find-pkg stronglift" - exit 1 + echo "Usage: $0 [action]" + echo " or: ADB_SERIAL= $0 [action]" + echo "" + echo "Actions:" + echo " (none) Full deploy" + echo " --status Show daemon status and current mode" + echo " --log Tail the daemon log" + echo " --stop Stop daemon (re-enables all apps)" + echo " --start Start daemon" + echo " --restart Restart daemon" + echo " --enable Force focus mode on" + echo " --disable Force focus mode off" + echo " --list List all third-party apps and whitelist status" + echo " --pull-log Download log file locally" + echo " --find-pkg Show installed packages matching a filter (e.g. --find-pkg pomodoro)" + echo " --capture-coords Capture current GPS as home location (run after WiFi setup)" + echo " --hosts-status Show hosts enforcer status on the phone" + echo " --hosts-log Show hosts enforcer log on the phone" + echo " --launcher-status Show launcher enforcer status on the phone" + echo " --launcher-log Show launcher enforcer log on the phone" + echo " --snapshot-launcher Snapshot installed Minimalist Phone APK + default HOME" + echo " --install-aurora Download & install Aurora Store (open-source Play Store alt)" + echo "" + echo "Examples:" + echo " $0 192.168.1.42" + echo " $0 192.168.1.42 --status" + echo " $0 192.168.1.42 --find-pkg stronglift" + exit 1 } # ---- Pre-flight checks ---- check_adb() { - if ! command -v adb >/dev/null 2>&1; then - echo "ERROR: adb not found. Install Android platform-tools first." - echo " Ubuntu/Debian: sudo apt install adb" - echo " Arch: sudo pacman -S android-tools" - exit 1 - fi + if ! command -v adb >/dev/null 2>&1; then + echo "ERROR: adb not found. Install Android platform-tools first." + echo " Ubuntu/Debian: sudo apt install adb" + echo " Arch: sudo pacman -S android-tools" + exit 1 + fi } check_coords() { - local lat lon - lat="$(grep '^.*HOME_LAT=' "$SCRIPT_DIR/config.sh" "$SCRIPT_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)" - lon="$(grep '^.*HOME_LON=' "$SCRIPT_DIR/config.sh" "$SCRIPT_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)" - if [ "$lat" = "0.000000" ] && [ "$lon" = "0.000000" ]; then - echo "ERROR: Home coordinates not set (all zeros)." - exit 1 - fi - # If both look like valid floats, use them; otherwise auto-capture from phone GPS. - if [[ -n "$lat" && "$lat" =~ ^[+-]?[0-9]+\.[0-9]+$ && -n "$lon" && "$lon" =~ ^[+-]?[0-9]+\.[0-9]+$ ]]; then - NEEDS_GPS_FETCH=0 - echo " Home location: $lat, $lon" - else - NEEDS_GPS_FETCH=1 - echo " Home location: placeholder — will be captured from phone GPS at deploy time." - fi + local lat lon + lat="$(grep '^.*HOME_LAT=' "$DEPLOY_DIR/config.sh" "$DEPLOY_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)" + lon="$(grep '^.*HOME_LON=' "$DEPLOY_DIR/config.sh" "$DEPLOY_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)" + if [ "$lat" = "0.000000" ] && [ "$lon" = "0.000000" ]; then + echo "ERROR: Home coordinates not set (all zeros)." + exit 1 + fi + # If both look like valid floats, use them; otherwise auto-capture from phone GPS. + if [[ -n "$lat" && "$lat" =~ ^[+-]?[0-9]+\.[0-9]+$ && -n "$lon" && "$lon" =~ ^[+-]?[0-9]+\.[0-9]+$ ]]; then + NEEDS_GPS_FETCH=0 + echo " Home location: $lat, $lon" + else + NEEDS_GPS_FETCH=1 + echo " Home location: placeholder — will be captured from phone GPS at deploy time." + fi } check_ip() { - if [[ -n "${ADB_SERIAL:-}" ]]; then - return 0 - fi + if [[ -n "${ADB_SERIAL:-}" ]]; then + return 0 + fi - if [ -z "$PHONE_IP" ]; then - echo "ERROR: Phone IP not provided." - echo "" - usage - fi + if [ -z "$PHONE_IP" ]; then + echo "ERROR: Phone IP not provided." + echo "" + usage + fi } connect_adb() { - if [[ -n "${ADB_SERIAL:-}" ]]; then - if ! adb devices | awk 'NR>1 && $2=="device"{print $1}' | grep -Fxq "${ADB_SERIAL}"; then - echo "ERROR: ADB_SERIAL '${ADB_SERIAL}' is not connected." - echo "Connect device via USB or pair wireless ADB first." - exit 1 - fi - ADB_TARGET=(-s "${ADB_SERIAL}") - echo "Using ADB_SERIAL target: ${ADB_SERIAL}" - return 0 - fi + if [[ -n "${ADB_SERIAL:-}" ]]; then + if ! adb devices | awk 'NR>1 && $2=="device"{print $1}' | grep -Fxq "${ADB_SERIAL}"; then + echo "ERROR: ADB_SERIAL '${ADB_SERIAL}' is not connected." + echo "Connect device via USB or pair wireless ADB first." + exit 1 + fi + ADB_TARGET=(-s "${ADB_SERIAL}") + echo "Using ADB_SERIAL target: ${ADB_SERIAL}" + return 0 + fi - echo "Connecting to $PHONE_IP:5555 ..." - adb connect "$PHONE_IP:5555" - sleep 1 - if ! adb devices | grep -q "$PHONE_IP"; then - echo "ERROR: Could not connect to $PHONE_IP:5555" - echo "Make sure wireless ADB is enabled and the phone is reachable." - exit 1 - fi - ADB_TARGET=(-s "$PHONE_IP:5555") - echo "Connected." + echo "Connecting to $PHONE_IP:5555 ..." + adb connect "$PHONE_IP:5555" + sleep 1 + if ! adb devices | grep -q "$PHONE_IP"; then + echo "ERROR: Could not connect to $PHONE_IP:5555" + echo "Make sure wireless ADB is enabled and the phone is reachable." + exit 1 + fi + ADB_TARGET=(-s "$PHONE_IP:5555") + echo "Connected." } # Wrapper: run a root shell command on the phone. # --mount-master was removed in Magisk v26+; plain su -c works and still # runs in the correct mount namespace for our use cases. adb_root() { - local command_text="$1" + local command_text="$1" - printf '%s\n' "$command_text" | adb_cmd shell su -c "sh -s" + printf '%s\n' "$command_text" | adb_cmd shell su -c "sh -s" } compute_file_hash() { - local path="$1" + local path="$1" - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$path" | awk '{print $1}' - return 0 - fi + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{print $1}' + return 0 + fi - md5sum "$path" | awk '{print $1}' + md5sum "$path" | awk '{print $1}' } # ============================================================ @@ -166,36 +170,36 @@ compute_file_hash() { GPS_MAX_WAIT_SECS=90 fetch_home_coords_from_phone() { - echo " Enabling location services on phone..." >&2 - adb_cmd shell settings put secure location_mode 3 2>/dev/null || true + echo " Enabling location services on phone..." >&2 + adb_cmd shell settings put secure location_mode 3 2>/dev/null || true - echo " Waiting for network/fused location fix (up to ${GPS_MAX_WAIT_SECS}s)..." >&2 - local waited=0 coords="" - while [[ -z "$coords" && $waited -lt $GPS_MAX_WAIT_SECS ]]; do - sleep 3 - waited=$((waited + 3)) - # Format on Android 10+: " last location=Location[fused LAT,LON ...]" - local raw - raw="$(adb_cmd shell dumpsys location 2>/dev/null \ - | grep 'last location=Location\[' \ - | grep -oE '[+-]?[0-9]+\.[0-9]+,[+-]?[0-9]+\.[0-9]+' \ - | head -1 || true)" - [[ -n "$raw" ]] && coords="$raw" - printf '.' >&2 - done - printf '\n' >&2 + echo " Waiting for network/fused location fix (up to ${GPS_MAX_WAIT_SECS}s)..." >&2 + local waited=0 coords="" + while [[ -z "$coords" && $waited -lt $GPS_MAX_WAIT_SECS ]]; do + sleep 3 + waited=$((waited + 3)) + # Format on Android 10+: " last location=Location[fused LAT,LON ...]" + local raw + raw="$(adb_cmd shell dumpsys location 2>/dev/null | + grep 'last location=Location\[' | + grep -oE '[+-]?[0-9]+\.[0-9]+,[+-]?[0-9]+\.[0-9]+' | + head -1 || true)" + [[ -n "$raw" ]] && coords="$raw" + printf '.' >&2 + done + printf '\n' >&2 - if [[ -z "$coords" ]]; then - echo "ERROR: No location fix after ${GPS_MAX_WAIT_SECS}s." >&2 - echo " Make sure the phone has cellular or WiFi data, then retry." >&2 - echo " Or set HOME_LAT/HOME_LON manually in config_secrets.sh." >&2 - return 1 - fi + if [[ -z "$coords" ]]; then + echo "ERROR: No location fix after ${GPS_MAX_WAIT_SECS}s." >&2 + echo " Make sure the phone has cellular or WiFi data, then retry." >&2 + echo " Or set HOME_LAT/HOME_LON manually in config_secrets.sh." >&2 + return 1 + fi - local lat="${coords%,*}" - local lon="${coords#*,}" - echo " GPS fix acquired: ${lat}, ${lon}" >&2 - printf '%s %s' "$lat" "$lon" + local lat="${coords%,*}" + local lon="${coords#*,}" + echo " GPS fix acquired: ${lat}, ${lon}" >&2 + printf '%s %s' "$lat" "$lon" } # ============================================================ @@ -208,90 +212,90 @@ fetch_home_coords_from_phone() { HOSTS_MODULE_REBOOT_WAIT_SECS=180 ensure_magisk_hosts_module() { - local state="absent" - if adb_root "test -d /data/adb/modules/hosts" >/dev/null 2>&1; then - if adb_root "test -f /data/adb/modules/hosts/disable -o -f /data/adb/modules/hosts/remove" >/dev/null 2>&1; then - state="disabled" - elif adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then - state="ok" - else - state="not-mounted" - fi - fi + local state="absent" + if adb_root "test -d /data/adb/modules/hosts" >/dev/null 2>&1; then + if adb_root "test -f /data/adb/modules/hosts/disable -o -f /data/adb/modules/hosts/remove" >/dev/null 2>&1; then + state="disabled" + elif adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then + state="ok" + else + state="not-mounted" + fi + fi - if [[ "$state" == "ok" ]]; then - echo " Magisk Systemless Hosts: active." - return 0 - fi + if [[ "$state" == "ok" ]]; then + echo " Magisk Systemless Hosts: active." + return 0 + fi - echo " Magisk Systemless Hosts state: ${state} — auto-installing..." + echo " Magisk Systemless Hosts state: ${state} — auto-installing..." - case "$state" in - absent) - adb_root "mkdir -p /data/adb/modules/hosts/system/etc" - # module.prop is required for Magisk to recognise and process the module. - adb_root "printf 'id=hosts\nname=Systemless Hosts\nversion=v1\nversionCode=1\nauthor=Magisk\ndescription=Replace /system/etc/hosts\n' \ + case "$state" in + absent) + adb_root "mkdir -p /data/adb/modules/hosts/system/etc" + # module.prop is required for Magisk to recognise and process the module. + adb_root "printf 'id=hosts\nname=Systemless Hosts\nversion=v1\nversionCode=1\nauthor=Magisk\ndescription=Replace /system/etc/hosts\n' \ > /data/adb/modules/hosts/module.prop" - # Seed a minimal hosts file so the mount target exists at first boot. - adb_root "printf '127.0.0.1 localhost\n::1 localhost\n' \ + # Seed a minimal hosts file so the mount target exists at first boot. + adb_root "printf '127.0.0.1 localhost\n::1 localhost\n' \ > /data/adb/modules/hosts/system/etc/hosts" - adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts" - ;; - disabled) - adb_root "rm -f /data/adb/modules/hosts/disable \ + adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts" + ;; + disabled) + adb_root "rm -f /data/adb/modules/hosts/disable \ /data/adb/modules/hosts/remove \ /data/adb/modules/hosts/update" - ;; - not-mounted) - : # module exists and enabled, just needs a reboot - ;; - esac + ;; + not-mounted) + : # module exists and enabled, just needs a reboot + ;; + esac - echo " Rebooting phone to activate Magisk Hosts module..." - adb_cmd reboot - # Give the device time to actually begin shutting down before we poll. - sleep 20 + echo " Rebooting phone to activate Magisk Hosts module..." + adb_cmd reboot + # Give the device time to actually begin shutting down before we poll. + sleep 20 - echo " Waiting for device to come back (up to ${HOSTS_MODULE_REBOOT_WAIT_SECS}s)..." - local waited=0 - # Re-establish wireless ADB connection if needed. - while true; do - if [[ -n "${PHONE_IP:-}" ]]; then - adb connect "${PHONE_IP}:5555" >/dev/null 2>&1 || true - fi - if adb_cmd shell echo ok 2>/dev/null | grep -q '^ok$'; then - break - fi - sleep 3 - waited=$((waited + 3)) - if [[ $waited -ge $HOSTS_MODULE_REBOOT_WAIT_SECS ]]; then - echo "ERROR: Device did not come back after ${HOSTS_MODULE_REBOOT_WAIT_SECS}s." - echo " Check USB connection or re-enable wireless ADB, then run deploy again." - exit 1 - fi - printf '.' - done - printf '\n' + echo " Waiting for device to come back (up to ${HOSTS_MODULE_REBOOT_WAIT_SECS}s)..." + local waited=0 + # Re-establish wireless ADB connection if needed. + while true; do + if [[ -n "${PHONE_IP:-}" ]]; then + adb connect "${PHONE_IP}:5555" >/dev/null 2>&1 || true + fi + if adb_cmd shell echo ok 2>/dev/null | grep -q '^ok$'; then + break + fi + sleep 3 + waited=$((waited + 3)) + if [[ $waited -ge $HOSTS_MODULE_REBOOT_WAIT_SECS ]]; then + echo "ERROR: Device did not come back after ${HOSTS_MODULE_REBOOT_WAIT_SECS}s." + echo " Check USB connection or re-enable wireless ADB, then run deploy again." + exit 1 + fi + printf '.' + done + printf '\n' - # Wait for Magisk early-init and root to be ready. - echo " Waiting for Magisk root to be available..." - waited=0 - while ! adb_root "id" 2>/dev/null | grep -q "uid=0"; do - sleep 3 - waited=$((waited + 3)) - [[ $waited -ge 60 ]] && echo "ERROR: Root not available after reboot." && exit 1 - printf '.' - done - printf '\n' + # Wait for Magisk early-init and root to be ready. + echo " Waiting for Magisk root to be available..." + waited=0 + while ! adb_root "id" 2>/dev/null | grep -q "uid=0"; do + sleep 3 + waited=$((waited + 3)) + [[ $waited -ge 60 ]] && echo "ERROR: Root not available after reboot." && exit 1 + printf '.' + done + printf '\n' - # Final assertion: the magic-mount must now be active. - if ! adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then - echo "ERROR: /system/etc/hosts is not magic-mounted after reboot." - echo " Magisk may not have applied the module correctly." - echo " Check the Magisk app for module errors and run deploy again." - exit 1 - fi - echo " Magisk Systemless Hosts module is now active." + # Final assertion: the magic-mount must now be active. + if ! adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then + echo "ERROR: /system/etc/hosts is not magic-mounted after reboot." + echo " Magisk may not have applied the module correctly." + echo " Check the Magisk app for module errors and run deploy again." + exit 1 + fi + echo " Magisk Systemless Hosts module is now active." } # ============================================================ @@ -309,474 +313,483 @@ AURORA_APK_URL="https://gitlab.com/-/project/6922885/uploads/2ee95ec85244b45cc86 AURORA_PACKAGE="com.aurora.store" do_install_aurora() { - connect_adb + connect_adb - # Check if already installed. - if adb_cmd shell pm list packages 2>/dev/null | grep -qx "package:${AURORA_PACKAGE}"; then - echo "Aurora Store is already installed (${AURORA_PACKAGE})." - return 0 - fi + # Check if already installed. + if adb_cmd shell pm list packages 2>/dev/null | grep -qx "package:${AURORA_PACKAGE}"; then + echo "Aurora Store is already installed (${AURORA_PACKAGE})." + return 0 + fi - echo "Downloading Aurora Store ${AURORA_VERSION}..." - local tmp_apk - tmp_apk="$(mktemp --suffix=.apk)" - if ! curl -fsSL --retry 3 -o "$tmp_apk" "$AURORA_APK_URL"; then - rm -f "$tmp_apk" - echo "ERROR: Failed to download Aurora Store from $AURORA_APK_URL" - echo "Manual download: https://auroraoss.com/" - return 1 - fi + echo "Downloading Aurora Store ${AURORA_VERSION}..." + local tmp_apk + tmp_apk="$(mktemp --suffix=.apk)" + if ! curl -fsSL --retry 3 -o "$tmp_apk" "$AURORA_APK_URL"; then + rm -f "$tmp_apk" + echo "ERROR: Failed to download Aurora Store from $AURORA_APK_URL" + echo "Manual download: https://auroraoss.com/" + return 1 + fi - echo "Installing Aurora Store..." - if adb_cmd install -r "$tmp_apk"; then - echo "Aurora Store ${AURORA_VERSION} installed successfully." - echo "Open Aurora Store on the phone, choose 'Anonymous' login, then install apps normally." - else - echo "ERROR: adb install failed. You can side-load manually:" - echo " adb install ${tmp_apk}" - fi - rm -f "$tmp_apk" + echo "Installing Aurora Store..." + if adb_cmd install -r "$tmp_apk"; then + echo "Aurora Store ${AURORA_VERSION} installed successfully." + echo "Open Aurora Store on the phone, choose 'Anonymous' login, then install apps normally." + else + echo "ERROR: adb install failed. You can side-load manually:" + echo " adb install ${tmp_apk}" + fi + rm -f "$tmp_apk" } # ============================================================ # DEPLOY # ============================================================ do_deploy() { - echo "=== Focus Mode Deployer ===" - echo "" - check_coords - echo "" + echo "=== Focus Mode Deployer ===" + echo "" + check_coords + echo "" - echo "[1/7] Connecting to phone..." - connect_adb + echo "[1/7] Connecting to phone..." + connect_adb - echo "[2/7] Verifying root access..." - if ! adb_root "id" | grep -q "uid=0"; then - echo "ERROR: Could not get root shell. Is Magisk installed?" - exit 1 - fi - echo " Root confirmed." + echo "[2/7] Verifying root access..." + if ! adb_root "id" | grep -q "uid=0"; then + echo "ERROR: Could not get root shell. Is Magisk installed?" + exit 1 + fi + echo " Root confirmed." - echo "[2.5] Ensuring Magisk Systemless Hosts module..." - ensure_magisk_hosts_module + echo "[2.5] Ensuring Magisk Systemless Hosts module..." + ensure_magisk_hosts_module - echo "[3/7] Creating directories on device..." - # Use world-writable staging dir so non-root adb push works - adb_cmd shell "mkdir -p /data/local/tmp/focus_stage" - adb_root "mkdir -p $REMOTE_DIR /data/adb/service.d" - adb_root "chmod 777 /data/local/tmp/focus_stage" + echo "[3/7] Creating directories on device..." + # Use world-writable staging dir so non-root adb push works + adb_cmd shell "mkdir -p /data/local/tmp/focus_stage" + adb_root "mkdir -p $REMOTE_DIR /data/adb/service.d" + adb_root "chmod 777 /data/local/tmp/focus_stage" - echo "[4/7] Uploading scripts..." - adb_cmd push "$SCRIPT_DIR/config.sh" "/data/local/tmp/focus_stage/config.sh" - adb_cmd push "$SCRIPT_DIR/focus_daemon.sh" "/data/local/tmp/focus_stage/focus_daemon.sh" - adb_cmd push "$SCRIPT_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh" - adb_cmd push "$SCRIPT_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh" - adb_cmd push "$SCRIPT_DIR/dns_enforcer.sh" "/data/local/tmp/focus_stage/dns_enforcer.sh" - adb_cmd push "$SCRIPT_DIR/launcher_enforcer.sh" "/data/local/tmp/focus_stage/launcher_enforcer.sh" - adb_cmd push "$SCRIPT_DIR/curfew_enforcer.sh" "/data/local/tmp/focus_stage/curfew_enforcer.sh" - adb_cmd push "$SCRIPT_DIR/tether_enforcer.sh" "/data/local/tmp/focus_stage/tether_enforcer.sh" - adb_cmd push "$SCRIPT_DIR/workout_detector.sh" "/data/local/tmp/focus_stage/workout_detector.sh" - adb_cmd push "$SCRIPT_DIR/magisk_service.sh" "/data/local/tmp/focus_stage/99-focus-mode.sh" + echo "[4/7] Uploading scripts..." + adb_cmd push "$DEPLOY_DIR/config.sh" "/data/local/tmp/focus_stage/config.sh" + adb_cmd push "$DEPLOY_DIR/focus_daemon.sh" "/data/local/tmp/focus_stage/focus_daemon.sh" + adb_cmd push "$DEPLOY_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh" + adb_cmd push "$DEPLOY_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh" + adb_cmd push "$DEPLOY_DIR/dns_enforcer.sh" "/data/local/tmp/focus_stage/dns_enforcer.sh" + adb_cmd push "$DEPLOY_DIR/launcher_enforcer.sh" "/data/local/tmp/focus_stage/launcher_enforcer.sh" + adb_cmd push "$DEPLOY_DIR/curfew_enforcer.sh" "/data/local/tmp/focus_stage/curfew_enforcer.sh" + adb_cmd push "$DEPLOY_DIR/tether_enforcer.sh" "/data/local/tmp/focus_stage/tether_enforcer.sh" + adb_cmd push "$DEPLOY_DIR/workout_detector.sh" "/data/local/tmp/focus_stage/workout_detector.sh" + adb_cmd push "$DEPLOY_DIR/magisk_service.sh" "/data/local/tmp/focus_stage/99-focus-mode.sh" - # ---- sqlite3 binary for workout_detector.sh ---- - # Stored outside the repo (binary-files policy). Built once via the NDK - # against the SQLite amalgamation; see workout_detector.sh comments for - # the recipe. ~1.6 MB stripped, aarch64, PIE, dynamically linked against - # bionic (Android 30+). - SQLITE3_BIN="$SCRIPT_DIR/../../testsAndMisc_binaries/phone_focus_mode/sqlite3" - if [ -f "$SQLITE3_BIN" ]; then - echo " Uploading sqlite3 binary ($(stat -c%s "$SQLITE3_BIN") bytes)..." - adb_cmd push "$SQLITE3_BIN" "/data/local/tmp/focus_stage/sqlite3" - else - echo " WARNING: sqlite3 binary not found at $SQLITE3_BIN" - echo " workout_detector will not function until you build & place it there." - fi + # ---- sqlite3 binary for workout_detector.sh ---- + # Stored outside the repo (binary-files policy). Built once via the NDK + # against the SQLite amalgamation; see workout_detector.sh comments for + # the recipe. ~1.6 MB stripped, aarch64, PIE, dynamically linked against + # bionic (Android 30+). + SQLITE3_BIN="$DEPLOY_DIR/../../testsAndMisc_binaries/phone_focus_mode/sqlite3" + if [ -f "$SQLITE3_BIN" ]; then + echo " Uploading sqlite3 binary ($(stat -c%s "$SQLITE3_BIN") bytes)..." + adb_cmd push "$SQLITE3_BIN" "/data/local/tmp/focus_stage/sqlite3" + else + echo " WARNING: sqlite3 binary not found at $SQLITE3_BIN" + echo " workout_detector will not function until you build & place it there." + fi - # Generate and upload the canonical hosts file (StevenBlack + custom entries). - # This mirrors what linux_configuration/hosts/install.sh installs on the PC. - HOSTS_GENERATOR="$SCRIPT_DIR/../linux_configuration/scripts/periodic_background/hosts/generate_hosts_file.sh" - if [ -f "$HOSTS_GENERATOR" ]; then - chmod +x "$HOSTS_GENERATOR" 2>/dev/null || true - echo " Generating canonical hosts file..." - HOSTS_TMP="$(mktemp)" - HOSTS_SHA_TMP="$(mktemp)" - if bash "$HOSTS_GENERATOR" "$HOSTS_TMP"; then - hosts_hash="$(compute_file_hash "$HOSTS_TMP")" - printf '%s\n' "$hosts_hash" > "$HOSTS_SHA_TMP" - echo " Uploading canonical hosts ($(wc -l < "$HOSTS_TMP") lines)..." - adb_cmd push "$HOSTS_TMP" "/data/local/tmp/focus_stage/hosts.canonical" - adb_cmd push "$HOSTS_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256" + # Generate and upload the canonical hosts file (StevenBlack + custom entries). + # This mirrors what linux_configuration/hosts/install.sh installs on the PC. + HOSTS_GENERATOR="$DEPLOY_DIR/../linux_configuration/scripts/periodic_background/hosts/generate_hosts_file.sh" + if [ -f "$HOSTS_GENERATOR" ]; then + chmod +x "$HOSTS_GENERATOR" 2>/dev/null || true + echo " Generating canonical hosts file..." + HOSTS_TMP="$(mktemp)" + HOSTS_SHA_TMP="$(mktemp)" + if bash "$HOSTS_GENERATOR" "$HOSTS_TMP"; then + hosts_hash="$(compute_file_hash "$HOSTS_TMP")" + printf '%s\n' "$hosts_hash" >"$HOSTS_SHA_TMP" + echo " Uploading canonical hosts ($(wc -l <"$HOSTS_TMP") lines)..." + adb_cmd push "$HOSTS_TMP" "/data/local/tmp/focus_stage/hosts.canonical" + adb_cmd push "$HOSTS_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256" - # ---- Workout-variant canonical ---- - # Same content as the full canonical, with all lines that block - # any of $WORKOUT_UNBLOCK_DOMAINS removed. Used by hosts_enforcer - # while a StrongLifts workout is in progress. - HOSTS_WORKOUT_TMP="$(mktemp)" - HOSTS_WORKOUT_SHA_TMP="$(mktemp)" - # Read $WORKOUT_UNBLOCK_DOMAINS from the freshly-staged config.sh - # so the generator and the runtime always agree on the domain set. - UNBLOCK_DOMAINS="$( - # shellcheck disable=SC1091 - ( . "$SCRIPT_DIR/config.sh" >/dev/null 2>&1; printf '%s\n' "$WORKOUT_UNBLOCK_DOMAINS" ) \ - | sed 's/[[:space:]]\{1,\}/\n/g' \ - | grep -vE '^[[:space:]]*(#|$)' \ - | sort -u - )" - if [ -n "$UNBLOCK_DOMAINS" ]; then - # Build an awk regex of exact-match domains anchored as the - # *value* column of a hosts entry (" " possibly - # followed by aliases). We strip any line whose first non-IP - # token matches one of the unblock domains. - WORKOUT_UNBLOCK_DOMAINS="$UNBLOCK_DOMAINS" \ - python3 "$SCRIPT_DIR/strip_workout_hosts.py" "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP" \ - || cp "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP" - workout_hash="$(compute_file_hash "$HOSTS_WORKOUT_TMP")" - printf '%s\n' "$workout_hash" > "$HOSTS_WORKOUT_SHA_TMP" - stripped_lines=$(($(wc -l < "$HOSTS_TMP") - $(wc -l < "$HOSTS_WORKOUT_TMP"))) - echo " Uploading workout-variant hosts (stripped $stripped_lines YouTube lines)..." - adb_cmd push "$HOSTS_WORKOUT_TMP" "/data/local/tmp/focus_stage/hosts.canonical.workout" - adb_cmd push "$HOSTS_WORKOUT_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256.workout" - fi - rm -f "$HOSTS_WORKOUT_TMP" "$HOSTS_WORKOUT_SHA_TMP" + # ---- Workout-variant canonical ---- + # Same content as the full canonical, with all lines that block + # any of $WORKOUT_UNBLOCK_DOMAINS removed. Used by hosts_enforcer + # while a StrongLifts workout is in progress. + HOSTS_WORKOUT_TMP="$(mktemp)" + HOSTS_WORKOUT_SHA_TMP="$(mktemp)" + # Read $WORKOUT_UNBLOCK_DOMAINS from the freshly-staged config.sh + # so the generator and the runtime always agree on the domain set. + UNBLOCK_DOMAINS="$( + # shellcheck disable=SC1091 + ( + . "$DEPLOY_DIR/config.sh" >/dev/null 2>&1 + printf '%s\n' "$WORKOUT_UNBLOCK_DOMAINS" + ) | + sed 's/[[:space:]]\{1,\}/\n/g' | + grep -vE '^[[:space:]]*(#|$)' | + sort -u + )" + if [ -n "$UNBLOCK_DOMAINS" ]; then + # Build an awk regex of exact-match domains anchored as the + # *value* column of a hosts entry (" " possibly + # followed by aliases). We strip any line whose first non-IP + # token matches one of the unblock domains. + WORKOUT_UNBLOCK_DOMAINS="$UNBLOCK_DOMAINS" \ + python3 "$DEPLOY_DIR/strip_workout_hosts.py" "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP" || + cp "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP" + workout_hash="$(compute_file_hash "$HOSTS_WORKOUT_TMP")" + printf '%s\n' "$workout_hash" >"$HOSTS_WORKOUT_SHA_TMP" + stripped_lines=$(($(wc -l <"$HOSTS_TMP") - $(wc -l <"$HOSTS_WORKOUT_TMP"))) + echo " Uploading workout-variant hosts (stripped $stripped_lines YouTube lines)..." + adb_cmd push "$HOSTS_WORKOUT_TMP" "/data/local/tmp/focus_stage/hosts.canonical.workout" + adb_cmd push "$HOSTS_WORKOUT_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256.workout" + fi + rm -f "$HOSTS_WORKOUT_TMP" "$HOSTS_WORKOUT_SHA_TMP" - rm -f "$HOSTS_TMP" - rm -f "$HOSTS_SHA_TMP" - else - rm -f "$HOSTS_TMP" - rm -f "$HOSTS_SHA_TMP" - echo " WARNING: failed to generate hosts file - skipping hosts enforcement" - fi - else - echo " WARNING: $HOSTS_GENERATOR not found - skipping hosts enforcement" - fi + rm -f "$HOSTS_TMP" + rm -f "$HOSTS_SHA_TMP" + else + rm -f "$HOSTS_TMP" + rm -f "$HOSTS_SHA_TMP" + echo " WARNING: failed to generate hosts file - skipping hosts enforcement" + fi + else + echo " WARNING: $HOSTS_GENERATOR not found - skipping hosts enforcement" + fi - # Only push config_secrets.sh if phone doesn't already have one - if adb_root "test -f $REMOTE_DIR/config_secrets.sh" 2>/dev/null; then - echo " config_secrets.sh already exists on phone - skipping (preserving real coords)" - elif [[ "${NEEDS_GPS_FETCH}" -eq 1 ]]; then - # Local config_secrets.sh has placeholder coords — capture current GPS from the phone. - # The phone is assumed to be at home during setup, so current location = home location. - local gps_result="" gps_lat="" gps_lon="" - if gps_result="$(fetch_home_coords_from_phone 2>&1)"; then - gps_lat="${gps_result% *}" - gps_lon="${gps_result#* }" - adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS at deploy time\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \ + # Only push config_secrets.sh if phone doesn't already have one + if adb_root "test -f $REMOTE_DIR/config_secrets.sh" 2>/dev/null; then + echo " config_secrets.sh already exists on phone - skipping (preserving real coords)" + elif [[ "${NEEDS_GPS_FETCH}" -eq 1 ]]; then + # Local config_secrets.sh has placeholder coords — capture current GPS from the phone. + # The phone is assumed to be at home during setup, so current location = home location. + local gps_result="" gps_lat="" gps_lon="" + if gps_result="$(fetch_home_coords_from_phone 2>&1)"; then + gps_lat="${gps_result% *}" + gps_lon="${gps_result#* }" + adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS at deploy time\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \ > $REMOTE_DIR/config_secrets.sh" - echo " Home coordinates written to phone: ${gps_lat}, ${gps_lon}" - else - # GPS unavailable (no WiFi/cellular yet on fresh phone). - # Write stub coords — focus mode stays OFF, hosts/DNS blocking still works. - # User should run: ./deploy.sh [ip] --capture-coords after configuring WiFi. - adb_root "printf '#!/system/bin/sh\n# STUB: run ./deploy.sh --capture-coords after WiFi setup\nexport HOME_LAT=\"0.000001\"\nexport HOME_LON=\"0.000001\"\n' \ + echo " Home coordinates written to phone: ${gps_lat}, ${gps_lon}" + else + # GPS unavailable (no WiFi/cellular yet on fresh phone). + # Write stub coords — focus mode stays OFF, hosts/DNS blocking still works. + # User should run: ./deploy.sh [ip] --capture-coords after configuring WiFi. + adb_root "printf '#!/system/bin/sh\n# STUB: run ./deploy.sh --capture-coords after WiFi setup\nexport HOME_LAT=\"0.000001\"\nexport HOME_LON=\"0.000001\"\n' \ > $REMOTE_DIR/config_secrets.sh" - echo " WARNING: GPS capture failed — focus mode location enforcement is DISABLED." - echo " Hosts/DNS blocking is active. After configuring WiFi, run:" - echo " ADB_SERIAL=${ADB_SERIAL:-\$PHONE_IP:5555} ./deploy.sh --capture-coords" - fi - else - echo " Pushing config_secrets.sh (first install)..." - adb_cmd push "$SCRIPT_DIR/config_secrets.sh" "/data/local/tmp/focus_stage/config_secrets.sh" - adb_root "cp /data/local/tmp/focus_stage/config_secrets.sh $REMOTE_DIR/config_secrets.sh" - fi + echo " WARNING: GPS capture failed — focus mode location enforcement is DISABLED." + echo " Hosts/DNS blocking is active. After configuring WiFi, run:" + echo " ADB_SERIAL=${ADB_SERIAL:-\$PHONE_IP:5555} ./deploy.sh --capture-coords" + fi + else + echo " Pushing config_secrets.sh (first install)..." + adb_cmd push "$DEPLOY_DIR/config_secrets.sh" "/data/local/tmp/focus_stage/config_secrets.sh" + adb_root "cp /data/local/tmp/focus_stage/config_secrets.sh $REMOTE_DIR/config_secrets.sh" + fi - # Move staged files into place with root - adb_root "cp /data/local/tmp/focus_stage/config.sh $REMOTE_DIR/config.sh" - adb_root "cp /data/local/tmp/focus_stage/focus_daemon.sh $REMOTE_DIR/focus_daemon.sh" - adb_root "cp /data/local/tmp/focus_stage/focus_ctl.sh $REMOTE_DIR/focus_ctl.sh" - adb_root "cp /data/local/tmp/focus_stage/hosts_enforcer.sh $REMOTE_DIR/hosts_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/dns_enforcer.sh $REMOTE_DIR/dns_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/launcher_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/curfew_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/tether_enforcer.sh $REMOTE_DIR/tether_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/workout_detector.sh $REMOTE_DIR/workout_detector.sh" - if adb_cmd shell "test -f /data/local/tmp/focus_stage/sqlite3" 2>/dev/null; then - adb_root "cp /data/local/tmp/focus_stage/sqlite3 $REMOTE_DIR/sqlite3" - adb_root "chmod 0755 $REMOTE_DIR/sqlite3" - fi - if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$SCRIPT_DIR/config.sh"; then - adb_root "cp /data/local/tmp/focus_stage/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh" - else - adb_root "rm -f /data/adb/service.d/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh.disabled" - fi - # Install canonical hosts and lock it down (only if generator produced it). - if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical" 2>/dev/null; then - # chattr -i first so we can overwrite a previously-locked canonical - adb_root "chattr -i $REMOTE_DIR/hosts.canonical 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.canonical $REMOTE_DIR/hosts.canonical" - adb_root "chmod 644 $REMOTE_DIR/hosts.canonical" - # Pre-compute the sha so the enforcer does not have to seed it. - adb_root "chattr -i $REMOTE_DIR/hosts.sha256 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.sha256 $REMOTE_DIR/hosts.sha256" - adb_root "chmod 644 $REMOTE_DIR/hosts.sha256" - adb_root "chattr +i $REMOTE_DIR/hosts.canonical 2>/dev/null; true" - adb_root "chattr +i $REMOTE_DIR/hosts.sha256 2>/dev/null; true" + # Move staged files into place with root + adb_root "cp /data/local/tmp/focus_stage/config.sh $REMOTE_DIR/config.sh" + adb_root "cp /data/local/tmp/focus_stage/focus_daemon.sh $REMOTE_DIR/focus_daemon.sh" + adb_root "cp /data/local/tmp/focus_stage/focus_ctl.sh $REMOTE_DIR/focus_ctl.sh" + adb_root "cp /data/local/tmp/focus_stage/hosts_enforcer.sh $REMOTE_DIR/hosts_enforcer.sh" + adb_root "cp /data/local/tmp/focus_stage/dns_enforcer.sh $REMOTE_DIR/dns_enforcer.sh" + adb_root "cp /data/local/tmp/focus_stage/launcher_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh" + adb_root "cp /data/local/tmp/focus_stage/curfew_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh" + adb_root "cp /data/local/tmp/focus_stage/tether_enforcer.sh $REMOTE_DIR/tether_enforcer.sh" + adb_root "cp /data/local/tmp/focus_stage/workout_detector.sh $REMOTE_DIR/workout_detector.sh" + if adb_cmd shell "test -f /data/local/tmp/focus_stage/sqlite3" 2>/dev/null; then + adb_root "cp /data/local/tmp/focus_stage/sqlite3 $REMOTE_DIR/sqlite3" + adb_root "chmod 0755 $REMOTE_DIR/sqlite3" + fi + if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$DEPLOY_DIR/config.sh"; then + adb_root "cp /data/local/tmp/focus_stage/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh" + else + adb_root "rm -f /data/adb/service.d/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh.disabled" + fi + # Install canonical hosts and lock it down (only if generator produced it). + if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical" 2>/dev/null; then + # chattr -i first so we can overwrite a previously-locked canonical + adb_root "chattr -i $REMOTE_DIR/hosts.canonical 2>/dev/null; true" + adb_root "cp /data/local/tmp/focus_stage/hosts.canonical $REMOTE_DIR/hosts.canonical" + adb_root "chmod 644 $REMOTE_DIR/hosts.canonical" + # Pre-compute the sha so the enforcer does not have to seed it. + adb_root "chattr -i $REMOTE_DIR/hosts.sha256 2>/dev/null; true" + adb_root "cp /data/local/tmp/focus_stage/hosts.sha256 $REMOTE_DIR/hosts.sha256" + adb_root "chmod 644 $REMOTE_DIR/hosts.sha256" + adb_root "chattr +i $REMOTE_DIR/hosts.canonical 2>/dev/null; true" + adb_root "chattr +i $REMOTE_DIR/hosts.sha256 2>/dev/null; true" - # ---- Workout-variant canonical (optional) ---- - # Same lockdown treatment as the full canonical. Pushed by the workout - # hosts generator block above. Missing variant means workout_detector\ - # will simply have no relaxed file to swap to (hosts_enforcer falls\ - # back to the full canonical). - if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical.workout" 2>/dev/null; then - adb_root "chattr -i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.canonical.workout $REMOTE_DIR/hosts.canonical.workout" - adb_root "chmod 644 $REMOTE_DIR/hosts.canonical.workout" - adb_root "chattr -i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.sha256.workout $REMOTE_DIR/hosts.sha256.workout" - adb_root "chmod 644 $REMOTE_DIR/hosts.sha256.workout" - adb_root "chattr +i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true" - adb_root "chattr +i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true" - fi + # ---- Workout-variant canonical (optional) ---- + # Same lockdown treatment as the full canonical. Pushed by the workout + # hosts generator block above. Missing variant means workout_detector\ + # will simply have no relaxed file to swap to (hosts_enforcer falls\ + # back to the full canonical). + if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical.workout" 2>/dev/null; then + adb_root "chattr -i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true" + adb_root "cp /data/local/tmp/focus_stage/hosts.canonical.workout $REMOTE_DIR/hosts.canonical.workout" + adb_root "chmod 644 $REMOTE_DIR/hosts.canonical.workout" + adb_root "chattr -i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true" + adb_root "cp /data/local/tmp/focus_stage/hosts.sha256.workout $REMOTE_DIR/hosts.sha256.workout" + adb_root "chmod 644 $REMOTE_DIR/hosts.sha256.workout" + adb_root "chattr +i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true" + adb_root "chattr +i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true" + fi - # Magisk Systemless Hosts module was ensured (and rebooted if needed) in - # step [2.5] above. Sanity-assert it's still active before writing to it. - if ! adb_root "test -f /system/etc/hosts" 2>/dev/null; then - echo "ERROR: /system/etc/hosts not magic-mounted — run deploy again." - exit 1 - fi + # Magisk Systemless Hosts module was ensured (and rebooted if needed) in + # step [2.5] above. Sanity-assert it's still active before writing to it. + if ! adb_root "test -f /system/etc/hosts" 2>/dev/null; then + echo "ERROR: /system/etc/hosts not magic-mounted — run deploy again." + exit 1 + fi - adb_root "mkdir -p /data/adb/modules/hosts/system/etc" - # Drop any +i lock the runtime hosts_enforcer may have set on the - # module dir / hosts file so we can update them. The enforcer will - # re-lock on its next poll cycle. Also pre-emptively delete any - # disable/remove markers that may exist on disk before we start. - adb_root "chattr -i /data/adb/modules/hosts /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; rm -f /data/adb/modules/hosts/disable /data/adb/modules/hosts/remove /data/adb/modules/hosts/update; true" - adb_root "cp $REMOTE_DIR/hosts.canonical /data/adb/modules/hosts/system/etc/hosts" - adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts" - # Lock the module dir to block the Magisk app's "Disable" / "Remove" - # buttons (they create marker files inside the dir). Files already - # in the dir stay mutable so the runtime enforcer can still update - # the hosts file on workout state changes. - adb_root "chattr +i /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; true" - adb_root "chattr +i /data/adb/modules/hosts 2>/dev/null; true" - echo " Magisk hosts module populated ($(adb_root "wc -l < /data/adb/modules/hosts/system/etc/hosts" 2>/dev/null | tr -d ' ') lines), locked against UI-disable. Reboot to activate /system/etc/hosts." - fi - adb_root "rm -rf /data/local/tmp/focus_stage" + adb_root "mkdir -p /data/adb/modules/hosts/system/etc" + # Drop any +i lock the runtime hosts_enforcer may have set on the + # module dir / hosts file so we can update them. The enforcer will + # re-lock on its next poll cycle. Also pre-emptively delete any + # disable/remove markers that may exist on disk before we start. + adb_root "chattr -i /data/adb/modules/hosts /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; rm -f /data/adb/modules/hosts/disable /data/adb/modules/hosts/remove /data/adb/modules/hosts/update; true" + adb_root "cp $REMOTE_DIR/hosts.canonical /data/adb/modules/hosts/system/etc/hosts" + adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts" + # Lock the module dir to block the Magisk app's "Disable" / "Remove" + # buttons (they create marker files inside the dir). Files already + # in the dir stay mutable so the runtime enforcer can still update + # the hosts file on workout state changes. + adb_root "chattr +i /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; true" + adb_root "chattr +i /data/adb/modules/hosts 2>/dev/null; true" + echo " Magisk hosts module populated ($(adb_root "wc -l < /data/adb/modules/hosts/system/etc/hosts" 2>/dev/null | tr -d ' ') lines), locked against UI-disable. Reboot to activate /system/etc/hosts." + fi + adb_root "rm -rf /data/local/tmp/focus_stage" - # Flush in-process DNS caches of browsers. Apps like Firefox and Chrome - # cache resolved IPs internally and bypass /etc/hosts until restarted. - echo " Flushing browser DNS caches..." - for _pkg in $BROWSER_PACKAGES; do - [ -n "$_pkg" ] || continue - adb_root "am force-stop '$_pkg' 2>/dev/null; true" - echo " force-stopped $_pkg" - done + # Flush in-process DNS caches of browsers. Apps like Firefox and Chrome + # cache resolved IPs internally and bypass /etc/hosts until restarted. + echo " Flushing browser DNS caches..." + # BROWSER_PACKAGES is set by the top-level `. config.sh` above and is never + # modified. shellcheck only flags it because config.sh is ALSO re-sourced + # inside a subshell further up (to re-read WORKOUT_UNBLOCK_DOMAINS from the + # freshly-staged copy); that subshell's assignments are discarded by design. + # The unquoted expansion is the intended word split over the package list. + # shellcheck disable=SC2031 + for _pkg in $BROWSER_PACKAGES; do + [ -n "$_pkg" ] || continue + adb_root "am force-stop '$_pkg' 2>/dev/null; true" + echo " force-stopped $_pkg" + done - # Disable Firefox DNS-over-HTTPS via user.js. Firefox uses hardcoded - # Cloudflare bootstrap IPs (104.16.248.249, 104.16.249.249) to reach - # mozilla.cloudflare-dns.com, completely bypassing /etc/hosts even - # after a fresh start. TRR mode 5 disables DoH so Firefox falls back - # to the system resolver which sees our 0.0.0.0 blocks. - echo " Disabling Firefox DNS-over-HTTPS..." - adb_root "for _p in /data/data/org.mozilla.fenix/files/mozilla/*/; do + # Disable Firefox DNS-over-HTTPS via user.js. Firefox uses hardcoded + # Cloudflare bootstrap IPs (104.16.248.249, 104.16.249.249) to reach + # mozilla.cloudflare-dns.com, completely bypassing /etc/hosts even + # after a fresh start. TRR mode 5 disables DoH so Firefox falls back + # to the system resolver which sees our 0.0.0.0 blocks. + echo " Disabling Firefox DNS-over-HTTPS..." + adb_root "for _p in /data/data/org.mozilla.fenix/files/mozilla/*/; do [ -f \"\${_p}prefs.js\" ] || continue grep -qF '\"network.trr.mode\"' \"\${_p}user.js\" 2>/dev/null \ || { printf 'user_pref(\"network.trr.mode\", 5);\\n' >> \"\${_p}user.js\" 2>/dev/null && echo \" Wrote DoH-disable pref to \${_p}user.js\"; } done; true" - echo "[5/7] Setting permissions..." - adb_root "chmod 755 $REMOTE_DIR/config.sh $REMOTE_DIR/focus_daemon.sh $REMOTE_DIR/focus_ctl.sh $REMOTE_DIR/hosts_enforcer.sh $REMOTE_DIR/dns_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh $REMOTE_DIR/tether_enforcer.sh $REMOTE_DIR/workout_detector.sh" || true - if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$SCRIPT_DIR/config.sh"; then - adb_root "chmod 755 /data/adb/service.d/99-focus-mode.sh" - fi - adb_root "touch $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log" - # State files need 666 so the daemons can write regardless of SELinux context drift - adb_root "chmod 666 $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log" || true + echo "[5/7] Setting permissions..." + adb_root "chmod 755 $REMOTE_DIR/config.sh $REMOTE_DIR/focus_daemon.sh $REMOTE_DIR/focus_ctl.sh $REMOTE_DIR/hosts_enforcer.sh $REMOTE_DIR/dns_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh $REMOTE_DIR/tether_enforcer.sh $REMOTE_DIR/workout_detector.sh" || true + if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$DEPLOY_DIR/config.sh"; then + adb_root "chmod 755 /data/adb/service.d/99-focus-mode.sh" + fi + adb_root "touch $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log" + # State files need 666 so the daemons can write regardless of SELinux context drift + adb_root "chmod 666 $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log" || true - echo "[6/7] Starting daemons..." - # Stop existing daemons, then start fresh - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "kill \$(cat $REMOTE_DIR/daemon.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/hosts_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/dns_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/tether_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/workout_detector.pid 2>/dev/null) 2>/dev/null; true" - sleep 1 - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - sleep 1 - adb_root "rm -f $REMOTE_DIR/daemon.pid $REMOTE_DIR/hosts_enforcer.pid $REMOTE_DIR/dns_enforcer.pid $REMOTE_DIR/launcher_enforcer.pid $REMOTE_DIR/tether_enforcer.pid $REMOTE_DIR/workout_detector.pid" - # Start hosts enforcer first so hosts are locked before user can react. - # Use --mount-master so bind mounts propagate to the global namespace - # (where app processes live). Without this, only our isolated `su` session - # would see the bind-mounted hosts file. - if adb_root "test -f $REMOTE_DIR/hosts.canonical" 2>/dev/null; then - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/hosts_enforcer.sh /dev/null 2>/dev/null &' - fi - # Start workout detector BEFORE the hosts enforcer's first integrity check - # so the enforcer sees a non-stale workout_active flag. The detector itself - # is harmless if no workout is in progress (it just writes 0). - if adb_root "test -x $REMOTE_DIR/sqlite3" 2>/dev/null; then - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/workout_detector.sh /dev/null 2>/dev/null &' - fi - # Start DNS enforcer (forces Private DNS off, blocks DoH/DoT). Always on. - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/dns_enforcer.sh /dev/null 2>/dev/null &' - # Start launcher enforcer only if a snapshot APK exists. If not, warn the - # user to install Minimalist Phone + run --snapshot-launcher first. - if adb_root "test -f $REMOTE_DIR/minimalist_launcher.apk" 2>/dev/null; then - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh /dev/null 2>/dev/null &' - else - echo " NOTE: launcher snapshot missing. Install Minimalist Phone via Aurora Store, then run:" - echo " $0 $PHONE_IP --snapshot-launcher" - fi - # Start night-curfew enforcer (grayscale + DND + optional net allow-list). - # Always on; self-gates on the clock + focus mode, no-op during the day. - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/curfew_enforcer.sh /dev/null 2>/dev/null &' - # Start hotspot/tethering enforcer (blocks forwarded/tethered traffic so a - # second phone on our hotspot can't bypass focus mode). Always on; self-gates - # on focus mode, no-op while away from home. - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/tether_enforcer.sh /dev/null 2>/dev/null &' - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/focus_daemon.sh /dev/null 2>/dev/null &' + echo "[6/7] Starting daemons..." + # Stop existing daemons, then start fresh + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" + adb_root "kill \$(cat $REMOTE_DIR/daemon.pid 2>/dev/null) 2>/dev/null; true" + adb_root "kill \$(cat $REMOTE_DIR/hosts_enforcer.pid 2>/dev/null) 2>/dev/null; true" + adb_root "kill \$(cat $REMOTE_DIR/dns_enforcer.pid 2>/dev/null) 2>/dev/null; true" + adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true" + adb_root "kill \$(cat $REMOTE_DIR/tether_enforcer.pid 2>/dev/null) 2>/dev/null; true" + adb_root "kill \$(cat $REMOTE_DIR/workout_detector.pid 2>/dev/null) 2>/dev/null; true" + sleep 1 + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" + adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" + sleep 1 + adb_root "rm -f $REMOTE_DIR/daemon.pid $REMOTE_DIR/hosts_enforcer.pid $REMOTE_DIR/dns_enforcer.pid $REMOTE_DIR/launcher_enforcer.pid $REMOTE_DIR/tether_enforcer.pid $REMOTE_DIR/workout_detector.pid" + # Start hosts enforcer first so hosts are locked before user can react. + # Use --mount-master so bind mounts propagate to the global namespace + # (where app processes live). Without this, only our isolated `su` session + # would see the bind-mounted hosts file. + if adb_root "test -f $REMOTE_DIR/hosts.canonical" 2>/dev/null; then + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/hosts_enforcer.sh /dev/null 2>/dev/null &' + fi + # Start workout detector BEFORE the hosts enforcer's first integrity check + # so the enforcer sees a non-stale workout_active flag. The detector itself + # is harmless if no workout is in progress (it just writes 0). + if adb_root "test -x $REMOTE_DIR/sqlite3" 2>/dev/null; then + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/workout_detector.sh /dev/null 2>/dev/null &' + fi + # Start DNS enforcer (forces Private DNS off, blocks DoH/DoT). Always on. + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/dns_enforcer.sh /dev/null 2>/dev/null &' + # Start launcher enforcer only if a snapshot APK exists. If not, warn the + # user to install Minimalist Phone + run --snapshot-launcher first. + if adb_root "test -f $REMOTE_DIR/minimalist_launcher.apk" 2>/dev/null; then + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh /dev/null 2>/dev/null &' + else + echo " NOTE: launcher snapshot missing. Install Minimalist Phone via Aurora Store, then run:" + echo " $0 $PHONE_IP --snapshot-launcher" + fi + # Start night-curfew enforcer (grayscale + DND + optional net allow-list). + # Always on; self-gates on the clock + focus mode, no-op during the day. + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/curfew_enforcer.sh /dev/null 2>/dev/null &' + # Start hotspot/tethering enforcer (blocks forwarded/tethered traffic so a + # second phone on our hotspot can't bypass focus mode). Always on; self-gates + # on focus mode, no-op while away from home. + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/tether_enforcer.sh /dev/null 2>/dev/null &' + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/focus_daemon.sh /dev/null 2>/dev/null &' - # Wait for hosts_enforcer to apply the bind mount and restart netd. - # hosts_enforcer.sh restarts netd once at startup (takes ~4 s); we wait - # 10 s total so the network is stable before the companion-app install. - sleep 10 + # Wait for hosts_enforcer to apply the bind mount and restart netd. + # hosts_enforcer.sh restarts netd once at startup (takes ~4 s); we wait + # 10 s total so the network is stable before the companion-app install. + sleep 10 - # ---- Companion status notification app ---- - APP_DIR="$SCRIPT_DIR/focus_status_app" - APK="$APP_DIR/build/focus_status.apk" - if [ -d "$APP_DIR" ]; then - echo "[7/7] Building & installing companion status-notification app..." - needs_rebuild=0 - if [ ! -f "$APK" ]; then - needs_rebuild=1 - elif [ "$APP_DIR/AndroidManifest.xml" -nt "$APK" ]; then - needs_rebuild=1 - elif [ "$APP_DIR/build.sh" -nt "$APK" ]; then - needs_rebuild=1 - elif find "$APP_DIR/java" -name '*.java' -newer "$APK" -print -quit 2>/dev/null | grep -q .; then - # Rebuild when any Java source changed, not just the manifest. - needs_rebuild=1 - fi - if [ "$needs_rebuild" -eq 1 ]; then - echo " Building APK..." - # Non-fatal: the companion UI is optional. If the Android SDK is - # missing (build.sh fails), warn and fall back to the existing APK - # rather than aborting the whole deploy and leaving the curfew core - # un-started. - if ! (cd "$APP_DIR" && bash build.sh) >/dev/null 2>&1; then - echo " WARNING: APK build failed (Android SDK missing?)." - echo " Keeping the previously-built APK if present;" - echo " the curfew daemons/enforcers are unaffected." - fi - fi - if [ -f "$APK" ]; then - echo " Installing APK..." - adb_cmd install -r "$APK" >/dev/null || true - # Grant runtime permission (Android 13+ requires it for notifications). - adb_cmd shell pm grant com.kuhy.focusstatus android.permission.POST_NOTIFICATIONS >/dev/null 2>&1 || true - # Pre-approve Magisk SU so the app never shows the approval prompt. - APP_UID="$( - adb_cmd shell dumpsys package com.kuhy.focusstatus 2>/dev/null \ - | awk 'match($0, /userId=[0-9]+/) {print substr($0, RSTART + 7, RLENGTH - 7); exit}' - )" - if [ -n "$APP_UID" ]; then - adb_cmd shell "su -c 'magisk --sqlite \"INSERT OR REPLACE INTO policies (uid,policy,until,logging,notification) VALUES ($APP_UID,2,0,1,1)\"'" >/dev/null 2>&1 || true - fi - # Launch the invisible activity which kicks off the foreground service. - adb_cmd shell am start -n com.kuhy.focusstatus/.LaunchActivity >/dev/null 2>&1 || true - echo " Companion app running (look for the ongoing 'Focus Mode' notification)." - else - echo " WARNING: APK build failed - skipping companion app install" - fi - fi + # ---- Companion status notification app ---- + APP_DIR="$DEPLOY_DIR/focus_status_app" + APK="$APP_DIR/build/focus_status.apk" + if [ -d "$APP_DIR" ]; then + echo "[7/7] Building & installing companion status-notification app..." + needs_rebuild=0 + if [ ! -f "$APK" ]; then + needs_rebuild=1 + elif [ "$APP_DIR/AndroidManifest.xml" -nt "$APK" ]; then + needs_rebuild=1 + elif [ "$APP_DIR/build.sh" -nt "$APK" ]; then + needs_rebuild=1 + elif find "$APP_DIR/java" -name '*.java' -newer "$APK" -print -quit 2>/dev/null | grep -q .; then + # Rebuild when any Java source changed, not just the manifest. + needs_rebuild=1 + fi + if [ "$needs_rebuild" -eq 1 ]; then + echo " Building APK..." + # Non-fatal: the companion UI is optional. If the Android SDK is + # missing (build.sh fails), warn and fall back to the existing APK + # rather than aborting the whole deploy and leaving the curfew core + # un-started. + if ! (cd "$APP_DIR" && bash build.sh) >/dev/null 2>&1; then + echo " WARNING: APK build failed (Android SDK missing?)." + echo " Keeping the previously-built APK if present;" + echo " the curfew daemons/enforcers are unaffected." + fi + fi + if [ -f "$APK" ]; then + echo " Installing APK..." + adb_cmd install -r "$APK" >/dev/null || true + # Grant runtime permission (Android 13+ requires it for notifications). + adb_cmd shell pm grant com.kuhy.focusstatus android.permission.POST_NOTIFICATIONS >/dev/null 2>&1 || true + # Pre-approve Magisk SU so the app never shows the approval prompt. + APP_UID="$( + adb_cmd shell dumpsys package com.kuhy.focusstatus 2>/dev/null | + awk 'match($0, /userId=[0-9]+/) {print substr($0, RSTART + 7, RLENGTH - 7); exit}' + )" + if [ -n "$APP_UID" ]; then + adb_cmd shell "su -c 'magisk --sqlite \"INSERT OR REPLACE INTO policies (uid,policy,until,logging,notification) VALUES ($APP_UID,2,0,1,1)\"'" >/dev/null 2>&1 || true + fi + # Launch the invisible activity which kicks off the foreground service. + adb_cmd shell am start -n com.kuhy.focusstatus/.LaunchActivity >/dev/null 2>&1 || true + echo " Companion app running (look for the ongoing 'Focus Mode' notification)." + else + echo " WARNING: APK build failed - skipping companion app install" + fi + fi - echo "" - echo "=== Deploy complete! ===" - echo "" - echo "Checking status..." - adb_root "sh $REMOTE_DIR/focus_ctl.sh status" - echo "" - echo "Boot autostart is disabled by default (FOCUS_BOOT_AUTOSTART=0)." - echo "No Magisk service.d hook is installed unless FOCUS_BOOT_AUTOSTART=1 in config.sh." - echo "Launcher enforcement does not auto-start on boot unless LAUNCHER_BOOT_AUTOSTART=1 is set in config.sh." - echo "" - echo "Useful commands:" - echo " $0 $PHONE_IP --status # Check mode and location" - echo " $0 $PHONE_IP --log # View daemon log" - echo " $0 $PHONE_IP --list # See all apps and whitelist status" - echo " $0 $PHONE_IP --enable # Force focus mode on for testing" - echo " $0 $PHONE_IP --disable # Force focus mode off" - echo " $0 $PHONE_IP --install-aurora # Install Aurora Store (Play Store alternative)" + echo "" + echo "=== Deploy complete! ===" + echo "" + echo "Checking status..." + adb_root "sh $REMOTE_DIR/focus_ctl.sh status" + echo "" + echo "Boot autostart is disabled by default (FOCUS_BOOT_AUTOSTART=0)." + echo "No Magisk service.d hook is installed unless FOCUS_BOOT_AUTOSTART=1 in config.sh." + echo "Launcher enforcement does not auto-start on boot unless LAUNCHER_BOOT_AUTOSTART=1 is set in config.sh." + echo "" + echo "Useful commands:" + echo " $0 $PHONE_IP --status # Check mode and location" + echo " $0 $PHONE_IP --log # View daemon log" + echo " $0 $PHONE_IP --list # See all apps and whitelist status" + echo " $0 $PHONE_IP --enable # Force focus mode on for testing" + echo " $0 $PHONE_IP --disable # Force focus mode off" + echo " $0 $PHONE_IP --install-aurora # Install Aurora Store (Play Store alternative)" } # ============================================================ # Control actions (post-deploy) # ============================================================ do_control() { - local ctl_cmd="$1" - connect_adb - adb_root "sh $REMOTE_DIR/focus_ctl.sh $ctl_cmd" + local ctl_cmd="$1" + connect_adb + adb_root "sh $REMOTE_DIR/focus_ctl.sh $ctl_cmd" } do_pull_log() { - connect_adb - echo "Downloading log..." - adb_cmd pull "$REMOTE_DIR/focus_mode.log" "./focus_mode_$(date +%Y%m%d_%H%M%S).log" - echo "Done." + connect_adb + echo "Downloading log..." + adb_cmd pull "$REMOTE_DIR/focus_mode.log" "./focus_mode_$(date +%Y%m%d_%H%M%S).log" + echo "Done." } do_capture_coords() { - # Standalone GPS capture for post-WiFi-setup use. - # Overwrites config_secrets.sh on the phone with the current location. - connect_adb - if ! adb_root "id" 2>/dev/null | grep -q "uid=0"; then - echo "ERROR: Root not available." - exit 1 - fi - echo "Capturing home coordinates from phone GPS..." - local gps_result gps_lat gps_lon - gps_result="$(fetch_home_coords_from_phone)" - gps_lat="${gps_result% *}" - gps_lon="${gps_result#* }" - adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \ + # Standalone GPS capture for post-WiFi-setup use. + # Overwrites config_secrets.sh on the phone with the current location. + connect_adb + if ! adb_root "id" 2>/dev/null | grep -q "uid=0"; then + echo "ERROR: Root not available." + exit 1 + fi + echo "Capturing home coordinates from phone GPS..." + local gps_result gps_lat gps_lon + gps_result="$(fetch_home_coords_from_phone)" + gps_lat="${gps_result% *}" + gps_lon="${gps_result#* }" + adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \ > $REMOTE_DIR/config_secrets.sh" - echo "Home coordinates updated on phone: ${gps_lat}, ${gps_lon}" - echo "Restarting focus daemon to apply new coordinates..." - adb_root "sh $REMOTE_DIR/focus_ctl.sh restart" 2>/dev/null || true - echo "Done." + echo "Home coordinates updated on phone: ${gps_lat}, ${gps_lon}" + echo "Restarting focus daemon to apply new coordinates..." + adb_root "sh $REMOTE_DIR/focus_ctl.sh restart" 2>/dev/null || true + echo "Done." } do_find_pkg() { - local filter="${3:-}" - if [ -z "$filter" ]; then - echo "Usage: $0 --find-pkg " - exit 1 - fi - connect_adb - echo "Packages matching '$filter':" - adb_cmd shell pm list packages | grep -i "$filter" | sed 's/^package:/ /' + local filter="${3:-}" + if [ -z "$filter" ]; then + echo "Usage: $0 --find-pkg " + exit 1 + fi + connect_adb + echo "Packages matching '$filter':" + adb_cmd shell pm list packages | grep -i "$filter" | sed 's/^package:/ /' } do_snapshot_launcher() { - # Run the on-device snapshot command. This captures the APK + HOME - # activity of the already-installed Minimalist Phone launcher into - # /data/adb/focus_mode/ so the launcher enforcer can restore it later. - # The user must install the launcher once (via Aurora/Play) before - # running this command - we only back up what's already there. - connect_adb - echo "Snapshotting currently-installed launcher APK..." - adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-snapshot" - echo "" - echo "Starting launcher enforcer..." - # Kill any previous enforcer so it picks up the new snapshot. - adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "rm -f $REMOTE_DIR/launcher_enforcer.pid" - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh /dev/null 2>/dev/null &' - sleep 3 - adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-status" + # Run the on-device snapshot command. This captures the APK + HOME + # activity of the already-installed Minimalist Phone launcher into + # /data/adb/focus_mode/ so the launcher enforcer can restore it later. + # The user must install the launcher once (via Aurora/Play) before + # running this command - we only back up what's already there. + connect_adb + echo "Snapshotting currently-installed launcher APK..." + adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-snapshot" + echo "" + echo "Starting launcher enforcer..." + # Kill any previous enforcer so it picks up the new snapshot. + adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true" + adb_root "rm -f $REMOTE_DIR/launcher_enforcer.pid" + adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh /dev/null 2>/dev/null &' + sleep 3 + adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-status" } # ============================================================ @@ -786,23 +799,35 @@ check_adb check_ip case "$ACTION" in - --deploy|"") do_deploy ;; - --status) do_control "status" ;; - --log) connect_adb; adb_root "sh $REMOTE_DIR/focus_ctl.sh log 100" ;; - --stop) do_control "stop" ;; - --start) do_control "start" ;; - --restart) do_control "restart" ;; - --enable) do_control "enable" ;; - --disable) do_control "disable" ;; - --list) do_control "list-apps" ;; - --pull-log) do_pull_log ;; - --find-pkg) do_find_pkg "$@" ;; - --hosts-status) do_control "hosts-status" ;; - --hosts-log) connect_adb; adb_root "sh $REMOTE_DIR/focus_ctl.sh hosts-log 100" ;; - --launcher-status) do_control "launcher-status" ;; - --launcher-log) connect_adb; adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-log 100" ;; - --capture-coords) do_capture_coords ;; - --snapshot-launcher) do_snapshot_launcher ;; - --install-aurora) do_install_aurora ;; - *) echo "Unknown action: $ACTION"; usage ;; +--deploy | "") do_deploy ;; +--status) do_control "status" ;; +--log) + connect_adb + adb_root "sh $REMOTE_DIR/focus_ctl.sh log 100" + ;; +--stop) do_control "stop" ;; +--start) do_control "start" ;; +--restart) do_control "restart" ;; +--enable) do_control "enable" ;; +--disable) do_control "disable" ;; +--list) do_control "list-apps" ;; +--pull-log) do_pull_log ;; +--find-pkg) do_find_pkg "$@" ;; +--hosts-status) do_control "hosts-status" ;; +--hosts-log) + connect_adb + adb_root "sh $REMOTE_DIR/focus_ctl.sh hosts-log 100" + ;; +--launcher-status) do_control "launcher-status" ;; +--launcher-log) + connect_adb + adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-log 100" + ;; +--capture-coords) do_capture_coords ;; +--snapshot-launcher) do_snapshot_launcher ;; +--install-aurora) do_install_aurora ;; +*) + echo "Unknown action: $ACTION" + usage + ;; esac diff --git a/phone_focus_mode/dns_enforcer.sh b/phone_focus_mode/dns_enforcer.sh index b251f365..e53e1dc6 100755 --- a/phone_focus_mode/dns_enforcer.sh +++ b/phone_focus_mode/dns_enforcer.sh @@ -36,135 +36,135 @@ touch "$DNS_LOG" chmod 666 "$DNS_LOG" 2>/dev/null || true log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >> "$DNS_LOG" + local ts + ts="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$ts] $1" >>"$DNS_LOG" } rotate_log() { - local lines - lines="$(wc -l < "$DNS_LOG" 2>/dev/null || echo 0)" - if [ "$lines" -gt 500 ]; then - local tmp="$DNS_LOG.tmp" - tail -n 500 "$DNS_LOG" > "$tmp" - mv "$tmp" "$DNS_LOG" - fi + local lines + lines="$(wc -l <"$DNS_LOG" 2>/dev/null || echo 0)" + if [ "$lines" -gt 500 ]; then + local tmp="$DNS_LOG.tmp" + tail -n 500 "$DNS_LOG" >"$tmp" + mv "$tmp" "$DNS_LOG" + fi } acquire_lock() { - if [ -f "$PIDFILE" ]; then - local old_pid - old_pid="$(cat "$PIDFILE")" - if kill -0 "$old_pid" 2>/dev/null; then - local cmdline - cmdline="$(tr '\0' ' ' < "/proc/$old_pid/cmdline" 2>/dev/null)" - if echo "$cmdline" | grep -q "dns_enforcer"; then - echo "dns_enforcer already running (PID $old_pid)" - exit 0 - fi - fi - rm -f "$PIDFILE" - fi - echo $$ > "$PIDFILE" + if [ -f "$PIDFILE" ]; then + local old_pid + old_pid="$(cat "$PIDFILE")" + if kill -0 "$old_pid" 2>/dev/null; then + local cmdline + cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)" + if echo "$cmdline" | grep -q "dns_enforcer"; then + echo "dns_enforcer already running (PID $old_pid)" + exit 0 + fi + fi + rm -f "$PIDFILE" + fi + echo $$ >"$PIDFILE" } # ---- Private DNS ---- ensure_private_dns_off() { - local mode - mode="$(settings get global private_dns_mode 2>/dev/null)" - # Possible values: "off", "opportunistic", "hostname", null (default=opportunistic) - if [ "$mode" != "off" ]; then - settings put global private_dns_mode off 2>/dev/null - log "Private DNS was '$mode' - forced to 'off'" - fi - # Clear any pinned DoT hostname so the "hostname" mode cannot be - # re-enabled silently by Settings UI. - local spec - spec="$(settings get global private_dns_specifier 2>/dev/null)" - if [ -n "$spec" ] && [ "$spec" != "null" ]; then - settings delete global private_dns_specifier 2>/dev/null - log "Cleared private_dns_specifier (was '$spec')" - fi + local mode + mode="$(settings get global private_dns_mode 2>/dev/null)" + # Possible values: "off", "opportunistic", "hostname", null (default=opportunistic) + if [ "$mode" != "off" ]; then + settings put global private_dns_mode off 2>/dev/null + log "Private DNS was '$mode' - forced to 'off'" + fi + # Clear any pinned DoT hostname so the "hostname" mode cannot be + # re-enabled silently by Settings UI. + local spec + spec="$(settings get global private_dns_specifier 2>/dev/null)" + if [ -n "$spec" ] && [ "$spec" != "null" ]; then + settings delete global private_dns_specifier 2>/dev/null + log "Cleared private_dns_specifier (was '$spec')" + fi } # ---- iptables chain management ---- ensure_chain() { - local ipt="$1" - # Create the chain if missing. - if ! "$ipt" -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then - "$ipt" -N "$DNS_IPT_CHAIN" 2>/dev/null || { - log "ERROR: could not create $ipt chain $DNS_IPT_CHAIN" - return 1 - } - log "Created $ipt chain $DNS_IPT_CHAIN" - fi - # Remove ALL existing OUTPUT -> chain jumps (handles duplicates from - # previous iptables lock races where -C returned error but -I succeeded). - local removed=0 - while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do - removed=$((removed + 1)) - done - # Insert exactly one jump at position 1 of OUTPUT. - if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then - if [ "$removed" -gt 1 ]; then - log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN" - fi - else - log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt" - return 1 - fi + local ipt="$1" + # Create the chain if missing. + if ! "$ipt" -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then + "$ipt" -N "$DNS_IPT_CHAIN" 2>/dev/null || { + log "ERROR: could not create $ipt chain $DNS_IPT_CHAIN" + return 1 + } + log "Created $ipt chain $DNS_IPT_CHAIN" + fi + # Remove ALL existing OUTPUT -> chain jumps (handles duplicates from + # previous iptables lock races where -C returned error but -I succeeded). + local removed=0 + while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do + removed=$((removed + 1)) + done + # Insert exactly one jump at position 1 of OUTPUT. + if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then + if [ "$removed" -gt 1 ]; then + log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN" + fi + else + log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt" + return 1 + fi } fill_chain_v4() { - # Flush and refill so we always converge to the intended rule set. - iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1 - # Drop DoT everywhere. This is a narrow port rule - there's no legit - # reason for arbitrary apps to talk 853/tcp on Android. - iptables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \ - --reject-with icmp-port-unreachable 2>/dev/null || true + # Flush and refill so we always converge to the intended rule set. + iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1 + # Drop DoT everywhere. This is a narrow port rule - there's no legit + # reason for arbitrary apps to talk 853/tcp on Android. + iptables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \ + --reject-with tcp-reset 2>/dev/null || true + iptables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \ + --reject-with icmp-port-unreachable 2>/dev/null || true - local ip - for ip in $DNS_DOH_IPV4; do - [ -z "$ip" ] && continue - [ "${ip#\#}" != "$ip" ] && continue - # Reject 443/tcp (DoH) and 53 (classic DNS) to well-known resolvers. - # We also block 53 so apps that try to talk to 1.1.1.1:53 directly - # (ignoring /etc/resolv.conf) still fall back to the system resolver. - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \ - --reject-with icmp-port-unreachable 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \ - --reject-with icmp-port-unreachable 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - done + local ip + for ip in $DNS_DOH_IPV4; do + [ -z "$ip" ] && continue + [ "${ip#\#}" != "$ip" ] && continue + # Reject 443/tcp (DoH) and 53 (classic DNS) to well-known resolvers. + # We also block 53 so apps that try to talk to 1.1.1.1:53 directly + # (ignoring /etc/resolv.conf) still fall back to the system resolver. + iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \ + --reject-with tcp-reset 2>/dev/null || true + iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \ + --reject-with icmp-port-unreachable 2>/dev/null || true + iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \ + --reject-with icmp-port-unreachable 2>/dev/null || true + iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \ + --reject-with tcp-reset 2>/dev/null || true + done } fill_chain_v6() { - ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1 - ip6tables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \ - --reject-with icmp6-port-unreachable 2>/dev/null || true + ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1 + ip6tables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \ + --reject-with tcp-reset 2>/dev/null || true + ip6tables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \ + --reject-with icmp6-port-unreachable 2>/dev/null || true - local ip - for ip in $DNS_DOH_IPV6; do - [ -z "$ip" ] && continue - [ "${ip#\#}" != "$ip" ] && continue - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \ - --reject-with icmp6-port-unreachable 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \ - --reject-with icmp6-port-unreachable 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - done + local ip + for ip in $DNS_DOH_IPV6; do + [ -z "$ip" ] && continue + [ "${ip#\#}" != "$ip" ] && continue + ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \ + --reject-with tcp-reset 2>/dev/null || true + ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \ + --reject-with icmp6-port-unreachable 2>/dev/null || true + ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \ + --reject-with icmp6-port-unreachable 2>/dev/null || true + ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \ + --reject-with tcp-reset 2>/dev/null || true + done } # ---- State-change gate ---- @@ -179,65 +179,71 @@ fill_chain_v6() { # redundant identical rebuilds are skipped. expected_rule_count() { - # 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP. - local n=2 ip - for ip in "$@"; do - [ -z "$ip" ] && continue - [ "${ip#\#}" != "$ip" ] && continue - n=$((n + 4)) - done - echo "$n" + # 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP. + local n=2 ip + for ip in "$@"; do + [ -z "$ip" ] && continue + [ "${ip#\#}" != "$ip" ] && continue + n=$((n + 4)) + done + echo "$n" } chain_intact() { - local ipt="$1" expected="$2" actual - "$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1 || return 1 - actual="$("$ipt" -S "$DNS_IPT_CHAIN" 2>/dev/null | grep -c '^-A')" - [ "$actual" = "$expected" ] + local ipt="$1" expected="$2" actual + "$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1 || return 1 + actual="$("$ipt" -S "$DNS_IPT_CHAIN" 2>/dev/null | grep -c '^-A')" + [ "$actual" = "$expected" ] } enforce_iptables() { - if command -v iptables >/dev/null 2>&1; then - if chain_intact iptables "$(expected_rule_count $DNS_DOH_IPV4)"; then - : - elif ensure_chain iptables && fill_chain_v4; then - log "iptables (v4) DNS chain rebuilt (was missing/tampered)" - fi - fi - if command -v ip6tables >/dev/null 2>&1; then - if chain_intact ip6tables "$(expected_rule_count $DNS_DOH_IPV6)"; then - : - elif ensure_chain ip6tables && fill_chain_v6; then - log "ip6tables (v6) DNS chain rebuilt (was missing/tampered)" - fi - fi + if command -v iptables >/dev/null 2>&1; then + # Intentional word split: expected_rule_count() iterates "$@" over the + # configured IP list. This runs under /system/bin/sh on the phone, where + # arrays do not exist, so an unquoted expansion is the POSIX way to do it. + # shellcheck disable=SC2086 + if chain_intact iptables "$(expected_rule_count $DNS_DOH_IPV4)"; then + : + elif ensure_chain iptables && fill_chain_v4; then + log "iptables (v4) DNS chain rebuilt (was missing/tampered)" + fi + fi + if command -v ip6tables >/dev/null 2>&1; then + # Intentional word split, as above. + # shellcheck disable=SC2086 + if chain_intact ip6tables "$(expected_rule_count $DNS_DOH_IPV6)"; then + : + elif ensure_chain ip6tables && fill_chain_v6; then + log "ip6tables (v6) DNS chain rebuilt (was missing/tampered)" + fi + fi } cleanup() { - # We intentionally leave the iptables chain in place on SIGTERM so - # stopping the enforcer for maintenance does not immediately re-open - # the DoH hole. `focus_ctl.sh dns-stop` does the explicit teardown. - log "dns_enforcer shutting down" - rm -f "$PIDFILE" - exit 0 + # We intentionally leave the iptables chain in place on SIGTERM so + # stopping the enforcer for maintenance does not immediately re-open + # the DoH hole. `focus_ctl.sh dns-stop` does the explicit teardown. + log "dns_enforcer shutting down" + rm -f "$PIDFILE" + exit 0 } trap cleanup INT TERM main() { - acquire_lock - log "dns_enforcer started (PID=$$)" + acquire_lock + log "dns_enforcer started (PID=$$)" - # Initial arm-up - ensure_private_dns_off - enforce_iptables + # Initial arm-up + ensure_private_dns_off + enforce_iptables - while true; do - ensure_private_dns_off - enforce_iptables - rotate_log - sleep "$DNS_CHECK_INTERVAL" - done + while true; do + ensure_private_dns_off + enforce_iptables + rotate_log + sleep "$DNS_CHECK_INTERVAL" + done } main "$@" diff --git a/phone_focus_mode/focus_ctl.sh b/phone_focus_mode/focus_ctl.sh index 07504041..b4607848 100755 --- a/phone_focus_mode/focus_ctl.sh +++ b/phone_focus_mode/focus_ctl.sh @@ -16,156 +16,160 @@ PIDFILE="$STATE_DIR/daemon.pid" # ---- Logging ---- log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >> "$LOG_FILE" + local ts + ts="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$ts] $1" >>"$LOG_FILE" } # Emit one valid package name per line from WHITELIST. # This strips comments/blank lines from the multi-line quoted string and avoids # treating heading text (e.g. "---") as package tokens. iter_whitelist_packages() { - printf '%s\n' "$WHITELIST" | while IFS= read -r line; do - line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" - case "$line" in - ""|\#*) continue ;; - esac + printf '%s\n' "$WHITELIST" | while IFS= read -r line; do + line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + case "$line" in + "" | \#*) continue ;; + esac - # Keep first token only; ignore any inline prose if present. - set -- $line - pkg="$1" + # Keep first token only; ignore any inline prose if present. + # Intentional word split to grab the first token. This runs under + # /system/bin/sh on the phone, where arrays do not exist, so an + # unquoted expansion is the POSIX way to do it. + # shellcheck disable=SC2086 + set -- $line + pkg="$1" - # Package names are dot-delimited identifiers. - case "$pkg" in - *.*) ;; - *) continue ;; - esac - case "$pkg" in - *[!A-Za-z0-9._]*) continue ;; - esac + # Package names are dot-delimited identifiers. + case "$pkg" in + *.*) ;; + *) continue ;; + esac + case "$pkg" in + *[!A-Za-z0-9._]*) continue ;; + esac - echo "$pkg" - done + echo "$pkg" + done } usage() { - echo "Usage: focus_ctl.sh " - echo "" - echo "Commands:" - echo " start - Start the focus mode daemon" - echo " stop - Stop the daemon and re-enable all apps" - echo " status - Show current mode, location and disabled apps" - echo " enable - Force focus mode on (regardless of location)" - echo " disable - Force focus mode off (regardless of location)" - echo " log - Show daemon log" - echo " list-apps - List all non-whitelisted third-party apps" - echo " whitelist - List currently whitelisted packages" - echo " restart - Restart the daemon" - echo " hosts-status - Show hosts enforcer state (mount + hash)" - echo " hosts-start - Start the hosts enforcer daemon" - echo " hosts-stop - Stop the hosts enforcer daemon" - echo " hosts-log - Show hosts enforcer log" - echo " dns-status - Show DNS enforcer state (Private DNS + iptables)" - echo " dns-start - Start the DNS enforcer daemon" - echo " dns-stop - Stop the DNS enforcer daemon (removes iptables chain)" - echo " dns-log - Show DNS enforcer log" - echo " launcher-status - Show launcher enforcer state" - echo " launcher-start - Start the launcher enforcer daemon" - echo " launcher-stop - Stop the launcher enforcer daemon" - echo " launcher-log - Show launcher enforcer log" - echo " launcher-snapshot - Back up currently-installed launcher APK" - echo " workout-status - Show StrongLifts workout-detection state" - echo " workout-start - Start the workout detector daemon" - echo " workout-stop - Stop the workout detector daemon (sets flag=0)" - echo " workout-log - Show workout detector log" - echo " recheck - Nudge the daemon to perform a fresh location check now" - echo " curfew-status - Show night-curfew + enforcer state" - echo " curfew-start - Start the curfew enforcer (grayscale/DND/net)" - echo " curfew-stop - Stop it and restore daytime display/DND" - echo " curfew-log - Show curfew enforcer log" - echo " curfew-test-on - Force curfew ACTIVE now (daytime validation)" - echo " curfew-test-off - Clear the test force" - echo " curfew-demo-on - Start a demo: full curfew now, easy one-tap off" - echo " curfew-demo-off - Stop the demo" - echo " curfew-off - Escape hatch: suspend curfew now (2am opt-out)" - echo " curfew-on - Re-arm curfew (clear the override)" - echo " tether-status - Show hotspot/tethering-block state" - echo " tether-start - Start the tether enforcer (FORWARD block + offload off)" - echo " tether-stop - Stop it and restore tethering (teardown FORWARD chain)" - echo " tether-log - Show tether enforcer log" - echo " tether-test-on - Force the tether block ACTIVE now (daytime validation)" - echo " tether-test-off - Clear the tether force" - echo " notif-status - Show companion status-notification details" - echo "" + echo "Usage: focus_ctl.sh " + echo "" + echo "Commands:" + echo " start - Start the focus mode daemon" + echo " stop - Stop the daemon and re-enable all apps" + echo " status - Show current mode, location and disabled apps" + echo " enable - Force focus mode on (regardless of location)" + echo " disable - Force focus mode off (regardless of location)" + echo " log - Show daemon log" + echo " list-apps - List all non-whitelisted third-party apps" + echo " whitelist - List currently whitelisted packages" + echo " restart - Restart the daemon" + echo " hosts-status - Show hosts enforcer state (mount + hash)" + echo " hosts-start - Start the hosts enforcer daemon" + echo " hosts-stop - Stop the hosts enforcer daemon" + echo " hosts-log - Show hosts enforcer log" + echo " dns-status - Show DNS enforcer state (Private DNS + iptables)" + echo " dns-start - Start the DNS enforcer daemon" + echo " dns-stop - Stop the DNS enforcer daemon (removes iptables chain)" + echo " dns-log - Show DNS enforcer log" + echo " launcher-status - Show launcher enforcer state" + echo " launcher-start - Start the launcher enforcer daemon" + echo " launcher-stop - Stop the launcher enforcer daemon" + echo " launcher-log - Show launcher enforcer log" + echo " launcher-snapshot - Back up currently-installed launcher APK" + echo " workout-status - Show StrongLifts workout-detection state" + echo " workout-start - Start the workout detector daemon" + echo " workout-stop - Stop the workout detector daemon (sets flag=0)" + echo " workout-log - Show workout detector log" + echo " recheck - Nudge the daemon to perform a fresh location check now" + echo " curfew-status - Show night-curfew + enforcer state" + echo " curfew-start - Start the curfew enforcer (grayscale/DND/net)" + echo " curfew-stop - Stop it and restore daytime display/DND" + echo " curfew-log - Show curfew enforcer log" + echo " curfew-test-on - Force curfew ACTIVE now (daytime validation)" + echo " curfew-test-off - Clear the test force" + echo " curfew-demo-on - Start a demo: full curfew now, easy one-tap off" + echo " curfew-demo-off - Stop the demo" + echo " curfew-off - Escape hatch: suspend curfew now (2am opt-out)" + echo " curfew-on - Re-arm curfew (clear the override)" + echo " tether-status - Show hotspot/tethering-block state" + echo " tether-start - Start the tether enforcer (FORWARD block + offload off)" + echo " tether-stop - Stop it and restore tethering (teardown FORWARD chain)" + echo " tether-log - Show tether enforcer log" + echo " tether-test-on - Force the tether block ACTIVE now (daytime validation)" + echo " tether-test-off - Clear the tether force" + echo " notif-status - Show companion status-notification details" + echo "" } # Helper to check if daemon is running daemon_pid() { - if [ -f "$PIDFILE" ]; then - local pid - pid="$(cat "$PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi + if [ -f "$PIDFILE" ]; then + local pid + pid="$(cat "$PIDFILE")" + if kill -0 "$pid" 2>/dev/null; then + echo "$pid" + fi + fi } cmd_start() { - local pid - pid="$(daemon_pid)" - if [ -n "$pid" ]; then - echo "Daemon already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/focus_daemon.sh" /dev/null 2>&1 & - sleep 2 - pid="$(daemon_pid)" - if [ -n "$pid" ]; then - echo "Daemon started (PID $pid)" - else - echo "ERROR: Daemon failed to start. Check log: $LOG_FILE" - fi + local pid + pid="$(daemon_pid)" + if [ -n "$pid" ]; then + echo "Daemon already running (PID $pid)" + return + fi + setsid sh "$SCRIPT_DIR/focus_daemon.sh" /dev/null 2>&1 & + sleep 2 + pid="$(daemon_pid)" + if [ -n "$pid" ]; then + echo "Daemon started (PID $pid)" + else + echo "ERROR: Daemon failed to start. Check log: $LOG_FILE" + fi } cmd_stop() { - local pid - pid="$(daemon_pid)" - if [ -z "$pid" ]; then - echo "Daemon not running" - # Clean up stale pidfile if present - rm -f "$PIDFILE" - else - kill -TERM "$pid" - echo "Daemon stopped (sent SIGTERM to PID $pid)" - fi + local pid + pid="$(daemon_pid)" + if [ -z "$pid" ]; then + echo "Daemon not running" + # Clean up stale pidfile if present + rm -f "$PIDFILE" + else + kill -TERM "$pid" + echo "Daemon stopped (sent SIGTERM to PID $pid)" + fi } cmd_status() { - local pid - pid="$(daemon_pid)" - local mode="unknown" - [ -f "$MODE_FILE" ] && mode="$(cat "$MODE_FILE")" + local pid + pid="$(daemon_pid)" + local mode="unknown" + [ -f "$MODE_FILE" ] && mode="$(cat "$MODE_FILE")" - echo "=== Focus Mode Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Mode: $mode" - echo "Home: $HOME_LAT, $HOME_LON (radius: ${RADIUS}m)" - echo "" + echo "=== Focus Mode Status ===" + if [ -n "$pid" ]; then + echo "Daemon: RUNNING (PID $pid)" + else + echo "Daemon: STOPPED" + fi + echo "Mode: $mode" + echo "Home: $HOME_LAT, $HOME_LON (radius: ${RADIUS}m)" + echo "" - # Show current location if available - location="$(dumpsys location 2>/dev/null \ - | grep -oE 'Location\[.*[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' \ - | grep -oE '[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' \ - | head -1)" + # Show current location if available + location="$(dumpsys location 2>/dev/null | + grep -oE 'Location\[.*[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' | + grep -oE '[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' | + head -1)" - if [ -n "$location" ]; then - lat="$(echo "$location" | cut -d',' -f1)" - lon="$(echo "$location" | cut -d',' -f2)" - dist="$(echo "$lat $lon $HOME_LAT $HOME_LON" | awk '{ + if [ -n "$location" ]; then + lat="$(echo "$location" | cut -d',' -f1)" + lon="$(echo "$location" | cut -d',' -f2)" + dist="$(echo "$lat $lon $HOME_LAT $HOME_LON" | awk '{ PI=3.14159265358979; R=6371000 a1=$1*PI/180; o1=$2*PI/180 a2=$3*PI/180; o2=$4*PI/180 @@ -173,258 +177,267 @@ cmd_status() { x=sin(da/2)^2+cos(a1)*cos(a2)*sin(dlon/2)^2 printf "%d", R*2*atan2(sqrt(x),sqrt(1-x)) }')" - echo "Location: $lat, $lon" - echo "Distance: ${dist}m from home" - else - echo "Location: unavailable" - fi + echo "Location: $lat, $lon" + echo "Distance: ${dist}m from home" + else + echo "Location: unavailable" + fi - echo "" - if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then - echo "=== Apps disabled by focus mode ===" - cat "$DISABLED_APPS_FILE" - else - echo "No apps currently disabled by focus mode" - fi + echo "" + if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then + echo "=== Apps disabled by focus mode ===" + cat "$DISABLED_APPS_FILE" + else + echo "No apps currently disabled by focus mode" + fi } cmd_enable() { - # Disable daemon temporarily, force focus - echo "Forcing focus mode ON..." - . "$SCRIPT_DIR/config.sh" + # Disable daemon temporarily, force focus + echo "Forcing focus mode ON..." + . "$SCRIPT_DIR/config.sh" - # Source common functions - inline here for standalone use - : > "$STATE_DIR/disabled_by_focus.txt" - local count=0 - for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do - # Check whitelist - whitelisted=0 - for w in $(iter_whitelist_packages); do - w_clean="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w_clean" ] && continue - [ "$pkg" = "$w_clean" ] && { whitelisted=1; break; } - done - [ "$whitelisted" -eq 1 ] && continue + # Source common functions - inline here for standalone use + : >"$STATE_DIR/disabled_by_focus.txt" + local count=0 + for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do + # Check whitelist + whitelisted=0 + for w in $(iter_whitelist_packages); do + w_clean="$(echo "$w" | tr -d '[:space:]')" + [ -z "$w_clean" ] && continue + [ "$pkg" = "$w_clean" ] && { + whitelisted=1 + break + } + done + [ "$whitelisted" -eq 1 ] && continue - # Check system protection - protected=0 - for prefix in $SYSTEM_NEVER_DISABLE; do - prefix_clean="$(echo "$prefix" | tr -d '[:space:]')" - [ -z "$prefix_clean" ] && continue - case "$pkg" in - "$prefix_clean"*) protected=1; break ;; - esac - done - [ "$protected" -eq 1 ] && continue + # Check system protection + protected=0 + for prefix in $SYSTEM_NEVER_DISABLE; do + prefix_clean="$(echo "$prefix" | tr -d '[:space:]')" + [ -z "$prefix_clean" ] && continue + case "$pkg" in + "$prefix_clean"*) + protected=1 + break + ;; + esac + done + [ "$protected" -eq 1 ] && continue - if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then - echo "$pkg" >> "$STATE_DIR/disabled_by_focus.txt" - count=$((count + 1)) - fi - done - echo "focus" > "$MODE_FILE" - echo "Done: disabled $count apps" + if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then + echo "$pkg" >>"$STATE_DIR/disabled_by_focus.txt" + count=$((count + 1)) + fi + done + echo "focus" >"$MODE_FILE" + echo "Done: disabled $count apps" } cmd_recheck() { - # Write the trigger file; the daemon's sleep_with_recheck() will pick it - # up within ~1 second and perform an immediate location check. - if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then - echo "Daemon not running - start it first with: focus_ctl.sh start" - return 1 - fi - touch "$RECHECK_TRIGGER" - chmod 666 "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Recheck requested. Tail the log to see the next reading:" - echo " tail -f $LOG_FILE" + # Write the trigger file; the daemon's sleep_with_recheck() will pick it + # up within ~1 second and perform an immediate location check. + if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then + echo "Daemon not running - start it first with: focus_ctl.sh start" + return 1 + fi + touch "$RECHECK_TRIGGER" + chmod 666 "$RECHECK_TRIGGER" 2>/dev/null || true + echo "Recheck requested. Tail the log to see the next reading:" + echo " tail -f $LOG_FILE" } cmd_notif_status() { - if [ -f "$STATUS_FILE" ]; then - echo "=== $STATUS_FILE ===" - cat "$STATUS_FILE" - echo - else - echo "No status snapshot yet (daemon has not written $STATUS_FILE)." - fi - if command -v dumpsys >/dev/null 2>&1; then - echo "=== Companion app state ===" - dumpsys package com.kuhy.focusstatus 2>/dev/null | grep -E 'enabled=|installed=|userId=' | head -5 || true - fi + if [ -f "$STATUS_FILE" ]; then + echo "=== $STATUS_FILE ===" + cat "$STATUS_FILE" + echo + else + echo "No status snapshot yet (daemon has not written $STATUS_FILE)." + fi + if command -v dumpsys >/dev/null 2>&1; then + echo "=== Companion app state ===" + dumpsys package com.kuhy.focusstatus 2>/dev/null | grep -E 'enabled=|installed=|userId=' | head -5 || true + fi } cmd_disable() { - echo "Forcing focus mode OFF..." - if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then - local count=0 - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1)) - done < "$DISABLED_APPS_FILE" - : > "$DISABLED_APPS_FILE" - echo "Done: re-enabled $count apps" - else - echo "No apps to re-enable" - fi - echo "normal" > "$MODE_FILE" + echo "Forcing focus mode OFF..." + if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then + local count=0 + while IFS= read -r pkg; do + [ -z "$pkg" ] && continue + pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1)) + done <"$DISABLED_APPS_FILE" + : >"$DISABLED_APPS_FILE" + echo "Done: re-enabled $count apps" + else + echo "No apps to re-enable" + fi + echo "normal" >"$MODE_FILE" } cmd_log() { - local lines="${1:-50}" - if [ -f "$LOG_FILE" ]; then - tail -n "$lines" "$LOG_FILE" - else - echo "Log file not found: $LOG_FILE" - fi + local lines="${1:-50}" + if [ -f "$LOG_FILE" ]; then + tail -n "$lines" "$LOG_FILE" + else + echo "Log file not found: $LOG_FILE" + fi } cmd_list_apps() { - echo "=== Third-party apps NOT in whitelist ===" - for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do - whitelisted=0 - for w in $(iter_whitelist_packages); do - w="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w" ] && continue - [ "$pkg" = "$w" ] && { whitelisted=1; break; } - done - if [ "$whitelisted" -eq 0 ]; then - # Check if currently disabled by focus mode - if grep -qF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null; then - echo " [BLOCKED] $pkg" - else - echo " [active] $pkg" - fi - fi - done - echo "" - echo "=== Whitelisted apps ===" - for w in $(iter_whitelist_packages); do - w="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w" ] && continue - echo " [allowed] $w" - done + echo "=== Third-party apps NOT in whitelist ===" + for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do + whitelisted=0 + for w in $(iter_whitelist_packages); do + w="$(echo "$w" | tr -d '[:space:]')" + [ -z "$w" ] && continue + [ "$pkg" = "$w" ] && { + whitelisted=1 + break + } + done + if [ "$whitelisted" -eq 0 ]; then + # Check if currently disabled by focus mode + if grep -qF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null; then + echo " [BLOCKED] $pkg" + else + echo " [active] $pkg" + fi + fi + done + echo "" + echo "=== Whitelisted apps ===" + for w in $(iter_whitelist_packages); do + w="$(echo "$w" | tr -d '[:space:]')" + [ -z "$w" ] && continue + echo " [allowed] $w" + done } cmd_whitelist() { - echo "=== Whitelisted packages ===" - for w in $(iter_whitelist_packages); do - w="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w" ] && continue - # Check if installed - if pm list packages "$w" 2>/dev/null | grep -qF "$w"; then - echo " [installed] $w" - else - echo " [not found] $w" - fi - done + echo "=== Whitelisted packages ===" + for w in $(iter_whitelist_packages); do + w="$(echo "$w" | tr -d '[:space:]')" + [ -z "$w" ] && continue + # Check if installed + if pm list packages "$w" 2>/dev/null | grep -qF "$w"; then + echo " [installed] $w" + else + echo " [not found] $w" + fi + done } HOSTS_PIDFILE="$STATE_DIR/hosts_enforcer.pid" hosts_enforcer_pid() { - if [ -f "$HOSTS_PIDFILE" ]; then - local pid - pid="$(cat "$HOSTS_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi + if [ -f "$HOSTS_PIDFILE" ]; then + local pid + pid="$(cat "$HOSTS_PIDFILE")" + if kill -0 "$pid" 2>/dev/null; then + echo "$pid" + fi + fi } cmd_hosts_status() { - local pid - pid="$(hosts_enforcer_pid)" - echo "=== Hosts Enforcer Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Canonical: $HOSTS_CANONICAL" - echo "Target: $HOSTS_TARGET" - if grep -qE "[[:space:]]${HOSTS_TARGET}[[:space:]]" /proc/self/mounts 2>/dev/null; then - # A mount exists on the target path, but on Android the OEM sometimes - # already mounts its own hosts file here. Trust the sha check below. - echo "Mount: present (integrity check below tells us if ours)" - else - echo "Mount: NOT mounted (unprotected)" - fi - if [ -f "$HOSTS_CANONICAL" ]; then - local expected actual - expected="$(cat "$HOSTS_SHA_FILE" 2>/dev/null)" - if command -v sha256sum >/dev/null 2>&1; then - actual="$(sha256sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')" - else - actual="$(md5sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')" - fi - echo "Expected: ${expected:-}" - echo "Actual: ${actual:-}" - if [ -n "$expected" ] && [ "$expected" = "$actual" ]; then - echo "Integrity: OK" - else - echo "Integrity: MISMATCH" - fi - else - echo "Canonical hosts file missing - run deploy.sh" - fi - # Magisk Systemless Hosts module protection state. - local module_dir="/data/adb/modules/hosts" - if [ -d "$module_dir" ]; then - local lock_state="UNLOCKED (Magisk app can disable!)" - if lsattr -d "$module_dir" 2>/dev/null | awk '{print $1}' | grep -q i; then - lock_state="LOCKED (chattr +i)" - fi - echo "Magisk dir: $module_dir [$lock_state]" - local marker_warn="" - for marker in disable remove update; do - if [ -e "$module_dir/$marker" ]; then - marker_warn="$marker_warn $marker" - fi - done - if [ -n "$marker_warn" ]; then - echo "WARN: Magisk markers present:$marker_warn (will be auto-removed by hosts_enforcer)" - fi - else - echo "Magisk dir: " - fi + local pid + pid="$(hosts_enforcer_pid)" + echo "=== Hosts Enforcer Status ===" + if [ -n "$pid" ]; then + echo "Daemon: RUNNING (PID $pid)" + else + echo "Daemon: STOPPED" + fi + echo "Canonical: $HOSTS_CANONICAL" + echo "Target: $HOSTS_TARGET" + if grep -qE "[[:space:]]${HOSTS_TARGET}[[:space:]]" /proc/self/mounts 2>/dev/null; then + # A mount exists on the target path, but on Android the OEM sometimes + # already mounts its own hosts file here. Trust the sha check below. + echo "Mount: present (integrity check below tells us if ours)" + else + echo "Mount: NOT mounted (unprotected)" + fi + if [ -f "$HOSTS_CANONICAL" ]; then + local expected actual + expected="$(cat "$HOSTS_SHA_FILE" 2>/dev/null)" + if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')" + else + actual="$(md5sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')" + fi + echo "Expected: ${expected:-}" + echo "Actual: ${actual:-}" + if [ -n "$expected" ] && [ "$expected" = "$actual" ]; then + echo "Integrity: OK" + else + echo "Integrity: MISMATCH" + fi + else + echo "Canonical hosts file missing - run deploy.sh" + fi + # Magisk Systemless Hosts module protection state. + local module_dir="/data/adb/modules/hosts" + if [ -d "$module_dir" ]; then + local lock_state="UNLOCKED (Magisk app can disable!)" + if lsattr -d "$module_dir" 2>/dev/null | awk '{print $1}' | grep -q i; then + lock_state="LOCKED (chattr +i)" + fi + echo "Magisk dir: $module_dir [$lock_state]" + local marker_warn="" + for marker in disable remove update; do + if [ -e "$module_dir/$marker" ]; then + marker_warn="$marker_warn $marker" + fi + done + if [ -n "$marker_warn" ]; then + echo "WARN: Magisk markers present:$marker_warn (will be auto-removed by hosts_enforcer)" + fi + else + echo "Magisk dir: " + fi } cmd_hosts_start() { - local pid - pid="$(hosts_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Hosts enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(hosts_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Hosts enforcer started (PID $pid)" - else - echo "ERROR: hosts enforcer failed to start. Check log: $HOSTS_LOG" - fi + local pid + pid="$(hosts_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Hosts enforcer already running (PID $pid)" + return + fi + setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" /dev/null 2>&1 & + sleep 2 + pid="$(hosts_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Hosts enforcer started (PID $pid)" + else + echo "ERROR: hosts enforcer failed to start. Check log: $HOSTS_LOG" + fi } cmd_hosts_stop() { - local pid - pid="$(hosts_enforcer_pid)" - if [ -z "$pid" ]; then - echo "Hosts enforcer not running" - rm -f "$HOSTS_PIDFILE" - return - fi - kill -TERM "$pid" - echo "Hosts enforcer stopped (sent SIGTERM to PID $pid)" + local pid + pid="$(hosts_enforcer_pid)" + if [ -z "$pid" ]; then + echo "Hosts enforcer not running" + rm -f "$HOSTS_PIDFILE" + return + fi + kill -TERM "$pid" + echo "Hosts enforcer stopped (sent SIGTERM to PID $pid)" } cmd_hosts_log() { - local lines="${1:-50}" - if [ -f "$HOSTS_LOG" ]; then - tail -n "$lines" "$HOSTS_LOG" - else - echo "Hosts enforcer log not found: $HOSTS_LOG" - fi + local lines="${1:-50}" + if [ -f "$HOSTS_LOG" ]; then + tail -n "$lines" "$HOSTS_LOG" + else + echo "Hosts enforcer log not found: $HOSTS_LOG" + fi } # ---- DNS enforcer ---- @@ -435,91 +448,91 @@ cmd_hosts_log() { DNS_PIDFILE="$STATE_DIR/dns_enforcer.pid" dns_enforcer_pid() { - if [ -f "$DNS_PIDFILE" ]; then - local pid - pid="$(cat "$DNS_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi + if [ -f "$DNS_PIDFILE" ]; then + local pid + pid="$(cat "$DNS_PIDFILE")" + if kill -0 "$pid" 2>/dev/null; then + echo "$pid" + fi + fi } cmd_dns_status() { - local pid - pid="$(dns_enforcer_pid)" - echo "=== DNS Enforcer Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - local mode spec - mode="$(settings get global private_dns_mode 2>/dev/null)" - spec="$(settings get global private_dns_specifier 2>/dev/null)" - echo "private_dns_mode: ${mode:-}" - echo "private_dns_specifier: ${spec:-}" - if iptables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then - local v4rules - v4rules="$(iptables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)" - echo "iptables $DNS_IPT_CHAIN: $v4rules rules" - else - echo "iptables $DNS_IPT_CHAIN: MISSING" - fi - if ip6tables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then - local v6rules - v6rules="$(ip6tables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)" - echo "ip6tables $DNS_IPT_CHAIN: $v6rules rules" - else - echo "ip6tables $DNS_IPT_CHAIN: MISSING" - fi + local pid + pid="$(dns_enforcer_pid)" + echo "=== DNS Enforcer Status ===" + if [ -n "$pid" ]; then + echo "Daemon: RUNNING (PID $pid)" + else + echo "Daemon: STOPPED" + fi + local mode spec + mode="$(settings get global private_dns_mode 2>/dev/null)" + spec="$(settings get global private_dns_specifier 2>/dev/null)" + echo "private_dns_mode: ${mode:-}" + echo "private_dns_specifier: ${spec:-}" + if iptables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then + local v4rules + v4rules="$(iptables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)" + echo "iptables $DNS_IPT_CHAIN: $v4rules rules" + else + echo "iptables $DNS_IPT_CHAIN: MISSING" + fi + if ip6tables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then + local v6rules + v6rules="$(ip6tables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)" + echo "ip6tables $DNS_IPT_CHAIN: $v6rules rules" + else + echo "ip6tables $DNS_IPT_CHAIN: MISSING" + fi } cmd_dns_start() { - local pid - pid="$(dns_enforcer_pid)" - if [ -n "$pid" ]; then - echo "DNS enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/dns_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(dns_enforcer_pid)" - if [ -n "$pid" ]; then - echo "DNS enforcer started (PID $pid)" - else - echo "ERROR: DNS enforcer failed to start. Check log: $DNS_LOG" - fi + local pid + pid="$(dns_enforcer_pid)" + if [ -n "$pid" ]; then + echo "DNS enforcer already running (PID $pid)" + return + fi + setsid sh "$SCRIPT_DIR/dns_enforcer.sh" /dev/null 2>&1 & + sleep 2 + pid="$(dns_enforcer_pid)" + if [ -n "$pid" ]; then + echo "DNS enforcer started (PID $pid)" + else + echo "ERROR: DNS enforcer failed to start. Check log: $DNS_LOG" + fi } cmd_dns_stop() { - local pid - pid="$(dns_enforcer_pid)" - if [ -z "$pid" ]; then - echo "DNS enforcer not running" - rm -f "$DNS_PIDFILE" - else - kill -TERM "$pid" - echo "DNS enforcer stopped (sent SIGTERM to PID $pid)" - fi - # Explicit teardown of the iptables chain so maintenance work can - # use DoH. The enforcer itself leaves the chain intact on TERM to - # keep the block closed between periodic re-applies. - iptables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true - iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || true - iptables -X "$DNS_IPT_CHAIN" 2>/dev/null || true - ip6tables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true - ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || true - ip6tables -X "$DNS_IPT_CHAIN" 2>/dev/null || true - echo "iptables chain $DNS_IPT_CHAIN removed" + local pid + pid="$(dns_enforcer_pid)" + if [ -z "$pid" ]; then + echo "DNS enforcer not running" + rm -f "$DNS_PIDFILE" + else + kill -TERM "$pid" + echo "DNS enforcer stopped (sent SIGTERM to PID $pid)" + fi + # Explicit teardown of the iptables chain so maintenance work can + # use DoH. The enforcer itself leaves the chain intact on TERM to + # keep the block closed between periodic re-applies. + iptables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true + iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || true + iptables -X "$DNS_IPT_CHAIN" 2>/dev/null || true + ip6tables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true + ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || true + ip6tables -X "$DNS_IPT_CHAIN" 2>/dev/null || true + echo "iptables chain $DNS_IPT_CHAIN removed" } cmd_dns_log() { - local lines="${1:-50}" - if [ -f "$DNS_LOG" ]; then - tail -n "$lines" "$DNS_LOG" - else - echo "DNS enforcer log not found: $DNS_LOG" - fi + local lines="${1:-50}" + if [ -f "$DNS_LOG" ]; then + tail -n "$lines" "$DNS_LOG" + else + echo "DNS enforcer log not found: $DNS_LOG" + fi } # ---- Launcher enforcer ---- @@ -528,144 +541,144 @@ LAUNCHER_PIDFILE="$STATE_DIR/launcher_enforcer.pid" DISABLED_COMPETITORS_FILE="$STATE_DIR/disabled_competitors.txt" launcher_enforcer_pid() { - if [ -f "$LAUNCHER_PIDFILE" ]; then - local pid - pid="$(cat "$LAUNCHER_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi + if [ -f "$LAUNCHER_PIDFILE" ]; then + local pid + pid="$(cat "$LAUNCHER_PIDFILE")" + if kill -0 "$pid" 2>/dev/null; then + echo "$pid" + fi + fi } cmd_launcher_snapshot() { - # Find the APK path for the currently-installed launcher and copy it - # to LAUNCHER_APK. Also capture the current HOME activity component. - local apk_path - apk_path="$(pm path "$LAUNCHER_PACKAGE" 2>/dev/null | head -1 | sed 's/^package://')" - if [ -z "$apk_path" ] || [ ! -f "$apk_path" ]; then - echo "ERROR: $LAUNCHER_PACKAGE is not installed. Install it once via Aurora/Play Store, then rerun this command." - return 1 - fi - mkdir -p "$(dirname "$LAUNCHER_APK")" - chattr -i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true - cp "$apk_path" "$LAUNCHER_APK" || return 1 - chmod 644 "$LAUNCHER_APK" - sha256sum "$LAUNCHER_APK" | awk '{print $1}' > "$LAUNCHER_SHA_FILE" - chmod 644 "$LAUNCHER_SHA_FILE" + # Find the APK path for the currently-installed launcher and copy it + # to LAUNCHER_APK. Also capture the current HOME activity component. + local apk_path + apk_path="$(pm path "$LAUNCHER_PACKAGE" 2>/dev/null | head -1 | sed 's/^package://')" + if [ -z "$apk_path" ] || [ ! -f "$apk_path" ]; then + echo "ERROR: $LAUNCHER_PACKAGE is not installed. Install it once via Aurora/Play Store, then rerun this command." + return 1 + fi + mkdir -p "$(dirname "$LAUNCHER_APK")" + chattr -i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true + cp "$apk_path" "$LAUNCHER_APK" || return 1 + chmod 644 "$LAUNCHER_APK" + sha256sum "$LAUNCHER_APK" | awk '{print $1}' >"$LAUNCHER_SHA_FILE" + chmod 644 "$LAUNCHER_SHA_FILE" - # Resolve the current HOME activity (or the launcher's default activity - # if it isn't yet the default). - local component - component="$(cmd package resolve-activity --brief \ - -c android.intent.category.HOME \ - -a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}')" - if [ -z "$component" ] || [ "${component%%/*}" != "$LAUNCHER_PACKAGE" ]; then - # Fall back to the launcher's MAIN/LAUNCHER activity - component="$(cmd package resolve-activity --brief \ - -c android.intent.category.LAUNCHER \ - -a android.intent.action.MAIN "$LAUNCHER_PACKAGE" 2>/dev/null \ - | awk 'NR==2{print}')" - fi - if [ -z "$component" ]; then - echo "ERROR: could not resolve HOME activity for $LAUNCHER_PACKAGE" - return 1 - fi - echo "$component" > "$LAUNCHER_ACTIVITY_FILE" - chmod 644 "$LAUNCHER_ACTIVITY_FILE" + # Resolve the current HOME activity (or the launcher's default activity + # if it isn't yet the default). + local component + component="$(cmd package resolve-activity --brief \ + -c android.intent.category.HOME \ + -a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}')" + if [ -z "$component" ] || [ "${component%%/*}" != "$LAUNCHER_PACKAGE" ]; then + # Fall back to the launcher's MAIN/LAUNCHER activity + component="$(cmd package resolve-activity --brief \ + -c android.intent.category.LAUNCHER \ + -a android.intent.action.MAIN "$LAUNCHER_PACKAGE" 2>/dev/null | + awk 'NR==2{print}')" + fi + if [ -z "$component" ]; then + echo "ERROR: could not resolve HOME activity for $LAUNCHER_PACKAGE" + return 1 + fi + echo "$component" >"$LAUNCHER_ACTIVITY_FILE" + chmod 644 "$LAUNCHER_ACTIVITY_FILE" - # Make snapshot immutable so even root-in-a-terminal can't overwrite - # it without first running `chattr -i`. - chattr +i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true + # Make snapshot immutable so even root-in-a-terminal can't overwrite + # it without first running `chattr -i`. + chattr +i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true - echo "Snapshot saved:" - echo " APK: $LAUNCHER_APK ($(wc -c < "$LAUNCHER_APK") bytes)" - echo " SHA256: $(cat "$LAUNCHER_SHA_FILE")" - echo " Activity: $component" + echo "Snapshot saved:" + echo " APK: $LAUNCHER_APK ($(wc -c <"$LAUNCHER_APK") bytes)" + echo " SHA256: $(cat "$LAUNCHER_SHA_FILE")" + echo " Activity: $component" } cmd_launcher_status() { - local pid - pid="$(launcher_enforcer_pid)" - echo "=== Launcher Enforcer Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Package: $LAUNCHER_PACKAGE" - if pm path "$LAUNCHER_PACKAGE" >/dev/null 2>&1; then - echo "Installed: YES ($(pm path "$LAUNCHER_PACKAGE" | head -1))" - else - echo "Installed: NO" - fi - local desired actual - desired="$(cat "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null)" - actual="$(cmd package resolve-activity --brief \ - -c android.intent.category.HOME -a android.intent.action.MAIN \ - 2>/dev/null | awk 'NR==2{print}')" - echo "Expected: ${desired:-}" - echo "Actual: ${actual:-}" - if [ -n "$desired" ] && [ "$desired" = "$actual" ]; then - echo "Default: OK (pinned)" - else - echo "Default: MISMATCH" - fi - echo "Snapshot: $LAUNCHER_APK" - if [ -f "$LAUNCHER_APK" ]; then - echo "Snapshot size: $(wc -c < "$LAUNCHER_APK") bytes" - fi - if [ -s "$DISABLED_COMPETITORS_FILE" ]; then - echo "Disabled competitors:" - sed 's/^/ - /' "$DISABLED_COMPETITORS_FILE" - fi + local pid + pid="$(launcher_enforcer_pid)" + echo "=== Launcher Enforcer Status ===" + if [ -n "$pid" ]; then + echo "Daemon: RUNNING (PID $pid)" + else + echo "Daemon: STOPPED" + fi + echo "Package: $LAUNCHER_PACKAGE" + if pm path "$LAUNCHER_PACKAGE" >/dev/null 2>&1; then + echo "Installed: YES ($(pm path "$LAUNCHER_PACKAGE" | head -1))" + else + echo "Installed: NO" + fi + local desired actual + desired="$(cat "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null)" + actual="$(cmd package resolve-activity --brief \ + -c android.intent.category.HOME -a android.intent.action.MAIN \ + 2>/dev/null | awk 'NR==2{print}')" + echo "Expected: ${desired:-}" + echo "Actual: ${actual:-}" + if [ -n "$desired" ] && [ "$desired" = "$actual" ]; then + echo "Default: OK (pinned)" + else + echo "Default: MISMATCH" + fi + echo "Snapshot: $LAUNCHER_APK" + if [ -f "$LAUNCHER_APK" ]; then + echo "Snapshot size: $(wc -c <"$LAUNCHER_APK") bytes" + fi + if [ -s "$DISABLED_COMPETITORS_FILE" ]; then + echo "Disabled competitors:" + sed 's/^/ - /' "$DISABLED_COMPETITORS_FILE" + fi } cmd_launcher_start() { - local pid - pid="$(launcher_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Launcher enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/launcher_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(launcher_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Launcher enforcer started (PID $pid)" - else - echo "ERROR: launcher enforcer failed to start. Check log: $LAUNCHER_LOG" - fi + local pid + pid="$(launcher_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Launcher enforcer already running (PID $pid)" + return + fi + setsid sh "$SCRIPT_DIR/launcher_enforcer.sh" /dev/null 2>&1 & + sleep 2 + pid="$(launcher_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Launcher enforcer started (PID $pid)" + else + echo "ERROR: launcher enforcer failed to start. Check log: $LAUNCHER_LOG" + fi } cmd_launcher_stop() { - local pid - pid="$(launcher_enforcer_pid)" - if [ -z "$pid" ]; then - echo "Launcher enforcer not running" - rm -f "$LAUNCHER_PIDFILE" - else - kill -TERM "$pid" - echo "Launcher enforcer stopped (sent SIGTERM to PID $pid)" - fi - # Re-enable any competitors we disabled so the device is usable if the - # enforcer is intentionally stopped (e.g. during maintenance). - if [ -s "$DISABLED_COMPETITORS_FILE" ]; then - while read -r pkg; do - [ -z "$pkg" ] && continue - pm enable --user 0 "$pkg" >/dev/null 2>&1 && \ - echo "Re-enabled competing launcher: $pkg" - done < "$DISABLED_COMPETITORS_FILE" - : > "$DISABLED_COMPETITORS_FILE" - fi + local pid + pid="$(launcher_enforcer_pid)" + if [ -z "$pid" ]; then + echo "Launcher enforcer not running" + rm -f "$LAUNCHER_PIDFILE" + else + kill -TERM "$pid" + echo "Launcher enforcer stopped (sent SIGTERM to PID $pid)" + fi + # Re-enable any competitors we disabled so the device is usable if the + # enforcer is intentionally stopped (e.g. during maintenance). + if [ -s "$DISABLED_COMPETITORS_FILE" ]; then + while read -r pkg; do + [ -z "$pkg" ] && continue + pm enable --user 0 "$pkg" >/dev/null 2>&1 && + echo "Re-enabled competing launcher: $pkg" + done <"$DISABLED_COMPETITORS_FILE" + : >"$DISABLED_COMPETITORS_FILE" + fi } cmd_launcher_log() { - local lines="${1:-50}" - if [ -f "$LAUNCHER_LOG" ]; then - tail -n "$lines" "$LAUNCHER_LOG" - else - echo "Launcher enforcer log not found: $LAUNCHER_LOG" - fi + local lines="${1:-50}" + if [ -f "$LAUNCHER_LOG" ]; then + tail -n "$lines" "$LAUNCHER_LOG" + else + echo "Launcher enforcer log not found: $LAUNCHER_LOG" + fi } # ---- Workout detector ---- @@ -673,113 +686,113 @@ cmd_launcher_log() { WORKOUT_PIDFILE="$STATE_DIR/workout_detector.pid" workout_detector_pid() { - if [ -f "$WORKOUT_PIDFILE" ]; then - local pid - pid="$(cat "$WORKOUT_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi + if [ -f "$WORKOUT_PIDFILE" ]; then + local pid + pid="$(cat "$WORKOUT_PIDFILE")" + if kill -0 "$pid" 2>/dev/null; then + echo "$pid" + fi + fi } cmd_workout_status() { - local pid - pid="$(workout_detector_pid)" - echo "=== Workout Detector Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Package: $WORKOUT_TRIGGER_PACKAGE" - if pm path "$WORKOUT_TRIGGER_PACKAGE" >/dev/null 2>&1; then - echo "Installed: YES" - else - echo "Installed: NO (detector will always report inactive)" - fi - echo "sqlite3: $WORKOUT_SQLITE3_BIN" - if [ -x "$WORKOUT_SQLITE3_BIN" ]; then - echo "sqlite3 ver: $("$WORKOUT_SQLITE3_BIN" -version 2>/dev/null | awk '{print $1}')" - else - echo "sqlite3 ver: " - fi - echo "DB path: $WORKOUT_DB_PATH" - if [ -f "$WORKOUT_DB_PATH" ]; then - echo "DB present: YES" - else - echo "DB present: NO" - fi - echo "Poll interval: ${WORKOUT_DETECTOR_INTERVAL}s" - local flag="" - if [ -f "$WORKOUT_ACTIVE_FILE" ]; then - flag="$(cat "$WORKOUT_ACTIVE_FILE" 2>/dev/null)" - fi - case "$flag" in - 1) echo "Workout flag: 1 (workout IN PROGRESS → YouTube hosts UNBLOCKED)" ;; - 0) echo "Workout flag: 0 (no workout → YouTube hosts BLOCKED)" ;; - *) echo "Workout flag: '$flag' (treated as 0, fail-closed)" ;; - esac - # Live one-shot query so the user can see ground truth without waiting - # for the next poll cycle. Best-effort — never fails the status command. - if [ -x "$WORKOUT_SQLITE3_BIN" ] && [ -f "$WORKOUT_DB_PATH" ]; then - local live_count - live_count="$("$WORKOUT_SQLITE3_BIN" "file:${WORKOUT_DB_PATH}?mode=ro" \ - "SELECT COUNT(*) FROM workouts WHERE start>0 AND (finish IS NULL OR finish=0);" \ - 2>/dev/null)" - echo "Live DB query: in-progress workouts = ${live_count:-}" - fi - if [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then - echo "Workout hosts: $HOSTS_CANONICAL_WORKOUT ($(wc -l < "$HOSTS_CANONICAL_WORKOUT" 2>/dev/null) lines)" - else - echo "Workout hosts: " - fi + local pid + pid="$(workout_detector_pid)" + echo "=== Workout Detector Status ===" + if [ -n "$pid" ]; then + echo "Daemon: RUNNING (PID $pid)" + else + echo "Daemon: STOPPED" + fi + echo "Package: $WORKOUT_TRIGGER_PACKAGE" + if pm path "$WORKOUT_TRIGGER_PACKAGE" >/dev/null 2>&1; then + echo "Installed: YES" + else + echo "Installed: NO (detector will always report inactive)" + fi + echo "sqlite3: $WORKOUT_SQLITE3_BIN" + if [ -x "$WORKOUT_SQLITE3_BIN" ]; then + echo "sqlite3 ver: $("$WORKOUT_SQLITE3_BIN" -version 2>/dev/null | awk '{print $1}')" + else + echo "sqlite3 ver: " + fi + echo "DB path: $WORKOUT_DB_PATH" + if [ -f "$WORKOUT_DB_PATH" ]; then + echo "DB present: YES" + else + echo "DB present: NO" + fi + echo "Poll interval: ${WORKOUT_DETECTOR_INTERVAL}s" + local flag="" + if [ -f "$WORKOUT_ACTIVE_FILE" ]; then + flag="$(cat "$WORKOUT_ACTIVE_FILE" 2>/dev/null)" + fi + case "$flag" in + 1) echo "Workout flag: 1 (workout IN PROGRESS → YouTube hosts UNBLOCKED)" ;; + 0) echo "Workout flag: 0 (no workout → YouTube hosts BLOCKED)" ;; + *) echo "Workout flag: '$flag' (treated as 0, fail-closed)" ;; + esac + # Live one-shot query so the user can see ground truth without waiting + # for the next poll cycle. Best-effort — never fails the status command. + if [ -x "$WORKOUT_SQLITE3_BIN" ] && [ -f "$WORKOUT_DB_PATH" ]; then + local live_count + live_count="$("$WORKOUT_SQLITE3_BIN" "file:${WORKOUT_DB_PATH}?mode=ro" \ + "SELECT COUNT(*) FROM workouts WHERE start>0 AND (finish IS NULL OR finish=0);" \ + 2>/dev/null)" + echo "Live DB query: in-progress workouts = ${live_count:-}" + fi + if [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then + echo "Workout hosts: $HOSTS_CANONICAL_WORKOUT ($(wc -l <"$HOSTS_CANONICAL_WORKOUT" 2>/dev/null) lines)" + else + echo "Workout hosts: " + fi } cmd_workout_start() { - local pid - pid="$(workout_detector_pid)" - if [ -n "$pid" ]; then - echo "Workout detector already running (PID $pid)" - return - fi - if [ ! -x "$WORKOUT_SQLITE3_BIN" ]; then - echo "ERROR: $WORKOUT_SQLITE3_BIN missing or not executable. Re-run deploy.sh." - return 1 - fi - setsid sh "$SCRIPT_DIR/workout_detector.sh" /dev/null 2>&1 & - sleep 2 - pid="$(workout_detector_pid)" - if [ -n "$pid" ]; then - echo "Workout detector started (PID $pid)" - else - echo "ERROR: Workout detector failed to start. Check log: $WORKOUT_DETECTOR_LOG" - fi + local pid + pid="$(workout_detector_pid)" + if [ -n "$pid" ]; then + echo "Workout detector already running (PID $pid)" + return + fi + if [ ! -x "$WORKOUT_SQLITE3_BIN" ]; then + echo "ERROR: $WORKOUT_SQLITE3_BIN missing or not executable. Re-run deploy.sh." + return 1 + fi + setsid sh "$SCRIPT_DIR/workout_detector.sh" /dev/null 2>&1 & + sleep 2 + pid="$(workout_detector_pid)" + if [ -n "$pid" ]; then + echo "Workout detector started (PID $pid)" + else + echo "ERROR: Workout detector failed to start. Check log: $WORKOUT_DETECTOR_LOG" + fi } cmd_workout_stop() { - local pid - pid="$(workout_detector_pid)" - if [ -z "$pid" ]; then - echo "Workout detector not running" - rm -f "$WORKOUT_PIDFILE" - else - kill -TERM "$pid" - echo "Workout detector stopped (sent SIGTERM to PID $pid)" - fi - # Fail-closed on manual stop: write 0 so the hosts enforcer reverts to - # the full-block canonical and YouTube goes back to being blocked. - printf '0\n' > "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true - chmod 666 "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true - echo "workout_active flag forced to 0" + local pid + pid="$(workout_detector_pid)" + if [ -z "$pid" ]; then + echo "Workout detector not running" + rm -f "$WORKOUT_PIDFILE" + else + kill -TERM "$pid" + echo "Workout detector stopped (sent SIGTERM to PID $pid)" + fi + # Fail-closed on manual stop: write 0 so the hosts enforcer reverts to + # the full-block canonical and YouTube goes back to being blocked. + printf '0\n' >"$WORKOUT_ACTIVE_FILE" 2>/dev/null || true + chmod 666 "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true + echo "workout_active flag forced to 0" } cmd_workout_log() { - local lines="${1:-50}" - if [ -f "$WORKOUT_DETECTOR_LOG" ]; then - tail -n "$lines" "$WORKOUT_DETECTOR_LOG" - else - echo "Workout detector log not found: $WORKOUT_DETECTOR_LOG" - fi + local lines="${1:-50}" + if [ -f "$WORKOUT_DETECTOR_LOG" ]; then + tail -n "$lines" "$WORKOUT_DETECTOR_LOG" + else + echo "Workout detector log not found: $WORKOUT_DETECTOR_LOG" + fi } # ============================================================ @@ -788,89 +801,91 @@ cmd_workout_log() { CURFEW_PIDFILE="$STATE_DIR/curfew_enforcer.pid" curfew_enforcer_pid() { - if [ -f "$CURFEW_PIDFILE" ]; then - local pid - pid="$(cat "$CURFEW_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi + if [ -f "$CURFEW_PIDFILE" ]; then + local pid + pid="$(cat "$CURFEW_PIDFILE")" + if kill -0 "$pid" 2>/dev/null; then + echo "$pid" + fi + fi } # Replicates focus_daemon.sh::is_curfew_now for status display. _ctl_dec() { - local n="$1" - while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done - printf '%s' "$n" + local n="$1" + while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done + printf '%s' "$n" } ctl_is_curfew_now() { - local now start end - now="$(date +%H%M 2>/dev/null)" - case "$now" in ''|*[!0-9]*) return 1 ;; esac - now="$(_ctl_dec "$now")"; start="$(_ctl_dec "$NIGHT_CURFEW_START")"; end="$(_ctl_dec "$NIGHT_CURFEW_END")" - if [ "$start" -le "$end" ]; then - [ "$now" -ge "$start" ] && [ "$now" -lt "$end" ] - else - [ "$now" -ge "$start" ] || [ "$now" -lt "$end" ] - fi + local now start end + now="$(date +%H%M 2>/dev/null)" + case "$now" in '' | *[!0-9]*) return 1 ;; esac + now="$(_ctl_dec "$now")" + start="$(_ctl_dec "$NIGHT_CURFEW_START")" + end="$(_ctl_dec "$NIGHT_CURFEW_END")" + if [ "$start" -le "$end" ]; then + [ "$now" -ge "$start" ] && [ "$now" -lt "$end" ] + else + [ "$now" -ge "$start" ] || [ "$now" -lt "$end" ] + fi } cmd_curfew_status() { - local pid - pid="$(curfew_enforcer_pid)" - echo "=== Night Curfew Status ===" - echo "Enabled: ${NIGHT_CURFEW_ENABLED}" - echo "Window: ${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END} (now $(date +%H%M))" - if [ -n "$pid" ]; then - echo "Enforcer: RUNNING (PID $pid)" - else - echo "Enforcer: STOPPED" - fi - ctl_is_curfew_now && echo "Within window: YES" || echo "Within window: no" - [ -e "$CURFEW_FORCE_FILE" ] && echo "Forced ON: YES (demo/test active)" - [ -e "$CURFEW_OVERRIDE_FILE" ] && echo "Override: YES (curfew SUSPENDED)" - if [ -f "$MODE_FILE" ]; then - echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)" - fi - [ -e "$CURFEW_ENFORCER_STATE" ] && echo "Applied now: YES (grayscale/DND locked)" \ - || echo "Applied now: no" - echo "Grayscale: ${CURFEW_GRAYSCALE_ENABLED} DND: ${CURFEW_DND_ENABLED} Net: ${CURFEW_NET_ENABLED}" - if iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then - echo "iptables $CURFEW_NET_IPT_CHAIN: $(iptables -S "$CURFEW_NET_IPT_CHAIN" 2>/dev/null | wc -l) rules" - else - echo "iptables $CURFEW_NET_IPT_CHAIN: absent (net curfew not applied)" - fi - if [ -f "$STATE_DIR/night_whitelist.txt" ]; then - echo "Night whitelist: $(wc -l < "$STATE_DIR/night_whitelist.txt" | tr -d ' ') apps allowed" - fi + local pid + pid="$(curfew_enforcer_pid)" + echo "=== Night Curfew Status ===" + echo "Enabled: ${NIGHT_CURFEW_ENABLED}" + echo "Window: ${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END} (now $(date +%H%M))" + if [ -n "$pid" ]; then + echo "Enforcer: RUNNING (PID $pid)" + else + echo "Enforcer: STOPPED" + fi + ctl_is_curfew_now && echo "Within window: YES" || echo "Within window: no" + [ -e "$CURFEW_FORCE_FILE" ] && echo "Forced ON: YES (demo/test active)" + [ -e "$CURFEW_OVERRIDE_FILE" ] && echo "Override: YES (curfew SUSPENDED)" + if [ -f "$MODE_FILE" ]; then + echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)" + fi + [ -e "$CURFEW_ENFORCER_STATE" ] && echo "Applied now: YES (grayscale/DND locked)" || + echo "Applied now: no" + echo "Grayscale: ${CURFEW_GRAYSCALE_ENABLED} DND: ${CURFEW_DND_ENABLED} Net: ${CURFEW_NET_ENABLED}" + if iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then + echo "iptables $CURFEW_NET_IPT_CHAIN: $(iptables -S "$CURFEW_NET_IPT_CHAIN" 2>/dev/null | wc -l) rules" + else + echo "iptables $CURFEW_NET_IPT_CHAIN: absent (net curfew not applied)" + fi + if [ -f "$STATE_DIR/night_whitelist.txt" ]; then + echo "Night whitelist: $(wc -l <"$STATE_DIR/night_whitelist.txt" | tr -d ' ') apps allowed" + fi } cmd_curfew_start() { - local pid - pid="$(curfew_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Curfew enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/curfew_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(curfew_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Curfew enforcer started (PID $pid)" - else - echo "ERROR: curfew enforcer failed to start. Check log: $CURFEW_ENFORCER_LOG" - fi + local pid + pid="$(curfew_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Curfew enforcer already running (PID $pid)" + return + fi + setsid sh "$SCRIPT_DIR/curfew_enforcer.sh" /dev/null 2>&1 & + sleep 2 + pid="$(curfew_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Curfew enforcer started (PID $pid)" + else + echo "ERROR: curfew enforcer failed to start. Check log: $CURFEW_ENFORCER_LOG" + fi } cmd_curfew_stop() { - local pid - pid="$(curfew_enforcer_pid)" - if [ -n "$pid" ]; then - kill "$pid" 2>/dev/null - echo "Curfew enforcer stopped (PID $pid) - daytime state restored" - else - echo "Curfew enforcer not running" - fi + local pid + pid="$(curfew_enforcer_pid)" + if [ -n "$pid" ]; then + kill "$pid" 2>/dev/null + echo "Curfew enforcer stopped (PID $pid) - daytime state restored" + else + echo "Curfew enforcer not running" + fi } cmd_curfew_log() { tail -n "${1:-50}" "$CURFEW_ENFORCER_LOG" 2>/dev/null || echo "No curfew log yet."; } @@ -878,49 +893,52 @@ cmd_curfew_log() { tail -n "${1:-50}" "$CURFEW_ENFORCER_LOG" 2>/dev/null || echo # Test hook: force curfew ACTIVE regardless of clock/location so the whole # stack can be validated during the day. Daemon re-checks within one tick. cmd_curfew_test_on() { - touch "$CURFEW_FORCE_FILE"; chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true - rm -f "$CURFEW_OVERRIDE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew FORCED ON. App sweep + enforcer will engage within a few seconds." - echo "Validate: open mBank (works), keyboard (works), Firefox (gone). Then: curfew-test-off" + touch "$CURFEW_FORCE_FILE" + chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true + rm -f "$CURFEW_OVERRIDE_FILE" + touch "$RECHECK_TRIGGER" 2>/dev/null || true + echo "Curfew FORCED ON. App sweep + enforcer will engage within a few seconds." + echo "Validate: open mBank (works), keyboard (works), Firefox (gone). Then: curfew-test-off" } cmd_curfew_test_off() { - rm -f "$CURFEW_FORCE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew force cleared. Back to clock-based behaviour." + rm -f "$CURFEW_FORCE_FILE" + touch "$RECHECK_TRIGGER" 2>/dev/null || true + echo "Curfew force cleared. Back to clock-based behaviour." } # Demo mode = the same force mechanism as the test hook, worded for an # on-demand demo. The companion app's Start/Stop demo button drives the same # force file. Easy off: curfew-demo-off (or tap "Stop demo curfew"). cmd_curfew_demo_on() { - touch "$CURFEW_FORCE_FILE"; chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true - rm -f "$CURFEW_OVERRIDE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Demo curfew STARTED - full curfew engaged now (apps, grayscale, DND, net)." - echo "Stop any time with: curfew-demo-off (or tap 'Stop demo curfew')" + touch "$CURFEW_FORCE_FILE" + chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true + rm -f "$CURFEW_OVERRIDE_FILE" + touch "$RECHECK_TRIGGER" 2>/dev/null || true + echo "Demo curfew STARTED - full curfew engaged now (apps, grayscale, DND, net)." + echo "Stop any time with: curfew-demo-off (or tap 'Stop demo curfew')" } cmd_curfew_demo_off() { - rm -f "$CURFEW_FORCE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Demo curfew STOPPED - back to clock-based behaviour." + rm -f "$CURFEW_FORCE_FILE" + touch "$RECHECK_TRIGGER" 2>/dev/null || true + echo "Demo curfew STOPPED - back to clock-based behaviour." } # Escape hatch: suspend curfew now (the 2am 'let me out' button). Survives # until you re-arm. Reachable on-device only via ADB (this command) unless a # root file-manager/terminal has been added to NIGHT_WHITELIST. cmd_curfew_off() { - touch "$CURFEW_OVERRIDE_FILE"; chmod 666 "$CURFEW_OVERRIDE_FILE" 2>/dev/null || true - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew SUSPENDED (override set: $CURFEW_OVERRIDE_FILE). Re-arm with: curfew-on" + touch "$CURFEW_OVERRIDE_FILE" + chmod 666 "$CURFEW_OVERRIDE_FILE" 2>/dev/null || true + touch "$RECHECK_TRIGGER" 2>/dev/null || true + echo "Curfew SUSPENDED (override set: $CURFEW_OVERRIDE_FILE). Re-arm with: curfew-on" } cmd_curfew_on() { - rm -f "$CURFEW_OVERRIDE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew re-armed (override cleared)." + rm -f "$CURFEW_OVERRIDE_FILE" + touch "$RECHECK_TRIGGER" 2>/dev/null || true + echo "Curfew re-armed (override cleared)." } # ============================================================ @@ -929,78 +947,78 @@ cmd_curfew_on() { TETHER_PIDFILE="$STATE_DIR/tether_enforcer.pid" tether_enforcer_pid() { - if [ -f "$TETHER_PIDFILE" ]; then - local pid - pid="$(cat "$TETHER_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi + if [ -f "$TETHER_PIDFILE" ]; then + local pid + pid="$(cat "$TETHER_PIDFILE")" + if kill -0 "$pid" 2>/dev/null; then + echo "$pid" + fi + fi } cmd_tether_status() { - local pid - pid="$(tether_enforcer_pid)" - echo "=== Hotspot / Tethering Block Status ===" - echo "Enabled: ${TETHER_ENFORCER_ENABLED}" - if [ -n "$pid" ]; then - echo "Enforcer: RUNNING (PID $pid)" - else - echo "Enforcer: STOPPED" - fi - if [ -f "$MODE_FILE" ]; then - echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)" - fi - [ -e "$TETHER_FORCE_FILE" ] && echo "Forced ON: YES (test active)" - [ -e "$TETHER_OVERRIDE_FILE" ] && echo "Override: YES (block SUSPENDED)" - [ -e "$TETHER_ENFORCER_STATE" ] && echo "Applied now: YES (offload off + FORWARD reject)" \ - || echo "Applied now: no" - local off - off="$(settings get global "$TETHER_OFFLOAD_KEY" 2>/dev/null)" - echo "tether_offload_disabled: ${off:-}" - if iptables -C FORWARD -j "$TETHER_IPT_CHAIN" >/dev/null 2>&1; then - echo "iptables $TETHER_IPT_CHAIN: pinned to FORWARD ($(iptables -S "$TETHER_IPT_CHAIN" 2>/dev/null | grep -c '^-A') rule)" - else - echo "iptables $TETHER_IPT_CHAIN: absent (block not applied)" - fi + local pid + pid="$(tether_enforcer_pid)" + echo "=== Hotspot / Tethering Block Status ===" + echo "Enabled: ${TETHER_ENFORCER_ENABLED}" + if [ -n "$pid" ]; then + echo "Enforcer: RUNNING (PID $pid)" + else + echo "Enforcer: STOPPED" + fi + if [ -f "$MODE_FILE" ]; then + echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)" + fi + [ -e "$TETHER_FORCE_FILE" ] && echo "Forced ON: YES (test active)" + [ -e "$TETHER_OVERRIDE_FILE" ] && echo "Override: YES (block SUSPENDED)" + [ -e "$TETHER_ENFORCER_STATE" ] && echo "Applied now: YES (offload off + FORWARD reject)" || + echo "Applied now: no" + local off + off="$(settings get global "$TETHER_OFFLOAD_KEY" 2>/dev/null)" + echo "tether_offload_disabled: ${off:-}" + if iptables -C FORWARD -j "$TETHER_IPT_CHAIN" >/dev/null 2>&1; then + echo "iptables $TETHER_IPT_CHAIN: pinned to FORWARD ($(iptables -S "$TETHER_IPT_CHAIN" 2>/dev/null | grep -c '^-A') rule)" + else + echo "iptables $TETHER_IPT_CHAIN: absent (block not applied)" + fi } cmd_tether_start() { - local pid - pid="$(tether_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Tether enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/tether_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(tether_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Tether enforcer started (PID $pid)" - else - echo "ERROR: tether enforcer failed to start. Check log: $TETHER_LOG" - fi + local pid + pid="$(tether_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Tether enforcer already running (PID $pid)" + return + fi + setsid sh "$SCRIPT_DIR/tether_enforcer.sh" /dev/null 2>&1 & + sleep 2 + pid="$(tether_enforcer_pid)" + if [ -n "$pid" ]; then + echo "Tether enforcer started (PID $pid)" + else + echo "ERROR: tether enforcer failed to start. Check log: $TETHER_LOG" + fi } cmd_tether_stop() { - local pid - pid="$(tether_enforcer_pid)" - if [ -z "$pid" ]; then - echo "Tether enforcer not running" - rm -f "$TETHER_PIDFILE" - else - kill -TERM "$pid" - echo "Tether enforcer stopped (sent SIGTERM to PID $pid)" - fi - # Explicit teardown so maintenance can tether even if the daemon was already - # dead (its own TERM handler reverts too; this is the belt-and-suspenders). - iptables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true - iptables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true - iptables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true - ip6tables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true - ip6tables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true - echo "FORWARD chain $TETHER_IPT_CHAIN removed (tethering restored)" + local pid + pid="$(tether_enforcer_pid)" + if [ -z "$pid" ]; then + echo "Tether enforcer not running" + rm -f "$TETHER_PIDFILE" + else + kill -TERM "$pid" + echo "Tether enforcer stopped (sent SIGTERM to PID $pid)" + fi + # Explicit teardown so maintenance can tether even if the daemon was already + # dead (its own TERM handler reverts too; this is the belt-and-suspenders). + iptables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true + iptables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true + iptables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true + ip6tables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true + ip6tables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true + ip6tables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true + echo "FORWARD chain $TETHER_IPT_CHAIN removed (tethering restored)" } cmd_tether_log() { tail -n "${1:-50}" "$TETHER_LOG" 2>/dev/null || echo "No tether log yet."; } @@ -1008,61 +1026,66 @@ cmd_tether_log() { tail -n "${1:-50}" "$TETHER_LOG" 2>/dev/null || echo "No teth # Test hook: force the block ACTIVE regardless of location so the full stack can # be validated during the day with a real second phone on the hotspot. cmd_tether_test_on() { - touch "$TETHER_FORCE_FILE"; chmod 666 "$TETHER_FORCE_FILE" 2>/dev/null || true - rm -f "$TETHER_OVERRIDE_FILE" - echo "Tether block FORCED ON. Enforcer engages within ${TETHER_CHECK_INTERVAL}s." - echo "Validate: second phone on the hotspot loses internet; this phone stays online." + touch "$TETHER_FORCE_FILE" + chmod 666 "$TETHER_FORCE_FILE" 2>/dev/null || true + rm -f "$TETHER_OVERRIDE_FILE" + echo "Tether block FORCED ON. Enforcer engages within ${TETHER_CHECK_INTERVAL}s." + echo "Validate: second phone on the hotspot loses internet; this phone stays online." } cmd_tether_test_off() { - rm -f "$TETHER_FORCE_FILE" - echo "Tether force cleared. Back to focus-mode-gated behaviour." + rm -f "$TETHER_FORCE_FILE" + echo "Tether force cleared. Back to focus-mode-gated behaviour." } case "$1" in - start) cmd_start ;; - stop) cmd_stop ;; - status) cmd_status ;; - enable) cmd_enable ;; - disable) cmd_disable ;; - log) cmd_log "${2:-50}" ;; - list-apps) cmd_list_apps ;; - whitelist) cmd_whitelist ;; - restart) cmd_stop; sleep 2; cmd_start ;; - hosts-status) cmd_hosts_status ;; - hosts-start) cmd_hosts_start ;; - hosts-stop) cmd_hosts_stop ;; - hosts-log) cmd_hosts_log "${2:-50}" ;; - dns-status) cmd_dns_status ;; - dns-start) cmd_dns_start ;; - dns-stop) cmd_dns_stop ;; - dns-log) cmd_dns_log "${2:-50}" ;; - launcher-status) cmd_launcher_status ;; - launcher-start) cmd_launcher_start ;; - launcher-stop) cmd_launcher_stop ;; - launcher-log) cmd_launcher_log "${2:-50}" ;; - launcher-snapshot) cmd_launcher_snapshot ;; - workout-status) cmd_workout_status ;; - workout-start) cmd_workout_start ;; - workout-stop) cmd_workout_stop ;; - workout-log) cmd_workout_log "${2:-50}" ;; - recheck) cmd_recheck ;; - notif-status) cmd_notif_status ;; - curfew-status) cmd_curfew_status ;; - curfew-start) cmd_curfew_start ;; - curfew-stop) cmd_curfew_stop ;; - curfew-log) cmd_curfew_log "${2:-50}" ;; - curfew-test-on) cmd_curfew_test_on ;; - curfew-test-off) cmd_curfew_test_off ;; - curfew-demo-on) cmd_curfew_demo_on ;; - curfew-demo-off) cmd_curfew_demo_off ;; - curfew-off) cmd_curfew_off ;; - curfew-on) cmd_curfew_on ;; - tether-status) cmd_tether_status ;; - tether-start) cmd_tether_start ;; - tether-stop) cmd_tether_stop ;; - tether-log) cmd_tether_log "${2:-50}" ;; - tether-test-on) cmd_tether_test_on ;; - tether-test-off) cmd_tether_test_off ;; - *) usage ;; +start) cmd_start ;; +stop) cmd_stop ;; +status) cmd_status ;; +enable) cmd_enable ;; +disable) cmd_disable ;; +log) cmd_log "${2:-50}" ;; +list-apps) cmd_list_apps ;; +whitelist) cmd_whitelist ;; +restart) + cmd_stop + sleep 2 + cmd_start + ;; +hosts-status) cmd_hosts_status ;; +hosts-start) cmd_hosts_start ;; +hosts-stop) cmd_hosts_stop ;; +hosts-log) cmd_hosts_log "${2:-50}" ;; +dns-status) cmd_dns_status ;; +dns-start) cmd_dns_start ;; +dns-stop) cmd_dns_stop ;; +dns-log) cmd_dns_log "${2:-50}" ;; +launcher-status) cmd_launcher_status ;; +launcher-start) cmd_launcher_start ;; +launcher-stop) cmd_launcher_stop ;; +launcher-log) cmd_launcher_log "${2:-50}" ;; +launcher-snapshot) cmd_launcher_snapshot ;; +workout-status) cmd_workout_status ;; +workout-start) cmd_workout_start ;; +workout-stop) cmd_workout_stop ;; +workout-log) cmd_workout_log "${2:-50}" ;; +recheck) cmd_recheck ;; +notif-status) cmd_notif_status ;; +curfew-status) cmd_curfew_status ;; +curfew-start) cmd_curfew_start ;; +curfew-stop) cmd_curfew_stop ;; +curfew-log) cmd_curfew_log "${2:-50}" ;; +curfew-test-on) cmd_curfew_test_on ;; +curfew-test-off) cmd_curfew_test_off ;; +curfew-demo-on) cmd_curfew_demo_on ;; +curfew-demo-off) cmd_curfew_demo_off ;; +curfew-off) cmd_curfew_off ;; +curfew-on) cmd_curfew_on ;; +tether-status) cmd_tether_status ;; +tether-start) cmd_tether_start ;; +tether-stop) cmd_tether_stop ;; +tether-log) cmd_tether_log "${2:-50}" ;; +tether-test-on) cmd_tether_test_on ;; +tether-test-off) cmd_tether_test_off ;; +*) usage ;; esac diff --git a/phone_focus_mode/focus_daemon.sh b/phone_focus_mode/focus_daemon.sh index 3f8ad3d9..d7334769 100755 --- a/phone_focus_mode/focus_daemon.sh +++ b/phone_focus_mode/focus_daemon.sh @@ -13,166 +13,166 @@ PIDFILE="$STATE_DIR/daemon.pid" # ---- PID lock: exit if already running ---- acquire_lock() { - mkdir -p "$STATE_DIR" - if [ -f "$PIDFILE" ]; then - local old_pid - old_pid="$(cat "$PIDFILE")" - if kill -0 "$old_pid" 2>/dev/null; then - # Verify the PID is actually a focus_daemon, not a reused PID - local cmdline - cmdline="$(cat /proc/$old_pid/cmdline 2>/dev/null | tr '\0' ' ')" - if echo "$cmdline" | grep -q "focus_daemon"; then - echo "Daemon already running (PID $old_pid), exiting." - exit 0 - fi - fi - # Stale or reused pidfile - rm -f "$PIDFILE" - fi - echo $$ > "$PIDFILE" + mkdir -p "$STATE_DIR" + if [ -f "$PIDFILE" ]; then + local old_pid + old_pid="$(cat "$PIDFILE")" + if kill -0 "$old_pid" 2>/dev/null; then + # Verify the PID is actually a focus_daemon, not a reused PID + local cmdline + cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)" + if echo "$cmdline" | grep -q "focus_daemon"; then + echo "Daemon already running (PID $old_pid), exiting." + exit 0 + fi + fi + # Stale or reused pidfile + rm -f "$PIDFILE" + fi + echo $$ >"$PIDFILE" } # ---- Logging ---- log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >> "$LOG_FILE" + local ts + ts="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$ts] $1" >>"$LOG_FILE" } rotate_log() { - local lines - lines="$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)" - if [ "$lines" -gt "$LOG_MAX_LINES" ]; then - local tmp="$LOG_FILE.tmp" - tail -n "$LOG_MAX_LINES" "$LOG_FILE" > "$tmp" - mv "$tmp" "$LOG_FILE" - fi + local lines + lines="$(wc -l <"$LOG_FILE" 2>/dev/null || echo 0)" + if [ "$lines" -gt "$LOG_MAX_LINES" ]; then + local tmp="$LOG_FILE.tmp" + tail -n "$LOG_MAX_LINES" "$LOG_FILE" >"$tmp" + mv "$tmp" "$LOG_FILE" + fi } # ---- Build helper files for fast package checks ---- build_whitelist_file() { - echo "$WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \ - | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/whitelist.txt" - # Sanity check: the WHITELIST string in config.sh is fragile - any - # literal double-quote inside a comment will close the heredoc and - # silently truncate the variable. Log the parsed line count so any - # future regression is visible in the log, and warn loudly if it - # falls below a known floor (we always have ~70+ entries). - local n - n=$(wc -l < "$STATE_DIR/whitelist.txt" 2>/dev/null | tr -d ' ') - log "Whitelist parsed: $n entries" - if [ "${n:-0}" -lt 30 ]; then - log "WARN: whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside WHITELIST string" - fi + echo "$WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' | + sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/whitelist.txt" + # Sanity check: the WHITELIST string in config.sh is fragile - any + # literal double-quote inside a comment will close the heredoc and + # silently truncate the variable. Log the parsed line count so any + # future regression is visible in the log, and warn loudly if it + # falls below a known floor (we always have ~70+ entries). + local n + n=$(wc -l <"$STATE_DIR/whitelist.txt" 2>/dev/null | tr -d ' ') + log "Whitelist parsed: $n entries" + if [ "${n:-0}" -lt 30 ]; then + log "WARN: whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside WHITELIST string" + fi } build_night_whitelist_file() { - # Strict allow-list used while the night curfew is active (see config.sh - # NIGHT_WHITELIST and is_curfew_now()). Parsed exactly like the day list. - echo "$NIGHT_WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \ - | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/night_whitelist.txt" - local n - n=$(wc -l < "$STATE_DIR/night_whitelist.txt" 2>/dev/null | tr -d ' ') - log "Night-curfew whitelist parsed: $n entries" - if [ "${n:-0}" -lt 10 ]; then - log "WARN: night whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside NIGHT_WHITELIST string" - fi + # Strict allow-list used while the night curfew is active (see config.sh + # NIGHT_WHITELIST and is_curfew_now()). Parsed exactly like the day list. + echo "$NIGHT_WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' | + sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/night_whitelist.txt" + local n + n=$(wc -l <"$STATE_DIR/night_whitelist.txt" 2>/dev/null | tr -d ' ') + log "Night-curfew whitelist parsed: $n entries" + if [ "${n:-0}" -lt 10 ]; then + log "WARN: night whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside NIGHT_WHITELIST string" + fi } build_sysprotect_file() { - echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' \ - | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/sysprotect.txt" + echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' | + sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/sysprotect.txt" } # $BLOCKED_SYSTEM_APPS is a static config value (never changes without a # daemon restart), so - like build_sysprotect_file above - this only needs # to run once at startup, not every enable_focus_mode() sweep. build_blocked_sys_file() { - echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \ - | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/blocked_sys.txt" + echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' | + sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/blocked_sys.txt" } reconcile_disabled_apps() { - [ -f "$DISABLED_APPS_FILE" ] || return + [ -f "$DISABLED_APPS_FILE" ] || return - local tmp_disabled="$STATE_DIR/disabled_by_focus.tmp" - : > "$tmp_disabled" + local tmp_disabled="$STATE_DIR/disabled_by_focus.tmp" + : >"$tmp_disabled" - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue + while IFS= read -r pkg; do + [ -z "$pkg" ] && continue - if is_allowed "$pkg"; then - pm install-existing --user 0 "$pkg" >/dev/null 2>&1 || true - pm enable "$pkg" >/dev/null 2>&1 || true - log "Re-enabled allowed app during state reconciliation: $pkg" - continue - fi + if is_allowed "$pkg"; then + pm install-existing --user 0 "$pkg" >/dev/null 2>&1 || true + pm enable "$pkg" >/dev/null 2>&1 || true + log "Re-enabled allowed app during state reconciliation: $pkg" + continue + fi - echo "$pkg" >> "$tmp_disabled" - done < "$DISABLED_APPS_FILE" + echo "$pkg" >>"$tmp_disabled" + done <"$DISABLED_APPS_FILE" - mv "$tmp_disabled" "$DISABLED_APPS_FILE" + mv "$tmp_disabled" "$DISABLED_APPS_FILE" } # ---- Initialization ---- init() { - mkdir -p "$STATE_DIR" - touch "$LOG_FILE" - touch "$DISABLED_APPS_FILE" - # Ensure state files are writable (survives reboot / permission drift) - chmod 666 "$LOG_FILE" "$DISABLED_APPS_FILE" "$PIDFILE" 2>/dev/null - # Status file must be world-readable (companion app reads it). - # State dir must be world-writable+executable so the companion app can - # drop the recheck trigger file (it runs as a normal app UID). - chmod 777 "$STATE_DIR" 2>/dev/null + mkdir -p "$STATE_DIR" + touch "$LOG_FILE" + touch "$DISABLED_APPS_FILE" + # Ensure state files are writable (survives reboot / permission drift) + chmod 666 "$LOG_FILE" "$DISABLED_APPS_FILE" "$PIDFILE" 2>/dev/null + # Status file must be world-readable (companion app reads it). + # State dir must be world-writable+executable so the companion app can + # drop the recheck trigger file (it runs as a normal app UID). + chmod 777 "$STATE_DIR" 2>/dev/null - if [ "$HOME_LAT" = "0.000000" ] && [ "$HOME_LON" = "0.000000" ]; then - log "ERROR: Home coordinates not set! Edit config_secrets.sh first." - exit 1 - fi + if [ "$HOME_LAT" = "0.000000" ] && [ "$HOME_LON" = "0.000000" ]; then + log "ERROR: Home coordinates not set! Edit config_secrets.sh first." + exit 1 + fi - if ! echo "$HOME_LAT" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then - log "ERROR: HOME_LAT is invalid ('$HOME_LAT'). Expected decimal degrees in config_secrets.sh" - exit 1 - fi + if ! echo "$HOME_LAT" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then + log "ERROR: HOME_LAT is invalid ('$HOME_LAT'). Expected decimal degrees in config_secrets.sh" + exit 1 + fi - if ! echo "$HOME_LON" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then - log "ERROR: HOME_LON is invalid ('$HOME_LON'). Expected decimal degrees in config_secrets.sh" - exit 1 - fi + if ! echo "$HOME_LON" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then + log "ERROR: HOME_LON is invalid ('$HOME_LON'). Expected decimal degrees in config_secrets.sh" + exit 1 + fi - build_whitelist_file - build_night_whitelist_file - build_sysprotect_file - build_blocked_sys_file - refresh_default_handlers - rotate_log + build_whitelist_file + build_night_whitelist_file + build_sysprotect_file + build_blocked_sys_file + refresh_default_handlers + rotate_log - if [ -f "$MODE_FILE" ]; then - CURRENT_MODE="$(cat "$MODE_FILE")" - else - CURRENT_MODE="normal" - fi + if [ -f "$MODE_FILE" ]; then + CURRENT_MODE="$(cat "$MODE_FILE")" + else + CURRENT_MODE="normal" + fi - if [ "$CURRENT_MODE" = "focus" ]; then - reconcile_disabled_apps - fi + if [ "$CURRENT_MODE" = "focus" ]; then + reconcile_disabled_apps + fi - log "Focus mode daemon started (PID=$$, mode=$CURRENT_MODE, home=$HOME_LAT,$HOME_LON, radius=${RADIUS}m)" - log "Intervals: focus=${CHECK_INTERVAL_FOCUS}s normal=${CHECK_INTERVAL_NORMAL}s" + log "Focus mode daemon started (PID=$$, mode=$CURRENT_MODE, home=$HOME_LAT,$HOME_LON, radius=${RADIUS}m)" + log "Intervals: focus=${CHECK_INTERVAL_FOCUS}s normal=${CHECK_INTERVAL_NORMAL}s" } # ---- Location ---- get_location() { - dumpsys location 2>/dev/null \ - | grep -oE '[-]?[0-9]{1,3}\.[0-9]{4,},[-]?[0-9]{1,3}\.[0-9]{4,}' \ - | head -1 + dumpsys location 2>/dev/null | + grep -oE '[-]?[0-9]{1,3}\.[0-9]{4,},[-]?[0-9]{1,3}\.[0-9]{4,}' | + head -1 } # ---- Distance Calculation (Haversine via awk) ---- calc_distance() { - echo "$1 $2 $3 $4" | awk '{ + echo "$1 $2 $3 $4" | awk '{ PI = 3.14159265358979323846 R = 6371000.0 lat1 = $1 * PI / 180.0 @@ -195,27 +195,29 @@ calc_distance() { # can never strand you behind the strict list — essentials stay reachable # either way, but the day list is the less-surprising default. _dec() { - # Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed - # as (sometimes invalid) octal by the shell's arithmetic. Portable across - # ash/mksh; keeps at least one digit so "0000" -> "0". - local n="$1" - while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done - printf '%s' "$n" + # Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed + # as (sometimes invalid) octal by the shell's arithmetic. Portable across + # ash/mksh; keeps at least one digit so "0000" -> "0". + local n="$1" + while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done + printf '%s' "$n" } is_curfew_now() { - local now start end - now="$(date +%H%M 2>/dev/null)" - case "$now" in - ''|*[!0-9]*) return 1 ;; - esac - now="$(_dec "$now")"; start="$(_dec "$NIGHT_CURFEW_START")"; end="$(_dec "$NIGHT_CURFEW_END")" - if [ "$start" -le "$end" ]; then - [ "$now" -ge "$start" ] && [ "$now" -lt "$end" ] - else - # Window wraps past midnight (e.g. 2300 -> 0500). - [ "$now" -ge "$start" ] || [ "$now" -lt "$end" ] - fi + local now start end + now="$(date +%H%M 2>/dev/null)" + case "$now" in + '' | *[!0-9]*) return 1 ;; + esac + now="$(_dec "$now")" + start="$(_dec "$NIGHT_CURFEW_START")" + end="$(_dec "$NIGHT_CURFEW_END")" + if [ "$start" -le "$end" ]; then + [ "$now" -ge "$start" ] && [ "$now" -lt "$end" ] + else + # Window wraps past midnight (e.g. 2300 -> 0500). + [ "$now" -ge "$start" ] || [ "$now" -lt "$end" ] + fi } # Curfew is ACTIVE when enabled, not manually overridden, and either forced on @@ -229,192 +231,192 @@ is_curfew_now() { # 10s tick for N enabled apps, all recomputing a value that cannot change # within a single tick. Result is cached in _CURFEW_TICK_RESULT. curfew_active() { - if [ -n "$_CURFEW_TICK_CACHED" ]; then - [ "$_CURFEW_TICK_RESULT" = "1" ] - return - fi - _CURFEW_TICK_CACHED=1 - _CURFEW_TICK_RESULT=0 - if [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] && [ ! -e "$CURFEW_OVERRIDE_FILE" ]; then - if [ -e "$CURFEW_FORCE_FILE" ] || is_curfew_now; then - _CURFEW_TICK_RESULT=1 - fi - fi - [ "$_CURFEW_TICK_RESULT" = "1" ] + if [ -n "$_CURFEW_TICK_CACHED" ]; then + [ "$_CURFEW_TICK_RESULT" = "1" ] + return + fi + _CURFEW_TICK_CACHED=1 + _CURFEW_TICK_RESULT=0 + if [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] && [ ! -e "$CURFEW_OVERRIDE_FILE" ]; then + if [ -e "$CURFEW_FORCE_FILE" ] || is_curfew_now; then + _CURFEW_TICK_RESULT=1 + fi + fi + [ "$_CURFEW_TICK_RESULT" = "1" ] } # ---- Check if package is allowed (whitelist or system-protected) ---- is_allowed() { - local pkg="$1" - # During the night curfew, swap the permissive day list for the strict - # night list. The sysprotect + default-handler guards below still apply on - # top of whichever list is active. - local list="$STATE_DIR/whitelist.txt" - if curfew_active; then - list="$STATE_DIR/night_whitelist.txt" - fi - # Exact match against the active whitelist file - if grep -qxF "$pkg" "$list" 2>/dev/null; then - return 0 - fi - # Prefix match against system-protect file - while IFS= read -r prefix; do - [ -z "$prefix" ] && continue - case "$pkg" in - "$prefix"*) return 0 ;; - esac - done < "$STATE_DIR/sysprotect.txt" - # Hard-stop guard: refuse to disable any package that is the current - # default handler for a critical role (Dialer / SMS / Home / Contacts). - # Without this, a misconfigured WHITELIST can disable the default Phone - # app and Android falls back to com.android.settings/.FallbackHome - - # the persistent "Phone is starting..." screen with broken SystemUI - # gestures (no swipe-up recents). Recovering requires `pm enable` over - # ADB. Treat the guard as last-resort safety net independent of WHITELIST - # contents so a future config edit can never wipe these out. - is_default_handler "$pkg" && return 0 - # The default browser is guarded only OUTSIDE curfew. At night the whole - # point is to disable browsers, so this guard must not re-allow it. - if ! curfew_active \ - && grep -qxF "$pkg" "$STATE_DIR/default_browser.txt" 2>/dev/null; then - return 0 - fi - return 1 + local pkg="$1" + # During the night curfew, swap the permissive day list for the strict + # night list. The sysprotect + default-handler guards below still apply on + # top of whichever list is active. + local list="$STATE_DIR/whitelist.txt" + if curfew_active; then + list="$STATE_DIR/night_whitelist.txt" + fi + # Exact match against the active whitelist file + if grep -qxF "$pkg" "$list" 2>/dev/null; then + return 0 + fi + # Prefix match against system-protect file + while IFS= read -r prefix; do + [ -z "$prefix" ] && continue + case "$pkg" in + "$prefix"*) return 0 ;; + esac + done <"$STATE_DIR/sysprotect.txt" + # Hard-stop guard: refuse to disable any package that is the current + # default handler for a critical role (Dialer / SMS / Home / Contacts). + # Without this, a misconfigured WHITELIST can disable the default Phone + # app and Android falls back to com.android.settings/.FallbackHome - + # the persistent "Phone is starting..." screen with broken SystemUI + # gestures (no swipe-up recents). Recovering requires `pm enable` over + # ADB. Treat the guard as last-resort safety net independent of WHITELIST + # contents so a future config edit can never wipe these out. + is_default_handler "$pkg" && return 0 + # The default browser is guarded only OUTSIDE curfew. At night the whole + # point is to disable browsers, so this guard must not re-allow it. + if ! curfew_active && + grep -qxF "$pkg" "$STATE_DIR/default_browser.txt" 2>/dev/null; then + return 0 + fi + return 1 } # ---- Default handler detection ---- # Refreshed once per focus_daemon tick into $STATE_DIR/default_handlers.txt. # Each line is a package name. Lookup is a cheap grep against this file. refresh_default_handlers() { - local f="$STATE_DIR/default_handlers.txt" - local tmp="$f.tmp" - : > "$tmp" - # Default Home (launcher). resolve-activity prints "Activity Resolver Table:" - # on line 1 and "/<.Activity>" on line 2 in --brief mode. - cmd package resolve-activity --brief \ - -c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null \ - | awk -F/ 'NR==2 && $1 != "" {print $1}' >> "$tmp" - # Default Dialer - local dialer - dialer="$(cmd telecom get-default-dialer 2>/dev/null | tr -d '[:space:]')" - [ -n "$dialer" ] && echo "$dialer" >> "$tmp" - # Default SMS handler (settings provider key) - local sms - sms="$(settings get secure sms_default_application 2>/dev/null | tr -d '[:space:]')" - [ -n "$sms" ] && [ "$sms" != "null" ] && echo "$sms" >> "$tmp" - # Default input method (active keyboard). Disabling the active IME with - # pm disable-user PERSISTS across reboot; a 1am reboot would then leave no - # keyboard to type any recovery command. Protect it day and night so the - # curfew can never lock you out of typing. - local ime - ime="$(settings get secure default_input_method 2>/dev/null | cut -d/ -f1)" - [ -n "$ime" ] && [ "$ime" != "null" ] && echo "$ime" >> "$tmp" - sort -u "$tmp" -o "$f" - rm -f "$tmp" + local f="$STATE_DIR/default_handlers.txt" + local tmp="$f.tmp" + : >"$tmp" + # Default Home (launcher). resolve-activity prints "Activity Resolver Table:" + # on line 1 and "/<.Activity>" on line 2 in --brief mode. + cmd package resolve-activity --brief \ + -c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null | + awk -F/ 'NR==2 && $1 != "" {print $1}' >>"$tmp" + # Default Dialer + local dialer + dialer="$(cmd telecom get-default-dialer 2>/dev/null | tr -d '[:space:]')" + [ -n "$dialer" ] && echo "$dialer" >>"$tmp" + # Default SMS handler (settings provider key) + local sms + sms="$(settings get secure sms_default_application 2>/dev/null | tr -d '[:space:]')" + [ -n "$sms" ] && [ "$sms" != "null" ] && echo "$sms" >>"$tmp" + # Default input method (active keyboard). Disabling the active IME with + # pm disable-user PERSISTS across reboot; a 1am reboot would then leave no + # keyboard to type any recovery command. Protect it day and night so the + # curfew can never lock you out of typing. + local ime + ime="$(settings get secure default_input_method 2>/dev/null | cut -d/ -f1)" + [ -n "$ime" ] && [ "$ime" != "null" ] && echo "$ime" >>"$tmp" + sort -u "$tmp" -o "$f" + rm -f "$tmp" - # Default Browser handler is tracked SEPARATELY and guarded only OUTSIDE - # the curfew window (see is_allowed). During curfew the whole point is to - # disable browsers, so the default-handler guard must not resurrect them. - local bf="$STATE_DIR/default_browser.txt" - cmd package resolve-activity --brief \ - -a android.intent.action.VIEW -d http://example.com 2>/dev/null \ - | awk -F/ 'NR==2 && $1 != "" {print $1}' > "$bf.tmp" 2>/dev/null - mv "$bf.tmp" "$bf" 2>/dev/null || : > "$bf" + # Default Browser handler is tracked SEPARATELY and guarded only OUTSIDE + # the curfew window (see is_allowed). During curfew the whole point is to + # disable browsers, so the default-handler guard must not resurrect them. + local bf="$STATE_DIR/default_browser.txt" + cmd package resolve-activity --brief \ + -a android.intent.action.VIEW -d http://example.com 2>/dev/null | + awk -F/ 'NR==2 && $1 != "" {print $1}' >"$bf.tmp" 2>/dev/null + mv "$bf.tmp" "$bf" 2>/dev/null || : >"$bf" } is_default_handler() { - local pkg="$1" - grep -qxF "$pkg" "$STATE_DIR/default_handlers.txt" 2>/dev/null + local pkg="$1" + grep -qxF "$pkg" "$STATE_DIR/default_handlers.txt" 2>/dev/null } # ---- Focus Mode Control ---- enable_focus_mode() { - local first_entry=0 - if [ "$CURRENT_MODE" != "focus" ]; then - first_entry=1 - log "ENABLING focus mode - restricting non-whitelisted apps" - : > "$DISABLED_APPS_FILE" - fi + local first_entry=0 + if [ "$CURRENT_MODE" != "focus" ]; then + first_entry=1 + log "ENABLING focus mode - restricting non-whitelisted apps" + : >"$DISABLED_APPS_FILE" + fi - # Refresh default-handler list every tick. The user may switch dialer / - # SMS / launcher between sweeps; the guard in is_allowed() consults this - # list so a newly-promoted handler is never disabled. - refresh_default_handlers + # Refresh default-handler list every tick. The user may switch dialer / + # SMS / launcher between sweeps; the guard in is_allowed() consults this + # list so a newly-promoted handler is never disabled. + refresh_default_handlers - # Blocked system app list is static; built once in init() (see - # build_blocked_sys_file), not rebuilt on every sweep. - local blocked_sys="$STATE_DIR/blocked_sys.txt" + # Blocked system app list is static; built once in init() (see + # build_blocked_sys_file), not rebuilt on every sweep. + local blocked_sys="$STATE_DIR/blocked_sys.txt" - # Periodic rescan catches third-party apps the user re-enabled (e.g. via - # Play Store or `pm enable` in a terminal) since the last tick. - # -e = enabled only, so we skip apps that are already disabled. - local tmp_pkgs="$STATE_DIR/pkg_list.txt" - pm list packages -3 -e 2>/dev/null | sed 's/^package://' > "$tmp_pkgs" - local newly_disabled=0 - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - is_allowed "$pkg" && continue - if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then - grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null \ - || echo "$pkg" >> "$DISABLED_APPS_FILE" - newly_disabled=$((newly_disabled + 1)) - fi - done < "$tmp_pkgs" - rm -f "$tmp_pkgs" + # Periodic rescan catches third-party apps the user re-enabled (e.g. via + # Play Store or `pm enable` in a terminal) since the last tick. + # -e = enabled only, so we skip apps that are already disabled. + local tmp_pkgs="$STATE_DIR/pkg_list.txt" + pm list packages -3 -e 2>/dev/null | sed 's/^package://' >"$tmp_pkgs" + local newly_disabled=0 + while IFS= read -r pkg; do + [ -z "$pkg" ] && continue + is_allowed "$pkg" && continue + if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then + grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null || + echo "$pkg" >>"$DISABLED_APPS_FILE" + newly_disabled=$((newly_disabled + 1)) + fi + done <"$tmp_pkgs" + rm -f "$tmp_pkgs" - # Disable-for-user-0 any blocked system apps (Play Store, browsers, - # package installer UI, terminal apps). - # IMPORTANT: We intentionally use pm disable-user (NOT pm uninstall) here. - # pm uninstall -k --user 0 removes the package from Android's user-0 - # package registry. If the daemon is killed with SIGKILL during a reboot - # (bypassing the cleanup trap), those packages stay uninstalled across the - # reboot. Android's bootloop-protection (MTK and others) then detects - # missing critical system packages and triggers recovery / factory wipe. - # pm disable-user leaves the package registered but inactive, so the - # PackageManager scan at next boot succeeds and no wipe occurs. - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then - grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null \ - || echo "$pkg" >> "$DISABLED_APPS_FILE" - newly_disabled=$((newly_disabled + 1)) - fi - done < "$blocked_sys" + # Disable-for-user-0 any blocked system apps (Play Store, browsers, + # package installer UI, terminal apps). + # IMPORTANT: We intentionally use pm disable-user (NOT pm uninstall) here. + # pm uninstall -k --user 0 removes the package from Android's user-0 + # package registry. If the daemon is killed with SIGKILL during a reboot + # (bypassing the cleanup trap), those packages stay uninstalled across the + # reboot. Android's bootloop-protection (MTK and others) then detects + # missing critical system packages and triggers recovery / factory wipe. + # pm disable-user leaves the package registered but inactive, so the + # PackageManager scan at next boot succeeds and no wipe occurs. + while IFS= read -r pkg; do + [ -z "$pkg" ] && continue + if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then + grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null || + echo "$pkg" >>"$DISABLED_APPS_FILE" + newly_disabled=$((newly_disabled + 1)) + fi + done <"$blocked_sys" - CURRENT_MODE="focus" - echo "focus" > "$MODE_FILE" + CURRENT_MODE="focus" + echo "focus" >"$MODE_FILE" - if [ "$first_entry" -eq 1 ]; then - local count - count=$(wc -l < "$DISABLED_APPS_FILE" 2>/dev/null || echo 0) - log "Focus mode enabled - disabled $count apps" - elif [ "$newly_disabled" -gt 0 ]; then - log "Focus mode re-sweep: re-disabled $newly_disabled apps (re-enabled by user?)" - fi + if [ "$first_entry" -eq 1 ]; then + local count + count=$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null || echo 0) + log "Focus mode enabled - disabled $count apps" + elif [ "$newly_disabled" -gt 0 ]; then + log "Focus mode re-sweep: re-disabled $newly_disabled apps (re-enabled by user?)" + fi - reconcile_disabled_apps + reconcile_disabled_apps } disable_focus_mode() { - [ "$CURRENT_MODE" = "normal" ] && return - log "DISABLING focus mode - re-enabling apps" + [ "$CURRENT_MODE" = "normal" ] && return + log "DISABLING focus mode - re-enabling apps" - local count=0 - if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then - # Re-enable all disabled apps (both 3rd-party and system apps). - # Both paths now use pm disable-user, so pm enable is the only - # restore command needed. - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1)) - done < "$DISABLED_APPS_FILE" - : > "$DISABLED_APPS_FILE" - fi + local count=0 + if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then + # Re-enable all disabled apps (both 3rd-party and system apps). + # Both paths now use pm disable-user, so pm enable is the only + # restore command needed. + while IFS= read -r pkg; do + [ -z "$pkg" ] && continue + pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1)) + done <"$DISABLED_APPS_FILE" + : >"$DISABLED_APPS_FILE" + fi - CURRENT_MODE="normal" - echo "normal" > "$MODE_FILE" - log "Focus mode disabled - re-enabled $count apps" + CURRENT_MODE="normal" + echo "normal" >"$MODE_FILE" + log "Focus mode disabled - re-enabled $count apps" } # ---- Status snapshot for companion notification app ---- @@ -422,40 +424,40 @@ disable_focus_mode() { # Fields: mode, lat, lon, distance_m, threshold_m, radius_m, disabled_count, # last_check_ts (unix), last_check_iso (human). write_status_snapshot() { - local mode="$1" lat="$2" lon="$3" dist="$4" thr="$5" - local count iso ts cf ov frc - count="$(wc -l < "$DISABLED_APPS_FILE" 2>/dev/null | tr -d ' ' || echo 0)" - [ -z "$count" ] && count=0 - ts="$(date +%s)" - iso="$(date '+%Y-%m-%d %H:%M:%S')" - # Curfew state for the companion app: 1/0 so it slots into the existing - # numeric JSON path. "curfew" = restrictions active now; "curfew_override" - # = the escape-hatch file is set (curfew suspended). - if curfew_active; then cf=1; else cf=0; fi - if [ -e "$CURFEW_OVERRIDE_FILE" ]; then ov=1; else ov=0; fi - # "curfew_force" = the demo/test force file is set (curfew forced on - # regardless of clock). Lets the companion app show Start/Stop demo. - if [ -e "$CURFEW_FORCE_FILE" ]; then frc=1; else frc=0; fi - local tmp="$STATUS_FILE.tmp" - # Shell-emitted JSON — keep values numeric where possible, strings quoted. - { - printf '{' - printf '"mode":"%s",' "$mode" - printf '"lat":"%s",' "${lat:-}" - printf '"lon":"%s",' "${lon:-}" - printf '"distance_m":%s,' "${dist:-null}" - printf '"threshold_m":%s,' "${thr:-null}" - printf '"radius_m":%s,' "$RADIUS" - printf '"disabled_count":%s,' "$count" - printf '"curfew":%s,' "$cf" - printf '"curfew_override":%s,' "$ov" - printf '"curfew_force":%s,' "$frc" - printf '"last_check_ts":%s,' "$ts" - printf '"last_check_iso":"%s"' "$iso" - printf '}\n' - } > "$tmp" 2>/dev/null || return 0 - mv "$tmp" "$STATUS_FILE" 2>/dev/null || true - chmod 644 "$STATUS_FILE" 2>/dev/null || true + local mode="$1" lat="$2" lon="$3" dist="$4" thr="$5" + local count iso ts cf ov frc + count="$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null | tr -d ' ' || echo 0)" + [ -z "$count" ] && count=0 + ts="$(date +%s)" + iso="$(date '+%Y-%m-%d %H:%M:%S')" + # Curfew state for the companion app: 1/0 so it slots into the existing + # numeric JSON path. "curfew" = restrictions active now; "curfew_override" + # = the escape-hatch file is set (curfew suspended). + if curfew_active; then cf=1; else cf=0; fi + if [ -e "$CURFEW_OVERRIDE_FILE" ]; then ov=1; else ov=0; fi + # "curfew_force" = the demo/test force file is set (curfew forced on + # regardless of clock). Lets the companion app show Start/Stop demo. + if [ -e "$CURFEW_FORCE_FILE" ]; then frc=1; else frc=0; fi + local tmp="$STATUS_FILE.tmp" + # Shell-emitted JSON — keep values numeric where possible, strings quoted. + { + printf '{' + printf '"mode":"%s",' "$mode" + printf '"lat":"%s",' "${lat:-}" + printf '"lon":"%s",' "${lon:-}" + printf '"distance_m":%s,' "${dist:-null}" + printf '"threshold_m":%s,' "${thr:-null}" + printf '"radius_m":%s,' "$RADIUS" + printf '"disabled_count":%s,' "$count" + printf '"curfew":%s,' "$cf" + printf '"curfew_override":%s,' "$ov" + printf '"curfew_force":%s,' "$frc" + printf '"last_check_ts":%s,' "$ts" + printf '"last_check_iso":"%s"' "$iso" + printf '}\n' + } >"$tmp" 2>/dev/null || return 0 + mv "$tmp" "$STATUS_FILE" 2>/dev/null || true + chmod 644 "$STATUS_FILE" 2>/dev/null || true } # ---- Sleep with early-wake on recheck trigger ---- @@ -463,25 +465,25 @@ write_status_snapshot() { # early. The file can be touched by the companion app (via "Re-check now" # button) or by `focus_ctl.sh recheck` from a shell. sleep_with_recheck() { - local total="$1" - local elapsed=0 - while [ "$elapsed" -lt "$total" ]; do - if [ -e "$RECHECK_TRIGGER" ]; then - rm -f "$RECHECK_TRIGGER" 2>/dev/null - log "Manual re-check triggered" - return 0 - fi - sleep 1 - elapsed=$((elapsed + 1)) - done + local total="$1" + local elapsed=0 + while [ "$elapsed" -lt "$total" ]; do + if [ -e "$RECHECK_TRIGGER" ]; then + rm -f "$RECHECK_TRIGGER" 2>/dev/null + log "Manual re-check triggered" + return 0 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done } # ---- Signal handlers ---- cleanup() { - log "Daemon shutting down - re-enabling all apps" - disable_focus_mode - rm -f "$PIDFILE" - exit 0 + log "Daemon shutting down - re-enabling all apps" + disable_focus_mode + rm -f "$PIDFILE" + exit 0 } # HUP is intentionally NOT trapped so the daemon survives ADB disconnects. @@ -490,51 +492,52 @@ trap cleanup INT TERM # ---- Main Loop ---- main() { - acquire_lock - init + acquire_lock + init - while true; do - # Invalidate the per-tick curfew_active() memo (see its definition). - _CURFEW_TICK_CACHED="" + while true; do + # Invalidate the per-tick curfew_active() memo (see its definition). + _CURFEW_TICK_CACHED="" - location="$(get_location)" + location="$(get_location)" - if [ -n "$location" ]; then - lat="$(echo "$location" | cut -d',' -f1)" - lon="$(echo "$location" | cut -d',' -f2)" - distance="$(calc_distance "$lat" "$lon" "$HOME_LAT" "$HOME_LON")" + if [ -n "$location" ]; then + lat="$(echo "$location" | cut -d',' -f1)" + lon="$(echo "$location" | cut -d',' -f2)" + distance="$(calc_distance "$lat" "$lon" "$HOME_LAT" "$HOME_LON")" - if [ "$CURRENT_MODE" = "focus" ]; then - threshold=$((RADIUS + HYSTERESIS)) - else - threshold=$((RADIUS - HYSTERESIS)) - fi + if [ "$CURRENT_MODE" = "focus" ]; then + threshold=$((RADIUS + HYSTERESIS)) + else + threshold=$((RADIUS - HYSTERESIS)) + fi - if [ "$distance" -le "$threshold" ] 2>/dev/null; then - enable_focus_mode - else - disable_focus_mode - fi + if [ "$distance" -le "$threshold" ] 2>/dev/null; then + enable_focus_mode + else + disable_focus_mode + fi - curfew_state="day"; curfew_active && curfew_state="CURFEW" - log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE | Curfew: $curfew_state" - write_status_snapshot "$CURRENT_MODE" "$lat" "$lon" "$distance" "$threshold" - else - log "Location unavailable - defaulting to focus mode (restrictions ON)" - enable_focus_mode - write_status_snapshot "$CURRENT_MODE" "" "" "null" "null" - fi + curfew_state="day" + curfew_active && curfew_state="CURFEW" + log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE | Curfew: $curfew_state" + write_status_snapshot "$CURRENT_MODE" "$lat" "$lon" "$distance" "$threshold" + else + log "Location unavailable - defaulting to focus mode (restrictions ON)" + enable_focus_mode + write_status_snapshot "$CURRENT_MODE" "" "" "null" "null" + fi - # Dynamic interval: shorter at home (can charge), longer away (save battery). - # sleep_with_recheck returns early if the companion app requests a recheck. - if [ "$CURRENT_MODE" = "focus" ]; then - sleep_with_recheck "$CHECK_INTERVAL_FOCUS" - else - sleep_with_recheck "$CHECK_INTERVAL_NORMAL" - fi + # Dynamic interval: shorter at home (can charge), longer away (save battery). + # sleep_with_recheck returns early if the companion app requests a recheck. + if [ "$CURRENT_MODE" = "focus" ]; then + sleep_with_recheck "$CHECK_INTERVAL_FOCUS" + else + sleep_with_recheck "$CHECK_INTERVAL_NORMAL" + fi - rotate_log - done + rotate_log + done } main "$@" diff --git a/phone_focus_mode/focus_status_app/build.sh b/phone_focus_mode/focus_status_app/build.sh index b3f08b36..6799e007 100755 --- a/phone_focus_mode/focus_status_app/build.sh +++ b/phone_focus_mode/focus_status_app/build.sh @@ -11,13 +11,23 @@ cd "$SCRIPT_DIR" SDK="${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}" # Pick highest-numbered build-tools directory. -BUILD_TOOLS_DIR="$(ls -1d "$SDK"/build-tools/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/$::')" +# A glob into an array rather than parsing ls: a build-tools path containing a +# space or newline would otherwise be split into garbage. +_bt_dirs=("$SDK"/build-tools/*/) +BUILD_TOOLS_DIR="$(printf '%s\n' "${_bt_dirs[@]}" | sort -V | tail -1 | sed 's:/$::')" # Pick highest-numbered platform. -PLATFORM_DIR="$(ls -1d "$SDK"/platforms/android-*/ 2>/dev/null | sort -V | tail -1 | sed 's:/$::')" +_plat_dirs=("$SDK"/platforms/android-*/) +PLATFORM_DIR="$(printf '%s\n' "${_plat_dirs[@]}" | sort -V | tail -1 | sed 's:/$::')" PLATFORM_JAR="$PLATFORM_DIR/android.jar" -[ -d "$BUILD_TOOLS_DIR" ] || { echo "ERROR: build-tools not found under $SDK" >&2; exit 1; } -[ -f "$PLATFORM_JAR" ] || { echo "ERROR: android.jar not found at $PLATFORM_JAR" >&2; exit 1; } +[ -d "$BUILD_TOOLS_DIR" ] || { + echo "ERROR: build-tools not found under $SDK" >&2 + exit 1 +} +[ -f "$PLATFORM_JAR" ] || { + echo "ERROR: android.jar not found at $PLATFORM_JAR" >&2 + exit 1 +} AAPT2="$BUILD_TOOLS_DIR/aapt2" D8="$BUILD_TOOLS_DIR/d8" @@ -25,7 +35,10 @@ ZIPALIGN="$BUILD_TOOLS_DIR/zipalign" APKSIGNER="$BUILD_TOOLS_DIR/apksigner" for tool in "$AAPT2" "$D8" "$ZIPALIGN" "$APKSIGNER"; do - [ -x "$tool" ] || { echo "ERROR: missing build tool: $tool" >&2; exit 1; } + [ -x "$tool" ] || { + echo "ERROR: missing build tool: $tool" >&2 + exit 1 + } done BUILD="$SCRIPT_DIR/build" @@ -35,23 +48,23 @@ mkdir -p "$BUILD/compiled-res" "$BUILD/classes" "$BUILD/dex" # ---- Compile resources (none for now, but aapt2 requires the dir) ---- mkdir -p "$SCRIPT_DIR/res" if find "$SCRIPT_DIR/res" -type f -print -quit | grep -q .; then - "$AAPT2" compile --dir "$SCRIPT_DIR/res" -o "$BUILD/compiled-res" + "$AAPT2" compile --dir "$SCRIPT_DIR/res" -o "$BUILD/compiled-res" fi # ---- Link resources + manifest into base APK ---- LINK_ARGS=( - --manifest "$SCRIPT_DIR/AndroidManifest.xml" - -I "$PLATFORM_JAR" - --java "$BUILD" - --min-sdk-version 29 - --target-sdk-version 35 - --version-code 1 - --version-name 1.0.0 - -o "$BUILD/base.apk" + --manifest "$SCRIPT_DIR/AndroidManifest.xml" + -I "$PLATFORM_JAR" + --java "$BUILD" + --min-sdk-version 29 + --target-sdk-version 35 + --version-code 1 + --version-name 1.0.0 + -o "$BUILD/base.apk" ) # Include any compiled res archives. for rfile in "$BUILD"/compiled-res/*.flat; do - [ -e "$rfile" ] && LINK_ARGS+=("$rfile") + [ -e "$rfile" ] && LINK_ARGS+=("$rfile") done "$AAPT2" link "${LINK_ARGS[@]}" @@ -61,9 +74,9 @@ JAVA_SRCS=() while IFS= read -r -d '' f; do JAVA_SRCS+=("$f"); done < <(find "$SCRIPT_DIR/java" "$BUILD" -name '*.java' -print0) javac -source 11 -target 11 \ - -classpath "$PLATFORM_JAR" \ - -d "$BUILD/classes" \ - "${JAVA_SRCS[@]}" + -classpath "$PLATFORM_JAR" \ + -d "$BUILD/classes" \ + "${JAVA_SRCS[@]}" # ---- Dex ---- CLASS_FILES=() @@ -80,18 +93,18 @@ cp "$BUILD/base.apk" "$BUILD/unsigned.apk" # ---- Sign with debug key (auto-generated on first build) ---- KEYSTORE="$SCRIPT_DIR/debug.keystore" if [ ! -f "$KEYSTORE" ]; then - echo "Generating debug keystore..." - keytool -genkeypair -v \ - -keystore "$KEYSTORE" -storepass android -keypass android \ - -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \ - -dname "CN=Focus Status Debug, OU=Dev, O=Dev, L=NA, ST=NA, C=NA" \ - >/dev/null 2>&1 + echo "Generating debug keystore..." + keytool -genkeypair -v \ + -keystore "$KEYSTORE" -storepass android -keypass android \ + -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \ + -dname "CN=Focus Status Debug, OU=Dev, O=Dev, L=NA, ST=NA, C=NA" \ + >/dev/null 2>&1 fi "$APKSIGNER" sign \ - --ks "$KEYSTORE" --ks-pass pass:android \ - --key-pass pass:android \ - --out "$BUILD/focus_status.apk" \ - "$BUILD/aligned.apk" + --ks "$KEYSTORE" --ks-pass pass:android \ + --key-pass pass:android \ + --out "$BUILD/focus_status.apk" \ + "$BUILD/aligned.apk" echo "" echo "Built: $BUILD/focus_status.apk" diff --git a/phone_focus_mode/lib/tests/test_adb_common.sh b/phone_focus_mode/lib/tests/test_adb_common.sh index 8e35a7d6..ebb9ae58 100755 --- a/phone_focus_mode/lib/tests/test_adb_common.sh +++ b/phone_focus_mode/lib/tests/test_adb_common.sh @@ -7,13 +7,13 @@ PASS=0 FAIL=0 _t_pass() { - PASS=$((PASS + 1)) - printf ' OK: %s\n' "$1" + PASS=$((PASS + 1)) + printf ' OK: %s\n' "$1" } _t_fail() { - FAIL=$((FAIL + 1)) - printf ' FAIL: %s\n' "$1" + FAIL=$((FAIL + 1)) + printf ' FAIL: %s\n' "$1" } TEST_TMPDIR="$(mktemp -d)" @@ -25,189 +25,198 @@ mkdir -p "${XDG_STATE_HOME}" source "${SCRIPT_DIR}/../adb_common.sh" ADB_MOCK_MODEL=$'Pixel "7";$(rm -rf /)`danger`\nline2' +# Single quotes are the whole point: this is an injection payload that must +# reach the code under test LITERALLY. Expanding $evil / `cmd` here would +# execute them in the test harness and test nothing. +# shellcheck disable=SC2016 ADB_MOCK_FINGERPRINT='google/pixel:14/UP1A.231005.007/$evil;`cmd`' adb() { - if [[ "$#" -eq 1 && "$1" == "devices" ]]; then - printf 'List of devices attached\nSERIAL123\tdevice\n' - return 0 - fi + if [[ "$#" -eq 1 && "$1" == "devices" ]]; then + printf 'List of devices attached\nSERIAL123\tdevice\n' + return 0 + fi - if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.product.model" ]]; then - printf '%s\r\n' "${ADB_MOCK_MODEL}" - return 0 - fi + if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.product.model" ]]; then + printf '%s\r\n' "${ADB_MOCK_MODEL}" + return 0 + fi - if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.build.fingerprint" ]]; then - printf '%s\r\n' "${ADB_MOCK_FINGERPRINT}" - return 0 - fi + if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.build.fingerprint" ]]; then + printf '%s\r\n' "${ADB_MOCK_FINGERPRINT}" + return 0 + fi - if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "su" && "$5" == "--mount-master" && "$6" == "-c" && "$7" == "echo ok" ]]; then - printf 'ok\n' - return 0 - fi + if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "su" && "$5" == "--mount-master" && "$6" == "-c" && "$7" == "echo ok" ]]; then + printf 'ok\n' + return 0 + fi - printf 'Unexpected adb invocation:' >&2 - printf ' %q' "$@" >&2 - printf '\n' >&2 - return 1 + printf 'Unexpected adb invocation:' >&2 + printf ' %q' "$@" >&2 + printf '\n' >&2 + return 1 } run_test() { - local name="$1" - shift + local name="$1" + shift - if "$@"; then - _t_pass "${name}" - else - _t_fail "${name}" - fi + if "$@"; then + _t_pass "${name}" + else + _t_fail "${name}" + fi } test_box_does_not_crash() { - _box "Test title" "line 1" "line 2" >/dev/null 2>&1 + _box "Test title" "line 1" "line 2" >/dev/null 2>&1 } test_check_cooldown_zero_allows() { - LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-zero" - adb_check_cooldown 0 "test_marker" + LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-zero" + adb_check_cooldown 0 "test_marker" } test_check_cooldown_blocks_when_marker_fresh() { - LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-block" - mkdir -p "${LAST_RUN_DIR}" - date +%s >"${LAST_RUN_DIR}/fresh_marker" + LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-block" + mkdir -p "${LAST_RUN_DIR}" + date +%s >"${LAST_RUN_DIR}/fresh_marker" - if adb_check_cooldown 9999 "fresh_marker"; then - return 1 - fi + if adb_check_cooldown 9999 "fresh_marker"; then + return 1 + fi - return 0 + return 0 } test_mark_last_run_creates_marker() { - LAST_RUN_DIR="${TEST_TMPDIR}/mark-last-run" - adb_mark_last_run "run_test" - [[ -f "${LAST_RUN_DIR}/run_test" ]] + LAST_RUN_DIR="${TEST_TMPDIR}/mark-last-run" + adb_mark_last_run "run_test" + [[ -f "${LAST_RUN_DIR}/run_test" ]] } test_acquire_lock_creates_lock_directory() { - LOCK_DIR="${TEST_TMPDIR}/lock-dir-create" - LOCK_FILE="" - adb_acquire_lock >/dev/null 2>&1 - [[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]] - _adb_release_lock + LOCK_DIR="${TEST_TMPDIR}/lock-dir-create" + LOCK_FILE="" + adb_acquire_lock >/dev/null 2>&1 + [[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]] + _adb_release_lock } test_acquire_lock_removes_stale_lock_directory() { - LOCK_DIR="${TEST_TMPDIR}/lock-dir-stale" - LOCK_FILE="${LOCK_DIR}/run_phone.lock" - mkdir -p "${LOCK_FILE}" - printf '999999\n' >"${LOCK_FILE}/pid" + LOCK_DIR="${TEST_TMPDIR}/lock-dir-stale" + LOCK_FILE="${LOCK_DIR}/run_phone.lock" + mkdir -p "${LOCK_FILE}" + printf '999999\n' >"${LOCK_FILE}/pid" - adb_acquire_lock >/dev/null 2>&1 - [[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]] - _adb_release_lock + adb_acquire_lock >/dev/null 2>&1 + [[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]] + _adb_release_lock } test_sanitize_device_string_removes_dangerous_chars() { - local sanitized - sanitized="$(_sanitize_device_string $'abc DEF-._:/$`";\n')" + local sanitized + sanitized="$(_sanitize_device_string $'abc DEF-._:/$`";\n')" - [[ "${sanitized}" == 'abc DEF-._:/' ]] + [[ "${sanitized}" == 'abc DEF-._:/' ]] } test_save_trusted_device_sanitizes_and_quotes() { - local trusted_contents + local trusted_contents - export ADB_SERIAL='SERIAL123$() ;' - adb_save_trusted_device + # Literal payload on purpose - see the note on ADB_MOCK_FINGERPRINT. + # shellcheck disable=SC2016 + export ADB_SERIAL='SERIAL123$() ;' + adb_save_trusted_device - [[ -f "${TRUSTED_DEVICE_FILE}" ]] || return 1 - trusted_contents="$(cat "${TRUSTED_DEVICE_FILE}")" + [[ -f "${TRUSTED_DEVICE_FILE}" ]] || return 1 + trusted_contents="$(cat "${TRUSTED_DEVICE_FILE}")" - [[ "${trusted_contents}" != *'$('* ]] - [[ "${trusted_contents}" != *';'* ]] - [[ "${trusted_contents}" != *'`'* ]] + # Asserting the sanitiser stripped these metacharacters, so the patterns + # must stay literal rather than expand. + # shellcheck disable=SC2016 + [[ "${trusted_contents}" != *'$('* ]] + [[ "${trusted_contents}" != *';'* ]] + [[ "${trusted_contents}" != *'`'* ]] - local -a trusted_values=() - mapfile -t trusted_values < <( - bash -c ' + local -a trusted_values=() + mapfile -t trusted_values < <( + bash -c ' set -euo pipefail source "$1" printf "%s\n" "${TRUSTED_SERIAL:-}" "${TRUSTED_MODEL:-}" "${TRUSTED_FINGERPRINT:-}" ' bash "${TRUSTED_DEVICE_FILE}" - ) + ) - [[ "${trusted_values[0]:-}" == 'SERIAL123 ' ]] - [[ "${trusted_values[1]:-}" == 'Pixel 7rm -rf /dangerline2' ]] - [[ "${trusted_values[2]:-}" == 'google/pixel:14/UP1A.231005.007/evilcmd' ]] + [[ "${trusted_values[0]:-}" == 'SERIAL123 ' ]] + [[ "${trusted_values[1]:-}" == 'Pixel 7rm -rf /dangerline2' ]] + [[ "${trusted_values[2]:-}" == 'google/pixel:14/UP1A.231005.007/evilcmd' ]] } test_verify_root_uses_root_shell() { - export ADB_SERIAL='SERIAL123' - adb_verify_root >/dev/null 2>&1 + export ADB_SERIAL='SERIAL123' + adb_verify_root >/dev/null 2>&1 } test_select_device_rejects_multiple_devices_even_with_trusted_record() { - unset ADB_SERIAL + unset ADB_SERIAL - adb_list_serials() { - printf 'SERIAL123\nSERIAL999\n' - } + adb_list_serials() { + printf 'SERIAL123\nSERIAL999\n' + } - cat >"${TRUSTED_DEVICE_FILE}" <<'EOF' + cat >"${TRUSTED_DEVICE_FILE}" <<'EOF' TRUSTED_SERIAL='SERIAL123' TRUSTED_MODEL='Pixel 7rm -rf /dangerline2' TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd' EOF - if (adb_select_device >/dev/null 2>&1); then - return 1 - fi + if (adb_select_device >/dev/null 2>&1); then + return 1 + fi - return 0 + return 0 } test_verify_trusted_identity_accepts_exact_match() { - export ADB_SERIAL='SERIAL123' - adb_save_trusted_device - adb_verify_trusted_identity >/dev/null 2>&1 + export ADB_SERIAL='SERIAL123' + adb_save_trusted_device + adb_verify_trusted_identity >/dev/null 2>&1 } test_verify_trusted_identity_rejects_model_mismatch() { - export ADB_SERIAL='SERIAL123' - adb_save_trusted_device + export ADB_SERIAL='SERIAL123' + adb_save_trusted_device - cat >"${TRUSTED_DEVICE_FILE}" <<'EOF' + cat >"${TRUSTED_DEVICE_FILE}" <<'EOF' TRUSTED_SERIAL='SERIAL123' TRUSTED_MODEL='Different Model' TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd' EOF - if (adb_verify_trusted_identity >/dev/null 2>&1); then - return 1 - fi + if (adb_verify_trusted_identity >/dev/null 2>&1); then + return 1 + fi - return 0 + return 0 } test_verify_trusted_identity_rejects_fingerprint_mismatch() { - export ADB_SERIAL='SERIAL123' - adb_save_trusted_device + export ADB_SERIAL='SERIAL123' + adb_save_trusted_device - cat >"${TRUSTED_DEVICE_FILE}" <<'EOF' + cat >"${TRUSTED_DEVICE_FILE}" <<'EOF' TRUSTED_SERIAL='SERIAL123' TRUSTED_MODEL='Pixel 7rm -rf /dangerline2' TRUSTED_FINGERPRINT='different/fingerprint' EOF - if (adb_verify_trusted_identity >/dev/null 2>&1); then - return 1 - fi + if (adb_verify_trusted_identity >/dev/null 2>&1); then + return 1 + fi - return 0 + return 0 } run_test "_box output without crash" test_box_does_not_crash diff --git a/phone_focus_mode/lib/tests/test_tether_enforcer.sh b/phone_focus_mode/lib/tests/test_tether_enforcer.sh index fb355801..c4850b26 100755 --- a/phone_focus_mode/lib/tests/test_tether_enforcer.sh +++ b/phone_focus_mode/lib/tests/test_tether_enforcer.sh @@ -12,11 +12,11 @@ SRC="$SCRIPT_DIR/../../tether_enforcer.sh" RUN="$(mktemp -d)" cleanup() { - if [ -n "${PID:-}" ]; then - kill "$PID" 2>/dev/null || true - wait "$PID" 2>/dev/null || true # let its TERM handler finish before rm - fi - rm -rf "$RUN" + if [ -n "${PID:-}" ]; then + kill "$PID" 2>/dev/null || true + wait "$PID" 2>/dev/null || true # let its TERM handler finish before rm + fi + rm -rf "$RUN" } trap cleanup EXIT mkdir -p "$RUN/bin" "$RUN/state" "$RUN/ipt" "$RUN/settings" "$RUN/calls" "$RUN/app" @@ -24,7 +24,7 @@ STATE="$RUN/state" # --- fake config.sh in the daemon's own dir (it sources "$SCRIPT_DIR/config.sh") --- cp "$SRC" "$RUN/app/tether_enforcer.sh" -cat > "$RUN/app/config.sh" <"$RUN/app/config.sh" < "$RUN/bin/settings" <"$RUN/bin/settings" <> "$RUN/calls/settings.log" @@ -52,7 +52,7 @@ esac exit 0 EOF # iptables/ip6tables: minimal chain-state model so chain_intact converges. -cat > "$RUN/bin/iptables" <"$RUN/bin/iptables" <> "%s/calls/cmd.log"\nexit 0\n' "$RUN" > "$RUN/bin/cmd" -printf '#!/usr/bin/env bash\necho 13\n' > "$RUN/bin/getprop" +printf '#!/usr/bin/env bash\necho "cmd $*" >> "%s/calls/cmd.log"\nexit 0\n' "$RUN" >"$RUN/bin/cmd" +printf '#!/usr/bin/env bash\necho 13\n' >"$RUN/bin/getprop" chmod +x "$RUN/bin/"* export PATH="$RUN/bin:$PATH" -PASS=0; FAIL=0 -ok() { PASS=$((PASS + 1)); printf ' OK: %s\n' "$1"; } -no() { FAIL=$((FAIL + 1)); printf ' FAIL: %s\n' "$1"; } +PASS=0 +FAIL=0 +ok() { + PASS=$((PASS + 1)) + printf ' OK: %s\n' "$1" +} +no() { + FAIL=$((FAIL + 1)) + printf ' FAIL: %s\n' "$1" +} applied() { [ -f "$STATE/tether_applied" ]; } offload() { cat "$RUN/settings/tether_offload_disabled" 2>/dev/null || echo ""; } -v4jump() { [ -f "$RUN/ipt/iptables/jump" ]; } -v6jump() { [ -f "$RUN/ipt/ip6tables/jump" ]; } +v4jump() { [ -f "$RUN/ipt/iptables/jump" ]; } +v6jump() { [ -f "$RUN/ipt/ip6tables/jump" ]; } v4rules() { grep -c '^-A' "$RUN/ipt/iptables/rules" 2>/dev/null || echo 0; } -softap() { grep -c 'stop-softap' "$RUN/calls/cmd.log" 2>/dev/null || echo 0; } +softap() { grep -c 'stop-softap' "$RUN/calls/cmd.log" 2>/dev/null || echo 0; } -sh "$RUN/app/tether_enforcer.sh" & PID=$! +sh "$RUN/app/tether_enforcer.sh" & +PID=$! sleep 2 # [A] Away from home -> block OFF -applied && no "A: applied while away" || ok "A: not applied while away" -[ "$(offload)" = "" ] && ok "A: offload untouched away" || no "A: offload changed away" -v4jump && no "A: v4 jump while away" || ok "A: no v4 jump while away" +if applied; then no "A: applied while away"; else ok "A: not applied while away"; fi +if [ "$(offload)" = "" ]; then ok "A: offload untouched away"; else no "A: offload changed away"; fi +if v4jump; then no "A: v4 jump while away"; else ok "A: no v4 jump while away"; fi # [B] force-on -> block ENGAGES (all three levers) -touch "$STATE/tether_force_on"; sleep 3 -applied && ok "B: applied on force" || no "B: not applied on force" -[ "$(offload)" = "1" ] && ok "B: offload disabled" || no "B: offload=$(offload)" -v4jump && ok "B: v4 FORWARD jump pinned" || no "B: no v4 jump" -v6jump && ok "B: v6 FORWARD jump pinned" || no "B: no v6 jump" -[ "$(v4rules)" = "1" ] && ok "B: single REJECT rule (no rebuild loop)" || no "B: v4 rules=$(v4rules)" -[ "$(softap)" -ge 1 ] && ok "B: stop-softap invoked" || no "B: softap not stopped" -[ "$(cat "$STATE/tether_offload.snap" 2>/dev/null)" = "null" ] && ok "B: offload snapshot captured" || no "B: bad snapshot" +touch "$STATE/tether_force_on" +sleep 3 +if applied; then ok "B: applied on force"; else no "B: not applied on force"; fi +if [ "$(offload)" = "1" ]; then ok "B: offload disabled"; else no "B: offload=$(offload)"; fi +if v4jump; then ok "B: v4 FORWARD jump pinned"; else no "B: no v4 jump"; fi +if v6jump; then ok "B: v6 FORWARD jump pinned"; else no "B: no v6 jump"; fi +if [ "$(v4rules)" = "1" ]; then ok "B: single REJECT rule (no rebuild loop)"; else no "B: v4 rules=$(v4rules)"; fi +if [ "$(softap)" -ge 1 ]; then ok "B: stop-softap invoked"; else no "B: softap not stopped"; fi +if [ "$(cat "$STATE/tether_offload.snap" 2>/dev/null)" = "null" ]; then ok "B: offload snapshot captured"; else no "B: bad snapshot"; fi # [C] clear force -> block REVERTS -rm -f "$STATE/tether_force_on"; sleep 3 -applied && no "C: still applied" || ok "C: reverted on force-clear" -[ "$(offload)" = "" ] && ok "C: offload restored" || no "C: offload=$(offload)" -v4jump && no "C: v4 jump remains" || ok "C: v4 chain torn down" +rm -f "$STATE/tether_force_on" +sleep 3 +if applied; then no "C: still applied"; else ok "C: reverted on force-clear"; fi +if [ "$(offload)" = "" ]; then ok "C: offload restored"; else no "C: offload=$(offload)"; fi +if v4jump; then no "C: v4 jump remains"; else ok "C: v4 chain torn down"; fi # [D] current_mode=focus -> ENGAGES via home gate -echo focus > "$STATE/current_mode.txt"; sleep 3 -applied && ok "D: applied at home (focus)" || no "D: not applied at home" -v4jump && ok "D: v4 jump pinned at home" || no "D: no v4 jump at home" +echo focus >"$STATE/current_mode.txt" +sleep 3 +if applied; then ok "D: applied at home (focus)"; else no "D: not applied at home"; fi +if v4jump; then ok "D: v4 jump pinned at home"; else no "D: no v4 jump at home"; fi # [E] override -> SUSPENDS even at home -touch "$STATE/tether_override"; sleep 3 -applied && no "E: applied despite override" || ok "E: suspended by override" -v4jump && no "E: v4 jump despite override" || ok "E: torn down by override" +touch "$STATE/tether_override" +sleep 3 +if applied; then no "E: applied despite override"; else ok "E: suspended by override"; fi +if v4jump; then no "E: v4 jump despite override"; else ok "E: torn down by override"; fi printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ]