mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Fail the build on any shellcheck finding; fix the 222 the old gate hid
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
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) <noreply@anthropic.com>
This commit is contained in:
parent
79829ec8a2
commit
b83ce5d3c7
@ -412,13 +412,25 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
name: 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
|
language: system
|
||||||
types: [shell]
|
types: [shell]
|
||||||
# shellcheck only understands sh/bash/dash/ksh; it cannot parse zsh
|
# shellcheck only understands sh/bash/dash/ksh; it cannot parse zsh
|
||||||
# syntax (e.g. extended globs like (#s)). zsh files are checked by the
|
# syntax (e.g. extended globs like (#s)). zsh files are checked by the
|
||||||
# zsh-syntax hook below instead.
|
# zsh-syntax hook below instead.
|
||||||
exclude_types: [zsh]
|
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)
|
# ZSH SYNTAX - zsh -n parse check (shellcheck has no zsh mode)
|
||||||
|
|||||||
20
.shellcheckrc
Normal file
20
.shellcheckrc
Normal file
@ -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=<repo path>` 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
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -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 <planted info-level canary>",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -116,8 +116,11 @@ parse_interactive_args() {
|
|||||||
|
|
||||||
# Handle common argument patterns for scripts with custom usage functions
|
# Handle common argument patterns for scripts with custom usage functions
|
||||||
# Usage: handle_arg_help_or_unknown "$1" usage_function err_function
|
# 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
|
# 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)
|
# 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() {
|
handle_arg_help_or_unknown() {
|
||||||
local arg="$1"
|
local arg="$1"
|
||||||
local usage_fn="${2:-usage}"
|
local usage_fn="${2:-usage}"
|
||||||
@ -137,7 +140,6 @@ handle_arg_help_or_unknown() {
|
|||||||
return 1 # Not a flag, let caller handle it
|
return 1 # Not a flag, let caller handle it
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize a setup script with common boilerplate
|
# 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
|
if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then
|
||||||
local regex wid
|
local regex wid
|
||||||
printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}"
|
printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}"
|
||||||
regex="${regex%|}" # strip trailing |
|
regex="${regex%|}" # strip trailing |
|
||||||
while IFS= read -r wid; do
|
while IFS= read -r wid; do
|
||||||
[[ -n $wid ]] || continue
|
[[ -n $wid ]] || continue
|
||||||
echo "focus app"
|
echo "focus app"
|
||||||
@ -199,7 +201,7 @@ is_focus_app_running() {
|
|||||||
for app in "${FOCUS_APPS_PROCESSES[@]}"; do
|
for app in "${FOCUS_APPS_PROCESSES[@]}"; do
|
||||||
for comm in /proc/[0-9]*/comm; do
|
for comm in /proc/[0-9]*/comm; do
|
||||||
[[ -r $comm ]] || continue
|
[[ -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
|
if [[ $_proc_comm == *"$app"* ]]; then
|
||||||
echo "$_proc_comm"
|
echo "$_proc_comm"
|
||||||
return 0
|
return 0
|
||||||
@ -450,7 +452,7 @@ get_second() {
|
|||||||
# Get Unix timestamp from boot (uptime in seconds)
|
# Get Unix timestamp from boot (uptime in seconds)
|
||||||
# Usage: boot_seconds=$(get_uptime_seconds)
|
# Usage: boot_seconds=$(get_uptime_seconds)
|
||||||
get_uptime_seconds() {
|
get_uptime_seconds() {
|
||||||
read -r uptime_with_fraction _ < /proc/uptime
|
read -r uptime_with_fraction _ </proc/uptime
|
||||||
printf '%.*f\n' 0 "$uptime_with_fraction"
|
printf '%.*f\n' 0 "$uptime_with_fraction"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,12 +18,12 @@ DRY_RUN=0
|
|||||||
STATUS_ONLY=0
|
STATUS_ONLY=0
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
RED='\033[0;31m'
|
RED=$'\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN=$'\033[0;32m'
|
||||||
YELLOW='\033[0;33m'
|
YELLOW=$'\033[0;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE=$'\033[0;34m'
|
||||||
CYAN='\033[0;36m'
|
CYAN=$'\033[0;36m'
|
||||||
NC='\033[0m' # No Color
|
NC=$'\033[0m' # No Color
|
||||||
|
|
||||||
# Get script and config directories
|
# Get script and config directories
|
||||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
@ -1320,17 +1320,17 @@ main
|
|||||||
# it hid dead hosts self-repair for five months. Make it impossible to miss.
|
# it hid dead hosts self-repair for five months. Make it impossible to miss.
|
||||||
######################################################################
|
######################################################################
|
||||||
if [[ ${#MISSING_SCRIPTS[@]} -gt 0 ]]; then
|
if [[ ${#MISSING_SCRIPTS[@]} -gt 0 ]]; then
|
||||||
printf "\n${RED}"
|
printf '\n%s' "$RED"
|
||||||
printf '=%.0s' {1..74}
|
printf '=%.0s' {1..74}
|
||||||
printf "\n SELF-REPAIR IS BROKEN — %d repair script(s) MISSING\n" "${#MISSING_SCRIPTS[@]}"
|
printf "\n SELF-REPAIR IS BROKEN — %d repair script(s) MISSING\n" "${#MISSING_SCRIPTS[@]}"
|
||||||
printf '=%.0s' {1..74}
|
printf '=%.0s' {1..74}
|
||||||
printf "${NC}\n"
|
printf '%s\n' "$NC"
|
||||||
for missing in "${MISSING_SCRIPTS[@]}"; do
|
for missing in "${MISSING_SCRIPTS[@]}"; do
|
||||||
err "$missing"
|
err "$missing"
|
||||||
done
|
done
|
||||||
printf "${RED}The services above are NOT being repaired — this tool found the\n"
|
printf '%sThe services above are NOT being repaired — this tool found the\n' "$RED"
|
||||||
printf "problem and then could not run the fix. Correct these paths first.${NC}\n"
|
printf 'problem and then could not run the fix. Correct these paths first.%s\n' "$NC"
|
||||||
printf "${CYAN}Also logged at error priority: journalctl -p err -t check-and-enable-services${NC}\n\n"
|
printf '%sAlso logged at error priority: journalctl -p err -t check-and-enable-services%s\n\n' "$CYAN" "$NC"
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -12,19 +12,22 @@ set -euo pipefail
|
|||||||
# Source common library for shared functions
|
# Source common library for shared functions
|
||||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
if [[ -f "$SCRIPT_DIR/../../lib/common.sh" ]]; then
|
if [[ -f "$SCRIPT_DIR/../../lib/common.sh" ]]; then
|
||||||
# shellcheck source=../../lib/common.sh
|
# shellcheck source=../../lib/common.sh
|
||||||
source "$SCRIPT_DIR/../../lib/common.sh"
|
source "$SCRIPT_DIR/../../lib/common.sh"
|
||||||
elif [[ -f "/usr/local/lib/common.sh" ]]; then
|
elif [[ -f "/usr/local/lib/common.sh" ]]; then
|
||||||
# shellcheck source=/usr/local/lib/common.sh
|
# shellcheck source=/usr/local/lib/common.sh
|
||||||
source "/usr/local/lib/common.sh"
|
# Deployed to /usr/local/lib at install time; no in-repo copy exists at that
|
||||||
|
# absolute path for the linter to follow.
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "/usr/local/lib/common.sh"
|
||||||
else
|
else
|
||||||
echo "ERROR: common.sh library not found"
|
echo "ERROR: common.sh library not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/music-parallelism"
|
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/music-parallelism"
|
||||||
mkdir -p "$LOG_DIR" 2> /dev/null || true
|
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
||||||
export LOG_FILE="$LOG_DIR/music-parallelism.log"
|
export LOG_FILE="$LOG_DIR/music-parallelism.log"
|
||||||
CHECK_INTERVAL=15
|
CHECK_INTERVAL=15
|
||||||
FAST_CHECK_INTERVAL=5
|
FAST_CHECK_INTERVAL=5
|
||||||
@ -33,60 +36,60 @@ ENFORCEMENT_COOLDOWN=20
|
|||||||
PROC_ROOT="${PROC_ROOT:-/proc}"
|
PROC_ROOT="${PROC_ROOT:-/proc}"
|
||||||
|
|
||||||
MUSIC_PROCESS_NAMES=(
|
MUSIC_PROCESS_NAMES=(
|
||||||
"youtube-music"
|
"youtube-music"
|
||||||
"spotify"
|
"spotify"
|
||||||
"tidal"
|
"tidal"
|
||||||
"deezer"
|
"deezer"
|
||||||
"amazon music"
|
"amazon music"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override focus apps with extended list for this script
|
# Override focus apps with extended list for this script
|
||||||
FOCUS_APPS_WINDOWS=(
|
FOCUS_APPS_WINDOWS=(
|
||||||
# IDEs and code editors - match window titles
|
# IDEs and code editors - match window titles
|
||||||
"Visual Studio Code"
|
"Visual Studio Code"
|
||||||
"VSCodium"
|
"VSCodium"
|
||||||
"Cursor"
|
"Cursor"
|
||||||
"IntelliJ IDEA"
|
"IntelliJ IDEA"
|
||||||
"PyCharm"
|
"PyCharm"
|
||||||
"WebStorm"
|
"WebStorm"
|
||||||
"CLion"
|
"CLion"
|
||||||
"Rider"
|
"Rider"
|
||||||
"Sublime Text"
|
"Sublime Text"
|
||||||
"Atom"
|
"Atom"
|
||||||
"Neovide"
|
"Neovide"
|
||||||
# Gaming
|
# Gaming
|
||||||
"Steam"
|
"Steam"
|
||||||
# Creative apps
|
# Creative apps
|
||||||
"Blender"
|
"Blender"
|
||||||
"Godot"
|
"Godot"
|
||||||
"Unity"
|
"Unity"
|
||||||
"Unreal Editor"
|
"Unreal Editor"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Music streaming services - browser tabs or electron apps
|
# Music streaming services - browser tabs or electron apps
|
||||||
# These will be killed when focus apps are detected
|
# These will be killed when focus apps are detected
|
||||||
MUSIC_SERVICES=(
|
MUSIC_SERVICES=(
|
||||||
# YouTube Music specific patterns (NOT regular YouTube)
|
# YouTube Music specific patterns (NOT regular YouTube)
|
||||||
"music.youtube.com"
|
"music.youtube.com"
|
||||||
"youtube-music" # Electron app
|
"youtube-music" # Electron app
|
||||||
"YouTube Music" # Window title
|
"YouTube Music" # Window title
|
||||||
# Spotify
|
# Spotify
|
||||||
"spotify"
|
"spotify"
|
||||||
"Spotify"
|
"Spotify"
|
||||||
# Tidal
|
# Tidal
|
||||||
"tidal"
|
"tidal"
|
||||||
"TIDAL"
|
"TIDAL"
|
||||||
# Deezer
|
# Deezer
|
||||||
"deezer"
|
"deezer"
|
||||||
# Amazon Music
|
# Amazon Music
|
||||||
"Amazon Music"
|
"Amazon Music"
|
||||||
"amazon music"
|
"amazon music"
|
||||||
# Apple Music (web)
|
# Apple Music (web)
|
||||||
"music.apple.com"
|
"music.apple.com"
|
||||||
# SoundCloud
|
# SoundCloud
|
||||||
"soundcloud.com"
|
"soundcloud.com"
|
||||||
# Pandora
|
# Pandora
|
||||||
"pandora.com"
|
"pandora.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly MUSIC_WINDOWS_PATTERN='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.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
|
MUSIC_FOUND_WINDOW=0
|
||||||
|
|
||||||
wait_seconds() {
|
wait_seconds() {
|
||||||
local timeout_s=$1
|
local timeout_s=$1
|
||||||
local start_ts end_ts elapsed_s remaining_s
|
local start_ts end_ts elapsed_s remaining_s
|
||||||
|
|
||||||
if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then
|
if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then
|
||||||
printf '%s\n' "$timeout_s" >> "$MUSIC_PARALLELISM_TEST_WAIT_LOG"
|
printf '%s\n' "$timeout_s" >>"$MUSIC_PARALLELISM_TEST_WAIT_LOG"
|
||||||
if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then
|
if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then
|
||||||
exit 99
|
exit 99
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf -v start_ts '%(%s)T' -1
|
printf -v start_ts '%(%s)T' -1
|
||||||
IFS= read -r -t "$timeout_s" || true
|
IFS= read -r -t "$timeout_s" || true
|
||||||
printf -v end_ts '%(%s)T' -1
|
printf -v end_ts '%(%s)T' -1
|
||||||
|
|
||||||
elapsed_s=$((end_ts - start_ts))
|
elapsed_s=$((end_ts - start_ts))
|
||||||
if (( elapsed_s < timeout_s )); then
|
if ((elapsed_s < timeout_s)); then
|
||||||
remaining_s=$((timeout_s - elapsed_s))
|
remaining_s=$((timeout_s - elapsed_s))
|
||||||
sleep "$remaining_s"
|
sleep "$remaining_s"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
contains_music_process() {
|
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
|
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||||
[[ -r $comm_file ]] || continue
|
[[ -r $comm_file ]] || continue
|
||||||
read -r comm_lower < "$comm_file" || continue
|
read -r comm_lower <"$comm_file" || continue
|
||||||
comm_lower=${comm_lower,,}
|
comm_lower=${comm_lower,,}
|
||||||
|
|
||||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls)
|
# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls)
|
||||||
find_music_services() {
|
find_music_services() {
|
||||||
local found_services=()
|
local found_services=()
|
||||||
MUSIC_FOUND_PROCESS=0
|
MUSIC_FOUND_PROCESS=0
|
||||||
MUSIC_FOUND_WINDOW=0
|
MUSIC_FOUND_WINDOW=0
|
||||||
|
|
||||||
# Check processes using /proc (fork-free)
|
# Check processes using /proc (fork-free)
|
||||||
if contains_music_process; then
|
if contains_music_process; then
|
||||||
MUSIC_FOUND_PROCESS=1
|
MUSIC_FOUND_PROCESS=1
|
||||||
found_services+=("music process")
|
found_services+=("music process")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check windows (use optimized is_focus_app_running logic: single xdotool regex call)
|
# 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 command -v xdotool &>/dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then
|
||||||
if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &> /dev/null 2>&1; then
|
if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &>/dev/null 2>&1; then
|
||||||
MUSIC_FOUND_WINDOW=1
|
MUSIC_FOUND_WINDOW=1
|
||||||
found_services+=("music service (window)")
|
found_services+=("music service (window)")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${#found_services[@]} -gt 0 ]]; then
|
if [[ ${#found_services[@]} -gt 0 ]]; then
|
||||||
printf '%s\n' "${found_services[@]}"
|
printf '%s\n' "${found_services[@]}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Kill music services
|
# Kill music services
|
||||||
kill_music_services() {
|
kill_music_services() {
|
||||||
local use_cached_detection="${1:-0}"
|
local use_cached_detection="${1:-0}"
|
||||||
local killed=false
|
local killed=false
|
||||||
local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com'
|
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_windows=1
|
||||||
local should_check_processes=1
|
local should_check_processes=1
|
||||||
|
|
||||||
if [[ $use_cached_detection -eq 1 ]]; then
|
if [[ $use_cached_detection -eq 1 ]]; then
|
||||||
should_check_windows=$MUSIC_FOUND_WINDOW
|
should_check_windows=$MUSIC_FOUND_WINDOW
|
||||||
should_check_processes=$MUSIC_FOUND_PROCESS
|
should_check_processes=$MUSIC_FOUND_PROCESS
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Close browser tabs for web-based music services via one xdotool search
|
# Close browser tabs for web-based music services via one xdotool search
|
||||||
if [[ $should_check_windows -eq 1 ]] && command -v xdotool &> /dev/null; then
|
if [[ $should_check_windows -eq 1 ]] && command -v xdotool &>/dev/null; then
|
||||||
local windows wid
|
local windows wid
|
||||||
windows=$(xdotool search --name "$window_pattern" 2> /dev/null || true)
|
windows=$(xdotool search --name "$window_pattern" 2>/dev/null || true)
|
||||||
for wid in $windows; do
|
for wid in $windows; do
|
||||||
[[ -n $wid ]] || continue
|
[[ -n $wid ]] || continue
|
||||||
xdotool windowclose "$wid" 2> /dev/null || true
|
xdotool windowclose "$wid" 2>/dev/null || true
|
||||||
killed=true
|
killed=true
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Kill app processes with /proc scan + builtin kill (fork-free in hot path)
|
# Kill app processes with /proc scan + builtin kill (fork-free in hot path)
|
||||||
if [[ $should_check_processes -eq 1 ]]; then
|
if [[ $should_check_processes -eq 1 ]]; then
|
||||||
local comm_file pid comm_lower token_lower
|
local comm_file pid comm_lower token_lower
|
||||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||||
[[ -r $comm_file ]] || continue
|
[[ -r $comm_file ]] || continue
|
||||||
read -r comm_lower < "$comm_file" || continue
|
read -r comm_lower <"$comm_file" || continue
|
||||||
comm_lower=${comm_lower,,}
|
comm_lower=${comm_lower,,}
|
||||||
pid=${comm_file#"$PROC_ROOT"/}
|
pid=${comm_file#"$PROC_ROOT"/}
|
||||||
pid=${pid%%/*}
|
pid=${pid%%/*}
|
||||||
|
|
||||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||||
if [[ $PROC_ROOT != "/proc" ]]; then
|
if [[ $PROC_ROOT != "/proc" ]]; then
|
||||||
# Test mode (fake proc tree): mark as killed without signaling host PIDs.
|
# Test mode (fake proc tree): mark as killed without signaling host PIDs.
|
||||||
killed=true
|
killed=true
|
||||||
elif kill -9 "$pid" 2> /dev/null; then
|
elif kill -9 "$pid" 2>/dev/null; then
|
||||||
killed=true
|
killed=true
|
||||||
fi
|
fi
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $killed; then
|
if $killed; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send notification to user
|
# Send notification to user
|
||||||
notify_user() {
|
notify_user() {
|
||||||
local focus_app="$1"
|
local focus_app="$1"
|
||||||
local message="Music stopped - focus mode active ($focus_app detected)"
|
local message="Music stopped - focus mode active ($focus_app detected)"
|
||||||
|
|
||||||
# Try to send desktop notification
|
# Try to send desktop notification
|
||||||
if command -v notify-send &> /dev/null; then
|
if command -v notify-send &>/dev/null; then
|
||||||
notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2> /dev/null || true
|
notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_message "$message"
|
log_message "$message"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Instant monitoring loop - uses polling at high frequency ONLY when focus app is detected
|
# 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.
|
# 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
|
# OPTIMIZATION: Single batched pgrep call instead of multiple separate calls
|
||||||
instant_monitor_loop() {
|
instant_monitor_loop() {
|
||||||
local next_enforcement_ts=0
|
local next_enforcement_ts=0
|
||||||
local current_ts=0
|
local current_ts=0
|
||||||
local focus_app=""
|
local focus_app=""
|
||||||
local sleep_interval="$IDLE_CHECK_INTERVAL"
|
local sleep_interval="$IDLE_CHECK_INTERVAL"
|
||||||
|
|
||||||
log_message "=== Music Parallelism INSTANT Monitor Started ==="
|
log_message "=== Music Parallelism INSTANT Monitor Started ==="
|
||||||
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
||||||
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
|
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 "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
|
while true; do
|
||||||
if focus_app=$(is_focus_app_running 2> /dev/null); then
|
if focus_app=$(is_focus_app_running 2>/dev/null); then
|
||||||
current_ts=$(get_timestamp)
|
current_ts=$(get_timestamp)
|
||||||
if (( current_ts >= next_enforcement_ts )); then
|
if ((current_ts >= next_enforcement_ts)); then
|
||||||
if find_music_services > /dev/null 2>&1; then
|
if find_music_services >/dev/null 2>&1; then
|
||||||
if kill_music_services 1; then
|
if kill_music_services 1; then
|
||||||
notify_user "$focus_app"
|
notify_user "$focus_app"
|
||||||
log_message "INSTANT KILL: Music services terminated"
|
log_message "INSTANT KILL: Music services terminated"
|
||||||
sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL"
|
sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
||||||
fi
|
fi
|
||||||
next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN))
|
next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN))
|
||||||
else
|
else
|
||||||
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
next_enforcement_ts=0
|
next_enforcement_ts=0
|
||||||
sleep_interval="$IDLE_CHECK_INTERVAL"
|
sleep_interval="$IDLE_CHECK_INTERVAL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wait_seconds "$sleep_interval"
|
wait_seconds "$sleep_interval"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main monitoring loop
|
# Main monitoring loop
|
||||||
monitor_loop() {
|
monitor_loop() {
|
||||||
log_message "=== Music Parallelism Monitor Started ==="
|
log_message "=== Music Parallelism Monitor Started ==="
|
||||||
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
||||||
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
|
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
|
||||||
log_message "Music services monitored: ${MUSIC_SERVICES[*]}"
|
log_message "Music services monitored: ${MUSIC_SERVICES[*]}"
|
||||||
log_message "Check interval: ${CHECK_INTERVAL}s"
|
log_message "Check interval: ${CHECK_INTERVAL}s"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# Check if a focus app is running
|
# Check if a focus app is running
|
||||||
local focus_app
|
local focus_app
|
||||||
if focus_app=$(is_focus_app_running); then
|
if focus_app=$(is_focus_app_running); then
|
||||||
# Focus app detected, check for music services
|
# Focus app detected, check for music services
|
||||||
local music_services
|
local music_services
|
||||||
if music_services=$(find_music_services); then
|
if music_services=$(find_music_services); then
|
||||||
log_message "Conflict detected: Focus app '$focus_app' running with music services"
|
log_message "Conflict detected: Focus app '$focus_app' running with music services"
|
||||||
log_message "Active music services: $music_services"
|
log_message "Active music services: $music_services"
|
||||||
|
|
||||||
# Kill the music services
|
# Kill the music services
|
||||||
if kill_music_services 1; then
|
if kill_music_services 1; then
|
||||||
notify_user "$focus_app"
|
notify_user "$focus_app"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wait_seconds "$CHECK_INTERVAL"
|
wait_seconds "$CHECK_INTERVAL"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show status
|
# Show status
|
||||||
show_status() {
|
show_status() {
|
||||||
echo "Music Parallelism Monitor Status"
|
echo "Music Parallelism Monitor Status"
|
||||||
echo "================================="
|
echo "================================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "Focus Applications (window-based detection):"
|
echo "Focus Applications (window-based detection):"
|
||||||
local focus_running=false
|
local focus_running=false
|
||||||
|
|
||||||
# Check windows (OPTIMIZED: single xdotool call with combined regex)
|
# Check windows (OPTIMIZED: single xdotool call with combined regex)
|
||||||
if command -v xdotool &> /dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then
|
if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then
|
||||||
local regex
|
local regex
|
||||||
printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}"
|
printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}"
|
||||||
regex="${regex%|}" # strip trailing |
|
regex="${regex%|}" # strip trailing |
|
||||||
if xdotool search --name "$regex" &> /dev/null 2>&1; then
|
if xdotool search --name "$regex" &>/dev/null 2>&1; then
|
||||||
echo " ✓ Focus window detected"
|
echo " ✓ Focus window detected"
|
||||||
focus_running=true
|
focus_running=true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check processes using shared /proc-based helper (fork-free)
|
# Check processes using shared /proc-based helper (fork-free)
|
||||||
if is_focus_app_running > /dev/null 2>&1; then
|
if is_focus_app_running >/dev/null 2>&1; then
|
||||||
echo " ✓ Focus process running"
|
echo " ✓ Focus process running"
|
||||||
focus_running=true
|
focus_running=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! $focus_running; then
|
if ! $focus_running; then
|
||||||
echo " (none detected)"
|
echo " (none detected)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Music Services:"
|
echo "Music Services:"
|
||||||
local music_running=false
|
local music_running=false
|
||||||
if music_services=$(find_music_services 2> /dev/null); then
|
if music_services=$(find_music_services 2>/dev/null); then
|
||||||
echo "$music_services" | while read -r svc; do
|
echo "$music_services" | while read -r svc; do
|
||||||
echo " ♪ $svc (RUNNING)"
|
echo " ♪ $svc (RUNNING)"
|
||||||
done
|
done
|
||||||
music_running=true
|
music_running=true
|
||||||
fi
|
fi
|
||||||
if ! $music_running; then
|
if ! $music_running; then
|
||||||
echo " (none detected)"
|
echo " (none detected)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if $focus_running && $music_running; then
|
if $focus_running && $music_running; then
|
||||||
echo "⚠️ CONFLICT: Focus app and music running together!"
|
echo "⚠️ CONFLICT: Focus app and music running together!"
|
||||||
echo " Music would be killed in monitoring mode."
|
echo " Music would be killed in monitoring mode."
|
||||||
elif $focus_running; then
|
elif $focus_running; then
|
||||||
echo "✓ Focus mode active (no music playing)"
|
echo "✓ Focus mode active (no music playing)"
|
||||||
elif $music_running; then
|
elif $music_running; then
|
||||||
echo "✓ Music playing (no focus app detected - this is fine)"
|
echo "✓ Music playing (no focus app detected - this is fine)"
|
||||||
else
|
else
|
||||||
echo "✓ Idle (nothing detected)"
|
echo "✓ Idle (nothing detected)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show usage
|
# Show usage
|
||||||
show_usage() {
|
show_usage() {
|
||||||
echo "Music Parallelism Prevention Script"
|
echo "Music Parallelism Prevention Script"
|
||||||
echo "===================================="
|
echo "===================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Usage: $0 [command]"
|
echo "Usage: $0 [command]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Commands:"
|
echo "Commands:"
|
||||||
echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)"
|
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 " 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 " status - Show current status of focus apps and music services"
|
||||||
echo " kill - Immediately kill all music services"
|
echo " kill - Immediately kill all music services"
|
||||||
echo " help - Show this help message"
|
echo " help - Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Description:"
|
echo "Description:"
|
||||||
echo " This script prevents multitasking between focus work and music."
|
echo " This script prevents multitasking between focus work and music."
|
||||||
echo " When a focus application (VS Code, Steam, etc.) is detected"
|
echo " When a focus application (VS Code, Steam, etc.) is detected"
|
||||||
echo " alongside a music streaming service, the music is stopped."
|
echo " alongside a music streaming service, the music is stopped."
|
||||||
echo ""
|
echo ""
|
||||||
echo " Music is allowed when no focus apps are running."
|
echo " Music is allowed when no focus apps are running."
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
case "${1:-instant}" in
|
case "${1:-instant}" in
|
||||||
monitor | start | run)
|
monitor | start | run)
|
||||||
monitor_loop
|
monitor_loop
|
||||||
;;
|
;;
|
||||||
instant | fast)
|
instant | fast)
|
||||||
instant_monitor_loop
|
instant_monitor_loop
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
show_status
|
show_status
|
||||||
;;
|
;;
|
||||||
kill)
|
kill)
|
||||||
log_message "Manual kill requested"
|
log_message "Manual kill requested"
|
||||||
if kill_music_services; then
|
if kill_music_services; then
|
||||||
echo "Music services killed"
|
echo "Music services killed"
|
||||||
else
|
else
|
||||||
echo "No music services found to kill"
|
echo "No music services found to kill"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
help | -h | --help)
|
help | -h | --help)
|
||||||
show_usage
|
show_usage
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown command: $1"
|
echo "Unknown command: $1"
|
||||||
show_usage
|
show_usage
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -5,58 +5,58 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
||||||
[[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.'
|
[[ $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"
|
source "$SCRIPT_DIR/persist_common.sh"
|
||||||
|
|
||||||
check_installed() {
|
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() {
|
check_running() {
|
||||||
local proc_file proc_name
|
local proc_file proc_name
|
||||||
for proc_file in /proc/[0-9]*/comm; do
|
for proc_file in /proc/[0-9]*/comm; do
|
||||||
[[ -r $proc_file ]] || continue
|
[[ -r $proc_file ]] || continue
|
||||||
read -r proc_name < "$proc_file" || continue
|
read -r proc_name <"$proc_file" || continue
|
||||||
case $proc_name in
|
case $proc_name in
|
||||||
aw-qt | aw-server)
|
aw-qt | aw-server)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
emit() {
|
emit() {
|
||||||
local state
|
local state
|
||||||
if ! check_installed; then
|
if ! check_installed; then
|
||||||
state='uninstalled'
|
state='uninstalled'
|
||||||
elif check_running; then
|
elif check_running; then
|
||||||
state='on'
|
state='on'
|
||||||
else
|
else
|
||||||
state='off'
|
state='off'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! i3blocks_update_if_changed_key "activitywatch_state" "$state"; then
|
if ! i3blocks_update_if_changed_key "activitywatch_state" "$state"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $state == 'uninstalled' ]]; then
|
if [[ $state == 'uninstalled' ]]; then
|
||||||
echo "AW uninstalled"
|
echo "AW uninstalled"
|
||||||
echo
|
echo
|
||||||
echo "#FF0000"
|
echo "#FF0000"
|
||||||
elif [[ $state == 'on' ]]; then
|
elif [[ $state == 'on' ]]; then
|
||||||
echo "AW on"
|
echo "AW on"
|
||||||
echo
|
echo
|
||||||
echo "#00FF00"
|
echo "#00FF00"
|
||||||
else
|
else
|
||||||
echo "AW off"
|
echo "AW off"
|
||||||
echo
|
echo
|
||||||
echo "#FF0000"
|
echo "#FF0000"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
is_persist_mode() {
|
is_persist_mode() {
|
||||||
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
HEARTBEAT_INTERVAL_S=60
|
HEARTBEAT_INTERVAL_S=60
|
||||||
@ -64,10 +64,10 @@ HEARTBEAT_INTERVAL_S=60
|
|||||||
emit
|
emit
|
||||||
|
|
||||||
if is_persist_mode; then
|
if is_persist_mode; then
|
||||||
# Intentionally calm heartbeat in persist mode: process-table event streams can
|
# Intentionally calm heartbeat in persist mode: process-table event streams can
|
||||||
# be extremely noisy and cause unnecessary churn.
|
# be extremely noisy and cause unnecessary churn.
|
||||||
while true; do
|
while true; do
|
||||||
i3blocks_wait_seconds "$HEARTBEAT_INTERVAL_S"
|
i3blocks_wait_seconds "$HEARTBEAT_INTERVAL_S"
|
||||||
emit
|
emit
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -5,55 +5,55 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
||||||
[[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.'
|
[[ $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"
|
source "$SCRIPT_DIR/persist_common.sh"
|
||||||
|
|
||||||
get_bluetooth_info() {
|
get_bluetooth_info() {
|
||||||
local info
|
local info
|
||||||
info=$(bluetoothctl info 2> /dev/null) || info=''
|
info=$(bluetoothctl info 2>/dev/null) || info=''
|
||||||
printf '%s\n' "$info"
|
printf '%s\n' "$info"
|
||||||
}
|
}
|
||||||
|
|
||||||
emit() {
|
emit() {
|
||||||
local bluetooth_info connected device line state_key
|
local bluetooth_info connected device line state_key
|
||||||
bluetooth_info=$(get_bluetooth_info)
|
bluetooth_info=$(get_bluetooth_info)
|
||||||
|
|
||||||
connected='no'
|
connected='no'
|
||||||
device=''
|
device=''
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
case $line in
|
case $line in
|
||||||
*'Connected: yes')
|
*'Connected: yes')
|
||||||
connected='yes'
|
connected='yes'
|
||||||
;;
|
;;
|
||||||
*'Alias: '*)
|
*'Alias: '*)
|
||||||
device=${line#*Alias: }
|
device=${line#*Alias: }
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done <<< "$bluetooth_info"
|
done <<<"$bluetooth_info"
|
||||||
|
|
||||||
state_key="$connected|$device"
|
state_key="$connected|$device"
|
||||||
if ! i3blocks_update_if_changed_key "bluetooth_state" "$state_key"; then
|
if ! i3blocks_update_if_changed_key "bluetooth_state" "$state_key"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $connected == yes && -n $device ]]; then
|
if [[ $connected == yes && -n $device ]]; then
|
||||||
echo " $device"
|
echo " $device"
|
||||||
echo
|
echo
|
||||||
echo "#50FA7B"
|
echo "#50FA7B"
|
||||||
else
|
else
|
||||||
echo " Disconnected"
|
echo " Disconnected"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
is_persist_mode() {
|
is_persist_mode() {
|
||||||
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_throttled() {
|
emit_throttled() {
|
||||||
if ! i3blocks_should_emit_by_interval_key "bluetooth_emit" "$EMIT_MIN_INTERVAL_S"; then
|
if ! i3blocks_should_emit_by_interval_key "bluetooth_emit" "$EMIT_MIN_INTERVAL_S"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
emit
|
emit
|
||||||
}
|
}
|
||||||
|
|
||||||
EMIT_MIN_INTERVAL_S=2
|
EMIT_MIN_INTERVAL_S=2
|
||||||
@ -61,12 +61,12 @@ EMIT_MIN_INTERVAL_S=2
|
|||||||
emit
|
emit
|
||||||
|
|
||||||
if is_persist_mode; then
|
if is_persist_mode; then
|
||||||
# React to BlueZ D-Bus signals instead of polling.
|
# React to BlueZ D-Bus signals instead of polling.
|
||||||
if command -v dbus-monitor > /dev/null 2>&1; then
|
if command -v dbus-monitor >/dev/null 2>&1; then
|
||||||
dbus-monitor --system "type='signal',sender='org.bluez'" 2> /dev/null |
|
dbus-monitor --system "type='signal',sender='org.bluez'" 2>/dev/null |
|
||||||
while read -r line; do
|
while read -r line; do
|
||||||
[[ $line == *"PropertiesChanged"* ]] || continue
|
[[ $line == *"PropertiesChanged"* ]] || continue
|
||||||
emit_throttled
|
emit_throttled
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -5,62 +5,62 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
||||||
[[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.'
|
[[ $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"
|
source "$SCRIPT_DIR/persist_common.sh"
|
||||||
|
|
||||||
find_ethernet_interface() {
|
find_ethernet_interface() {
|
||||||
local iface_path iface
|
local iface_path iface
|
||||||
for iface_path in /sys/class/net/*; do
|
for iface_path in /sys/class/net/*; do
|
||||||
iface=${iface_path##*/}
|
iface=${iface_path##*/}
|
||||||
[[ $iface == lo ]] && continue
|
[[ $iface == lo ]] && continue
|
||||||
[[ -d ${iface_path}/wireless ]] && continue
|
[[ -d ${iface_path}/wireless ]] && continue
|
||||||
[[ -r ${iface_path}/operstate ]] || continue
|
[[ -r ${iface_path}/operstate ]] || continue
|
||||||
printf '%s\n' "$iface"
|
printf '%s\n' "$iface"
|
||||||
return 0
|
return 0
|
||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
emit() {
|
emit() {
|
||||||
local iface state addr_output output_line
|
local iface state addr_output output_line
|
||||||
iface=$(find_ethernet_interface) || {
|
iface=$(find_ethernet_interface) || {
|
||||||
output_line=' down'
|
output_line=' down'
|
||||||
if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then
|
if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then
|
||||||
printf '%s\n' "$output_line"
|
printf '%s\n' "$output_line"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
read -r state < "/sys/class/net/${iface}/operstate"
|
read -r state <"/sys/class/net/${iface}/operstate"
|
||||||
if [[ $state != up ]]; then
|
if [[ $state != up ]]; then
|
||||||
output_line=' down'
|
output_line=' down'
|
||||||
if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then
|
if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then
|
||||||
printf '%s\n' "$output_line"
|
printf '%s\n' "$output_line"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
addr_output=$(ip -o -4 addr show dev "$iface" scope global 2> /dev/null) || addr_output=''
|
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
|
if [[ $addr_output =~ ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+) ]]; then
|
||||||
output_line=" ${BASH_REMATCH[1]}"
|
output_line=" ${BASH_REMATCH[1]}"
|
||||||
else
|
else
|
||||||
output_line=' down'
|
output_line=' down'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then
|
if i3blocks_update_if_changed_key "ethernet_output" "$output_line"; then
|
||||||
printf '%s\n' "$output_line"
|
printf '%s\n' "$output_line"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
is_persist_mode() {
|
is_persist_mode() {
|
||||||
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_throttled() {
|
emit_throttled() {
|
||||||
if ! i3blocks_should_emit_by_interval_key "ethernet_emit" "$EMIT_MIN_INTERVAL_S"; then
|
if ! i3blocks_should_emit_by_interval_key "ethernet_emit" "$EMIT_MIN_INTERVAL_S"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
emit
|
emit
|
||||||
}
|
}
|
||||||
|
|
||||||
EMIT_MIN_INTERVAL_S=2
|
EMIT_MIN_INTERVAL_S=2
|
||||||
@ -68,9 +68,9 @@ EMIT_MIN_INTERVAL_S=2
|
|||||||
emit
|
emit
|
||||||
|
|
||||||
if is_persist_mode; then
|
if is_persist_mode; then
|
||||||
ip monitor link address route 2> /dev/null |
|
ip monitor link address route 2>/dev/null |
|
||||||
while read -r line; do
|
while read -r line; do
|
||||||
[[ $line == *"eth"* || $line == *"en"* || $line == *"inet "* ]] || continue
|
[[ $line == *"eth"* || $line == *"en"* || $line == *"inet "* ]] || continue
|
||||||
emit_throttled
|
emit_throttled
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -14,81 +14,80 @@ set -u
|
|||||||
|
|
||||||
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
||||||
[[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.'
|
[[ $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"
|
source "$SCRIPT_DIR/persist_common.sh"
|
||||||
|
|
||||||
# Nerd Font glyph: display / desktop icon (U+F108).
|
# Nerd Font glyph: display / desktop icon (U+F108).
|
||||||
ICON=$'\uf108'
|
ICON=$'\uf108'
|
||||||
|
|
||||||
emit() {
|
emit() {
|
||||||
local temp=$1 load=$2 color
|
local temp=$1 load=$2 color
|
||||||
if [[ $load == 'N/A' ]]; then
|
if [[ $load == 'N/A' ]]; then
|
||||||
color='#FFFFFF'
|
color='#FFFFFF'
|
||||||
elif ((load < 50)); then
|
elif ((load < 50)); then
|
||||||
color='#50FA7B'
|
color='#50FA7B'
|
||||||
elif ((load < 75)); then
|
elif ((load < 75)); then
|
||||||
color='#F1FA8C'
|
color='#F1FA8C'
|
||||||
else
|
else
|
||||||
color='#FF5555'
|
color='#FF5555'
|
||||||
fi
|
fi
|
||||||
printf '<span color="%s">%s %s°C, %s%%</span>\n' \
|
printf '<span color="%s">%s %s°C, %s%%</span>\n' \
|
||||||
"$color" "$ICON" "$temp" "$load"
|
"$color" "$ICON" "$temp" "$load"
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_if_changed() {
|
emit_if_changed() {
|
||||||
local temp=$1 load=$2
|
local temp=$1 load=$2
|
||||||
if ! i3blocks_update_if_changed_key "gpu_metric" "$temp|$load"; then
|
if ! i3blocks_update_if_changed_key "gpu_metric" "$temp|$load"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
emit "$temp" "$load"
|
emit "$temp" "$load"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Prefer NVIDIA if present (persist via --loop).
|
# Prefer NVIDIA if present (persist via --loop).
|
||||||
if command -v nvidia-smi > /dev/null 2>&1; then
|
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||||
# One child process for the lifetime of i3blocks; emits CSV every 5s.
|
# One child process for the lifetime of i3blocks; emits CSV every 5s.
|
||||||
nvidia-smi \
|
nvidia-smi \
|
||||||
--query-gpu=temperature.gpu,utilization.gpu \
|
--query-gpu=temperature.gpu,utilization.gpu \
|
||||||
--format=csv,noheader,nounits \
|
--format=csv,noheader,nounits \
|
||||||
--loop=5 2> /dev/null |
|
--loop=5 2>/dev/null |
|
||||||
while IFS=',' read -r temp load; do
|
while IFS=',' read -r temp load; do
|
||||||
# Strip leading/trailing whitespace using parameter expansion.
|
# Strip leading/trailing whitespace using parameter expansion.
|
||||||
temp=${temp## }
|
temp=${temp## }
|
||||||
temp=${temp%% }
|
temp=${temp%% }
|
||||||
load=${load## }
|
load=${load## }
|
||||||
load=${load%% }
|
load=${load%% }
|
||||||
[[ -z $temp || -z $load ]] && continue
|
[[ -z $temp || -z $load ]] && continue
|
||||||
emit_if_changed "$temp" "$load"
|
emit_if_changed "$temp" "$load"
|
||||||
done
|
done
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# AMD fallback: read sysfs directly; emit once (i3blocks restarts on exit).
|
# AMD fallback: read sysfs directly; emit once (i3blocks restarts on exit).
|
||||||
amdgpu=''
|
amdgpu=''
|
||||||
for d in /sys/class/hwmon/hwmon*/; do
|
for d in /sys/class/hwmon/hwmon*/; do
|
||||||
[[ -r ${d}name ]] || continue
|
[[ -r ${d}name ]] || continue
|
||||||
read -r n < "${d}name"
|
read -r n <"${d}name"
|
||||||
[[ $n == amdgpu ]] && {
|
[[ $n == amdgpu ]] && {
|
||||||
amdgpu=$d
|
amdgpu=$d
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
done
|
done
|
||||||
if [[ -n $amdgpu ]]; then
|
if [[ -n $amdgpu ]]; then
|
||||||
temp='N/A'
|
temp='N/A'
|
||||||
if [[ -r ${amdgpu}temp1_input ]]; then
|
if [[ -r ${amdgpu}temp1_input ]]; then
|
||||||
read -r milli < "${amdgpu}temp1_input"
|
read -r milli <"${amdgpu}temp1_input"
|
||||||
temp=$((milli / 1000))
|
temp=$((milli / 1000))
|
||||||
fi
|
fi
|
||||||
load='N/A'
|
load='N/A'
|
||||||
# drm card matching the amdgpu hwmon exposes gpu_busy_percent.
|
# drm card matching the amdgpu hwmon exposes gpu_busy_percent.
|
||||||
for card in /sys/class/drm/card*/device/gpu_busy_percent; do
|
for card in /sys/class/drm/card*/device/gpu_busy_percent; do
|
||||||
[[ -r $card ]] && {
|
[[ -r $card ]] && {
|
||||||
read -r load < "$card"
|
read -r load <"$card"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
done
|
done
|
||||||
emit_if_changed "$temp" "$load"
|
emit_if_changed "$temp" "$load"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '<span color="#FF5555">%s No supported GPU</span>\n' "$ICON"
|
printf '<span color="#FF5555">%s No supported GPU</span>\n' "$ICON"
|
||||||
|
|||||||
@ -5,67 +5,67 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
||||||
[[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.'
|
[[ $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"
|
source "$SCRIPT_DIR/persist_common.sh"
|
||||||
|
|
||||||
if ! command -v warp-cli > /dev/null 2>&1; then
|
if ! command -v warp-cli >/dev/null 2>&1; then
|
||||||
echo " N/A"
|
echo " N/A"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
is_persist_mode() {
|
is_persist_mode() {
|
||||||
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
WARP_POLL_INTERVAL_S=120
|
WARP_POLL_INTERVAL_S=120
|
||||||
|
|
||||||
read_status() {
|
read_status() {
|
||||||
local status line
|
local status line
|
||||||
status=''
|
status=''
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
case $line in
|
case $line in
|
||||||
'Status update: '*)
|
'Status update: '*)
|
||||||
status=${line#Status update: }
|
status=${line#Status update: }
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done < <(warp-cli status 2> /dev/null)
|
done < <(warp-cli status 2>/dev/null)
|
||||||
printf '%s\n' "$status"
|
printf '%s\n' "$status"
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_status() {
|
emit_status() {
|
||||||
local status=$1
|
local status=$1
|
||||||
if [[ $status == Connected ]]; then
|
if [[ $status == Connected ]]; then
|
||||||
echo "🔒 !!! WARP CONNECTED !!!"
|
echo "🔒 !!! WARP CONNECTED !!!"
|
||||||
echo
|
echo
|
||||||
echo "#FFFF00"
|
echo "#FFFF00"
|
||||||
elif [[ $status == Disconnected ]]; then
|
elif [[ $status == Disconnected ]]; then
|
||||||
echo "WARP disconnected"
|
echo "WARP disconnected"
|
||||||
echo
|
echo
|
||||||
echo "#00FF00"
|
echo "#00FF00"
|
||||||
else
|
else
|
||||||
echo "⚠️ ! WARP unknown !"
|
echo "⚠️ ! WARP unknown !"
|
||||||
echo
|
echo
|
||||||
echo "#FF0000"
|
echo "#FF0000"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_if_changed() {
|
emit_if_changed() {
|
||||||
local status=$1
|
local status=$1
|
||||||
if ! i3blocks_update_if_changed_key "warp_status" "$status"; then
|
if ! i3blocks_update_if_changed_key "warp_status" "$status"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
emit_status "$status"
|
emit_status "$status"
|
||||||
}
|
}
|
||||||
|
|
||||||
current_status=$(read_status)
|
current_status=$(read_status)
|
||||||
emit_status "$current_status"
|
emit_status "$current_status"
|
||||||
if is_persist_mode; then
|
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
|
fi
|
||||||
if is_persist_mode; then
|
if is_persist_mode; then
|
||||||
while true; do
|
while true; do
|
||||||
i3blocks_wait_seconds "$WARP_POLL_INTERVAL_S"
|
i3blocks_wait_seconds "$WARP_POLL_INTERVAL_S"
|
||||||
current_status=$(read_status)
|
current_status=$(read_status)
|
||||||
emit_if_changed "$current_status"
|
emit_if_changed "$current_status"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -5,90 +5,90 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
SCRIPT_DIR=${BASH_SOURCE[0]%/*}
|
||||||
[[ $SCRIPT_DIR == "${BASH_SOURCE[0]}" ]] && SCRIPT_DIR='.'
|
[[ $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"
|
source "$SCRIPT_DIR/persist_common.sh"
|
||||||
|
|
||||||
find_wifi_interface() {
|
find_wifi_interface() {
|
||||||
local line
|
local line
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
case $line in
|
case $line in
|
||||||
*'Interface '*)
|
*'Interface '*)
|
||||||
printf '%s\n' "${line##*Interface }"
|
printf '%s\n' "${line##*Interface }"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done < <(iw dev 2> /dev/null)
|
done < <(iw dev 2>/dev/null)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
emit() {
|
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) || {
|
wifi_interface=$(find_wifi_interface) || {
|
||||||
output_line=' down'
|
output_line=' down'
|
||||||
if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then
|
if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then
|
||||||
echo "$output_line"
|
echo "$output_line"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ssid=''
|
ssid=''
|
||||||
signal=''
|
signal=''
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
case $line in
|
case $line in
|
||||||
'SSID: '*)
|
'SSID: '*)
|
||||||
ssid=${line#SSID: }
|
ssid=${line#SSID: }
|
||||||
;;
|
;;
|
||||||
'signal: '*)
|
'signal: '*)
|
||||||
signal=${line#signal: }
|
signal=${line#signal: }
|
||||||
signal=${signal% dBm}
|
signal=${signal% dBm}
|
||||||
;;
|
;;
|
||||||
'Not connected.'*)
|
'Not connected.'*)
|
||||||
ssid=''
|
ssid=''
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done < <(iw dev "$wifi_interface" link 2> /dev/null)
|
done < <(iw dev "$wifi_interface" link 2>/dev/null)
|
||||||
|
|
||||||
if [[ -z $ssid ]]; then
|
if [[ -z $ssid ]]; then
|
||||||
output_line=' down'
|
output_line=' down'
|
||||||
if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then
|
if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then
|
||||||
echo "$output_line"
|
echo "$output_line"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ip_address=''
|
ip_address=''
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
[[ $line == *' inet '* ]] || continue
|
[[ $line == *' inet '* ]] || continue
|
||||||
read -r -a fields <<< "$line"
|
read -r -a fields <<<"$line"
|
||||||
for ((i = 0; i < ${#fields[@]}; i++)); do
|
for ((i = 0; i < ${#fields[@]}; i++)); do
|
||||||
if [[ ${fields[i]} == inet && $((i + 1)) -lt ${#fields[@]} ]]; then
|
if [[ ${fields[i]} == inet && $((i + 1)) -lt ${#fields[@]} ]]; then
|
||||||
ip_address=${fields[i + 1]%%/*}
|
ip_address=${fields[i + 1]%%/*}
|
||||||
break 2
|
break 2
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done < <(ip -o -4 addr show dev "$wifi_interface" scope global 2> /dev/null)
|
done < <(ip -o -4 addr show dev "$wifi_interface" scope global 2>/dev/null)
|
||||||
|
|
||||||
if [[ -n $ip_address ]]; then
|
if [[ -n $ip_address ]]; then
|
||||||
output_line=" $ssid ($signal dBm) $ip_address"
|
output_line=" $ssid ($signal dBm) $ip_address"
|
||||||
else
|
else
|
||||||
output_line=" $ssid ($signal dBm)"
|
output_line=" $ssid ($signal dBm)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then
|
if i3blocks_update_if_changed_key "wifi_output" "$output_line"; then
|
||||||
echo "$output_line"
|
echo "$output_line"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
is_persist_mode() {
|
is_persist_mode() {
|
||||||
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
[[ ${BLOCK_INTERVAL:-} == "persist" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_throttled() {
|
emit_throttled() {
|
||||||
if ! i3blocks_should_emit_by_interval_key "wifi_emit" "$EMIT_MIN_INTERVAL_S"; then
|
if ! i3blocks_should_emit_by_interval_key "wifi_emit" "$EMIT_MIN_INTERVAL_S"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
emit
|
emit
|
||||||
}
|
}
|
||||||
|
|
||||||
EMIT_MIN_INTERVAL_S=2
|
EMIT_MIN_INTERVAL_S=2
|
||||||
@ -96,9 +96,9 @@ EMIT_MIN_INTERVAL_S=2
|
|||||||
emit
|
emit
|
||||||
|
|
||||||
if is_persist_mode; then
|
if is_persist_mode; then
|
||||||
ip monitor link address route 2> /dev/null |
|
ip monitor link address route 2>/dev/null |
|
||||||
while read -r line; do
|
while read -r line; do
|
||||||
[[ $line == *"wlan"* || $line == *"wl"* || $line == *"inet "* ]] || continue
|
[[ $line == *"wlan"* || $line == *"wl"* || $line == *"inet "* ]] || continue
|
||||||
emit_throttled
|
emit_throttled
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -100,7 +100,11 @@ $installed_packages
|
|||||||
package:$package
|
package:$package
|
||||||
"*)
|
"*)
|
||||||
log "Blocked app detected: $package - Uninstalling..."
|
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
|
esac
|
||||||
done <"$BLOCKED_APPS_FILE"
|
done <"$BLOCKED_APPS_FILE"
|
||||||
|
|||||||
@ -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).
|
# Optional DHCP-server mode (for routers that cannot advertise a custom DNS).
|
||||||
readonly DHCP_CONF="/etc/dnsmasq.d/lan-dhcp.conf"
|
readonly DHCP_CONF="/etc/dnsmasq.d/lan-dhcp.conf"
|
||||||
readonly DHCP_LEASE="12h"
|
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"
|
readonly DHCP_END_HOST="150"
|
||||||
# Feed generator (chattr +i but still executable) and firewall owner script.
|
# Feed generator (chattr +i but still executable) and firewall owner script.
|
||||||
readonly GEN="${SCRIPT_DIR}/../../periodic_background/hosts/generate_hosts_file.sh"
|
readonly GEN="${SCRIPT_DIR}/../../periodic_background/hosts/generate_hosts_file.sh"
|
||||||
@ -442,10 +442,21 @@ cmd_status() {
|
|||||||
detect_lan
|
detect_lan
|
||||||
echo "=== LAN DNS blocker status ==="
|
echo "=== LAN DNS blocker status ==="
|
||||||
|
|
||||||
is_service_active dnsmasq && status_line 0 "dnsmasq: active" || status_line 1 "dnsmasq: NOT active"
|
if is_service_active dnsmasq; then
|
||||||
is_service_enabled dnsmasq && status_line 0 "dnsmasq: enabled at boot" || status_line 1 "dnsmasq: NOT enabled"
|
status_line 0 "dnsmasq: active"
|
||||||
[[ -f $DNSMASQ_DROPIN ]] && status_line 0 "Restart=always drop-in present" || status_line 1 "Restart drop-in missing"
|
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
|
if [[ -f $FEED ]]; then
|
||||||
status_line 0 "blocklist feed: $(wc -l <"$FEED") lines, updated $(date -r "$FEED" '+%Y-%m-%d %H:%M')"
|
status_line 0 "blocklist feed: $(wc -l <"$FEED") lines, updated $(date -r "$FEED" '+%Y-%m-%d %H:%M')"
|
||||||
else
|
else
|
||||||
@ -472,10 +483,11 @@ cmd_status() {
|
|||||||
status_line 0 "firewall: no default-drop ruleset loaded (DNS/DHCP not filtered)"
|
status_line 0 "firewall: no default-drop ruleset loaded (DNS/DHCP not filtered)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
systemctl is-active dns-blocklist-refresh.timer &>/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))" ||
|
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"
|
status_line 1 "refresh timer: NOT active"
|
||||||
|
fi
|
||||||
if [[ -f $DHCP_CONF ]]; then
|
if [[ -f $DHCP_CONF ]]; then
|
||||||
if ss -ulnp 2>/dev/null | grep -q ':67 '; 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)"
|
status_line 0 "DHCP mode: ON (serving leases; PC is the LAN DHCP server)"
|
||||||
@ -492,12 +504,16 @@ cmd_status() {
|
|||||||
local blocked passthru
|
local blocked passthru
|
||||||
blocked="$(dig +short +time=2 +tries=1 @"${LAN_IP}" youtube.com 2>/dev/null | head -1)"
|
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)"
|
passthru="$(dig +short +time=2 +tries=1 @"${LAN_IP}" example.com 2>/dev/null | head -1)"
|
||||||
[[ $blocked == "0.0.0.0" ]] &&
|
if [[ $blocked == "0.0.0.0" ]]; then
|
||||||
status_line 0 "youtube.com -> ${blocked} (BLOCKED, correct)" ||
|
status_line 0 "youtube.com -> ${blocked} (BLOCKED, correct)"
|
||||||
|
else
|
||||||
status_line 1 "youtube.com -> ${blocked:-<no answer>} (expected 0.0.0.0)"
|
status_line 1 "youtube.com -> ${blocked:-<no answer>} (expected 0.0.0.0)"
|
||||||
[[ -n $passthru && $passthru != "0.0.0.0" ]] &&
|
fi
|
||||||
status_line 0 "example.com -> ${passthru} (passthrough, correct)" ||
|
if [[ -n $passthru && $passthru != "0.0.0.0" ]]; then
|
||||||
|
status_line 0 "example.com -> ${passthru} (passthrough, correct)"
|
||||||
|
else
|
||||||
status_line 1 "example.com -> ${passthru:-<no answer>} (expected a real IP)"
|
status_line 1 "example.com -> ${passthru:-<no answer>} (expected a real IP)"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
log_warn "install 'dig' (bind) to run the live resolution test."
|
log_warn "install 'dig' (bind) to run the live resolution test."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -67,9 +67,12 @@ die() {
|
|||||||
exit 1
|
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() {
|
usage() {
|
||||||
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
||||||
exit 0
|
exit "${1:-0}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Phase 1: preflight -----------------------------------------------------
|
# --- Phase 1: preflight -----------------------------------------------------
|
||||||
@ -394,13 +397,21 @@ check_http() {
|
|||||||
status_cmd() {
|
status_cmd() {
|
||||||
print_setup_header "KCD2 dice solver status"
|
print_setup_header "KCD2 dice solver status"
|
||||||
|
|
||||||
has_cmd node && status_line 0 "node present" || status_line 1 "node missing"
|
if has_cmd node; then
|
||||||
has_cmd pnpm && status_line 0 "pnpm present" || status_line 1 "pnpm missing"
|
status_line 0 "node present"
|
||||||
|
else
|
||||||
[[ -f "${DICE_SRC}/dist/index.html" ]] &&
|
status_line 1 "node missing"
|
||||||
status_line 0 "build present (dist/index.html)" ||
|
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"
|
status_line 1 "build missing — run setup"
|
||||||
|
fi
|
||||||
if docker ps --format '{{.Names}}' | grep -qx "$DICE_CONTAINER"; then
|
if docker ps --format '{{.Names}}' | grep -qx "$DICE_CONTAINER"; then
|
||||||
status_line 0 "${DICE_CONTAINER} container running"
|
status_line 0 "${DICE_CONTAINER} container running"
|
||||||
local pid listeners
|
local pid listeners
|
||||||
@ -418,14 +429,16 @@ status_cmd() {
|
|||||||
status_line "$(check_http "http://127.0.0.1:${DICE_PORT}/" "$DICE_DOMAIN")" \
|
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})"
|
"local static server (127.0.0.1:${DICE_PORT}, Host: ${DICE_DOMAIN})"
|
||||||
|
|
||||||
[[ -f $DICE_SNIPPET ]] &&
|
if [[ -f $DICE_SNIPPET ]]; then
|
||||||
status_line 0 "Caddy snippet present" ||
|
status_line 0 "Caddy snippet present"
|
||||||
|
else
|
||||||
status_line 1 "Caddy snippet missing"
|
status_line 1 "Caddy snippet missing"
|
||||||
|
fi
|
||||||
getent hosts "$DICE_DOMAIN" >/dev/null &&
|
if getent hosts "$DICE_DOMAIN" >/dev/null; then
|
||||||
status_line 0 "${DICE_DOMAIN} resolves" ||
|
status_line 0 "${DICE_DOMAIN} resolves"
|
||||||
|
else
|
||||||
status_line 1 "${DICE_DOMAIN} does not resolve"
|
status_line 1 "${DICE_DOMAIN} does not resolve"
|
||||||
|
fi
|
||||||
status_line "$(check_http "https://${DICE_DOMAIN}/")" \
|
status_line "$(check_http "https://${DICE_DOMAIN}/")" \
|
||||||
"external https://${DICE_DOMAIN}/"
|
"external https://${DICE_DOMAIN}/"
|
||||||
|
|
||||||
@ -438,10 +451,11 @@ status_cmd() {
|
|||||||
status_line 1 "post-commit hook missing — re-run setup"
|
status_line 1 "post-commit hook missing — re-run setup"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -f $UNIT_FILE ]] &&
|
if [[ -f $UNIT_FILE ]]; then
|
||||||
status_line 0 "${UNIT_NAME} installed" ||
|
status_line 0 "${UNIT_NAME} installed"
|
||||||
|
else
|
||||||
status_line 1 "${UNIT_NAME} missing — re-run setup"
|
status_line 1 "${UNIT_NAME} missing — re-run setup"
|
||||||
|
fi
|
||||||
if systemctl --user is-failed --quiet "$UNIT_NAME"; then
|
if systemctl --user is-failed --quiet "$UNIT_NAME"; then
|
||||||
status_line 1 "last auto-rebuild FAILED — journalctl --user -u ${UNIT_NAME}"
|
status_line 1 "last auto-rebuild FAILED — journalctl --user -u ${UNIT_NAME}"
|
||||||
else
|
else
|
||||||
@ -475,8 +489,7 @@ main() {
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log_error "Unknown command: $cmd"
|
log_error "Unknown command: $cmd"
|
||||||
usage
|
usage 1
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,9 +66,13 @@ die() {
|
|||||||
exit 1
|
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() {
|
usage() {
|
||||||
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
||||||
exit 0
|
exit "${1:-0}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Phase 1: preflight -----------------------------------------------------
|
# --- Phase 1: preflight -----------------------------------------------------
|
||||||
@ -320,13 +324,21 @@ check_http() {
|
|||||||
status_cmd() {
|
status_cmd() {
|
||||||
print_setup_header "Personal website status"
|
print_setup_header "Personal website status"
|
||||||
|
|
||||||
has_cmd node && status_line 0 "node present" || status_line 1 "node missing"
|
if has_cmd node; then
|
||||||
has_cmd pnpm && status_line 0 "pnpm present" || status_line 1 "pnpm missing"
|
status_line 0 "node present"
|
||||||
|
else
|
||||||
[[ -f "${WEBSITE_SRC}/dist/index.html" ]] &&
|
status_line 1 "node missing"
|
||||||
status_line 0 "build present (dist/index.html)" ||
|
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"
|
status_line 1 "build missing — run setup"
|
||||||
|
fi
|
||||||
if docker ps --format '{{.Names}}' | grep -qx personal-website; then
|
if docker ps --format '{{.Names}}' | grep -qx personal-website; then
|
||||||
status_line 0 "personal-website container running"
|
status_line 0 "personal-website container running"
|
||||||
else
|
else
|
||||||
@ -336,20 +348,26 @@ status_cmd() {
|
|||||||
status_line "$(check_http "http://127.0.0.1:${WEBSITE_PORT}/")" \
|
status_line "$(check_http "http://127.0.0.1:${WEBSITE_PORT}/")" \
|
||||||
"local static server (127.0.0.1:${WEBSITE_PORT})"
|
"local static server (127.0.0.1:${WEBSITE_PORT})"
|
||||||
|
|
||||||
[[ -f $WEBSITE_SNIPPET ]] &&
|
if [[ -f $WEBSITE_SNIPPET ]]; then
|
||||||
status_line 0 "Caddy website snippet present" ||
|
status_line 0 "Caddy website snippet present"
|
||||||
|
else
|
||||||
status_line 1 "Caddy website snippet missing"
|
status_line 1 "Caddy website snippet missing"
|
||||||
grep -qE '^[[:space:]]*import[[:space:]]+/etc/caddy/sites' "$CADDYFILE" 2>/dev/null &&
|
fi
|
||||||
status_line 0 "root Caddyfile uses snippet imports" ||
|
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"
|
status_line 1 "root Caddyfile not migrated to imports"
|
||||||
|
fi
|
||||||
getent hosts "$WEBSITE_DOMAIN" >/dev/null &&
|
if getent hosts "$WEBSITE_DOMAIN" >/dev/null; then
|
||||||
status_line 0 "${WEBSITE_DOMAIN} resolves" ||
|
status_line 0 "${WEBSITE_DOMAIN} resolves"
|
||||||
|
else
|
||||||
status_line 1 "${WEBSITE_DOMAIN} does not resolve"
|
status_line 1 "${WEBSITE_DOMAIN} does not resolve"
|
||||||
getent hosts "$GITEA_DOMAIN" >/dev/null &&
|
fi
|
||||||
status_line 0 "${GITEA_DOMAIN} resolves" ||
|
if getent hosts "$GITEA_DOMAIN" >/dev/null; then
|
||||||
|
status_line 0 "${GITEA_DOMAIN} resolves"
|
||||||
|
else
|
||||||
status_line 1 "${GITEA_DOMAIN} does not resolve"
|
status_line 1 "${GITEA_DOMAIN} does not resolve"
|
||||||
|
fi
|
||||||
status_line "$(check_http "https://${WEBSITE_DOMAIN}/")" \
|
status_line "$(check_http "https://${WEBSITE_DOMAIN}/")" \
|
||||||
"external https://${WEBSITE_DOMAIN}/"
|
"external https://${WEBSITE_DOMAIN}/"
|
||||||
status_line "$(check_http "https://${GITEA_DOMAIN}/")" \
|
status_line "$(check_http "https://${GITEA_DOMAIN}/")" \
|
||||||
@ -370,8 +388,7 @@ main() {
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log_error "Unknown command: $cmd"
|
log_error "Unknown command: $cmd"
|
||||||
usage
|
usage 1
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,19 +145,23 @@ collect_basics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_cpu_governor() {
|
check_cpu_governor() {
|
||||||
local gov_files
|
# An ARRAY, not a newline-joined string: the awk/grep calls below need one
|
||||||
gov_files=$(find /sys/devices/system/cpu -maxdepth 3 -name scaling_governor 2>/dev/null || true)
|
# 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)."
|
add_action "CPU governor files not found (may be unsupported on this platform)."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local summary
|
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"
|
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."
|
add_finding "CPU governor includes 'powersave' on one or more cores; this can make high-end hardware feel slow."
|
||||||
fi
|
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)
|
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)
|
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 pstate: ${pstate:-unknown}"
|
||||||
echo "NVIDIA power: ${power:-unknown}W" >>"$REPORT_FILE"
|
echo "NVIDIA util: ${util:-unknown}%"
|
||||||
|
echo "NVIDIA power: ${power:-unknown}W"
|
||||||
|
} >>"$REPORT_FILE"
|
||||||
|
|
||||||
if [[ ${pstate:-} == "P0" && ${util:-100} -le 5 ]]; then
|
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."
|
add_finding "NVIDIA GPU is in P0 high-performance state while mostly idle; this can increase heat and trigger thermal limits."
|
||||||
|
|||||||
@ -20,8 +20,8 @@ IFS=$'\n\t'
|
|||||||
|
|
||||||
# Source common library for log functions
|
# Source common library for log functions
|
||||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
# shellcheck source=../../lib/common.sh
|
# shellcheck source=../../../lib/common.sh
|
||||||
source "$SCRIPT_DIR/../../lib/common.sh"
|
source "$SCRIPT_DIR/../../../lib/common.sh"
|
||||||
|
|
||||||
DO_POLICY=false
|
DO_POLICY=false
|
||||||
SET_DEFAULT=false
|
SET_DEFAULT=false
|
||||||
|
|||||||
@ -26,11 +26,11 @@ SCRIPT_NAME="$(basename "$0")"
|
|||||||
|
|
||||||
# Source common library for log functions
|
# Source common library for log functions
|
||||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
# shellcheck source=../../lib/common.sh
|
# shellcheck source=../../../lib/common.sh
|
||||||
source "$SCRIPT_DIR/../../lib/common.sh"
|
source "$SCRIPT_DIR/../../../lib/common.sh"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat << EOF
|
cat <<EOF
|
||||||
${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler
|
${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@ -47,158 +47,158 @@ AUTO_INSTALL=false
|
|||||||
RUN_TEST=false
|
RUN_TEST=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-y | --yes)
|
-y | --yes)
|
||||||
AUTO_INSTALL=true
|
AUTO_INSTALL=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--test)
|
--test)
|
||||||
RUN_TEST=true
|
RUN_TEST=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-h | --help)
|
-h | --help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log_error "Unknown argument: $1"
|
log_error "Unknown argument: $1"
|
||||||
usage
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
require_cmd() {
|
require_cmd() {
|
||||||
if ! command -v "$1" > /dev/null 2>&1; then
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_deps_arch() {
|
ensure_deps_arch() {
|
||||||
# Best-effort install for Arch-based systems
|
# Best-effort install for Arch-based systems
|
||||||
if [[ $AUTO_INSTALL != true ]]; then
|
if [[ $AUTO_INSTALL != true ]]; then
|
||||||
log_warn "Skipping package installation (use -y to auto-install)."
|
log_warn "Skipping package installation (use -y to auto-install)."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! require_cmd pacman; then
|
if ! require_cmd pacman; then
|
||||||
log_warn "Not an Arch-based system (pacman not found). Skipping auto-install."
|
log_warn "Not an Arch-based system (pacman not found). Skipping auto-install."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk)
|
local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk)
|
||||||
log_info "Installing/ensuring packages: ${pkgs[*]}"
|
log_info "Installing/ensuring packages: ${pkgs[*]}"
|
||||||
if ! require_cmd sudo; then
|
if ! require_cmd sudo; then
|
||||||
log_warn "sudo not found; attempting pacman directly (may fail)."
|
log_warn "sudo not found; attempting pacman directly (may fail)."
|
||||||
sudo_cmd=""
|
sudo_cmd=""
|
||||||
else
|
else
|
||||||
sudo_cmd="sudo"
|
sudo_cmd="sudo"
|
||||||
fi
|
fi
|
||||||
# Use --needed to avoid reinstalling
|
# Use --needed to avoid reinstalling
|
||||||
set +e
|
set +e
|
||||||
$sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}"
|
$sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}"
|
||||||
local rc=$?
|
local rc=$?
|
||||||
set -e
|
set -e
|
||||||
if [[ $rc -ne 0 ]]; then
|
if [[ $rc -ne 0 ]]; then
|
||||||
log_warn "Package install may have failed or been partial. Continuing anyway."
|
log_warn "Package install may have failed or been partial. Continuing anyway."
|
||||||
else
|
else
|
||||||
log_ok "Dependencies installed/verified."
|
log_ok "Dependencies installed/verified."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_dir="$HOME/.local/share/applications"
|
desktop_dir="$HOME/.local/share/applications"
|
||||||
mkdir -p "$desktop_dir"
|
mkdir -p "$desktop_dir"
|
||||||
|
|
||||||
detect_unityhub() {
|
detect_unityhub() {
|
||||||
# Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD
|
# Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD
|
||||||
local install_type="UNKNOWN" exec_cmd=""
|
local install_type="UNKNOWN" exec_cmd=""
|
||||||
|
|
||||||
# 1) Flatpak
|
# 1) Flatpak
|
||||||
if command -v flatpak > /dev/null 2>&1; then
|
if command -v flatpak >/dev/null 2>&1; then
|
||||||
if flatpak info com.unity.UnityHub > /dev/null 2>&1; then
|
if flatpak info com.unity.UnityHub >/dev/null 2>&1; then
|
||||||
install_type="FLATPAK"
|
install_type="FLATPAK"
|
||||||
exec_cmd="flatpak run com.unity.UnityHub %U"
|
exec_cmd="flatpak run com.unity.UnityHub %U"
|
||||||
echo "$install_type|$exec_cmd"
|
echo "$install_type|$exec_cmd"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2) Native binary in PATH
|
# 2) Native binary in PATH
|
||||||
if command -v unityhub > /dev/null 2>&1; then
|
if command -v unityhub >/dev/null 2>&1; then
|
||||||
local path
|
local path
|
||||||
path="$(command -v unityhub)"
|
path="$(command -v unityhub)"
|
||||||
install_type="NATIVE"
|
install_type="NATIVE"
|
||||||
exec_cmd="$path %U"
|
exec_cmd="$path %U"
|
||||||
echo "$install_type|$exec_cmd"
|
echo "$install_type|$exec_cmd"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3) Search desktop files for Unity Hub Exec
|
# 3) Search desktop files for Unity Hub Exec
|
||||||
local search_dirs=(
|
local search_dirs=(
|
||||||
"$HOME/.local/share/applications"
|
"$HOME/.local/share/applications"
|
||||||
"/usr/share/applications"
|
"/usr/share/applications"
|
||||||
"/var/lib/flatpak/exports/share/applications"
|
"/var/lib/flatpak/exports/share/applications"
|
||||||
"$HOME/.local/share/flatpak/exports/share/applications"
|
"$HOME/.local/share/flatpak/exports/share/applications"
|
||||||
)
|
)
|
||||||
local found_exec=""
|
local found_exec=""
|
||||||
for d in "${search_dirs[@]}"; do
|
for d in "${search_dirs[@]}"; do
|
||||||
[[ -d $d ]] || continue
|
[[ -d $d ]] || continue
|
||||||
# prefer official naming when present
|
# prefer official naming when present
|
||||||
local f
|
local f
|
||||||
for f in "$d"/*.desktop; do
|
for f in "$d"/*.desktop; do
|
||||||
[[ -e $f ]] || continue
|
[[ -e $f ]] || continue
|
||||||
if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2> /dev/null ||
|
if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2>/dev/null ||
|
||||||
grep -qiE 'Exec=.*unityhub' "$f" 2> /dev/null; then
|
grep -qiE 'Exec=.*unityhub' "$f" 2>/dev/null; then
|
||||||
local exec_line
|
local exec_line
|
||||||
exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')"
|
exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')"
|
||||||
if [[ -n $exec_line ]]; then
|
if [[ -n $exec_line ]]; then
|
||||||
found_exec="$exec_line"
|
found_exec="$exec_line"
|
||||||
break 2
|
break 2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -n $found_exec ]]; then
|
if [[ -n $found_exec ]]; then
|
||||||
# Normalize: ensure %U present
|
# Normalize: ensure %U present
|
||||||
if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then
|
if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then
|
||||||
found_exec+=" %U"
|
found_exec+=" %U"
|
||||||
fi
|
fi
|
||||||
if [[ $found_exec == flatpak* ]]; then
|
if [[ $found_exec == flatpak* ]]; then
|
||||||
install_type="FLATPAK"
|
install_type="FLATPAK"
|
||||||
elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then
|
elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then
|
||||||
install_type="APPIMAGE"
|
install_type="APPIMAGE"
|
||||||
else
|
else
|
||||||
install_type="NATIVE"
|
install_type="NATIVE"
|
||||||
fi
|
fi
|
||||||
echo "$install_type|$found_exec"
|
echo "$install_type|$found_exec"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4) Try common AppImage locations
|
# 4) Try common AppImage locations
|
||||||
local ai_candidates=(
|
local ai_candidates=(
|
||||||
"$HOME/Applications/UnityHub*.AppImage"
|
"$HOME/Applications/UnityHub*.AppImage"
|
||||||
"$HOME/.local/bin/UnityHub*.AppImage"
|
"$HOME/.local/bin/UnityHub*.AppImage"
|
||||||
"/opt/UnityHub*/UnityHub*.AppImage"
|
"/opt/UnityHub*/UnityHub*.AppImage"
|
||||||
)
|
)
|
||||||
local ai
|
local ai
|
||||||
for ai in "${ai_candidates[@]}"; do
|
for ai in "${ai_candidates[@]}"; do
|
||||||
for p in $ai; do
|
for p in $ai; do
|
||||||
if [[ -f $p && -x $p ]]; then
|
if [[ -f $p && -x $p ]]; then
|
||||||
install_type="APPIMAGE"
|
install_type="APPIMAGE"
|
||||||
exec_cmd="$p %U"
|
exec_cmd="$p %U"
|
||||||
echo "$install_type|$exec_cmd"
|
echo "$install_type|$exec_cmd"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "$install_type|$exec_cmd"
|
echo "$install_type|$exec_cmd"
|
||||||
}
|
}
|
||||||
|
|
||||||
create_handler_desktop() {
|
create_handler_desktop() {
|
||||||
local exec_cmd="$1"
|
local exec_cmd="$1"
|
||||||
local dest="$desktop_dir/unityhub-url-handler.desktop"
|
local dest="$desktop_dir/unityhub-url-handler.desktop"
|
||||||
log_info "Writing handler desktop entry: $dest"
|
log_info "Writing handler desktop entry: $dest"
|
||||||
cat > "$dest" << DESK
|
cat >"$dest" <<DESK
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Unity Hub URL Handler
|
Name=Unity Hub URL Handler
|
||||||
Comment=Handle unityhub:// links for Unity Hub sign-in
|
Comment=Handle unityhub:// links for Unity Hub sign-in
|
||||||
@ -211,82 +211,82 @@ StartupWMClass=Unity Hub
|
|||||||
MimeType=x-scheme-handler/unityhub;x-scheme-handler/unity;
|
MimeType=x-scheme-handler/unityhub;x-scheme-handler/unity;
|
||||||
NoDisplay=true
|
NoDisplay=true
|
||||||
DESK
|
DESK
|
||||||
log_ok "Desktop entry created/updated."
|
log_ok "Desktop entry created/updated."
|
||||||
echo "$dest"
|
echo "$dest"
|
||||||
}
|
}
|
||||||
|
|
||||||
register_mime_handler() {
|
register_mime_handler() {
|
||||||
local desktop_file="$1"
|
local desktop_file="$1"
|
||||||
# Update desktop database if available
|
# Update desktop database if available
|
||||||
if command -v update-desktop-database > /dev/null 2>&1; then
|
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||||
update-desktop-database "$desktop_dir" || true
|
update-desktop-database "$desktop_dir" || true
|
||||||
else
|
else
|
||||||
log_warn "update-desktop-database not found (install desktop-file-utils)."
|
log_warn "update-desktop-database not found (install desktop-file-utils)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Register as default handler for both schemes
|
# Register as default handler for both schemes
|
||||||
if command -v xdg-mime > /dev/null 2>&1; then
|
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/unityhub || true
|
||||||
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true
|
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true
|
||||||
else
|
else
|
||||||
log_error "xdg-mime not found (install xdg-utils)."
|
log_error "xdg-mime not found (install xdg-utils)."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
log_ok "MIME handler registered for unityhub:// (and unity://)."
|
log_ok "MIME handler registered for unityhub:// (and unity://)."
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_registration() {
|
verify_registration() {
|
||||||
local expected cur1 cur2
|
local expected cur1 cur2
|
||||||
expected="$(basename "$1")"
|
expected="$(basename "$1")"
|
||||||
cur1="$(xdg-mime query default x-scheme-handler/unityhub 2> /dev/null || true)"
|
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)"
|
cur2="$(xdg-mime query default x-scheme-handler/unity 2>/dev/null || true)"
|
||||||
log_info "Current handler (unityhub): ${cur1:-<none>}"
|
log_info "Current handler (unityhub): ${cur1:-<none>}"
|
||||||
log_info "Current handler (unity): ${cur2:-<none>}"
|
log_info "Current handler (unity): ${cur2:-<none>}"
|
||||||
if [[ $cur1 == "$expected" ]]; then
|
if [[ $cur1 == "$expected" ]]; then
|
||||||
log_ok "unityhub scheme correctly set to $expected"
|
log_ok "unityhub scheme correctly set to $expected"
|
||||||
else
|
else
|
||||||
log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})."
|
log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
maybe_test_open() {
|
maybe_test_open() {
|
||||||
if [[ $RUN_TEST == true ]]; then
|
if [[ $RUN_TEST == true ]]; then
|
||||||
log_info "Opening test link: unityhub://v1/editor-signin"
|
log_info "Opening test link: unityhub://v1/editor-signin"
|
||||||
if command -v xdg-open > /dev/null 2>&1; then
|
if command -v xdg-open >/dev/null 2>&1; then
|
||||||
xdg-open 'unityhub://v1/editor-signin' > /dev/null 2>&1 || true
|
xdg-open 'unityhub://v1/editor-signin' >/dev/null 2>&1 || true
|
||||||
log_ok "Test link invoked. Check if Unity Hub launches or focuses."
|
log_ok "Test link invoked. Check if Unity Hub launches or focuses."
|
||||||
else
|
else
|
||||||
log_warn "xdg-open not found; cannot run test automatically."
|
log_warn "xdg-open not found; cannot run test automatically."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'"
|
log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
log_info "Ensuring required tools (optional)."
|
log_info "Ensuring required tools (optional)."
|
||||||
ensure_deps_arch
|
ensure_deps_arch
|
||||||
|
|
||||||
log_info "Detecting Unity Hub installation..."
|
log_info "Detecting Unity Hub installation..."
|
||||||
IFS='|' read -r install_type exec_cmd < <(detect_unityhub)
|
IFS='|' read -r install_type exec_cmd < <(detect_unityhub)
|
||||||
log_info "Detected type: $install_type"
|
log_info "Detected type: $install_type"
|
||||||
if [[ -z ${exec_cmd:-} ]]; then
|
if [[ -z ${exec_cmd:-} ]]; then
|
||||||
log_warn "Could not find Unity Hub executable automatically."
|
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 using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'"
|
||||||
log_warn "- If native (AUR): ensure 'unityhub' is in PATH"
|
log_warn "- If native (AUR): ensure 'unityhub' is in PATH"
|
||||||
log_warn "- If AppImage: place it in ~/Applications and make it executable"
|
log_warn "- If AppImage: place it in ~/Applications and make it executable"
|
||||||
log_error "Aborting—no Exec command available to create handler."
|
log_error "Aborting—no Exec command available to create handler."
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
log_info "Using Exec: $exec_cmd"
|
log_info "Using Exec: $exec_cmd"
|
||||||
|
|
||||||
local desktop_file
|
local desktop_file
|
||||||
desktop_file="$(create_handler_desktop "$exec_cmd")"
|
desktop_file="$(create_handler_desktop "$exec_cmd")"
|
||||||
|
|
||||||
register_mime_handler "$desktop_file"
|
register_mime_handler "$desktop_file"
|
||||||
verify_registration "$desktop_file"
|
verify_registration "$desktop_file"
|
||||||
|
|
||||||
cat << 'NOTE'
|
cat <<'NOTE'
|
||||||
---
|
---
|
||||||
Next steps:
|
Next steps:
|
||||||
- Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub.
|
- 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
|
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 "$@"
|
main "$@"
|
||||||
|
|||||||
@ -13,21 +13,21 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Source common library for shared functions
|
# Source common library for shared functions
|
||||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
# shellcheck source=../../lib/common.sh
|
# shellcheck source=../../../lib/common.sh
|
||||||
source "$SCRIPT_DIR/../../lib/common.sh"
|
source "$SCRIPT_DIR/../../../lib/common.sh"
|
||||||
|
|
||||||
YES=false
|
YES=false
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-y | --yes)
|
-y | --yes)
|
||||||
YES=true
|
YES=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
RN_TARGET_DIR=${RN_TARGET_DIR:-"$(dirname "$0")/models"}
|
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"
|
dest="$RN_TARGET_DIR/$RN_TARGET_NAME"
|
||||||
|
|
||||||
if [[ -f $dest ]]; then
|
if [[ -f $dest ]]; then
|
||||||
echo "Model already exists at: $dest"
|
echo "Model already exists at: $dest"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! $YES; then
|
if ! $YES; then
|
||||||
if ! ask_yes_no "Download RNNoise model to $dest?"; then
|
if ! ask_yes_no "Download RNNoise model to $dest?"; then
|
||||||
echo "Aborted."
|
echo "Aborted."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! has_cmd curl && ! has_cmd wget; then
|
if ! has_cmd curl && ! has_cmd wget; then
|
||||||
echo "Error: Need curl or wget to download RNNoise model." >&2
|
echo "Error: Need curl or wget to download RNNoise model." >&2
|
||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Helper: try to download a URL to destination, exit 0 on success
|
# Helper: try to download a URL to destination, exit 0 on success
|
||||||
# Usage: try_download_model URL DEST
|
# Usage: try_download_model URL DEST
|
||||||
try_download_model() {
|
try_download_model() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local dest="$2"
|
local dest="$2"
|
||||||
local tmp
|
local tmp
|
||||||
tmp=$(mktemp)
|
tmp=$(mktemp)
|
||||||
echo "Attempting to download RNNoise model from: $url" >&2
|
echo "Attempting to download RNNoise model from: $url" >&2
|
||||||
if has_cmd curl; then
|
if has_cmd curl; then
|
||||||
curl -fsSL "$url" -o "$tmp" 2> /dev/null || true
|
curl -fsSL "$url" -o "$tmp" 2>/dev/null || true
|
||||||
else
|
else
|
||||||
wget -qO "$tmp" "$url" 2> /dev/null || true
|
wget -qO "$tmp" "$url" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
if [[ -s $tmp ]]; then
|
if [[ -s $tmp ]]; then
|
||||||
mv "$tmp" "$dest"
|
mv "$tmp" "$dest"
|
||||||
echo "Saved RNNoise model to: $dest" >&2
|
echo "Saved RNNoise model to: $dest" >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
rm -f "$tmp" || true
|
rm -f "$tmp" || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Priority 1: explicit URL
|
# Priority 1: explicit URL
|
||||||
if [[ -n ${RN_URL:-} ]]; then
|
if [[ -n ${RN_URL:-} ]]; then
|
||||||
echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2
|
echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2
|
||||||
try_download_model "$RN_URL" "$dest"
|
try_download_model "$RN_URL" "$dest"
|
||||||
echo "Warning: RN_URL download failed; continuing to fallback sources." >&2
|
echo "Warning: RN_URL download failed; continuing to fallback sources." >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Priority 2: rnnoise-nu known models (GregorR)
|
# Priority 2: rnnoise-nu known models (GregorR)
|
||||||
NU_URLS=(
|
NU_URLS=(
|
||||||
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/sh.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/lq.rnnn"
|
||||||
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/mp.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/bd.rnnn"
|
||||||
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn"
|
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn"
|
||||||
)
|
)
|
||||||
for u in "${NU_URLS[@]}"; do
|
for u in "${NU_URLS[@]}"; do
|
||||||
try_download_model "$u" "$dest"
|
try_download_model "$u" "$dest"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Priority 2b: arnndn-models fallback (richardpl)
|
# Priority 2b: arnndn-models fallback (richardpl)
|
||||||
RNNDN_URLS=(
|
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
|
for u in "${RNNDN_URLS[@]}"; do
|
||||||
try_download_model "$u" "$dest"
|
try_download_model "$u" "$dest"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Priority 3: repo archives (rnnoise-nu and arnndn-models)
|
# Priority 3: repo archives (rnnoise-nu and arnndn-models)
|
||||||
ARCHIVES=(
|
ARCHIVES=(
|
||||||
"https://github.com/GregorR/rnnoise-nu/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"
|
"https://github.com/richardpl/arnndn-models/archive/refs/heads/master.zip"
|
||||||
)
|
)
|
||||||
for aurl in "${ARCHIVES[@]}"; do
|
for aurl in "${ARCHIVES[@]}"; do
|
||||||
echo "Attempting to download archive: $aurl" >&2
|
echo "Attempting to download archive: $aurl" >&2
|
||||||
tmpdir=$(mktemp -d)
|
tmpdir=$(mktemp -d)
|
||||||
archive="$tmpdir/models.zip"
|
archive="$tmpdir/models.zip"
|
||||||
set +e
|
set +e
|
||||||
if has_cmd curl; then
|
if has_cmd curl; then
|
||||||
curl -fL "$aurl" -o "$archive"
|
curl -fL "$aurl" -o "$archive"
|
||||||
else
|
else
|
||||||
wget -O "$archive" "$aurl"
|
wget -O "$archive" "$aurl"
|
||||||
fi
|
fi
|
||||||
status=$?
|
status=$?
|
||||||
set -e
|
set -e
|
||||||
if [[ $status -ne 0 ]]; then
|
if [[ $status -ne 0 ]]; then
|
||||||
rm -rf "$tmpdir" || true
|
rm -rf "$tmpdir" || true
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if has_cmd bsdtar; then
|
if has_cmd bsdtar; then
|
||||||
bsdtar -xf "$archive" -C "$tmpdir"
|
bsdtar -xf "$archive" -C "$tmpdir"
|
||||||
elif has_cmd unzip; then
|
elif has_cmd unzip; then
|
||||||
unzip -q "$archive" -d "$tmpdir"
|
unzip -q "$archive" -d "$tmpdir"
|
||||||
else
|
else
|
||||||
echo "Warning: Need bsdtar or unzip to extract archive; skipping archive method." >&2
|
echo "Warning: Need bsdtar or unzip to extract archive; skipping archive method." >&2
|
||||||
rm -rf "$tmpdir" || true
|
rm -rf "$tmpdir" || true
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
mapfile -t nnfiles < <(bash -lc 'shopt -s globstar nullglob; for f in '"$tmpdir"'/**/*.rnnn '"$tmpdir"'/**/*.nn; do [[ -f "$f" ]] && echo "$f"; done')
|
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
|
if [[ ${#nnfiles[@]} -gt 0 ]]; then
|
||||||
cp -f "${nnfiles[0]}" "$dest"
|
cp -f "${nnfiles[0]}" "$dest"
|
||||||
echo "Saved RNNoise model to: $dest (from archive)" >&2
|
echo "Saved RNNoise model to: $dest (from archive)" >&2
|
||||||
rm -rf "$tmpdir" || true
|
rm -rf "$tmpdir" || true
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
rm -rf "$tmpdir" || true
|
rm -rf "$tmpdir" || true
|
||||||
done
|
done
|
||||||
|
|
||||||
# Priority 4: Arch-based AUR packages and search only .nn/.rnnn
|
# Priority 4: Arch-based AUR packages and search only .nn/.rnnn
|
||||||
if has_cmd yay; then
|
if has_cmd yay; then
|
||||||
echo "Attempting to install AUR packages that may include RNNoise models..." >&2
|
echo "Attempting to install AUR packages that may include RNNoise models..." >&2
|
||||||
set +e
|
set +e
|
||||||
yay -S --noconfirm denoiseit-git 2> /dev/null
|
yay -S --noconfirm denoiseit-git 2>/dev/null
|
||||||
yay -S --noconfirm speech-denoiser-git 2> /dev/null
|
yay -S --noconfirm speech-denoiser-git 2>/dev/null
|
||||||
set -e
|
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)
|
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
|
if [[ ${#found[@]} -gt 0 ]]; then
|
||||||
echo "Found candidate models:" >&2
|
echo "Found candidate models:" >&2
|
||||||
printf ' %s\n' "${found[@]}" >&2
|
printf ' %s\n' "${found[@]}" >&2
|
||||||
cp -f "${found[0]}" "$dest"
|
cp -f "${found[0]}" "$dest"
|
||||||
echo "Copied model to: $dest" >&2
|
echo "Copied model to: $dest" >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Error: Could not obtain an RNNoise model automatically." >&2
|
echo "Error: Could not obtain an RNNoise model automatically." >&2
|
||||||
|
|||||||
@ -9,117 +9,117 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Source common library for shared functions
|
# Source common library for shared functions
|
||||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
# shellcheck source=../../lib/common.sh
|
# shellcheck source=../../../lib/common.sh
|
||||||
source "$SCRIPT_DIR/../../lib/common.sh"
|
source "$SCRIPT_DIR/../../../lib/common.sh"
|
||||||
|
|
||||||
print_info() {
|
print_info() {
|
||||||
echo "[info] $*"
|
echo "[info] $*"
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_distro() {
|
detect_distro() {
|
||||||
if [[ -f /etc/os-release ]]; then
|
if [[ -f /etc/os-release ]]; then
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
echo "${ID:-unknown}"
|
echo "${ID:-unknown}"
|
||||||
else
|
else
|
||||||
echo "unknown"
|
echo "unknown"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
local distro
|
local distro
|
||||||
distro=$(detect_distro)
|
distro=$(detect_distro)
|
||||||
print_info "Detected distro: $distro"
|
print_info "Detected distro: $distro"
|
||||||
|
|
||||||
if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then
|
if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then
|
||||||
print_info "Your ffmpeg already supports arnndn."
|
print_info "Your ffmpeg already supports arnndn."
|
||||||
else
|
else
|
||||||
case "$distro" in
|
case "$distro" in
|
||||||
ubuntu | debian)
|
ubuntu | debian)
|
||||||
print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source."
|
print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source."
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg"
|
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"
|
echo " - source build (recommended for latest): run this script to build from source"
|
||||||
;;
|
;;
|
||||||
arch | manjaro | endeavouros)
|
arch | manjaro | endeavouros)
|
||||||
print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg"
|
print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg"
|
||||||
;;
|
;;
|
||||||
fedora)
|
fedora)
|
||||||
print_info "On Fedora, try: sudo dnf install ffmpeg"
|
print_info "On Fedora, try: sudo dnf install ffmpeg"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
print_info "Distro not recognized; will offer source build."
|
print_info "Distro not recognized; will offer source build."
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ask_yes_no "Build FFmpeg from source with rnnoise/arnndn support now?"; then
|
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?"
|
echo "This will clone FFmpeg and build locally under ./ffmpeg-build. Continue?"
|
||||||
if ! ask_yes_no "Proceed"; then
|
if ! ask_yes_no "Proceed"; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
set -x
|
set -x
|
||||||
mkdir -p ffmpeg-build && cd ffmpeg-build
|
mkdir -p ffmpeg-build && cd ffmpeg-build
|
||||||
# Prepare repository
|
# Prepare repository
|
||||||
if [[ -d FFmpeg ]]; then
|
if [[ -d FFmpeg ]]; then
|
||||||
if [[ -d FFmpeg/.git ]]; then
|
if [[ -d FFmpeg/.git ]]; then
|
||||||
if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then
|
if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then
|
||||||
set +e
|
set +e
|
||||||
git -C FFmpeg fetch --all --tags --prune
|
git -C FFmpeg fetch --all --tags --prune
|
||||||
git -C FFmpeg pull --rebase --ff-only || true
|
git -C FFmpeg pull --rebase --ff-only || true
|
||||||
set -e
|
set -e
|
||||||
else
|
else
|
||||||
if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then
|
if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then
|
||||||
rm -rf FFmpeg
|
rm -rf FFmpeg
|
||||||
else
|
else
|
||||||
echo "Keeping existing FFmpeg directory as-is."
|
echo "Keeping existing FFmpeg directory as-is."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then
|
if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then
|
||||||
rm -rf FFmpeg
|
rm -rf FFmpeg
|
||||||
else
|
else
|
||||||
echo "Cannot proceed with a non-git FFmpeg directory present. Aborting."
|
echo "Cannot proceed with a non-git FFmpeg directory present. Aborting."
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Dependencies
|
# Dependencies
|
||||||
if [[ $distro == "ubuntu" || $distro == "debian" ]]; then
|
if [[ $distro == "ubuntu" || $distro == "debian" ]]; then
|
||||||
sudo apt update
|
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
|
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
|
elif [[ $distro == "arch" || $distro == "manjaro" || $distro == "endeavouros" ]]; then
|
||||||
sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise
|
sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise
|
||||||
elif [[ $distro == "fedora" ]]; then
|
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
|
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
|
else
|
||||||
echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2
|
echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2
|
||||||
fi
|
fi
|
||||||
if [[ ! -d FFmpeg/.git ]]; then
|
if [[ ! -d FFmpeg/.git ]]; then
|
||||||
git clone https://github.com/FFmpeg/FFmpeg.git --depth=1
|
git clone https://github.com/FFmpeg/FFmpeg.git --depth=1
|
||||||
fi
|
fi
|
||||||
cd FFmpeg
|
cd FFmpeg
|
||||||
RN_FLAG=""
|
RN_FLAG=""
|
||||||
# Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported
|
# Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported
|
||||||
if ./configure --help | grep -q "librnnoise"; then
|
if ./configure --help | grep -q "librnnoise"; then
|
||||||
RN_FLAG="--enable-librnnoise"
|
RN_FLAG="--enable-librnnoise"
|
||||||
else
|
else
|
||||||
echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2
|
echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
./configure \
|
./configure \
|
||||||
--enable-gpl --enable-nonfree \
|
--enable-gpl --enable-nonfree \
|
||||||
--enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \
|
--enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \
|
||||||
--enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \
|
--enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \
|
||||||
--enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \
|
--enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \
|
||||||
--enable-libdav1d --enable-libaom --enable-libsvtav1 \
|
--enable-libdav1d --enable-libaom --enable-libsvtav1 \
|
||||||
${RN_FLAG} \
|
${RN_FLAG} \
|
||||||
--enable-ffplay --enable-ffprobe
|
--enable-ffplay --enable-ffprobe
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide."
|
echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide."
|
||||||
set +x
|
set +x
|
||||||
else
|
else
|
||||||
echo "Skipped building from source."
|
echo "Skipped building from source."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@ -4,8 +4,8 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Source common library for shared functions
|
# Source common library for shared functions
|
||||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
# shellcheck source=../../lib/common.sh
|
# shellcheck source=../../../lib/common.sh
|
||||||
source "$SCRIPT_DIR/../../lib/common.sh"
|
source "$SCRIPT_DIR/../../../lib/common.sh"
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
SCRIPT_NAME="$(basename "$0")"
|
||||||
|
|
||||||
@ -15,158 +15,158 @@ BLUE="\033[34m"
|
|||||||
RESET="\033[0m"
|
RESET="\033[0m"
|
||||||
|
|
||||||
info() {
|
info() {
|
||||||
printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*"
|
printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
warn() {
|
warn() {
|
||||||
printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2
|
printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
error() {
|
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() {
|
ensure_pacman_packages() {
|
||||||
install_missing_pacman_packages python git curl jq code
|
install_missing_pacman_packages python git curl jq code
|
||||||
}
|
}
|
||||||
|
|
||||||
install_uv() {
|
install_uv() {
|
||||||
if command -v uv > /dev/null 2>&1; then
|
if command -v uv >/dev/null 2>&1; then
|
||||||
info "uv is already installed."
|
info "uv is already installed."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Installing uv toolchain manager via official installer."
|
info "Installing uv toolchain manager via official installer."
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
local local_bin="$HOME/.local/bin"
|
local local_bin="$HOME/.local/bin"
|
||||||
if [[ :$PATH: != *":$local_bin:"* ]]; then
|
if [[ :$PATH: != *":$local_bin:"* ]]; then
|
||||||
warn "Adding $local_bin to PATH in ~/.profile and ~/.zshrc. Open a new shell to apply."
|
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/.profile"
|
||||||
printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.zshrc"
|
printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >>"$HOME/.zshrc"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_unity_hub() {
|
ensure_unity_hub() {
|
||||||
if command -v unityhub > /dev/null 2>&1; then
|
if command -v unityhub >/dev/null 2>&1; then
|
||||||
info "Unity Hub already installed."
|
info "Unity Hub already installed."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v yay > /dev/null 2>&1; then
|
if command -v yay >/dev/null 2>&1; then
|
||||||
info "Installing Unity Hub from AUR using yay."
|
info "Installing Unity Hub from AUR using yay."
|
||||||
yay -S --needed --noconfirm unityhub
|
yay -S --needed --noconfirm unityhub
|
||||||
elif command -v flatpak > /dev/null 2>&1; then
|
elif command -v flatpak >/dev/null 2>&1; then
|
||||||
warn "Unity Hub not found. Attempting Flatpak installation."
|
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"
|
flatpak install -y com.unity.UnityHub || warn "Flatpak installation failed. Install Unity Hub manually via https://unity.com/download"
|
||||||
else
|
else
|
||||||
warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download."
|
warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
sync_unity_mcp_repo() {
|
sync_unity_mcp_repo() {
|
||||||
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
|
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||||
local unity_mcp_root="$data_home/UnityMCP"
|
local unity_mcp_root="$data_home/UnityMCP"
|
||||||
local repo_dir="$unity_mcp_root/unity-mcp-repo"
|
local repo_dir="$unity_mcp_root/unity-mcp-repo"
|
||||||
local server_link="$unity_mcp_root/UnityMcpServer"
|
local server_link="$unity_mcp_root/UnityMcpServer"
|
||||||
local candidates=(
|
local candidates=(
|
||||||
"UnityMcpServer"
|
"UnityMcpServer"
|
||||||
"UnityMcpBridge/UnityMcpServer"
|
"UnityMcpBridge/UnityMcpServer"
|
||||||
"UnityMcpBridge/UnityMcpServer~"
|
"UnityMcpBridge/UnityMcpServer~"
|
||||||
)
|
)
|
||||||
local server_subdir=""
|
local server_subdir=""
|
||||||
|
|
||||||
mkdir -p "$unity_mcp_root"
|
mkdir -p "$unity_mcp_root"
|
||||||
|
|
||||||
if [[ -d "$repo_dir/.git" ]]; then
|
if [[ -d "$repo_dir/.git" ]]; then
|
||||||
info "Updating existing unity-mcp repository."
|
info "Updating existing unity-mcp repository."
|
||||||
git -C "$repo_dir" pull --ff-only
|
git -C "$repo_dir" pull --ff-only
|
||||||
else
|
else
|
||||||
info "Cloning unity-mcp repository."
|
info "Cloning unity-mcp repository."
|
||||||
rm -rf "$repo_dir"
|
rm -rf "$repo_dir"
|
||||||
git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir"
|
git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for candidate in "${candidates[@]}"; do
|
for candidate in "${candidates[@]}"; do
|
||||||
if [[ -d "$repo_dir/$candidate/src" ]]; then
|
if [[ -d "$repo_dir/$candidate/src" ]]; then
|
||||||
server_subdir="$candidate"
|
server_subdir="$candidate"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z $server_subdir ]]; then
|
if [[ -z $server_subdir ]]; then
|
||||||
error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}"
|
error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}"
|
||||||
error "Repository layout may have changed. Inspect $repo_dir for the new server location."
|
error "Repository layout may have changed. Inspect $repo_dir for the new server location."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ln -sfn "$repo_dir/$server_subdir" "$server_link"
|
ln -sfn "$repo_dir/$server_subdir" "$server_link"
|
||||||
info "UnityMcpServer synchronized at $server_link (source: $server_subdir)"
|
info "UnityMcpServer synchronized at $server_link (source: $server_subdir)"
|
||||||
}
|
}
|
||||||
|
|
||||||
configure_vscode_mcp() {
|
configure_vscode_mcp() {
|
||||||
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
|
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||||
local server_src="$data_home/UnityMCP/UnityMcpServer/src"
|
local server_src="$data_home/UnityMCP/UnityMcpServer/src"
|
||||||
local mcp_config_dir="$HOME/.config/Code/User"
|
local mcp_config_dir="$HOME/.config/Code/User"
|
||||||
local mcp_config="$mcp_config_dir/mcp.json"
|
local mcp_config="$mcp_config_dir/mcp.json"
|
||||||
local tmp
|
local tmp
|
||||||
|
|
||||||
if [[ ! -d $server_src ]]; then
|
if [[ ! -d $server_src ]]; then
|
||||||
error "Server source directory $server_src is missing."
|
error "Server source directory $server_src is missing."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$mcp_config_dir"
|
mkdir -p "$mcp_config_dir"
|
||||||
|
|
||||||
if [[ ! -f $mcp_config ]]; then
|
if [[ ! -f $mcp_config ]]; then
|
||||||
info "Creating new VS Code MCP configuration at $mcp_config"
|
info "Creating new VS Code MCP configuration at $mcp_config"
|
||||||
echo '{}' > "$mcp_config"
|
echo '{}' >"$mcp_config"
|
||||||
else
|
else
|
||||||
info "Updating existing VS Code MCP configuration at $mcp_config"
|
info "Updating existing VS Code MCP configuration at $mcp_config"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
|
|
||||||
if ! jq '.' "$mcp_config" > /dev/null 2>&1; then
|
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."
|
error "Existing $mcp_config is not valid JSON. Please fix it before running this script again."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
jq \
|
jq \
|
||||||
--arg path "$server_src" \
|
--arg path "$server_src" \
|
||||||
'(.servers //= {}) |
|
'(.servers //= {}) |
|
||||||
.servers.unityMCP = {
|
.servers.unityMCP = {
|
||||||
command: "uv",
|
command: "uv",
|
||||||
args: ["--directory", $path, "run", "server.py"],
|
args: ["--directory", $path, "run", "server.py"],
|
||||||
type: "stdio"
|
type: "stdio"
|
||||||
}' \
|
}' \
|
||||||
"$mcp_config" > "$tmp"
|
"$mcp_config" >"$tmp"
|
||||||
|
|
||||||
mv "$tmp" "$mcp_config"
|
mv "$tmp" "$mcp_config"
|
||||||
info "VS Code MCP server configuration updated for UnityMCP."
|
info "VS Code MCP server configuration updated for UnityMCP."
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_python_version() {
|
verify_python_version() {
|
||||||
|
|
||||||
require_command python "python"
|
require_command python "python"
|
||||||
local version
|
local version
|
||||||
version="$(
|
version="$(
|
||||||
python - << 'PY'
|
python - <<'PY'
|
||||||
import sys
|
import sys
|
||||||
print("%d.%d.%d" % sys.version_info[:3])
|
print("%d.%d.%d" % sys.version_info[:3])
|
||||||
PY
|
PY
|
||||||
)"
|
)"
|
||||||
local major minor
|
local major minor
|
||||||
IFS='.' read -r major minor _ <<< "$version"
|
IFS='.' read -r major minor _ <<<"$version"
|
||||||
if ((major < 3 || (major == 3 && minor < 12))); then
|
if ((major < 3 || (major == 3 && minor < 12))); then
|
||||||
error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing."
|
error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
info "Python version $version satisfies requirement (>= 3.12)."
|
info "Python version $version satisfies requirement (>= 3.12)."
|
||||||
}
|
}
|
||||||
|
|
||||||
print_next_steps() {
|
print_next_steps() {
|
||||||
cat << 'EOT'
|
cat <<'EOT'
|
||||||
|
|
||||||
Next steps:
|
Next steps:
|
||||||
1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer.
|
1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer.
|
||||||
@ -190,22 +190,22 @@ EOT
|
|||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
if [[ ! -f /etc/arch-release ]]; then
|
if [[ ! -f /etc/arch-release ]]; then
|
||||||
error "This script is intended for Arch Linux."
|
error "This script is intended for Arch Linux."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Ensuring base dependencies are installed."
|
info "Ensuring base dependencies are installed."
|
||||||
require_command sudo "sudo"
|
require_command sudo "sudo"
|
||||||
require_command pacman "pacman"
|
require_command pacman "pacman"
|
||||||
ensure_pacman_packages
|
ensure_pacman_packages
|
||||||
verify_python_version
|
verify_python_version
|
||||||
install_uv
|
install_uv
|
||||||
ensure_unity_hub
|
ensure_unity_hub
|
||||||
sync_unity_mcp_repo
|
sync_unity_mcp_repo
|
||||||
configure_vscode_mcp
|
configure_vscode_mcp
|
||||||
print_next_steps
|
print_next_steps
|
||||||
info "Setup complete. Follow the next steps above to finish configuration inside Unity."
|
info "Setup complete. Follow the next steps above to finish configuration inside Unity."
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@ -113,7 +113,7 @@ find_files() {
|
|||||||
# Not a git repo - fall back to manual exclusion
|
# Not a git repo - fall back to manual exclusion
|
||||||
local find_args=()
|
local find_args=()
|
||||||
for i in "${!patterns[@]}"; do
|
for i in "${!patterns[@]}"; do
|
||||||
if [ $i -eq 0 ]; then
|
if [ "$i" -eq 0 ]; then
|
||||||
find_args+=(-name "${patterns[$i]}")
|
find_args+=(-name "${patterns[$i]}")
|
||||||
else
|
else
|
||||||
find_args+=(-o -name "${patterns[$i]}")
|
find_args+=(-o -name "${patterns[$i]}")
|
||||||
@ -125,7 +125,7 @@ find_files() {
|
|||||||
# No filtering - find all files
|
# No filtering - find all files
|
||||||
local find_args=()
|
local find_args=()
|
||||||
for i in "${!patterns[@]}"; do
|
for i in "${!patterns[@]}"; do
|
||||||
if [ $i -eq 0 ]; then
|
if [ "$i" -eq 0 ]; then
|
||||||
find_args+=(-name "${patterns[$i]}")
|
find_args+=(-name "${patterns[$i]}")
|
||||||
else
|
else
|
||||||
find_args+=(-o -name "${patterns[$i]}")
|
find_args+=(-o -name "${patterns[$i]}")
|
||||||
@ -302,7 +302,7 @@ if [ "$RESPECT_GITIGNORE" = true ] && is_git_repo; then
|
|||||||
} | sort -u | wc -l)
|
} | sort -u | wc -l)
|
||||||
echo "Files: $FILE_COUNT (respecting .gitignore)"
|
echo "Files: $FILE_COUNT (respecting .gitignore)"
|
||||||
elif [ "$RESPECT_GITIGNORE" = true ]; then
|
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
|
else
|
||||||
echo "Files: $(find . -type f | wc -l)"
|
echo "Files: $(find . -type f | wc -l)"
|
||||||
fi
|
fi
|
||||||
@ -336,7 +336,7 @@ print_header "STEP 4: Fast Keyword Analysis (Code vs Comments)"
|
|||||||
fast_count() {
|
fast_count() {
|
||||||
local top_n="${1:-50}"
|
local top_n="${1:-50}"
|
||||||
if command -v counts &>/dev/null; then
|
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
|
else
|
||||||
python3 "$SCRIPT_DIR/fast_count.py" "$top_n"
|
python3 "$SCRIPT_DIR/fast_count.py" "$top_n"
|
||||||
fi
|
fi
|
||||||
@ -387,14 +387,14 @@ HAS_GO=false
|
|||||||
HAS_RUST=false
|
HAS_RUST=false
|
||||||
HAS_JAVA=false
|
HAS_JAVA=false
|
||||||
|
|
||||||
((${LANG_FILES[c]} + ${LANG_FILES[cpp]} + ${LANG_FILES[h]} > 0)) && HAS_C_FAMILY=true
|
((LANG_FILES[c] + LANG_FILES[cpp] + LANG_FILES[h] > 0)) && HAS_C_FAMILY=true
|
||||||
((${LANG_FILES[python]} > 0)) && HAS_PYTHON=true
|
((LANG_FILES[python] > 0)) && HAS_PYTHON=true
|
||||||
((${LANG_FILES[javascript]} + ${LANG_FILES[typescript]} > 0)) && HAS_JS_FAMILY=true
|
((LANG_FILES[javascript] + LANG_FILES[typescript] > 0)) && HAS_JS_FAMILY=true
|
||||||
((${LANG_FILES[shell]} > 0)) && HAS_SHELL=true
|
((LANG_FILES[shell] > 0)) && HAS_SHELL=true
|
||||||
((${LANG_FILES[ruby]} > 0)) && HAS_RUBY=true
|
((LANG_FILES[ruby] > 0)) && HAS_RUBY=true
|
||||||
((${LANG_FILES[go]} > 0)) && HAS_GO=true
|
((LANG_FILES[go] > 0)) && HAS_GO=true
|
||||||
((${LANG_FILES[rust]} > 0)) && HAS_RUST=true
|
((LANG_FILES[rust] > 0)) && HAS_RUST=true
|
||||||
((${LANG_FILES[java]} > 0)) && HAS_JAVA=true
|
((LANG_FILES[java] > 0)) && HAS_JAVA=true
|
||||||
|
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
# Language-specific keyword definitions
|
# Language-specific keyword definitions
|
||||||
@ -735,7 +735,7 @@ if [ -f "$RESULTS_DIR/tags" ]; then
|
|||||||
|
|
||||||
print_subheader "Symbol Types Distribution"
|
print_subheader "Symbol Types Distribution"
|
||||||
# Fast: extract single-letter kind code after ;" and count
|
# 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
|
case $kind in
|
||||||
f) echo "$count functions" ;;
|
f) echo "$count functions" ;;
|
||||||
v) echo "$count variables" ;;
|
v) echo "$count variables" ;;
|
||||||
|
|||||||
@ -32,14 +32,14 @@ cd "$TRACKS_DIR"
|
|||||||
|
|
||||||
# Tracks to download (add/remove as needed)
|
# Tracks to download (add/remove as needed)
|
||||||
declare -A TRACKS=(
|
declare -A TRACKS=(
|
||||||
["python"]="https://github.com/exercism/python.git"
|
["python"]="https://github.com/exercism/python.git"
|
||||||
["c"]="https://github.com/exercism/c.git"
|
["c"]="https://github.com/exercism/c.git"
|
||||||
["cpp"]="https://github.com/exercism/cpp.git"
|
["cpp"]="https://github.com/exercism/cpp.git"
|
||||||
["javascript"]="https://github.com/exercism/javascript.git"
|
["javascript"]="https://github.com/exercism/javascript.git"
|
||||||
["typescript"]="https://github.com/exercism/typescript.git"
|
["typescript"]="https://github.com/exercism/typescript.git"
|
||||||
["rust"]="https://github.com/exercism/rust.git"
|
["rust"]="https://github.com/exercism/rust.git"
|
||||||
["go"]="https://github.com/exercism/go.git"
|
["go"]="https://github.com/exercism/go.git"
|
||||||
["bash"]="https://github.com/exercism/bash.git"
|
["bash"]="https://github.com/exercism/bash.git"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optional tracks (uncomment to include)
|
# Optional tracks (uncomment to include)
|
||||||
@ -52,22 +52,22 @@ echo "Downloading ${#TRACKS[@]} tracks to: $TRACKS_DIR"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
for track in "${!TRACKS[@]}"; do
|
for track in "${!TRACKS[@]}"; do
|
||||||
url="${TRACKS[$track]}"
|
url="${TRACKS[$track]}"
|
||||||
|
|
||||||
if [[ -d $track ]]; then
|
if [[ -d $track ]]; then
|
||||||
info "Updating $track..."
|
info "Updating $track..."
|
||||||
(cd "$track" && git pull --quiet) && success "$track updated"
|
(cd "$track" && git pull --quiet) && success "$track updated"
|
||||||
else
|
else
|
||||||
info "Cloning $track..."
|
info "Cloning $track..."
|
||||||
git clone --depth 1 "$url" && success "$track cloned"
|
git clone --depth 1 "$url" && success "$track cloned"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Show exercise count
|
# Show exercise count
|
||||||
if [[ -d "$track/exercises/practice" ]]; then
|
if [[ -d "$track/exercises/practice" ]]; then
|
||||||
count=$(ls "$track/exercises/practice" | wc -l)
|
count=$(find "$track/exercises/practice" -mindepth 1 -maxdepth 1 | wc -l)
|
||||||
echo " → $count practice exercises available"
|
echo " → $count practice exercises available"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
@ -99,8 +99,8 @@ echo "=============================================="
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Track summary:"
|
echo "Track summary:"
|
||||||
for track in "${!TRACKS[@]}"; do
|
for track in "${!TRACKS[@]}"; do
|
||||||
if [[ -d "$track/exercises/practice" ]]; then
|
if [[ -d "$track/exercises/practice" ]]; then
|
||||||
count=$(ls "$track/exercises/practice" 2> /dev/null | wc -l)
|
count=$(find "$track/exercises/practice" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l)
|
||||||
printf " %-15s %3d exercises\n" "$track" "$count"
|
printf " %-15s %3d exercises\n" "$track" "$count"
|
||||||
fi
|
fi
|
||||||
done | sort
|
done | sort
|
||||||
|
|||||||
@ -498,8 +498,10 @@ if [ -d "$PER_LANG_DIR" ]; then
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Process keywords by language
|
# 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
|
for keyword_file in "$PER_LANG_DIR"/keywords_*.txt; do
|
||||||
[ ! -f "$keyword_file" ] && continue
|
[ ! -f "$keyword_file" ] && continue
|
||||||
@ -523,12 +525,14 @@ if [ -d "$PER_LANG_DIR" ]; then
|
|||||||
*) display_lang="$lang" ;;
|
*) display_lang="$lang" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "### $display_lang Keywords" >>"$DOCS_FILE"
|
{
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "### $display_lang Keywords"
|
||||||
echo "| Keyword | Count | Documentation |" >>"$DOCS_FILE"
|
echo ""
|
||||||
echo "|---------|-------|---------------|" >>"$DOCS_FILE"
|
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
|
[ -z "$term" ] && continue
|
||||||
[[ $term =~ ^[#] ]] && continue # Skip comment lines
|
[[ $term =~ ^[#] ]] && continue # Skip comment lines
|
||||||
url=$(get_doc_url "$term" "$doc_lang")
|
url=$(get_doc_url "$term" "$doc_lang")
|
||||||
@ -538,8 +542,10 @@ if [ -d "$PER_LANG_DIR" ]; then
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Process functions by language
|
# 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
|
for func_file in "$PER_LANG_DIR"/functions_*.txt; do
|
||||||
[ ! -f "$func_file" ] && continue
|
[ ! -f "$func_file" ] && continue
|
||||||
@ -561,12 +567,14 @@ if [ -d "$PER_LANG_DIR" ]; then
|
|||||||
*) display_lang="$lang" ;;
|
*) display_lang="$lang" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "### $display_lang Functions" >>"$DOCS_FILE"
|
{
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "### $display_lang Functions"
|
||||||
echo "| Function | Count | Documentation |" >>"$DOCS_FILE"
|
echo ""
|
||||||
echo "|----------|-------|---------------|" >>"$DOCS_FILE"
|
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
|
[ -z "$term" ] && continue
|
||||||
[[ $term =~ ^(if|for|while|switch|catch|elif)$ ]] && continue
|
[[ $term =~ ^(if|for|while|switch|catch|elif)$ ]] && continue
|
||||||
url=$(get_doc_url "$term" "$doc_lang")
|
url=$(get_doc_url "$term" "$doc_lang")
|
||||||
@ -576,8 +584,10 @@ if [ -d "$PER_LANG_DIR" ]; then
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Process imports by language
|
# 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
|
for import_file in "$PER_LANG_DIR"/imports_*.txt; do
|
||||||
[ ! -f "$import_file" ] && continue
|
[ ! -f "$import_file" ] && continue
|
||||||
@ -599,10 +609,12 @@ if [ -d "$PER_LANG_DIR" ]; then
|
|||||||
*) display_lang="$lang" ;;
|
*) display_lang="$lang" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "### $display_lang" >>"$DOCS_FILE"
|
{
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "### $display_lang"
|
||||||
echo "| Import | Count | Documentation |" >>"$DOCS_FILE"
|
echo ""
|
||||||
echo "|--------|-------|---------------|" >>"$DOCS_FILE"
|
echo "| Import | Count | Documentation |"
|
||||||
|
echo "|--------|-------|---------------|"
|
||||||
|
} >>"$DOCS_FILE"
|
||||||
|
|
||||||
head -20 "$import_file" | while read -r count import; do
|
head -20 "$import_file" | while read -r count import; do
|
||||||
[ -z "$import" ] && continue
|
[ -z "$import" ] && continue
|
||||||
@ -613,7 +625,7 @@ if [ -d "$PER_LANG_DIR" ]; then
|
|||||||
module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//')
|
module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//')
|
||||||
url=$(get_doc_url "$module" "$doc_lang")
|
url=$(get_doc_url "$module" "$doc_lang")
|
||||||
fi
|
fi
|
||||||
import_escaped=$(echo "$import" | sed 's/|/\\|/g')
|
import_escaped="${import//|/\\|}"
|
||||||
echo "| \`$import_escaped\` | $count | [docs]($url) |" >>"$DOCS_FILE"
|
echo "| \`$import_escaped\` | $count | [docs]($url) |" >>"$DOCS_FILE"
|
||||||
done
|
done
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "" >>"$DOCS_FILE"
|
||||||
@ -624,12 +636,14 @@ else
|
|||||||
echo -e "${YELLOW}No per-language files found, using combined analysis${NC}"
|
echo -e "${YELLOW}No per-language files found, using combined analysis${NC}"
|
||||||
|
|
||||||
if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then
|
if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then
|
||||||
echo "## Language Keywords" >>"$DOCS_FILE"
|
{
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "## Language Keywords"
|
||||||
echo "| Keyword | Count | Documentation |" >>"$DOCS_FILE"
|
echo ""
|
||||||
echo "|---------|-------|---------------|" >>"$DOCS_FILE"
|
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
|
[ -z "$term" ] && continue
|
||||||
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
||||||
echo "| \`$term\` | $count | [docs]($url) |" >>"$DOCS_FILE"
|
echo "| \`$term\` | $count | [docs]($url) |" >>"$DOCS_FILE"
|
||||||
@ -638,12 +652,14 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then
|
if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then
|
||||||
echo "## Function/Method Calls" >>"$DOCS_FILE"
|
{
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "## Function/Method Calls"
|
||||||
echo "| Function | Count | Documentation |" >>"$DOCS_FILE"
|
echo ""
|
||||||
echo "|----------|-------|---------------|" >>"$DOCS_FILE"
|
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
|
[ -z "$term" ] && continue
|
||||||
[[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue
|
[[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue
|
||||||
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
||||||
@ -653,25 +669,29 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$RESULTS_DIR/grep_imports.txt" ]; then
|
if [ -f "$RESULTS_DIR/grep_imports.txt" ]; then
|
||||||
echo "## Imports/Includes" >>"$DOCS_FILE"
|
{
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "## Imports/Includes"
|
||||||
echo "| Import | Count | Documentation |" >>"$DOCS_FILE"
|
echo ""
|
||||||
echo "|--------|-------|---------------|" >>"$DOCS_FILE"
|
echo "| Import | Count | Documentation |"
|
||||||
|
echo "|--------|-------|---------------|"
|
||||||
|
} >>"$DOCS_FILE"
|
||||||
|
|
||||||
head -20 "$RESULTS_DIR/grep_imports.txt" | while read -r count import; do
|
head -20 "$RESULTS_DIR/grep_imports.txt" | while read -r count import; do
|
||||||
[ -z "$import" ] && continue
|
[ -z "$import" ] && continue
|
||||||
module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//')
|
module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//')
|
||||||
url=$(get_doc_url "$module" "$PRIMARY_LANG")
|
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"
|
echo "| \`$import_escaped\` | $count | [docs]($url) |" >>"$DOCS_FILE"
|
||||||
done
|
done
|
||||||
echo "" >>"$DOCS_FILE"
|
echo "" >>"$DOCS_FILE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "" >>"$DOCS_FILE"
|
{
|
||||||
echo "---" >>"$DOCS_FILE"
|
echo ""
|
||||||
echo "*Generated by analyze_repo.sh + generate_study_materials.sh*" >>"$DOCS_FILE"
|
echo "---"
|
||||||
|
echo "*Generated by analyze_repo.sh + generate_study_materials.sh*"
|
||||||
|
} >>"$DOCS_FILE"
|
||||||
|
|
||||||
echo -e "${GREEN}Created: $DOCS_FILE${NC}"
|
echo -e "${GREEN}Created: $DOCS_FILE${NC}"
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
@ -694,7 +714,7 @@ EOF
|
|||||||
# Generate cards for top keywords
|
# Generate cards for top keywords
|
||||||
if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then
|
if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then
|
||||||
echo "# Keywords" >>"$ANKI_FILE"
|
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
|
[ -z "$term" ] && continue
|
||||||
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
||||||
|
|
||||||
@ -730,9 +750,11 @@ fi
|
|||||||
|
|
||||||
# Generate cards for top functions
|
# Generate cards for top functions
|
||||||
if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then
|
if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then
|
||||||
echo "" >>"$ANKI_FILE"
|
{
|
||||||
echo "# Functions" >>"$ANKI_FILE"
|
echo ""
|
||||||
head -$TOP_N "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do
|
echo "# Functions"
|
||||||
|
} >>"$ANKI_FILE"
|
||||||
|
head -n "$TOP_N" "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do
|
||||||
[ -z "$term" ] && continue
|
[ -z "$term" ] && continue
|
||||||
[[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue
|
[[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue
|
||||||
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
url=$(get_doc_url "$term" "$PRIMARY_LANG")
|
||||||
@ -780,7 +802,7 @@ generate_keywords_with_docs() {
|
|||||||
local keywords_file="$RESULTS_DIR/grep_keywords.txt"
|
local keywords_file="$RESULTS_DIR/grep_keywords.txt"
|
||||||
[ ! -f "$keywords_file" ] && echo "No keywords found" && return
|
[ ! -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
|
local count
|
||||||
count=$(echo "$line" | awk '{print $1}')
|
count=$(echo "$line" | awk '{print $1}')
|
||||||
local keyword
|
local keyword
|
||||||
@ -797,7 +819,7 @@ generate_functions_with_docs() {
|
|||||||
local functions_file="$RESULTS_DIR/grep_function_calls.txt"
|
local functions_file="$RESULTS_DIR/grep_function_calls.txt"
|
||||||
[ ! -f "$functions_file" ] && echo "No functions found" && return
|
[ ! -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
|
local count
|
||||||
count=$(echo "$line" | awk '{print $1}')
|
count=$(echo "$line" | awk '{print $1}')
|
||||||
local func
|
local func
|
||||||
|
|||||||
@ -34,286 +34,297 @@ echo ""
|
|||||||
|
|
||||||
# Install Exercism CLI
|
# Install Exercism CLI
|
||||||
install_exercism_cli() {
|
install_exercism_cli() {
|
||||||
if command -v exercism &> /dev/null; then
|
if command -v exercism &>/dev/null; then
|
||||||
local version
|
local version
|
||||||
version=$(exercism version 2> /dev/null | head -1)
|
version=$(exercism version 2>/dev/null | head -1)
|
||||||
success "Exercism CLI already installed: $version"
|
success "Exercism CLI already installed: $version"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Installing Exercism CLI..."
|
echo "Installing Exercism CLI..."
|
||||||
|
|
||||||
# Try package managers first
|
# Try package managers first
|
||||||
if command -v pacman &> /dev/null; then
|
if command -v pacman &>/dev/null; then
|
||||||
# Check AUR
|
# Check AUR
|
||||||
if command -v yay &> /dev/null; then
|
if command -v yay &>/dev/null; then
|
||||||
yay -S --noconfirm exercism-bin
|
yay -S --noconfirm exercism-bin
|
||||||
success "Exercism CLI installed via AUR"
|
success "Exercism CLI installed via AUR"
|
||||||
return 0
|
return 0
|
||||||
elif command -v paru &> /dev/null; then
|
elif command -v paru &>/dev/null; then
|
||||||
paru -S --noconfirm exercism-bin
|
paru -S --noconfirm exercism-bin
|
||||||
success "Exercism CLI installed via AUR"
|
success "Exercism CLI installed via AUR"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
elif command -v brew &> /dev/null; then
|
elif command -v brew &>/dev/null; then
|
||||||
brew install exercism
|
brew install exercism
|
||||||
success "Exercism CLI installed via Homebrew"
|
success "Exercism CLI installed via Homebrew"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Manual installation from GitHub releases
|
# Manual installation from GitHub releases
|
||||||
info "Installing from GitHub releases..."
|
info "Installing from GitHub releases..."
|
||||||
|
|
||||||
local arch
|
local arch
|
||||||
case "$(uname -m)" in
|
case "$(uname -m)" in
|
||||||
x86_64) arch="x86_64" ;;
|
x86_64) arch="x86_64" ;;
|
||||||
aarch64 | arm64) arch="arm64" ;;
|
aarch64 | arm64) arch="arm64" ;;
|
||||||
armv7l) arch="armv7" ;;
|
armv7l) arch="armv7" ;;
|
||||||
i686) arch="i386" ;;
|
i686) arch="i386" ;;
|
||||||
*)
|
*)
|
||||||
error "Unsupported architecture: $(uname -m)"
|
error "Unsupported architecture: $(uname -m)"
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
local os="linux"
|
local os="linux"
|
||||||
[[ "$(uname -s)" == "Darwin" ]] && os="darwin"
|
[[ "$(uname -s)" == "Darwin" ]] && os="darwin"
|
||||||
|
|
||||||
# Get latest release
|
# Get latest release
|
||||||
local latest_url="https://api.github.com/repos/exercism/cli/releases/latest"
|
local latest_url="https://api.github.com/repos/exercism/cli/releases/latest"
|
||||||
local download_url
|
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
|
if [[ -z $download_url ]]; then
|
||||||
error "Could not find download URL for your system"
|
error "Could not find download URL for your system"
|
||||||
echo "Please install manually from: https://exercism.org/docs/using/solving-exercises/working-locally"
|
echo "Please install manually from: https://exercism.org/docs/using/solving-exercises/working-locally"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Downloading from: $download_url"
|
echo "Downloading from: $download_url"
|
||||||
local temp_dir
|
local temp_dir
|
||||||
temp_dir=$(mktemp -d)
|
temp_dir=$(mktemp -d)
|
||||||
|
|
||||||
curl -fL --progress-bar "$download_url" -o "$temp_dir/exercism.tar.gz"
|
curl -fL --progress-bar "$download_url" -o "$temp_dir/exercism.tar.gz"
|
||||||
tar -xzf "$temp_dir/exercism.tar.gz" -C "$temp_dir"
|
tar -xzf "$temp_dir/exercism.tar.gz" -C "$temp_dir"
|
||||||
|
|
||||||
# Install to ~/.local/bin
|
# Install to ~/.local/bin
|
||||||
mkdir -p "$HOME/.local/bin"
|
mkdir -p "$HOME/.local/bin"
|
||||||
mv "$temp_dir/exercism" "$HOME/.local/bin/"
|
mv "$temp_dir/exercism" "$HOME/.local/bin/"
|
||||||
chmod +x "$HOME/.local/bin/exercism"
|
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
|
# Check PATH
|
||||||
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||||
warn "Add ~/.local/bin to your PATH:"
|
warn "Add ~/.local/bin to your PATH:"
|
||||||
echo ' export PATH="$HOME/.local/bin:$PATH"'
|
# Printing a line for the USER to paste into their shell rc: $HOME and
|
||||||
fi
|
# $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 workspace
|
||||||
configure_exercism() {
|
configure_exercism() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Configuring Exercism ==="
|
echo "=== Configuring Exercism ==="
|
||||||
|
|
||||||
mkdir -p "$EXERCISM_DIR"
|
mkdir -p "$EXERCISM_DIR"
|
||||||
|
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
if exercism configure 2>&1 | grep -q "workspace"; then
|
if exercism configure 2>&1 | grep -q "workspace"; then
|
||||||
success "Exercism already configured"
|
success "Exercism already configured"
|
||||||
else
|
else
|
||||||
# Set workspace directory
|
# Set workspace directory
|
||||||
exercism configure --workspace="$EXERCISM_DIR"
|
exercism configure --workspace="$EXERCISM_DIR"
|
||||||
success "Workspace set to: $EXERCISM_DIR"
|
success "Workspace set to: $EXERCISM_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
info "To fully configure Exercism with your account:"
|
info "To fully configure Exercism with your account:"
|
||||||
echo " 1. Create free account at https://exercism.org"
|
echo " 1. Create free account at https://exercism.org"
|
||||||
echo " 2. Go to https://exercism.org/settings/api_cli"
|
echo " 2. Go to https://exercism.org/settings/api_cli"
|
||||||
echo " 3. Copy your API token"
|
echo " 3. Copy your API token"
|
||||||
echo " 4. Run: exercism configure --token=YOUR_TOKEN"
|
echo " 4. Run: exercism configure --token=YOUR_TOKEN"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install test runners for languages
|
# Install test runners for languages
|
||||||
install_test_runners() {
|
install_test_runners() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Installing Test Runners ==="
|
echo "=== Installing Test Runners ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Python - pytest
|
# Python - pytest
|
||||||
if command -v python3 &> /dev/null; then
|
if command -v python3 &>/dev/null; then
|
||||||
if python3 -c "import pytest" 2> /dev/null; then
|
if python3 -c "import pytest" 2>/dev/null; then
|
||||||
success "Python: pytest already installed"
|
success "Python: pytest already installed"
|
||||||
else
|
else
|
||||||
info "Installing pytest for Python exercises..."
|
info "Installing pytest for Python exercises..."
|
||||||
pip3 install --user pytest 2> /dev/null && success "Python: pytest installed" || warn "Python: install pytest manually"
|
if pip3 install --user pytest 2>/dev/null; then
|
||||||
fi
|
success "Python: pytest installed"
|
||||||
fi
|
else
|
||||||
|
warn "Python: install pytest manually"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# JavaScript/TypeScript - Node.js + npm
|
# JavaScript/TypeScript - Node.js + npm
|
||||||
if command -v node &> /dev/null; then
|
if command -v node &>/dev/null; then
|
||||||
success "JavaScript/TypeScript: Node.js available ($(node --version))"
|
success "JavaScript/TypeScript: Node.js available ($(node --version))"
|
||||||
info " Tests run with: npm test (or jest)"
|
info " Tests run with: npm test (or jest)"
|
||||||
else
|
else
|
||||||
warn "JavaScript/TypeScript: Install Node.js for JS/TS exercises"
|
warn "JavaScript/TypeScript: Install Node.js for JS/TS exercises"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# C - gcc + criterion/cmocka
|
# C - gcc + criterion/cmocka
|
||||||
if command -v gcc &> /dev/null; then
|
if command -v gcc &>/dev/null; then
|
||||||
success "C: gcc available"
|
success "C: gcc available"
|
||||||
info " Some C exercises use Unity test framework (included in exercise)"
|
info " Some C exercises use Unity test framework (included in exercise)"
|
||||||
else
|
else
|
||||||
warn "C: Install gcc for C exercises"
|
warn "C: Install gcc for C exercises"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# C++ - g++ + Catch2/doctest
|
# C++ - g++ + Catch2/doctest
|
||||||
if command -v g++ &> /dev/null; then
|
if command -v g++ &>/dev/null; then
|
||||||
success "C++: g++ available"
|
success "C++: g++ available"
|
||||||
info " C++ exercises use Catch2 (header-only, included in exercise)"
|
info " C++ exercises use Catch2 (header-only, included in exercise)"
|
||||||
else
|
else
|
||||||
warn "C++: Install g++ for C++ exercises"
|
warn "C++: Install g++ for C++ exercises"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rust
|
# Rust
|
||||||
if command -v cargo &> /dev/null; then
|
if command -v cargo &>/dev/null; then
|
||||||
success "Rust: cargo available (tests with: cargo test)"
|
success "Rust: cargo available (tests with: cargo test)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Go
|
# Go
|
||||||
if command -v go &> /dev/null; then
|
if command -v go &>/dev/null; then
|
||||||
success "Go: go available (tests with: go test)"
|
success "Go: go available (tests with: go test)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Download exercises for a track (language)
|
# Download exercises for a track (language)
|
||||||
download_track() {
|
download_track() {
|
||||||
local track="$1"
|
local track="$1"
|
||||||
local count="${2:-10}"
|
local count="${2:-10}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
info "Downloading $count exercises for $track track..."
|
info "Downloading $count exercises for $track track..."
|
||||||
|
|
||||||
# Get list of exercises
|
# Get list of exercises
|
||||||
local exercises
|
local exercises
|
||||||
exercises=$(curl -fsSL "https://exercism.org/api/v2/tracks/${track}/exercises" 2> /dev/null |
|
exercises=$(curl -fsSL "https://exercism.org/api/v2/tracks/${track}/exercises" 2>/dev/null |
|
||||||
grep -oP '"slug":"\K[^"]+' | head -n "$count")
|
grep -oP '"slug":"\K[^"]+' | head -n "$count")
|
||||||
|
|
||||||
if [[ -z $exercises ]]; then
|
if [[ -z $exercises ]]; then
|
||||||
warn "Could not fetch exercise list for $track"
|
warn "Could not fetch exercise list for $track"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local downloaded=0
|
local downloaded=0
|
||||||
for exercise in $exercises; do
|
for exercise in $exercises; do
|
||||||
local exercise_dir="$EXERCISM_DIR/$track/$exercise"
|
local exercise_dir="$EXERCISM_DIR/$track/$exercise"
|
||||||
if [[ -d $exercise_dir ]]; then
|
if [[ -d $exercise_dir ]]; then
|
||||||
echo " [exists] $exercise"
|
echo " [exists] $exercise"
|
||||||
else
|
else
|
||||||
if exercism download --track="$track" --exercise="$exercise" 2> /dev/null; then
|
if exercism download --track="$track" --exercise="$exercise" 2>/dev/null; then
|
||||||
echo " [downloaded] $exercise"
|
echo " [downloaded] $exercise"
|
||||||
((downloaded++))
|
((downloaded++))
|
||||||
else
|
else
|
||||||
echo " [failed] $exercise (may require auth)"
|
echo " [failed] $exercise (may require auth)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
success "Downloaded $downloaded new exercises for $track"
|
success "Downloaded $downloaded new exercises for $track"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show available tracks and usage
|
# Show available tracks and usage
|
||||||
show_usage() {
|
show_usage() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
echo " Exercism Usage Guide"
|
echo " Exercism Usage Guide"
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}Download exercises:${NC}"
|
echo -e "${CYAN}Download exercises:${NC}"
|
||||||
echo " exercism download --track=python --exercise=hello-world"
|
echo " exercism download --track=python --exercise=hello-world"
|
||||||
echo " exercism download --track=javascript --exercise=two-fer"
|
echo " exercism download --track=javascript --exercise=two-fer"
|
||||||
echo " exercism download --track=c --exercise=isogram"
|
echo " exercism download --track=c --exercise=isogram"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}Run tests locally:${NC}"
|
echo -e "${CYAN}Run tests locally:${NC}"
|
||||||
echo " Python: cd ~/exercism/python/hello-world && pytest"
|
echo " Python: cd ~/exercism/python/hello-world && pytest"
|
||||||
echo " JavaScript: cd ~/exercism/javascript/hello-world && npm test"
|
echo " JavaScript: cd ~/exercism/javascript/hello-world && npm test"
|
||||||
echo " TypeScript: cd ~/exercism/typescript/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/c/hello-world && make test"
|
||||||
echo " C++: cd ~/exercism/cpp/hello-world && make"
|
echo " C++: cd ~/exercism/cpp/hello-world && make"
|
||||||
echo " Rust: cd ~/exercism/rust/hello-world && cargo test"
|
echo " Rust: cd ~/exercism/rust/hello-world && cargo test"
|
||||||
echo " Go: cd ~/exercism/go/hello-world && go test"
|
echo " Go: cd ~/exercism/go/hello-world && go test"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}Submit solution (when online):${NC}"
|
echo -e "${CYAN}Submit solution (when online):${NC}"
|
||||||
echo " exercism submit solution.py"
|
echo " exercism submit solution.py"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}Popular tracks:${NC}"
|
echo -e "${CYAN}Popular tracks:${NC}"
|
||||||
echo " python, javascript, typescript, c, cpp, rust, go, java, ruby"
|
echo " python, javascript, typescript, c, cpp, rust, go, java, ruby"
|
||||||
echo " bash, elixir, haskell, kotlin, swift, csharp, php, sql"
|
echo " bash, elixir, haskell, kotlin, swift, csharp, php, sql"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}Batch download (requires API token):${NC}"
|
echo -e "${CYAN}Batch download (requires API token):${NC}"
|
||||||
echo " # Download first 20 Python exercises:"
|
echo " # Download first 20 Python exercises:"
|
||||||
echo ' for ex in $(exercism download --track=python 2>&1 | head -20); do'
|
# Example commands printed for the user to copy; $(...) and $ex must stay
|
||||||
echo ' exercism download --track=python --exercise=$ex'
|
# literal rather than execute here.
|
||||||
echo " done"
|
# shellcheck disable=SC2016
|
||||||
echo ""
|
echo ' for ex in $(exercism download --track=python 2>&1 | head -20); do'
|
||||||
echo "Exercises are in: $EXERCISM_DIR"
|
# shellcheck disable=SC2016
|
||||||
echo ""
|
echo ' exercism download --track=python --exercise=$ex'
|
||||||
echo "=============================================="
|
echo " done"
|
||||||
|
echo ""
|
||||||
|
echo "Exercises are in: $EXERCISM_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "=============================================="
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
main() {
|
main() {
|
||||||
# Step 1: Install CLI
|
# Step 1: Install CLI
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Step 1: Installing Exercism CLI ==="
|
echo "=== Step 1: Installing Exercism CLI ==="
|
||||||
install_exercism_cli
|
install_exercism_cli
|
||||||
|
|
||||||
# Step 2: Configure
|
# Step 2: Configure
|
||||||
configure_exercism
|
configure_exercism
|
||||||
|
|
||||||
# Step 3: Install test runners
|
# Step 3: Install test runners
|
||||||
install_test_runners
|
install_test_runners
|
||||||
|
|
||||||
# Step 4: Download sample exercises
|
# Step 4: Download sample exercises
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Step 4: Downloading Sample Exercises ==="
|
echo "=== Step 4: Downloading Sample Exercises ==="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Downloading a few starter exercises for common languages..."
|
echo "Downloading a few starter exercises for common languages..."
|
||||||
echo "(Full download requires API token from exercism.org)"
|
echo "(Full download requires API token from exercism.org)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Try to download hello-world for each track
|
# Try to download hello-world for each track
|
||||||
local tracks=("python" "javascript" "typescript" "c" "cpp")
|
local tracks=("python" "javascript" "typescript" "c" "cpp")
|
||||||
|
|
||||||
for track in "${tracks[@]}"; do
|
for track in "${tracks[@]}"; do
|
||||||
local exercise_dir="$EXERCISM_DIR/$track/hello-world"
|
local exercise_dir="$EXERCISM_DIR/$track/hello-world"
|
||||||
if [[ -d $exercise_dir ]]; then
|
if [[ -d $exercise_dir ]]; then
|
||||||
echo " [$track] hello-world already exists"
|
echo " [$track] hello-world already exists"
|
||||||
else
|
else
|
||||||
if exercism download --track="$track" --exercise="hello-world" 2> /dev/null; then
|
if exercism download --track="$track" --exercise="hello-world" 2>/dev/null; then
|
||||||
success "[$track] hello-world downloaded"
|
success "[$track] hello-world downloaded"
|
||||||
else
|
else
|
||||||
warn "[$track] hello-world requires authentication"
|
warn "[$track] hello-world requires authentication"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Show usage
|
# Show usage
|
||||||
show_usage
|
show_usage
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
success "Installation complete!"
|
success "Installation complete!"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Sign up at https://exercism.org (free)"
|
echo " 1. Sign up at https://exercism.org (free)"
|
||||||
echo " 2. Get your token from https://exercism.org/settings/api_cli"
|
echo " 2. Get your token from https://exercism.org/settings/api_cli"
|
||||||
echo " 3. Run: exercism configure --token=YOUR_TOKEN"
|
echo " 3. Run: exercism configure --token=YOUR_TOKEN"
|
||||||
echo " 4. Download exercises and code offline!"
|
echo " 4. Download exercises and code offline!"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@ -39,40 +39,43 @@ echo ""
|
|||||||
echo "=== 1. Installing Python NLP-based Plagiarism Tools ==="
|
echo "=== 1. Installing Python NLP-based Plagiarism Tools ==="
|
||||||
|
|
||||||
# Check for Python 3
|
# Check for Python 3
|
||||||
if ! command -v python3 &> /dev/null; then
|
if ! command -v python3 &>/dev/null; then
|
||||||
error "Python 3 is required but not installed."
|
error "Python 3 is required but not installed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create virtual environment
|
# Create virtual environment
|
||||||
if [ ! -d "$VENV_DIR" ]; then
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
echo "Creating Python virtual environment..."
|
echo "Creating Python virtual environment..."
|
||||||
python3 -m venv "$VENV_DIR"
|
python3 -m venv "$VENV_DIR"
|
||||||
success "Virtual environment created at $VENV_DIR"
|
success "Virtual environment created at $VENV_DIR"
|
||||||
else
|
else
|
||||||
warn "Virtual environment already exists at $VENV_DIR"
|
warn "Virtual environment already exists at $VENV_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Activate and install packages
|
# 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"
|
source "$VENV_DIR/bin/activate"
|
||||||
|
|
||||||
echo "Installing Python packages for text similarity detection..."
|
echo "Installing Python packages for text similarity detection..."
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
|
|
||||||
pip install --progress-bar on \
|
pip install --progress-bar on \
|
||||||
scikit-learn \
|
scikit-learn \
|
||||||
nltk \
|
nltk \
|
||||||
spacy \
|
spacy \
|
||||||
gensim \
|
gensim \
|
||||||
numpy \
|
numpy \
|
||||||
pandas \
|
pandas \
|
||||||
python-docx \
|
python-docx \
|
||||||
PyPDF2 \
|
PyPDF2 \
|
||||||
beautifulsoup4 \
|
beautifulsoup4 \
|
||||||
lxml \
|
lxml \
|
||||||
textdistance \
|
textdistance \
|
||||||
fuzzywuzzy \
|
fuzzywuzzy \
|
||||||
python-Levenshtein
|
python-Levenshtein
|
||||||
|
|
||||||
success "Python NLP packages installed"
|
success "Python NLP packages installed"
|
||||||
|
|
||||||
@ -90,11 +93,11 @@ success "NLTK data downloaded"
|
|||||||
|
|
||||||
# Download spaCy English model (small)
|
# Download spaCy English model (small)
|
||||||
echo "Downloading spaCy English model..."
|
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"
|
success "spaCy model installed"
|
||||||
|
|
||||||
# Create a simple plagiarism checker script
|
# Create a simple plagiarism checker script
|
||||||
cat > "$INSTALL_DIR/check_plagiarism.py" << 'PYEOF'
|
cat >"$INSTALL_DIR/check_plagiarism.py" <<'PYEOF'
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Simple Text Plagiarism Checker
|
Simple Text Plagiarism Checker
|
||||||
@ -325,7 +328,7 @@ success "Created plagiarism checker script at $INSTALL_DIR/check_plagiarism.py"
|
|||||||
|
|
||||||
# Create convenience wrapper
|
# Create convenience wrapper
|
||||||
mkdir -p "$HOME/.local/bin"
|
mkdir -p "$HOME/.local/bin"
|
||||||
cat > "$HOME/.local/bin/plagcheck" << WRAPEOF
|
cat >"$HOME/.local/bin/plagcheck" <<WRAPEOF
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Wrapper for plagiarism checker
|
# Wrapper for plagiarism checker
|
||||||
source "$VENV_DIR/bin/activate"
|
source "$VENV_DIR/bin/activate"
|
||||||
@ -344,14 +347,14 @@ echo "=== 2. Installing Sherlock Text Plagiarism Detector ==="
|
|||||||
|
|
||||||
SHERLOCK_DIR="$INSTALL_DIR/sherlock"
|
SHERLOCK_DIR="$INSTALL_DIR/sherlock"
|
||||||
if [ ! -d "$SHERLOCK_DIR" ]; then
|
if [ ! -d "$SHERLOCK_DIR" ]; then
|
||||||
# There are several Sherlock implementations; using a popular Python one
|
# There are several Sherlock implementations; using a popular Python one
|
||||||
if command -v git &> /dev/null; then
|
if command -v git &>/dev/null; then
|
||||||
# Clone a text-based similarity tool
|
# Clone a text-based similarity tool
|
||||||
git clone --depth 1 https://github.com/Zedeldi/sherlock-py.git "$SHERLOCK_DIR" 2> /dev/null || {
|
git clone --depth 1 https://github.com/Zedeldi/sherlock-py.git "$SHERLOCK_DIR" 2>/dev/null || {
|
||||||
warn "Could not clone sherlock-py, trying alternative..."
|
warn "Could not clone sherlock-py, trying alternative..."
|
||||||
# Alternative: Create a simple n-gram based sherlock
|
# Alternative: Create a simple n-gram based sherlock
|
||||||
mkdir -p "$SHERLOCK_DIR"
|
mkdir -p "$SHERLOCK_DIR"
|
||||||
cat > "$SHERLOCK_DIR/sherlock.py" << 'SHERLOCKEOF'
|
cat >"$SHERLOCK_DIR/sherlock.py" <<'SHERLOCKEOF'
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Sherlock - Simple text plagiarism detector using n-gram fingerprinting.
|
Sherlock - Simple text plagiarism detector using n-gram fingerprinting.
|
||||||
@ -443,14 +446,14 @@ def main():
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
SHERLOCKEOF
|
SHERLOCKEOF
|
||||||
chmod +x "$SHERLOCK_DIR/sherlock.py"
|
chmod +x "$SHERLOCK_DIR/sherlock.py"
|
||||||
}
|
}
|
||||||
success "Sherlock installed at $SHERLOCK_DIR"
|
success "Sherlock installed at $SHERLOCK_DIR"
|
||||||
else
|
else
|
||||||
warn "Git not available, skipping Sherlock installation"
|
warn "Git not available, skipping Sherlock installation"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "Sherlock already installed at $SHERLOCK_DIR"
|
warn "Sherlock already installed at $SHERLOCK_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@ -459,17 +462,17 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "=== 3. Checking for Ferret (Java-based plagiarism tool) ==="
|
echo "=== 3. Checking for Ferret (Java-based plagiarism tool) ==="
|
||||||
|
|
||||||
if command -v java &> /dev/null; then
|
if command -v java &>/dev/null; then
|
||||||
FERRET_DIR="$INSTALL_DIR/ferret"
|
FERRET_DIR="$INSTALL_DIR/ferret"
|
||||||
if [ ! -d "$FERRET_DIR" ]; then
|
if [ ! -d "$FERRET_DIR" ]; then
|
||||||
mkdir -p "$FERRET_DIR"
|
mkdir -p "$FERRET_DIR"
|
||||||
echo "Ferret is a Java-based tool from University of Hertfordshire."
|
echo "Ferret is a Java-based tool from University of Hertfordshire."
|
||||||
echo "Download manually from: https://homepages.herts.ac.uk/~comqcln/Ferret/"
|
echo "Download manually from: https://homepages.herts.ac.uk/~comqcln/Ferret/"
|
||||||
echo "Place JAR file in: $FERRET_DIR"
|
echo "Place JAR file in: $FERRET_DIR"
|
||||||
warn "Ferret requires manual download (academic license)"
|
warn "Ferret requires manual download (academic license)"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "Java not installed, skipping Ferret"
|
warn "Java not installed, skipping Ferret"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@ -478,16 +481,16 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "=== 4. WCopyfind Information (Windows tool, needs Wine) ==="
|
echo "=== 4. WCopyfind Information (Windows tool, needs Wine) ==="
|
||||||
|
|
||||||
if command -v wine &> /dev/null; then
|
if command -v wine &>/dev/null; then
|
||||||
echo "Wine is available. WCopyfind can be run via Wine."
|
echo "Wine is available. WCopyfind can be run via Wine."
|
||||||
echo "Download from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/"
|
echo "Download from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/"
|
||||||
echo "Run with: wine /path/to/WCopyfind.exe"
|
echo "Run with: wine /path/to/WCopyfind.exe"
|
||||||
warn "WCopyfind requires manual download"
|
warn "WCopyfind requires manual download"
|
||||||
else
|
else
|
||||||
echo "Wine not installed. To use WCopyfind:"
|
echo "Wine not installed. To use WCopyfind:"
|
||||||
echo " 1. Install wine: sudo apt install wine (or equivalent)"
|
echo " 1. Install wine: sudo apt install wine (or equivalent)"
|
||||||
echo " 2. Download WCopyfind from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/"
|
echo " 2. Download WCopyfind from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/"
|
||||||
warn "WCopyfind skipped (Wine not available)"
|
warn "WCopyfind skipped (Wine not available)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@ -521,12 +524,18 @@ echo " plagcheck thesis_v1.pdf thesis_v2.pdf --detailed"
|
|||||||
echo " plagcheck --dir ./student_papers/ --threshold 0.4"
|
echo " plagcheck --dir ./student_papers/ --threshold 0.4"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Note: Ensure ~/.local/bin is in your PATH:"
|
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 ' export PATH="$HOME/.local/bin:$PATH"'
|
||||||
echo ""
|
echo ""
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
|
|
||||||
# Add to PATH reminder
|
# Add to PATH reminder
|
||||||
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||||
warn "Add ~/.local/bin to your PATH by adding this to ~/.bashrc or ~/.zshrc:"
|
warn "Add ~/.local/bin to your PATH by adding this to ~/.bashrc or ~/.zshrc:"
|
||||||
echo ' export PATH="$HOME/.local/bin:$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"'
|
||||||
fi
|
fi
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,11 @@ ensure_adb_installed() {
|
|||||||
if command -v pacman &>/dev/null; then
|
if command -v pacman &>/dev/null; then
|
||||||
sudo pacman -S --noconfirm android-tools || die "Failed to install android-tools"
|
sudo pacman -S --noconfirm android-tools || die "Failed to install android-tools"
|
||||||
elif command -v apt-get &>/dev/null; then
|
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
|
elif command -v dnf &>/dev/null; then
|
||||||
sudo dnf install -y android-tools || die "Failed to install android-tools"
|
sudo dnf install -y android-tools || die "Failed to install android-tools"
|
||||||
else
|
else
|
||||||
|
|||||||
@ -42,10 +42,12 @@ fix_cloudflare_key() {
|
|||||||
|
|
||||||
if [[ ${expired:-0} -gt 0 ]]; then
|
if [[ ${expired:-0} -gt 0 ]]; then
|
||||||
log "Refreshing expired Cloudflare WARP GPG key..."
|
log "Refreshing expired Cloudflare WARP GPG key..."
|
||||||
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg \
|
if curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg |
|
||||||
| gpg --yes --dearmor -o "$keyring" 2>/dev/null \
|
gpg --yes --dearmor -o "$keyring" 2>/dev/null; then
|
||||||
&& log "Cloudflare key refreshed." \
|
log "Cloudflare key refreshed."
|
||||||
|| log "WARNING: Could not refresh Cloudflare key (network issue?). Skipping."
|
else
|
||||||
|
log "WARNING: Could not refresh Cloudflare key (network issue?). Skipping."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,8 +68,8 @@ fix_wine_legacy_key() {
|
|||||||
|
|
||||||
# Export key to modern location
|
# Export key to modern location
|
||||||
gpg --no-default-keyring --keyring "$legacy_keyring" \
|
gpg --no-default-keyring --keyring "$legacy_keyring" \
|
||||||
--export "$wine_key_id" \
|
--export "$wine_key_id" |
|
||||||
| gpg --yes --dearmor -o "$modern_keyring" 2>/dev/null
|
gpg --yes --dearmor -o "$modern_keyring" 2>/dev/null
|
||||||
|
|
||||||
# Remove from legacy keyring (suppress the deprecation warning)
|
# Remove from legacy keyring (suppress the deprecation warning)
|
||||||
apt-key del "$wine_key_id" >/dev/null 2>&1 || true
|
apt-key del "$wine_key_id" >/dev/null 2>&1 || true
|
||||||
|
|||||||
@ -8,22 +8,22 @@ REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
|||||||
TARGET_SCRIPT="$REPO_DIR/scripts/periodic_background/utils/android_guardian/service.sh"
|
TARGET_SCRIPT="$REPO_DIR/scripts/periodic_background/utils/android_guardian/service.sh"
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf 'FAIL: %s\n' "$1" >&2
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_equals() {
|
assert_equals() {
|
||||||
local expected="$1"
|
local expected="$1"
|
||||||
local actual="$2"
|
local actual="$2"
|
||||||
local context="$3"
|
local context="$3"
|
||||||
if [[ "$expected" != "$actual" ]]; then
|
if [[ "$expected" != "$actual" ]]; then
|
||||||
fail "$context (expected: '$expected', actual: '$actual')"
|
fail "$context (expected: '$expected', actual: '$actual')"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$TMP_DIR"
|
rm -rf "$TMP_DIR"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
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'
|
printf 'Checking skip-main avoids boot side effects...\n'
|
||||||
SKIP_MAIN_GUARDIAN_DIR="$TMP_DIR/skip-main-guardian"
|
SKIP_MAIN_GUARDIAN_DIR="$TMP_DIR/skip-main-guardian"
|
||||||
ANDROID_GUARDIAN_SKIP_MAIN=1 \
|
ANDROID_GUARDIAN_SKIP_MAIN=1 \
|
||||||
ANDROID_GUARDIAN_DIR="$SKIP_MAIN_GUARDIAN_DIR" \
|
ANDROID_GUARDIAN_DIR="$SKIP_MAIN_GUARDIAN_DIR" \
|
||||||
ANDROID_GUARDIAN_MODULE_DIR="$TMP_DIR/skip-main-module" \
|
ANDROID_GUARDIAN_MODULE_DIR="$TMP_DIR/skip-main-module" \
|
||||||
sh "$TARGET_SCRIPT"
|
sh "$TARGET_SCRIPT"
|
||||||
|
|
||||||
[[ ! -d "$SKIP_MAIN_GUARDIAN_DIR" ]] \
|
[[ ! -d "$SKIP_MAIN_GUARDIAN_DIR" ]] ||
|
||||||
|| fail 'skip-main should prevent guardian boot setup side effects'
|
fail 'skip-main should prevent guardian boot setup side effects'
|
||||||
|
|
||||||
printf 'Checking guardian loop cadence constants...\n'
|
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)
|
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'
|
assert_equals '5' "$sleep_seconds" 'guardian loop should keep the 5 second base sleep'
|
||||||
|
|
||||||
printf 'Checking guardian loop protects module every tick...\n'
|
printf 'Checking guardian loop protects module every tick...\n'
|
||||||
grep -q 'protect_module' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" \
|
grep -q 'protect_module' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" ||
|
||||||
|| fail 'guardian loop must always protect the module each tick'
|
fail 'guardian loop must always protect the module each tick'
|
||||||
|
|
||||||
printf 'Checking blocked-app scans are cached per pass...\n'
|
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" \
|
# Literal search pattern: the $ must reach grep as text, not be expanded
|
||||||
|| fail 'guardian service should cache installed packages once per blocked-app scan'
|
# 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
|
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
|
fi
|
||||||
|
|
||||||
printf 'Checking guardian loop uses skip-main guard for tests...\n'
|
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" \
|
grep -q 'ANDROID_GUARDIAN_SKIP_MAIN' "$WORKTREE/scripts/periodic_background/utils/android_guardian/service.sh" ||
|
||||||
|| fail 'guardian service should support skip-main testing'
|
fail 'guardian service should support skip-main testing'
|
||||||
|
|
||||||
printf 'android_guardian service regression checks passed.\n'
|
printf 'android_guardian service regression checks passed.\n'
|
||||||
|
|||||||
@ -60,7 +60,7 @@ assert_order() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( first_line < second_line )); then
|
if ((first_line < second_line)); then
|
||||||
echo "PASS: $message"
|
echo "PASS: $message"
|
||||||
else
|
else
|
||||||
echo "FAIL: $message"
|
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' \
|
assert_contains "$WRAPPER_FILE" 'manual_guard_lib_fallback=1' \
|
||||||
"wrapper tracks whether manual hosts guard fallback was used"
|
"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/' \
|
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"
|
"installer deploys shared hosts guard hook helpers"
|
||||||
|
|
||||||
|
|||||||
@ -13,209 +13,212 @@ echo ""
|
|||||||
# Test 1: Check wrapper syntax
|
# Test 1: Check wrapper syntax
|
||||||
echo "[TEST 1] Checking wrapper script syntax..."
|
echo "[TEST 1] Checking wrapper script syntax..."
|
||||||
if bash -n "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
if bash -n "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
||||||
echo "✓ Wrapper script syntax is valid"
|
echo "✓ Wrapper script syntax is valid"
|
||||||
else
|
else
|
||||||
echo "✗ Wrapper script has syntax errors"
|
echo "✗ Wrapper script has syntax errors"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 2: Check installer syntax
|
# Test 2: Check installer syntax
|
||||||
echo "[TEST 2] Checking installer script syntax..."
|
echo "[TEST 2] Checking installer script syntax..."
|
||||||
if bash -n "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
if bash -n "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
||||||
echo "✓ Installer script syntax is valid"
|
echo "✓ Installer script syntax is valid"
|
||||||
else
|
else
|
||||||
echo "✗ Installer script has syntax errors"
|
echo "✗ Installer script has syntax errors"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 3: Check VirtualBox enforcement script syntax
|
# Test 3: Check VirtualBox enforcement script syntax
|
||||||
echo "[TEST 3] Checking VirtualBox enforcement script syntax..."
|
echo "[TEST 3] Checking VirtualBox enforcement script syntax..."
|
||||||
if bash -n "$VBOX_DIR/enforce_vbox_hosts.sh"; then
|
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
|
else
|
||||||
echo "✗ VirtualBox enforcement script has syntax errors"
|
echo "✗ VirtualBox enforcement script has syntax errors"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 4: Verify integrity check function exists
|
# Test 4: Verify integrity check function exists
|
||||||
echo "[TEST 4] Verifying integrity check function exists in wrapper..."
|
echo "[TEST 4] Verifying integrity check function exists in wrapper..."
|
||||||
if grep -q "verify_policy_integrity()" "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
if grep -q "verify_policy_integrity()" "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
||||||
echo "✓ Integrity verification function found"
|
echo "✓ Integrity verification function found"
|
||||||
else
|
else
|
||||||
echo "✗ Integrity verification function not found"
|
echo "✗ Integrity verification function not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 5: Verify hardcoded VirtualBox cleanup function exists
|
# Test 5: Verify hardcoded VirtualBox cleanup function exists
|
||||||
echo "[TEST 5] Verifying 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
|
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
|
else
|
||||||
echo "✗ Hardcoded VirtualBox cleanup function not found"
|
echo "✗ Hardcoded VirtualBox cleanup function not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 6: Verify VirtualBox cleanup uses VBoxManage directly
|
# Test 6: Verify VirtualBox cleanup uses VBoxManage directly
|
||||||
echo "[TEST 6] Verifying VirtualBox cleanup uses VBoxManage directly..."
|
echo "[TEST 6] Verifying VirtualBox cleanup uses VBoxManage directly..."
|
||||||
if grep -q "VBoxManage" "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
if grep -q "VBoxManage" "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
||||||
echo "✓ VirtualBox cleanup logic found"
|
echo "✓ VirtualBox cleanup logic found"
|
||||||
else
|
else
|
||||||
echo "✗ VirtualBox cleanup logic not found"
|
echo "✗ VirtualBox cleanup logic not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 7: Verify integrity check is called early in execution
|
# Test 7: Verify integrity check is called early in execution
|
||||||
echo "[TEST 7] Verifying integrity check is called before operations..."
|
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
|
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
|
else
|
||||||
echo "✗ Integrity check not found in early execution"
|
echo "✗ Integrity check not found in early execution"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 8: Verify installer creates integrity file
|
# Test 8: Verify installer creates integrity file
|
||||||
echo "[TEST 8] Verifying installer creates integrity checksums..."
|
echo "[TEST 8] Verifying installer creates integrity checksums..."
|
||||||
if grep -q "INTEGRITY_FILE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
if grep -q "INTEGRITY_FILE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
||||||
echo "✓ Installer references integrity file"
|
echo "✓ Installer references integrity file"
|
||||||
else
|
else
|
||||||
echo "✗ Installer does not create integrity file"
|
echo "✗ Installer does not create integrity file"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 9: Verify installer uses chattr to make files immutable
|
# Test 9: Verify installer uses chattr to make files immutable
|
||||||
echo "[TEST 9] Verifying installer makes policy files immutable..."
|
echo "[TEST 9] Verifying installer makes policy files immutable..."
|
||||||
if grep -q "chattr +i" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
if grep -q "chattr +i" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
||||||
echo "✓ Installer sets immutable attributes"
|
echo "✓ Installer sets immutable attributes"
|
||||||
else
|
else
|
||||||
echo "✗ Installer does not set immutable attributes"
|
echo "✗ Installer does not set immutable attributes"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 10: Verify VirtualBox cleanup enforcement is integrated
|
# Test 10: Verify VirtualBox cleanup enforcement is integrated
|
||||||
echo "[TEST 10] Verifying VirtualBox cleanup is integrated into wrapper..."
|
echo "[TEST 10] Verifying VirtualBox cleanup is integrated into wrapper..."
|
||||||
if grep -q "auto_remove_virtualbox_vms" "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
if grep -q "auto_remove_virtualbox_vms" "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
||||||
echo "✓ VirtualBox cleanup integration found"
|
echo "✓ VirtualBox cleanup integration found"
|
||||||
else
|
else
|
||||||
echo "✗ VirtualBox cleanup integration not found"
|
echo "✗ VirtualBox cleanup integration not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 11: Verify VirtualBox script can show help
|
# Test 11: Verify VirtualBox script can show help
|
||||||
echo "[TEST 11] Testing VirtualBox enforcement script 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)
|
# 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
|
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
|
else
|
||||||
echo "✗ VirtualBox enforcement script help text not found"
|
echo "✗ VirtualBox enforcement script help text not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 12: Verify installer installs VirtualBox enforcement script
|
# Test 12: Verify installer installs VirtualBox enforcement script
|
||||||
echo "[TEST 12] Verifying installer handles VirtualBox enforcement script..."
|
echo "[TEST 12] Verifying installer handles VirtualBox enforcement script..."
|
||||||
if grep -q "VBOX_ENFORCE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
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
|
else
|
||||||
echo "✗ Installer does not include VirtualBox enforcement script"
|
echo "✗ Installer does not include VirtualBox enforcement script"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 13: Verify makepkg capped wrapper script syntax
|
# Test 13: Verify makepkg capped wrapper script syntax
|
||||||
echo "[TEST 13] Checking makepkg capped wrapper syntax..."
|
echo "[TEST 13] Checking makepkg capped wrapper syntax..."
|
||||||
if bash -n "$WRAPPER_DIR/makepkg_capped.sh"; then
|
if bash -n "$WRAPPER_DIR/makepkg_capped.sh"; then
|
||||||
echo "✓ makepkg capped wrapper syntax is valid"
|
echo "✓ makepkg capped wrapper syntax is valid"
|
||||||
else
|
else
|
||||||
echo "✗ makepkg capped wrapper has syntax errors"
|
echo "✗ makepkg capped wrapper has syntax errors"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 14: Verify pacman wrapper exposes makepkg capped command
|
# Test 14: Verify pacman wrapper exposes makepkg capped command
|
||||||
echo "[TEST 14] Verifying pacman wrapper supports --makepkg-capped..."
|
echo "[TEST 14] Verifying pacman wrapper supports --makepkg-capped..."
|
||||||
if grep -q -- "--makepkg-capped" "$WRAPPER_DIR/pacman_wrapper.sh"; then
|
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
|
else
|
||||||
echo "✗ pacman wrapper makepkg capped command missing"
|
echo "✗ pacman wrapper makepkg capped command missing"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 15: Verify installer deploys makepkg capped wrapper
|
# Test 15: Verify installer deploys makepkg capped wrapper
|
||||||
echo "[TEST 15] Verifying 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
|
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
|
else
|
||||||
echo "✗ Installer does not include makepkg capped deployment"
|
echo "✗ Installer does not include makepkg capped deployment"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 16: Verify mkpkg helper script syntax
|
# Test 16: Verify mkpkg helper script syntax
|
||||||
echo "[TEST 16] Checking mkpkg helper script syntax..."
|
echo "[TEST 16] Checking mkpkg helper script syntax..."
|
||||||
if bash -n "$WRAPPER_DIR/mkpkg.sh"; then
|
if bash -n "$WRAPPER_DIR/mkpkg.sh"; then
|
||||||
echo "✓ mkpkg helper script syntax is valid"
|
echo "✓ mkpkg helper script syntax is valid"
|
||||||
else
|
else
|
||||||
echo "✗ mkpkg helper script has syntax errors"
|
echo "✗ mkpkg helper script has syntax errors"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 17: Verify installer deploys mkpkg helper
|
# Test 17: Verify installer deploys mkpkg helper
|
||||||
echo "[TEST 17] Verifying installer deploys mkpkg helper..."
|
echo "[TEST 17] Verifying installer deploys mkpkg helper..."
|
||||||
if grep -q "MKPKG" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
if grep -q "MKPKG" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
||||||
echo "✓ Installer includes mkpkg helper deployment"
|
echo "✓ Installer includes mkpkg helper deployment"
|
||||||
else
|
else
|
||||||
echo "✗ Installer does not include mkpkg helper deployment"
|
echo "✗ Installer does not include mkpkg helper deployment"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 18: Verify installer runs in strict mode
|
# Test 18: Verify installer runs in strict mode
|
||||||
echo "[TEST 18] Verifying installer uses strict shell mode..."
|
echo "[TEST 18] Verifying installer uses strict shell mode..."
|
||||||
if grep -q "set -euo pipefail" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
if grep -q "set -euo pipefail" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
|
||||||
echo "✓ Installer strict mode enabled"
|
echo "✓ Installer strict mode enabled"
|
||||||
else
|
else
|
||||||
echo "✗ Installer strict mode not enabled"
|
echo "✗ Installer strict mode not enabled"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 19: Verify installer handles immutable files during updates
|
# Test 19: Verify installer handles immutable files during updates
|
||||||
echo "[TEST 19] Verifying installer unlocks immutable files before copy/write..."
|
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
|
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
|
else
|
||||||
echo "✗ Installer immutable-file handling missing"
|
echo "✗ Installer immutable-file handling missing"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test: shared lock library syntax
|
# Test: shared lock library syntax
|
||||||
echo "[TEST] Checking shared lock library syntax..."
|
echo "[TEST] Checking shared lock library syntax..."
|
||||||
if bash -n "$WRAPPER_DIR/pacman_lock_lib.sh"; then
|
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
|
else
|
||||||
echo "✗ pacman_lock_lib.sh has syntax errors"
|
echo "✗ pacman_lock_lib.sh has syntax errors"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test: wrapper sources the shared lock library
|
# Test: wrapper sources the shared lock library
|
||||||
echo "[TEST] Verifying 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
|
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
|
else
|
||||||
echo "✗ Wrapper does not source pacman_lock_lib.sh"
|
echo "✗ Wrapper does not source pacman_lock_lib.sh"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test: the lock library is covered by the integrity manifest
|
# Test: the lock library is covered by the integrity manifest
|
||||||
echo "[TEST] Verifying installer checksums the lock library..."
|
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
|
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
|
else
|
||||||
echo "✗ Lock library not checksummed by installer"
|
echo "✗ Lock library not checksummed by installer"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test: lib is sourced AFTER integrity verification (tamper caught before source)
|
# Test: lib is sourced AFTER integrity verification (tamper caught before source)
|
||||||
echo "[TEST] Verifying lock library is sourced after integrity check..."
|
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
|
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
|
else
|
||||||
echo "✗ Lock library not sourced after integrity verification"
|
echo "✗ Lock library not sourced after integrity verification"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@ -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"
|
SETUP_SCRIPT="$REPO_DIR/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh"
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf 'FAIL: %s\n' "$1" >&2
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_equals() {
|
assert_equals() {
|
||||||
local expected="$1"
|
local expected="$1"
|
||||||
local actual="$2"
|
local actual="$2"
|
||||||
local context="$3"
|
local context="$3"
|
||||||
if [[ "$expected" != "$actual" ]]; then
|
if [[ "$expected" != "$actual" ]]; then
|
||||||
fail "$context (expected: '$expected', actual: '$actual')"
|
fail "$context (expected: '$expected', actual: '$actual')"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$TMP_DIR"
|
rm -rf "$TMP_DIR"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
@ -81,42 +81,46 @@ EOF
|
|||||||
chmod +x "$BIN_DIR/tee"
|
chmod +x "$BIN_DIR/tee"
|
||||||
|
|
||||||
run_case() {
|
run_case() {
|
||||||
local expected_mode="$1"
|
local expected_mode="$1"
|
||||||
local busctl_present="$2"
|
local busctl_present="$2"
|
||||||
local sleep_log="$TMP_DIR/sleep.log"
|
local sleep_log="$TMP_DIR/sleep.log"
|
||||||
local mode_file="$TMP_DIR/mode.log"
|
local mode_file="$TMP_DIR/mode.log"
|
||||||
|
|
||||||
: >"$sleep_log"
|
: >"$sleep_log"
|
||||||
: >"$mode_file"
|
: >"$mode_file"
|
||||||
if [[ $busctl_present -eq 0 ]]; then
|
if [[ $busctl_present -eq 0 ]]; then
|
||||||
mv "$BIN_DIR/busctl" "$BIN_DIR/busctl.off"
|
mv "$BIN_DIR/busctl" "$BIN_DIR/busctl.off"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mode=$(env -i PATH="$BIN_DIR" SLEEP_LOG="$sleep_log" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \
|
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'; \
|
"source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \
|
||||||
timer_needs_restoration() { return 1; }; \
|
timer_needs_restoration() { return 1; }; \
|
||||||
restore_timer() { :; }; \
|
restore_timer() { :; }; \
|
||||||
monitor_with_dbus() { printf 'dbus'; }; \
|
monitor_with_dbus() { printf 'dbus'; }; \
|
||||||
monitor_with_polling() { printf 'polling'; }; \
|
monitor_with_polling() { printf 'polling'; }; \
|
||||||
start_monitoring")
|
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
|
if [[ -f "$BIN_DIR/busctl.off" ]]; then
|
||||||
mv "$BIN_DIR/busctl.off" "$BIN_DIR/busctl"
|
mv "$BIN_DIR/busctl.off" "$BIN_DIR/busctl"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_dbus_throttle_case() {
|
run_dbus_throttle_case() {
|
||||||
local ts_sequence="$1"
|
local ts_sequence="$1"
|
||||||
local expected_calls="$2"
|
local expected_calls="$2"
|
||||||
local check_interval="${3:-30}"
|
local check_interval="${3:-30}"
|
||||||
local calls
|
local calls
|
||||||
local counter_file="$TMP_DIR/timer_checks.log"
|
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"
|
source "$1"
|
||||||
CHECK_INTERVAL="$TEST_CHECK_INTERVAL"
|
CHECK_INTERVAL="$TEST_CHECK_INTERVAL"
|
||||||
timer_needs_restoration() { printf "x\n" >> "$COUNTER_FILE"; return 1; }
|
timer_needs_restoration() { printf "x\n" >> "$COUNTER_FILE"; return 1; }
|
||||||
@ -142,7 +146,7 @@ run_dbus_throttle_case() {
|
|||||||
printf "%s" "$timer_checks"
|
printf "%s" "$timer_checks"
|
||||||
' _ "$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh")
|
' _ "$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'
|
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'
|
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 \
|
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); \
|
start=\$(printf '%(%s)T' -1); \
|
||||||
wait_seconds 1; \
|
wait_seconds 1; \
|
||||||
end=\$(printf '%(%s)T' -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'
|
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'
|
printf 'Checking installer template stays in sync with the event-driven monitor...\n'
|
||||||
grep -Fq 'monitor_with_dbus()' "$SETUP_SCRIPT" \
|
grep -Fq 'monitor_with_dbus()' "$SETUP_SCRIPT" ||
|
||||||
|| fail 'setup_midnight_shutdown.sh should install the D-Bus monitor helper'
|
fail 'setup_midnight_shutdown.sh should install the D-Bus monitor helper'
|
||||||
grep -Fq 'start_monitoring()' "$SETUP_SCRIPT" \
|
grep -Fq 'start_monitoring()' "$SETUP_SCRIPT" ||
|
||||||
|| fail 'setup_midnight_shutdown.sh should install the start_monitoring dispatcher'
|
fail 'setup_midnight_shutdown.sh should install the start_monitoring dispatcher'
|
||||||
grep -Fq 'if command -v busctl &>/dev/null; then' "$SETUP_SCRIPT" \
|
grep -Fq 'if command -v busctl &>/dev/null; then' "$SETUP_SCRIPT" ||
|
||||||
|| fail 'setup_midnight_shutdown.sh should prefer busctl when available'
|
fail 'setup_midnight_shutdown.sh should prefer busctl when available'
|
||||||
grep -Fq 'current_epoch now_ts' "$SETUP_SCRIPT" \
|
grep -Fq 'current_epoch now_ts' "$SETUP_SCRIPT" ||
|
||||||
|| fail 'setup_midnight_shutdown.sh should use out-var epoch helper in D-Bus throttling path'
|
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
|
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
|
fi
|
||||||
|
# Literal source text, as above.
|
||||||
|
# shellcheck disable=SC2016
|
||||||
if grep -Fq 'now_ts=$(current_epoch)' "$TARGET_SCRIPT"; then
|
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
|
fi
|
||||||
grep -Fq 'OnUnitActiveSec=300' "$SETUP_SCRIPT" \
|
grep -Fq 'OnUnitActiveSec=300' "$SETUP_SCRIPT" ||
|
||||||
|| fail 'setup_midnight_shutdown.sh should run watchdog timer at 300s cadence'
|
fail 'setup_midnight_shutdown.sh should run watchdog timer at 300s cadence'
|
||||||
grep -Fq 'wait_seconds()' "$SETUP_SCRIPT" \
|
grep -Fq 'wait_seconds()' "$SETUP_SCRIPT" ||
|
||||||
|| fail 'setup_midnight_shutdown.sh should install builtin wait helper in polling fallback'
|
fail 'setup_midnight_shutdown.sh should install builtin wait helper in polling fallback'
|
||||||
grep -Fq 'wait_seconds "$CHECK_INTERVAL"' "$TARGET_SCRIPT" \
|
# Literal source text, as above.
|
||||||
|| fail 'runtime shutdown monitor polling fallback should use builtin wait helper'
|
# 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'
|
printf 'shutdown-timer-monitor.sh regression checks passed.\n'
|
||||||
|
|||||||
@ -8,12 +8,12 @@ REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
|||||||
INSTALLER="$REPO_DIR/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh"
|
INSTALLER="$REPO_DIR/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh"
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf 'FAIL: %s\n' "$1" >&2
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
logger_template=$(
|
logger_template=$(
|
||||||
awk '
|
awk '
|
||||||
/cat > "\$HOME\/.local\/bin\/nvidia-pmon-logger\.sh" << '\''SCRIPT'\''/ {capture=1; next}
|
/cat > "\$HOME\/.local\/bin\/nvidia-pmon-logger\.sh" << '\''SCRIPT'\''/ {capture=1; next}
|
||||||
capture && /^SCRIPT$/ {capture=0; exit}
|
capture && /^SCRIPT$/ {capture=0; exit}
|
||||||
capture {print}
|
capture {print}
|
||||||
@ -23,30 +23,36 @@ logger_template=$(
|
|||||||
[[ -n $logger_template ]] || fail 'could not extract nvidia-pmon-logger template from installer'
|
[[ -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'
|
printf 'Checking pmon logger template avoids read -t busy-loop pattern...\n'
|
||||||
! grep -q 'read -r -t' <<< "$logger_template" \
|
! grep -q 'read -r -t' <<<"$logger_template" ||
|
||||||
|| fail 'logger template must not use read -t as sleep surrogate'
|
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
|
# 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
|
# sleeps exactly once, until the next midnight, and blocks on `wait` for the
|
||||||
# pmon child — strictly fewer wakeups. Assert that property rather than the
|
# pmon child — strictly fewer wakeups. Assert that property rather than the
|
||||||
# old literal, so an accidental return to polling still fails the test.
|
# old literal, so an accidental return to polling still fails the test.
|
||||||
printf 'Checking pmon logger template sleeps until rollover rather than polling...\n'
|
printf 'Checking pmon logger template sleeps until rollover rather than polling...\n'
|
||||||
grep -q 'sleep "\$(seconds_until_next_day)"' <<< "$logger_template" \
|
# Literal search pattern: the $ must reach grep as text, not be expanded
|
||||||
|| fail 'logger template must sleep until the next day boundary'
|
# 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" \
|
grep -q 'seconds_until_next_day()' <<<"$logger_template" ||
|
||||||
|| fail 'logger template must define seconds_until_next_day'
|
fail 'logger template must define seconds_until_next_day'
|
||||||
|
|
||||||
printf 'Checking pmon logger template blocks on wait instead of spinning...\n'
|
printf 'Checking pmon logger template blocks on wait instead of spinning...\n'
|
||||||
grep -qE '^\s*wait "\$pmon_pid"' <<< "$logger_template" \
|
# Literal search pattern: the $ must reach grep as text, not be expanded
|
||||||
|| fail 'logger template must block on wait for the pmon child'
|
# 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'
|
printf 'Checking pmon logger template uses fork-free date builtin...\n'
|
||||||
grep -q "printf '%(%Y%m%d)T' -1" <<< "$logger_template" \
|
grep -q "printf '%(%Y%m%d)T' -1" <<<"$logger_template" ||
|
||||||
|| fail 'logger template must use bash printf time builtin for current day'
|
fail 'logger template must use bash printf time builtin for current day'
|
||||||
|
|
||||||
printf 'Checking pmon logger template avoids external date command...\n'
|
printf 'Checking pmon logger template avoids external date command...\n'
|
||||||
! grep -q 'date +%Y%m%d' <<< "$logger_template" \
|
! grep -q 'date +%Y%m%d' <<<"$logger_template" ||
|
||||||
|| fail 'logger template must not call external date command in hot path'
|
fail 'logger template must not call external date command in hot path'
|
||||||
|
|
||||||
printf 'Usage monitoring installer efficiency tests passed.\n'
|
printf 'Usage monitoring installer efficiency tests passed.\n'
|
||||||
|
|||||||
@ -26,78 +26,85 @@ BOLD='\033[1m'
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="${SCRIPT_DIR}"
|
PROJECT_ROOT="${SCRIPT_DIR}"
|
||||||
PYTHON_PATHS=(
|
PYTHON_PATHS=(
|
||||||
"PYTHON"
|
"PYTHON"
|
||||||
"articles"
|
"articles"
|
||||||
"poker-modifier-app"
|
"poker-modifier-app"
|
||||||
"tests"
|
"tests"
|
||||||
)
|
)
|
||||||
EXCLUDE_PATHS=(
|
EXCLUDE_PATHS=(
|
||||||
".venv"
|
".venv"
|
||||||
"__pycache__"
|
"__pycache__"
|
||||||
".git"
|
".git"
|
||||||
"Bash/ffmpeg-build"
|
"Bash/ffmpeg-build"
|
||||||
".pytest_cache"
|
".pytest_cache"
|
||||||
".ruff_cache"
|
".ruff_cache"
|
||||||
".mypy_cache"
|
".mypy_cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build exclude pattern for find
|
# Build exclude pattern for find
|
||||||
EXCLUDE_PATTERN=""
|
EXCLUDE_PATTERN=""
|
||||||
for path in "${EXCLUDE_PATHS[@]}"; do
|
for path in "${EXCLUDE_PATHS[@]}"; do
|
||||||
EXCLUDE_PATTERN="${EXCLUDE_PATTERN} -path '*/${path}/*' -prune -o"
|
EXCLUDE_PATTERN="${EXCLUDE_PATTERN} -path '*/${path}/*' -prune -o"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
FIX_MODE=false
|
FIX_MODE=false
|
||||||
QUICK_MODE=false
|
QUICK_MODE=false
|
||||||
REPORT_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=""
|
||||||
|
TARGET_FILES_ARR=()
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--fix|-f)
|
--fix | -f)
|
||||||
FIX_MODE=true
|
FIX_MODE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--quick|-q)
|
--quick | -q)
|
||||||
QUICK_MODE=true
|
QUICK_MODE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--report|-r)
|
--report | -r)
|
||||||
REPORT_MODE=true
|
REPORT_MODE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--help|-h)
|
--help | -h)
|
||||||
echo "Usage: $0 [OPTIONS] [FILES...]"
|
echo "Usage: $0 [OPTIONS] [FILES...]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --fix, -f Auto-fix issues where possible"
|
echo " --fix, -f Auto-fix issues where possible"
|
||||||
echo " --quick, -q Quick mode (ruff + mypy only)"
|
echo " --quick, -q Quick mode (ruff + mypy only)"
|
||||||
echo " --report, -r Generate detailed reports to ./lint-reports/"
|
echo " --report, -r Generate detailed reports to ./lint-reports/"
|
||||||
echo " --help, -h Show this help message"
|
echo " --help, -h Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 # Lint all Python files"
|
echo " $0 # Lint all Python files"
|
||||||
echo " $0 --fix # Lint and auto-fix"
|
echo " $0 --fix # Lint and auto-fix"
|
||||||
echo " $0 PYTHON/ # Lint specific directory"
|
echo " $0 PYTHON/ # Lint specific directory"
|
||||||
echo " $0 --quick --fix # Quick lint with auto-fix"
|
echo " $0 --quick --fix # Quick lint with auto-fix"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
TARGET_FILES="${TARGET_FILES} $1"
|
TARGET_FILES="${TARGET_FILES} $1"
|
||||||
shift
|
TARGET_FILES_ARR+=("$1")
|
||||||
;;
|
shift
|
||||||
esac
|
;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# If no target specified, use default paths
|
# If no target specified, use default paths
|
||||||
if [[ -z "${TARGET_FILES}" ]]; then
|
if [[ -z "${TARGET_FILES}" ]]; then
|
||||||
TARGET_FILES="${PYTHON_PATHS[*]}"
|
TARGET_FILES="${PYTHON_PATHS[*]}"
|
||||||
|
TARGET_FILES_ARR=("${PYTHON_PATHS[@]}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create reports directory if needed
|
# Create reports directory if needed
|
||||||
if [[ "${REPORT_MODE}" == true ]]; then
|
if [[ "${REPORT_MODE}" == true ]]; then
|
||||||
mkdir -p "${PROJECT_ROOT}/lint-reports"
|
mkdir -p "${PROJECT_ROOT}/lint-reports"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Track overall status
|
# Track overall status
|
||||||
@ -109,70 +116,70 @@ FAILED_TOOLS=()
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
print_header() {
|
print_header() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
||||||
echo -e "${BOLD}${BLUE} $1${NC}"
|
echo -e "${BOLD}${BLUE} $1${NC}"
|
||||||
echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_subheader() {
|
print_subheader() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
echo -e "${CYAN} $1${NC}"
|
echo -e "${CYAN} $1${NC}"
|
||||||
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_success() {
|
print_success() {
|
||||||
echo -e "${GREEN}✓${NC} $1"
|
echo -e "${GREEN}✓${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_warning() {
|
print_warning() {
|
||||||
echo -e "${YELLOW}⚠${NC} $1"
|
echo -e "${YELLOW}⚠${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_error() {
|
print_error() {
|
||||||
echo -e "${RED}✗${NC} $1"
|
echo -e "${RED}✗${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_info() {
|
print_info() {
|
||||||
echo -e "${BLUE}ℹ${NC} $1"
|
echo -e "${BLUE}ℹ${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_tool() {
|
run_tool() {
|
||||||
local tool_name="$1"
|
local tool_name="$1"
|
||||||
local tool_cmd="$2"
|
local tool_cmd="$2"
|
||||||
local report_file="${PROJECT_ROOT}/lint-reports/${tool_name}.txt"
|
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 [[ "${REPORT_MODE}" == true ]]; then
|
||||||
if eval "${tool_cmd}" 2>&1 | tee "${report_file}"; then
|
if eval "${tool_cmd}" 2>&1 | tee "${report_file}"; then
|
||||||
print_success "${tool_name} passed"
|
print_success "${tool_name} passed"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
print_error "${tool_name} found issues (see ${report_file})"
|
print_error "${tool_name} found issues (see ${report_file})"
|
||||||
FAILED_TOOLS+=("${tool_name}")
|
FAILED_TOOLS+=("${tool_name}")
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if eval "${tool_cmd}"; then
|
if eval "${tool_cmd}"; then
|
||||||
print_success "${tool_name} passed"
|
print_success "${tool_name} passed"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
print_error "${tool_name} found issues"
|
print_error "${tool_name} found issues"
|
||||||
FAILED_TOOLS+=("${tool_name}")
|
FAILED_TOOLS+=("${tool_name}")
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
check_tool() {
|
check_tool() {
|
||||||
if command -v "$1" &> /dev/null; then
|
if command -v "$1" &>/dev/null; then
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
print_warning "$1 not found, skipping..."
|
print_warning "$1 not found, skipping..."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@ -192,135 +199,135 @@ cd "${PROJECT_ROOT}"
|
|||||||
# RUFF - Primary linter and formatter
|
# RUFF - Primary linter and formatter
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool ruff; then
|
if check_tool ruff; then
|
||||||
if [[ "${FIX_MODE}" == true ]]; then
|
if [[ "${FIX_MODE}" == true ]]; then
|
||||||
run_tool "ruff-lint" "ruff check --fix --show-fixes ${TARGET_FILES}" || OVERALL_STATUS=1
|
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
|
run_tool "ruff-format" "ruff format ${TARGET_FILES}" || OVERALL_STATUS=1
|
||||||
else
|
else
|
||||||
run_tool "ruff-lint" "ruff check ${TARGET_FILES}" || OVERALL_STATUS=1
|
run_tool "ruff-lint" "ruff check ${TARGET_FILES}" || OVERALL_STATUS=1
|
||||||
run_tool "ruff-format-check" "ruff format --check ${TARGET_FILES}" || OVERALL_STATUS=1
|
run_tool "ruff-format-check" "ruff format --check ${TARGET_FILES}" || OVERALL_STATUS=1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MYPY - Static type checking
|
# MYPY - Static type checking
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool mypy; then
|
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
|
fi
|
||||||
|
|
||||||
# Quick mode exits here
|
# Quick mode exits here
|
||||||
if [[ "${QUICK_MODE}" == true ]]; then
|
if [[ "${QUICK_MODE}" == true ]]; then
|
||||||
print_header "Quick Lint Complete"
|
print_header "Quick Lint Complete"
|
||||||
if [[ ${#FAILED_TOOLS[@]} -gt 0 ]]; then
|
if [[ ${#FAILED_TOOLS[@]} -gt 0 ]]; then
|
||||||
print_error "Failed tools: ${FAILED_TOOLS[*]}"
|
print_error "Failed tools: ${FAILED_TOOLS[*]}"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
print_success "All quick checks passed!"
|
print_success "All quick checks passed!"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# PYLINT - Comprehensive linting
|
# PYLINT - Comprehensive linting
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool pylint; then
|
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
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# BANDIT - Security linting
|
# BANDIT - Security linting
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool bandit; then
|
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
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# VULTURE - Dead code detection
|
# VULTURE - Dead code detection
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool vulture; then
|
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
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# FLAKE8 - Traditional linter
|
# FLAKE8 - Traditional linter
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool flake8; then
|
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
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# PYCODESTYLE - PEP 8 style checker
|
# PYCODESTYLE - PEP 8 style checker
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool pycodestyle; then
|
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
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# PYDOCSTYLE - Docstring style checker
|
# PYDOCSTYLE - Docstring style checker
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool pydocstyle; then
|
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
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RADON - Complexity metrics
|
# RADON - Complexity metrics
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool radon; then
|
if check_tool radon; then
|
||||||
print_subheader "Running radon (complexity analysis)..."
|
print_subheader "Running radon (complexity analysis)..."
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${MAGENTA}Cyclomatic Complexity:${NC}"
|
echo -e "${MAGENTA}Cyclomatic Complexity:${NC}"
|
||||||
radon cc -a -s ${TARGET_FILES} || true
|
radon cc -a -s "${TARGET_FILES_ARR[@]}" || true
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${MAGENTA}Maintainability Index:${NC}"
|
echo -e "${MAGENTA}Maintainability Index:${NC}"
|
||||||
radon mi -s ${TARGET_FILES} || true
|
radon mi -s "${TARGET_FILES_ARR[@]}" || true
|
||||||
|
|
||||||
if [[ "${REPORT_MODE}" == true ]]; then
|
if [[ "${REPORT_MODE}" == true ]]; then
|
||||||
radon cc -a -s ${TARGET_FILES} > "${PROJECT_ROOT}/lint-reports/radon-cc.txt" 2>&1 || true
|
radon cc -a -s "${TARGET_FILES_ARR[@]}" >"${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
|
radon mi -s "${TARGET_FILES_ARR[@]}" >"${PROJECT_ROOT}/lint-reports/radon-mi.txt" 2>&1 || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# INTERROGATE - Docstring coverage
|
# INTERROGATE - Docstring coverage
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool interrogate; then
|
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
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# PYRIGHT - Microsoft's type checker (optional, very strict)
|
# PYRIGHT - Microsoft's type checker (optional, very strict)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool pyright; then
|
if check_tool pyright; then
|
||||||
run_tool "pyright" "pyright ${TARGET_FILES}" || OVERALL_STATUS=1
|
run_tool "pyright" "pyright ${TARGET_FILES}" || OVERALL_STATUS=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# AUTOFLAKE - Unused imports/variables (fix mode only)
|
# AUTOFLAKE - Unused imports/variables (fix mode only)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if [[ "${FIX_MODE}" == true ]] && check_tool autoflake; then
|
if [[ "${FIX_MODE}" == true ]] && check_tool autoflake; then
|
||||||
print_subheader "Running autoflake (removing unused imports)..."
|
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 {} \;
|
find "${TARGET_FILES_ARR[@]}" -name "*.py" -type f -exec autoflake --in-place --remove-all-unused-imports --remove-unused-variables {} \;
|
||||||
print_success "autoflake completed"
|
print_success "autoflake completed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# PYUPGRADE - Upgrade Python syntax (fix mode only)
|
# PYUPGRADE - Upgrade Python syntax (fix mode only)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if [[ "${FIX_MODE}" == true ]] && check_tool pyupgrade; then
|
if [[ "${FIX_MODE}" == true ]] && check_tool pyupgrade; then
|
||||||
print_subheader "Running pyupgrade (upgrading syntax to Python 3.10+)..."
|
print_subheader "Running pyupgrade (upgrading syntax to Python 3.10+)..."
|
||||||
find ${TARGET_FILES} -name "*.py" -type f -exec pyupgrade --py310-plus {} \;
|
find "${TARGET_FILES_ARR[@]}" -name "*.py" -type f -exec pyupgrade --py310-plus {} \;
|
||||||
print_success "pyupgrade completed"
|
print_success "pyupgrade completed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CODESPELL - Spell checking
|
# CODESPELL - Spell checking
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
if check_tool codespell; then
|
if check_tool codespell; then
|
||||||
if [[ "${FIX_MODE}" == true ]]; then
|
if [[ "${FIX_MODE}" == true ]]; then
|
||||||
run_tool "codespell" "codespell -w --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1
|
run_tool "codespell" "codespell -w --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1
|
||||||
else
|
else
|
||||||
run_tool "codespell" "codespell --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1
|
run_tool "codespell" "codespell --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@ -330,17 +337,17 @@ print_header "Linting Summary"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ ${OVERALL_STATUS} -ne 0 ]]; then
|
if [[ ${OVERALL_STATUS} -ne 0 ]]; then
|
||||||
print_error "The following tools reported issues:"
|
print_error "The following tools reported issues:"
|
||||||
for tool in "${FAILED_TOOLS[@]}"; do
|
for tool in "${FAILED_TOOLS[@]}"; do
|
||||||
echo " - ${tool}"
|
echo " - ${tool}"
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
if [[ "${REPORT_MODE}" == true ]]; then
|
if [[ "${REPORT_MODE}" == true ]]; then
|
||||||
print_info "Detailed reports saved to: ${PROJECT_ROOT}/lint-reports/"
|
print_info "Detailed reports saved to: ${PROJECT_ROOT}/lint-reports/"
|
||||||
fi
|
fi
|
||||||
print_info "Run with --fix to auto-fix issues where possible"
|
print_info "Run with --fix to auto-fix issues where possible"
|
||||||
exit ${OVERALL_STATUS}
|
exit ${OVERALL_STATUS}
|
||||||
else
|
else
|
||||||
print_success "All linting checks passed!"
|
print_success "All linting checks passed!"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
197
meta/run.sh
197
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"
|
ARTIFACT_INIT_SCRIPT="$SCRIPT_DIR/meta/scripts/init_agent_artifacts.sh"
|
||||||
|
|
||||||
if [[ ! -f "$REPORT_SCRIPT" ]]; then
|
if [[ ! -f "$REPORT_SCRIPT" ]]; then
|
||||||
echo "Error: usage_report.py not found at: $REPORT_SCRIPT" >&2
|
echo "Error: usage_report.py not found at: $REPORT_SCRIPT" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$ARTIFACT_INIT_SCRIPT" ]]; then
|
if [[ ! -f "$ARTIFACT_INIT_SCRIPT" ]]; then
|
||||||
echo "Error: init_agent_artifacts.sh not found at: $ARTIFACT_INIT_SCRIPT" >&2
|
echo "Error: init_agent_artifacts.sh not found at: $ARTIFACT_INIT_SCRIPT" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Profiling mode: trace fork-heavy scripts over time
|
# Profiling mode: trace fork-heavy scripts over time
|
||||||
profile_polling_scripts() {
|
profile_polling_scripts() {
|
||||||
local duration="${1:-60}"
|
local duration="${1:-60}"
|
||||||
echo "=== Polling Script Profiler (${duration}s) ===" >&2
|
echo "=== Polling Script Profiler (${duration}s) ===" >&2
|
||||||
echo "Tracing fork/exec calls in shell scripts..." >&2
|
echo "Tracing fork/exec calls in shell scripts..." >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
# Find common polling script processes and trace them
|
# Find common polling script processes and trace them
|
||||||
local trace_file="/tmp/polling_trace_$$.txt"
|
local trace_file="/tmp/polling_trace_$$.txt"
|
||||||
|
|
||||||
# Use perf/strace to capture system calls
|
# Use perf/strace to capture system calls
|
||||||
(
|
(
|
||||||
timeout "$duration" strace -f -e trace=clone,execve -c -p $$ 2>&1 || true
|
timeout "$duration" strace -f -e trace=clone,execve -c -p $$ 2>&1 || true
|
||||||
) > "$trace_file" 2>&1
|
) >"$trace_file" 2>&1
|
||||||
|
|
||||||
echo "Trace completed. Analyzing results:" >&2
|
echo "Trace completed. Analyzing results:" >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
# Show fork/exec heavy processes
|
# Show fork/exec heavy processes
|
||||||
if ! grep -e "execve" -e "clone" "$trace_file" | head -20; then
|
if ! grep -e "execve" -e "clone" "$trace_file" | head -20; then
|
||||||
:
|
:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f "$trace_file"
|
rm -f "$trace_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Diagnostic mode: find inefficient patterns in shell scripts
|
# Diagnostic mode: find inefficient patterns in shell scripts
|
||||||
diagnose_polling_scripts() {
|
diagnose_polling_scripts() {
|
||||||
echo "=== Shell Script Efficiency Audit ===" >&2
|
echo "=== Shell Script Efficiency Audit ===" >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
local issues_found=0
|
local issues_found=0
|
||||||
|
|
||||||
# Check for common anti-patterns
|
# Check for common anti-patterns
|
||||||
echo "Checking for anti-patterns in shell scripts..." >&2
|
echo "Checking for anti-patterns in shell scripts..." >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
# Pattern 1: while true with sleep (no event-driven check)
|
# Pattern 1: while true with sleep (no event-driven check)
|
||||||
echo "1. Polling loops (while true + sleep):" >&2
|
echo "1. Polling loops (while true + sleep):" >&2
|
||||||
set +e
|
set +e
|
||||||
grep -r "while true\|while :" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \
|
grep -r "while true\|while :" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null |
|
||||||
| grep -v "Binary" | grep -v ".git" | head -5
|
grep -v "Binary" | grep -v ".git" | head -5
|
||||||
set -e
|
set -e
|
||||||
issues_found=$((issues_found + 1))
|
issues_found=$((issues_found + 1))
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
# Pattern 2: $(date +...) calls in loops (fork-heavy)
|
# Pattern 2: $(date +...) calls in loops (fork-heavy)
|
||||||
echo "2. Excessive date calls (each forks a process):" >&2
|
echo "2. Excessive date calls (each forks a process):" >&2
|
||||||
set +e
|
set +e
|
||||||
grep -r '\$(date' --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \
|
# Literal search pattern: the $ must reach grep as text, not be expanded
|
||||||
| grep -v "Binary" | grep -v ".git" | head -5
|
# by this shell.
|
||||||
set -e
|
# shellcheck disable=SC2016
|
||||||
issues_found=$((issues_found + 1))
|
grep -r '\$(date' --include="*.sh" "$SCRIPT_DIR" 2>/dev/null |
|
||||||
echo "" >&2
|
grep -v "Binary" | grep -v ".git" | head -5
|
||||||
|
set -e
|
||||||
|
issues_found=$((issues_found + 1))
|
||||||
|
echo "" >&2
|
||||||
|
|
||||||
# Pattern 3: pgrep/xdotool in loops
|
# Pattern 3: pgrep/xdotool in loops
|
||||||
echo "3. Process inspection in loops (pgrep, xdotool):" >&2
|
echo "3. Process inspection in loops (pgrep, xdotool):" >&2
|
||||||
set +e
|
set +e
|
||||||
grep -r "while.*pgrep\|while.*xdotool\|pgrep.*while" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \
|
grep -r "while.*pgrep\|while.*xdotool\|pgrep.*while" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null |
|
||||||
| grep -v "Binary" | grep -v ".git" | head -5
|
grep -v "Binary" | grep -v ".git" | head -5
|
||||||
set -e
|
set -e
|
||||||
issues_found=$((issues_found + 1))
|
issues_found=$((issues_found + 1))
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
# Pattern 4: pipes in hot paths
|
# Pattern 4: pipes in hot paths
|
||||||
echo "4. Heavy pipes in polling scripts (| awk, | grep, | tr):" >&2
|
echo "4. Heavy pipes in polling scripts (| awk, | grep, | tr):" >&2
|
||||||
set +e
|
set +e
|
||||||
while_true_file_list="$(mktemp)"
|
while_true_file_list="$(mktemp)"
|
||||||
heavy_pipe_matches="$(mktemp)"
|
heavy_pipe_matches="$(mktemp)"
|
||||||
grep -r "while true" --include="*.sh" "$SCRIPT_DIR" > "$while_true_file_list" 2>/dev/null
|
grep -r "while true" --include="*.sh" "$SCRIPT_DIR" >"$while_true_file_list" 2>/dev/null
|
||||||
if [ -s "$while_true_file_list" ]; then
|
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
|
xargs grep -l -e " | awk" -e " | grep" -e " | tr" <"$while_true_file_list" >"$heavy_pipe_matches" 2>/dev/null
|
||||||
head -5 "$heavy_pipe_matches"
|
head -5 "$heavy_pipe_matches"
|
||||||
fi
|
fi
|
||||||
rm -f "$while_true_file_list" "$heavy_pipe_matches"
|
rm -f "$while_true_file_list" "$heavy_pipe_matches"
|
||||||
set -e
|
set -e
|
||||||
issues_found=$((issues_found + 1))
|
issues_found=$((issues_found + 1))
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
# Pattern 5: sleep with very short intervals
|
# Pattern 5: sleep with very short intervals
|
||||||
echo "5. Aggressive polling (sleep < 1s):" >&2
|
echo "5. Aggressive polling (sleep < 1s):" >&2
|
||||||
set +e
|
set +e
|
||||||
grep -rE "sleep 0\.[0-9]|sleep 0[^0-9]" --include="*.sh" "$SCRIPT_DIR" 2>/dev/null \
|
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
|
grep -v "Binary" | grep -v ".git" | head -5
|
||||||
set -e
|
set -e
|
||||||
issues_found=$((issues_found + 1))
|
issues_found=$((issues_found + 1))
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
echo "=== Recommendations ===" >&2
|
echo "=== Recommendations ===" >&2
|
||||||
echo "1. Replace 'while true + sleep' with event-driven I/O (inotifywait, read -t, etc.)" >&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 "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 "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 "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 "5. Use i3blocks interval=persist + event loop instead of polling mode" >&2
|
||||||
echo "6. Increase polling intervals: 1s → 5s → 10s where acceptable" >&2
|
echo "6. Increase polling intervals: 1s → 5s → 10s where acceptable" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
# Handle special modes
|
# Handle special modes
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
--profile)
|
--profile)
|
||||||
profile_polling_scripts "${2:-60}"
|
profile_polling_scripts "${2:-60}"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
--diagnose)
|
--diagnose)
|
||||||
diagnose_polling_scripts
|
diagnose_polling_scripts
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
--init-artifacts)
|
--init-artifacts)
|
||||||
shift
|
shift
|
||||||
exec "$ARTIFACT_INIT_SCRIPT" "$@"
|
exec "$ARTIFACT_INIT_SCRIPT" "$@"
|
||||||
;;
|
;;
|
||||||
--help)
|
--help)
|
||||||
grep '^# Usage:' "$0" | sed 's/^# //' | head -1
|
grep '^# Usage:' "$0" | sed 's/^# //' | head -1
|
||||||
grep '^# ' "$0" | sed 's/^# / /'
|
grep '^# ' "$0" | sed 's/^# / /'
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Default: run usage_report.py with all remaining args
|
# Default: run usage_report.py with all remaining args
|
||||||
|
|||||||
@ -11,48 +11,50 @@ errors=0
|
|||||||
files_checked=0
|
files_checked=0
|
||||||
|
|
||||||
# Patterns to detect (these are red flags for fork storms)
|
# 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=(
|
declare -A patterns=(
|
||||||
['while_true_with_sleep']='while\s+(true|:).*sleep'
|
['while_true_with_sleep']='while\s+(true|:).*sleep'
|
||||||
['date_in_loop']='while.*date|for.*date'
|
['date_in_loop']='while.*date|for.*date'
|
||||||
['pgrep_in_loop']='while.*pgrep|for.*pgrep'
|
['pgrep_in_loop']='while.*pgrep|for.*pgrep'
|
||||||
['xdotool_in_loop']='while.*xdotool|for.*xdotool'
|
['xdotool_in_loop']='while.*xdotool|for.*xdotool'
|
||||||
['pipe_fork_chain']='|.*|.*|.*|'
|
['pipe_fork_chain']='|.*|.*|.*|'
|
||||||
['subshell_date']='$\(date\s+\+'
|
['subshell_date']='$\(date\s+\+'
|
||||||
['backtick_date']='`date\s+\+'
|
['backtick_date']='`date\s+\+'
|
||||||
['excessive_fork_chain']='(\$\(.*\|.*\|.*\)|`.*\|.*\|`)'
|
['excessive_fork_chain']='(\$\(.*\|.*\|.*\)|`.*\|.*\|`)'
|
||||||
['sleep_less_than_one']='sleep\s+0\.[0-9]'
|
['sleep_less_than_one']='sleep\s+0\.[0-9]'
|
||||||
)
|
)
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $(basename "$0") [files...]"
|
echo "Usage: $(basename "$0") [files...]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Checks shell scripts for polling anti-patterns that cause fork storms."
|
echo "Checks shell scripts for polling anti-patterns that cause fork storms."
|
||||||
echo "Exit code 0 = no issues, 1 = issues found"
|
echo "Exit code 0 = no issues, 1 = issues found"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Patterns detected:"
|
echo "Patterns detected:"
|
||||||
echo " - while true + sleep (should use event-driven I/O)"
|
echo " - while true + sleep (should use event-driven I/O)"
|
||||||
echo " - \$(date +...) in loops (forks process)"
|
echo " - \$(date +...) in loops (forks process)"
|
||||||
echo " - pgrep/xdotool in loops (forks process)"
|
echo " - pgrep/xdotool in loops (forks process)"
|
||||||
echo " - Multiple piped commands (each | forks)"
|
echo " - Multiple piped commands (each | forks)"
|
||||||
echo " - Aggressive polling (sleep < 1s)"
|
echo " - Aggressive polling (sleep < 1s)"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
check_file() {
|
check_file() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
|
|
||||||
# Skip non-shell files
|
# Skip non-shell files
|
||||||
if ! grep -qE '^#!.*/(ba)?sh' "$file" 2>/dev/null; then
|
if ! grep -qE '^#!.*/(ba)?sh' "$file" 2>/dev/null; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
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.
|
# 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.
|
# '\'' embeds a literal single quote inside a bash single-quoted string.
|
||||||
local findings
|
local findings
|
||||||
findings=$(awk '
|
findings=$(awk '
|
||||||
/^[[:space:]]*([a-zA-Z0-9_]*_(loop|daemon)|poll[a-zA-Z0-9_]*)[[:space:]]*\(\)/ { in_poll=1 }
|
/^[[:space:]]*([a-zA-Z0-9_]*_(loop|daemon)|poll[a-zA-Z0-9_]*)[[:space:]]*\(\)/ { in_poll=1 }
|
||||||
/^[[:space:]]*\}/ { in_poll=0 }
|
/^[[:space:]]*\}/ { in_poll=0 }
|
||||||
in_poll {
|
in_poll {
|
||||||
@ -77,58 +79,58 @@ check_file() {
|
|||||||
}
|
}
|
||||||
' "$file")
|
' "$file")
|
||||||
|
|
||||||
if [[ -n "$findings" ]]; then
|
if [[ -n "$findings" ]]; then
|
||||||
echo " $file:"
|
echo " $file:"
|
||||||
while IFS= read -r finding; do
|
while IFS= read -r finding; do
|
||||||
echo " Line $finding"
|
echo " Line $finding"
|
||||||
done <<< "$findings"
|
done <<<"$findings"
|
||||||
errors=$((errors + 1))
|
errors=$((errors + 1))
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
provide_suggestions() {
|
provide_suggestions() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "📋 Optimization Tips:"
|
echo "📋 Optimization Tips:"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Replace polling loops with:"
|
echo "Replace polling loops with:"
|
||||||
echo " • inotifywait/fanotify for file system events"
|
echo " • inotifywait/fanotify for file system events"
|
||||||
echo " • timerfd for interval-based tasks"
|
echo " • timerfd for interval-based tasks"
|
||||||
echo " • select/poll for I/O multiplexing"
|
echo " • select/poll for I/O multiplexing"
|
||||||
echo " • systemd timers for scheduled tasks"
|
echo " • systemd timers for scheduled tasks"
|
||||||
echo " • dbus signals for system events"
|
echo " • dbus signals for system events"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Reduce fork overhead:"
|
echo "Reduce fork overhead:"
|
||||||
echo " • Cache \$(date +%s) in variable, update periodically"
|
echo " • Cache \$(date +%s) in variable, update periodically"
|
||||||
echo " • Use /proc filesystem instead of pgrep"
|
echo " • Use /proc filesystem instead of pgrep"
|
||||||
echo " • Consolidate commands with && instead of separate invocations"
|
echo " • Consolidate commands with && instead of separate invocations"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
# Show usage if requested
|
# Show usage if requested
|
||||||
[[ "$#" -eq 0 || "$1" == "-h" || "$1" == "--help" ]] && usage
|
[[ "$#" -eq 0 || "$1" == "-h" || "$1" == "--help" ]] && usage
|
||||||
|
|
||||||
# Check all provided files
|
# Check all provided files
|
||||||
for file in "$@"; do
|
for file in "$@"; do
|
||||||
check_file "$file" || true
|
check_file "$file" || true
|
||||||
done
|
done
|
||||||
|
|
||||||
# Report results
|
# Report results
|
||||||
if [[ $files_checked -eq 0 ]]; then
|
if [[ $files_checked -eq 0 ]]; then
|
||||||
echo "ℹ️ No shell scripts to check"
|
echo "ℹ️ No shell scripts to check"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $errors -gt 0 ]]; then
|
if [[ $errors -gt 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "❌ Found $errors issue(s) in shell scripts"
|
echo "❌ Found $errors issue(s) in shell scripts"
|
||||||
provide_suggestions
|
provide_suggestions
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "✓ No polling anti-patterns detected in $files_checked file(s)"
|
echo "✓ No polling anti-patterns detected in $files_checked file(s)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@ -7,16 +7,16 @@
|
|||||||
#
|
#
|
||||||
# Safe by default: nothing is deleted without --clean AND user confirmation.
|
# Safe by default: nothing is deleted without --clean AND user confirmation.
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED=$'\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN=$'\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW=$'\033[1;33m'
|
||||||
CYAN='\033[0;36m'
|
CYAN=$'\033[0;36m'
|
||||||
BOLD='\033[1m'
|
BOLD=$'\033[1m'
|
||||||
RESET='\033[0m'
|
RESET=$'\033[0m'
|
||||||
|
|
||||||
CLEAN=false
|
CLEAN=false
|
||||||
if [[ "${1:-}" == "--clean" ]]; then
|
if [[ "${1:-}" == "--clean" ]]; then
|
||||||
CLEAN=true
|
CLEAN=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TOTAL_RECLAIMABLE=0
|
TOTAL_RECLAIMABLE=0
|
||||||
@ -24,66 +24,66 @@ TOTAL_RECLAIMABLE=0
|
|||||||
# ───────────────────── helpers ─────────────────────
|
# ───────────────────── helpers ─────────────────────
|
||||||
|
|
||||||
human_readable() {
|
human_readable() {
|
||||||
local kb=$1
|
local kb=$1
|
||||||
if (( kb >= 1048576 )); then
|
if ((kb >= 1048576)); then
|
||||||
printf "%.1f GB" "$(echo "scale=1; $kb / 1048576" | bc)"
|
printf "%.1f GB" "$(echo "scale=1; $kb / 1048576" | bc)"
|
||||||
elif (( kb >= 1024 )); then
|
elif ((kb >= 1024)); then
|
||||||
printf "%.1f MB" "$(echo "scale=1; $kb / 1024" | bc)"
|
printf "%.1f MB" "$(echo "scale=1; $kb / 1024" | bc)"
|
||||||
else
|
else
|
||||||
printf "%d KB" "$kb"
|
printf "%d KB" "$kb"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
dir_size_kb() {
|
dir_size_kb() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
if [[ -d "$dir" ]]; then
|
if [[ -d "$dir" ]]; then
|
||||||
du -sk "$dir" 2>/dev/null | awk '{print $1}'
|
du -sk "$dir" 2>/dev/null | awk '{print $1}'
|
||||||
else
|
else
|
||||||
echo 0
|
echo 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
report() {
|
report() {
|
||||||
local label="$1" size_kb="$2" detail="${3:-}"
|
local label="$1" size_kb="$2" detail="${3:-}"
|
||||||
if (( size_kb > 0 )); then
|
if ((size_kb > 0)); then
|
||||||
local hr
|
local hr
|
||||||
hr=$(human_readable "$size_kb")
|
hr=$(human_readable "$size_kb")
|
||||||
printf "${YELLOW}%-40s${RESET} %10s" "$label" "$hr"
|
printf "${YELLOW}%-40s${RESET} %10s" "$label" "$hr"
|
||||||
if [[ -n "$detail" ]]; then
|
if [[ -n "$detail" ]]; then
|
||||||
printf " ${CYAN}(%s)${RESET}" "$detail"
|
printf " ${CYAN}(%s)${RESET}" "$detail"
|
||||||
fi
|
fi
|
||||||
printf "\n"
|
printf "\n"
|
||||||
TOTAL_RECLAIMABLE=$(( TOTAL_RECLAIMABLE + size_kb ))
|
TOTAL_RECLAIMABLE=$((TOTAL_RECLAIMABLE + size_kb))
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Returns 0 if user confirms, 1 otherwise. Always 1 in dry-run.
|
# Returns 0 if user confirms, 1 otherwise. Always 1 in dry-run.
|
||||||
confirm() {
|
confirm() {
|
||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
if $CLEAN; then
|
if $CLEAN; then
|
||||||
printf "${BOLD} → %s [y/N]: ${RESET}" "$prompt"
|
printf "${BOLD} → %s [y/N]: ${RESET}" "$prompt"
|
||||||
read -r ans
|
read -r ans
|
||||||
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Safe wrapper: confirm + action, never fails under errexit
|
# Safe wrapper: confirm + action, never fails under errexit
|
||||||
try_clean() {
|
try_clean() {
|
||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
shift
|
shift
|
||||||
if confirm "$prompt"; then
|
if confirm "$prompt"; then
|
||||||
"$@"
|
"$@"
|
||||||
printf "${GREEN} ✓ Done${RESET}\n"
|
printf "%s ✓ Done%s\n" "${GREEN}" "${RESET}"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ───────────────────── checks ─────────────────────
|
# ───────────────────── 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 "${BOLD}%-40s %10s${RESET}\n" "Category" "Reclaimable"
|
||||||
printf '%s\n' "$(printf '%.0s─' {1..60})"
|
printf '%s\n' "$(printf '%.0s─' {1..60})"
|
||||||
|
|
||||||
@ -91,48 +91,48 @@ printf '%s\n' "$(printf '%.0s─' {1..60})"
|
|||||||
trash_dir="$HOME/.local/share/Trash"
|
trash_dir="$HOME/.local/share/Trash"
|
||||||
size=$(dir_size_kb "$trash_dir")
|
size=$(dir_size_kb "$trash_dir")
|
||||||
report "Trash" "$size" "empty trash"
|
report "Trash" "$size" "empty trash"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Empty trash?" \
|
try_clean "Empty trash?" \
|
||||||
rm -rf "${trash_dir}/files" "${trash_dir}/info" "${trash_dir}/expunged"
|
rm -rf "${trash_dir}/files" "${trash_dir}/info" "${trash_dir}/expunged"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Pacman cache
|
# 2. Pacman cache
|
||||||
pacman_cache_size=$(dir_size_kb "/var/cache/pacman/pkg")
|
pacman_cache_size=$(dir_size_kb "/var/cache/pacman/pkg")
|
||||||
if (( pacman_cache_size > 0 )); then
|
if ((pacman_cache_size > 0)); then
|
||||||
if command -v paccache &>/dev/null; then
|
if command -v paccache &>/dev/null; then
|
||||||
pkg_count=$(ls /var/cache/pacman/pkg/ 2>/dev/null | wc -l)
|
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" \
|
report "Pacman cache ($pkg_count pkgs)" "$pacman_cache_size" \
|
||||||
"sudo paccache -rk2"
|
"sudo paccache -rk2"
|
||||||
try_clean "Clean pacman cache (keep last 2 versions)?" \
|
try_clean "Clean pacman cache (keep last 2 versions)?" \
|
||||||
sudo paccache -rk2
|
sudo paccache -rk2
|
||||||
else
|
else
|
||||||
report "Pacman cache (install pacman-contrib)" "$pacman_cache_size" \
|
report "Pacman cache (install pacman-contrib)" "$pacman_cache_size" \
|
||||||
"pacman -S pacman-contrib && paccache -rk2"
|
"pacman -S pacman-contrib && paccache -rk2"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Yay build cache
|
# 3. Yay build cache
|
||||||
yay_cache="$HOME/.cache/yay"
|
yay_cache="$HOME/.cache/yay"
|
||||||
size=$(dir_size_kb "$yay_cache")
|
size=$(dir_size_kb "$yay_cache")
|
||||||
report "Yay AUR build cache" "$size" "rm -rf ~/.cache/yay"
|
report "Yay AUR build cache" "$size" "rm -rf ~/.cache/yay"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear yay build cache?" rm -rf "${yay_cache:?}"
|
try_clean "Clear yay build cache?" rm -rf "${yay_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Pip cache
|
# 4. Pip cache
|
||||||
pip_cache="$HOME/.cache/pip"
|
pip_cache="$HOME/.cache/pip"
|
||||||
size=$(dir_size_kb "$pip_cache")
|
size=$(dir_size_kb "$pip_cache")
|
||||||
report "Pip cache" "$size" "pip cache purge"
|
report "Pip cache" "$size" "pip cache purge"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear pip cache?" rm -rf "${pip_cache:?}"
|
try_clean "Clear pip cache?" rm -rf "${pip_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. uv cache
|
# 5. uv cache
|
||||||
uv_cache="$HOME/.cache/uv"
|
uv_cache="$HOME/.cache/uv"
|
||||||
size=$(dir_size_kb "$uv_cache")
|
size=$(dir_size_kb "$uv_cache")
|
||||||
report "uv cache" "$size" "uv cache clean"
|
report "uv cache" "$size" "uv cache clean"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear uv cache?" rm -rf "${uv_cache:?}"
|
try_clean "Clear uv cache?" rm -rf "${uv_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 6. HuggingFace cache
|
# 6. HuggingFace cache
|
||||||
@ -149,160 +149,159 @@ report "Suno AI cache" "$size" "manual review recommended"
|
|||||||
go_cache="$HOME/.cache/go-build"
|
go_cache="$HOME/.cache/go-build"
|
||||||
size=$(dir_size_kb "$go_cache")
|
size=$(dir_size_kb "$go_cache")
|
||||||
report "Go build cache" "$size" "go clean -cache"
|
report "Go build cache" "$size" "go clean -cache"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear Go build cache?" rm -rf "${go_cache:?}"
|
try_clean "Clear Go build cache?" rm -rf "${go_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 9. Yarn cache
|
# 9. Yarn cache
|
||||||
yarn_cache="$HOME/.cache/yarn"
|
yarn_cache="$HOME/.cache/yarn"
|
||||||
size=$(dir_size_kb "$yarn_cache")
|
size=$(dir_size_kb "$yarn_cache")
|
||||||
report "Yarn cache" "$size" "yarn cache clean"
|
report "Yarn cache" "$size" "yarn cache clean"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear yarn cache?" rm -rf "${yarn_cache:?}"
|
try_clean "Clear yarn cache?" rm -rf "${yarn_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 10. npm cache
|
# 10. npm cache
|
||||||
npm_cache="$HOME/.npm/_cacache"
|
npm_cache="$HOME/.npm/_cacache"
|
||||||
size=$(dir_size_kb "$npm_cache")
|
size=$(dir_size_kb "$npm_cache")
|
||||||
report "npm cache" "$size" "npm cache clean --force"
|
report "npm cache" "$size" "npm cache clean --force"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear npm cache?" rm -rf "${npm_cache:?}"
|
try_clean "Clear npm cache?" rm -rf "${npm_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 11. Electron cache
|
# 11. Electron cache
|
||||||
electron_cache="$HOME/.cache/electron"
|
electron_cache="$HOME/.cache/electron"
|
||||||
size=$(dir_size_kb "$electron_cache")
|
size=$(dir_size_kb "$electron_cache")
|
||||||
report "Electron cache" "$size"
|
report "Electron cache" "$size"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear Electron cache?" rm -rf "${electron_cache:?}"
|
try_clean "Clear Electron cache?" rm -rf "${electron_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 12. Thumbnails
|
# 12. Thumbnails
|
||||||
thumb_cache="$HOME/.cache/thumbnails"
|
thumb_cache="$HOME/.cache/thumbnails"
|
||||||
size=$(dir_size_kb "$thumb_cache")
|
size=$(dir_size_kb "$thumb_cache")
|
||||||
report "Thumbnails cache" "$size"
|
report "Thumbnails cache" "$size"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear thumbnails?" rm -rf "${thumb_cache:?}"
|
try_clean "Clear thumbnails?" rm -rf "${thumb_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 13. vscode-cpptools cache
|
# 13. vscode-cpptools cache
|
||||||
cpptools_cache="$HOME/.cache/vscode-cpptools"
|
cpptools_cache="$HOME/.cache/vscode-cpptools"
|
||||||
size=$(dir_size_kb "$cpptools_cache")
|
size=$(dir_size_kb "$cpptools_cache")
|
||||||
if (( size > 102400 )); then
|
if ((size > 102400)); then
|
||||||
report "VS Code cpptools cache" "$size"
|
report "VS Code cpptools cache" "$size"
|
||||||
try_clean "Clear cpptools cache?" rm -rf "${cpptools_cache:?}"
|
try_clean "Clear cpptools cache?" rm -rf "${cpptools_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 14. Bazel cache
|
# 14. Bazel cache
|
||||||
bazel_cache="$HOME/.cache/bazel"
|
bazel_cache="$HOME/.cache/bazel"
|
||||||
size=$(dir_size_kb "$bazel_cache")
|
size=$(dir_size_kb "$bazel_cache")
|
||||||
report "Bazel cache" "$size"
|
report "Bazel cache" "$size"
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
try_clean "Clear bazel cache?" rm -rf "${bazel_cache:?}"
|
try_clean "Clear bazel cache?" rm -rf "${bazel_cache:?}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 15. Browser caches (report only)
|
# 15. Browser caches (report only)
|
||||||
for browser_cache in \
|
for browser_cache in \
|
||||||
"$HOME/.cache/thorium" \
|
"$HOME/.cache/thorium" \
|
||||||
"$HOME/.cache/google-chrome" \
|
"$HOME/.cache/google-chrome" \
|
||||||
"$HOME/.cache/mozilla" \
|
"$HOME/.cache/mozilla" \
|
||||||
"$HOME/.cache/BraveSoftware"; do
|
"$HOME/.cache/BraveSoftware"; do
|
||||||
if [[ -d "$browser_cache" ]]; then
|
if [[ -d "$browser_cache" ]]; then
|
||||||
name=$(basename "$browser_cache")
|
name=$(basename "$browser_cache")
|
||||||
size=$(dir_size_kb "$browser_cache")
|
size=$(dir_size_kb "$browser_cache")
|
||||||
if (( size > 102400 )); then
|
if ((size > 102400)); then
|
||||||
report "Browser cache: $name" "$size" "close browser first"
|
report "Browser cache: $name" "$size" "close browser first"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# 16. Docker
|
# 16. Docker
|
||||||
if command -v docker &>/dev/null; then
|
if command -v docker &>/dev/null; then
|
||||||
docker_dir="/var/lib/docker"
|
docker_dir="/var/lib/docker"
|
||||||
size=$(dir_size_kb "$docker_dir" || echo 0)
|
size=$(dir_size_kb "$docker_dir" || echo 0)
|
||||||
if (( size > 1048576 )); then
|
if ((size > 1048576)); then
|
||||||
report "Docker (images+build cache)" "$size" "docker system prune -a"
|
report "Docker (images+build cache)" "$size" "docker system prune -a"
|
||||||
try_clean "Docker system prune (removes all unused)?" \
|
try_clean "Docker system prune (removes all unused)?" \
|
||||||
docker system prune -af --volumes
|
docker system prune -af --volumes
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 17. Orphan packages
|
# 17. Orphan packages
|
||||||
orphans=$(pacman -Qdtq 2>/dev/null || true)
|
orphans=$(pacman -Qdtq 2>/dev/null || true)
|
||||||
orphan_count=0
|
orphan_count=0
|
||||||
if [[ -n "$orphans" ]]; then
|
if [[ -n "$orphans" ]]; then
|
||||||
orphan_count=$(echo "$orphans" | wc -l)
|
orphan_count=$(echo "$orphans" | wc -l)
|
||||||
fi
|
fi
|
||||||
if (( orphan_count > 0 )); then
|
if ((orphan_count > 0)); then
|
||||||
orphan_size=$(echo "$orphans" | xargs pacman -Qi 2>/dev/null \
|
orphan_size=$(echo "$orphans" | xargs pacman -Qi 2>/dev/null |
|
||||||
| awk '/Installed Size/{
|
awk '/Installed Size/{
|
||||||
size=$4; unit=$5;
|
size=$4; unit=$5;
|
||||||
if (unit ~ /GiB/) total += size*1048576;
|
if (unit ~ /GiB/) total += size*1048576;
|
||||||
else if (unit ~ /MiB/) total += size*1024;
|
else if (unit ~ /MiB/) total += size*1024;
|
||||||
else if (unit ~ /KiB/) total += size;
|
else if (unit ~ /KiB/) total += size;
|
||||||
} END{printf "%d", total}' || echo 0)
|
} END{printf "%d", total}' || echo 0)
|
||||||
report "Orphan packages ($orphan_count)" "$orphan_size" \
|
report "Orphan packages ($orphan_count)" "$orphan_size" \
|
||||||
"sudo pacman -Rns \$(pacman -Qdtq)"
|
"sudo pacman -Rns \$(pacman -Qdtq)"
|
||||||
if confirm "Remove $orphan_count orphan packages?"; then
|
if confirm "Remove $orphan_count orphan packages?"; then
|
||||||
# shellcheck disable=SC2046
|
# shellcheck disable=SC2046
|
||||||
sudo pacman -Rns $(pacman -Qdtq) --noconfirm
|
sudo pacman -Rns $(pacman -Qdtq) --noconfirm
|
||||||
printf "${GREEN} ✓ Orphans removed${RESET}\n"
|
printf "%s ✓ Orphans removed%s\n" "${GREEN}" "${RESET}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 18. Journal logs — vacuum to 200M
|
# 18. Journal logs — vacuum to 200M
|
||||||
journal_line=$(journalctl --disk-usage 2>&1 || true)
|
journal_line=$(journalctl --disk-usage 2>&1 || true)
|
||||||
journal_kb=0
|
journal_kb=0
|
||||||
if [[ "$journal_line" =~ ([0-9.]+)G ]]; then
|
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
|
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
|
fi
|
||||||
if (( journal_kb > 204800 )); then
|
if ((journal_kb > 204800)); then
|
||||||
excess=$(( journal_kb - 204800 ))
|
excess=$((journal_kb - 204800))
|
||||||
report "Journal logs (excess over 200M)" "$excess" "journalctl --vacuum-size=200M"
|
report "Journal logs (excess over 200M)" "$excess" "journalctl --vacuum-size=200M"
|
||||||
try_clean "Vacuum journal to 200M?" \
|
try_clean "Vacuum journal to 200M?" \
|
||||||
sudo journalctl --vacuum-size=200M
|
sudo journalctl --vacuum-size=200M
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 19. AUR source dirs — old archives (>30 days)
|
# 19. AUR source dirs — old archives (>30 days)
|
||||||
aur_dir="$HOME/aur"
|
aur_dir="$HOME/aur"
|
||||||
if [[ -d "$aur_dir" ]]; then
|
if [[ -d "$aur_dir" ]]; then
|
||||||
old_archives_kb=$(find "$aur_dir" \
|
old_archives_kb=$(find "$aur_dir" \
|
||||||
\( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \
|
\( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \
|
||||||
-o -name "*.zip" -o -name "*.tar.bz2" \) \
|
-o -name "*.zip" -o -name "*.tar.bz2" \) \
|
||||||
-mtime +30 -printf '%k\n' 2>/dev/null \
|
-mtime +30 -printf '%k\n' 2>/dev/null |
|
||||||
| awk '{t+=$1} END{print t+0}')
|
awk '{t+=$1} END{print t+0}')
|
||||||
if (( old_archives_kb > 0 )); then
|
if ((old_archives_kb > 0)); then
|
||||||
report "AUR old archives (>30d)" "$old_archives_kb" \
|
report "AUR old archives (>30d)" "$old_archives_kb" \
|
||||||
"find ~/aur '*.pkg.tar.zst' -mtime +30 -delete"
|
"find ~/aur '*.pkg.tar.zst' -mtime +30 -delete"
|
||||||
if confirm "Delete old AUR archives (>30 days)?"; then
|
if confirm "Delete old AUR archives (>30 days)?"; then
|
||||||
find "$aur_dir" \
|
find "$aur_dir" \
|
||||||
\( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \
|
\( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \
|
||||||
-o -name "*.zip" -o -name "*.tar.bz2" \) \
|
-o -name "*.zip" -o -name "*.tar.bz2" \) \
|
||||||
-mtime +30 -delete
|
-mtime +30 -delete
|
||||||
printf "${GREEN} ✓ Old AUR archives deleted${RESET}\n"
|
printf "%s ✓ Old AUR archives deleted%s\n" "${GREEN}" "${RESET}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ───────────────────── report-only items ─────────────────────
|
# ───────────────────── 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 \
|
for manual_entry in \
|
||||||
"$HOME/Downloads/too_big:~/Downloads/too_big" \
|
"$HOME/Downloads/too_big:~/Downloads/too_big" \
|
||||||
"$HOME/Downloads:~/Downloads total" \
|
"$HOME/Downloads:~/Downloads total" \
|
||||||
"$HOME/inne:~/inne" \
|
"$HOME/inne:~/inne" \
|
||||||
"/Games:/Games — still playing?"; do
|
"/Games:/Games — still playing?"; do
|
||||||
dir="${manual_entry%%:*}"
|
dir="${manual_entry%%:*}"
|
||||||
label="${manual_entry#*:}"
|
label="${manual_entry#*:}"
|
||||||
size=$(dir_size_kb "$dir")
|
size=$(dir_size_kb "$dir")
|
||||||
if (( size > 0 )); then
|
if ((size > 0)); then
|
||||||
hr=$(human_readable "$size")
|
hr=$(human_readable "$size")
|
||||||
printf "${RED}%-40s %10s${RESET} ${CYAN}(review manually)${RESET}\n" \
|
printf "${RED}%-40s %10s${RESET} ${CYAN}(review manually)${RESET}\n" \
|
||||||
"$label" "$hr"
|
"$label" "$hr"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# ───────────────────── summary ─────────────────────
|
# ───────────────────── summary ─────────────────────
|
||||||
@ -311,20 +310,20 @@ printf "\n%s\n" "$(printf '%.0s─' {1..60})"
|
|||||||
total_hr=$(human_readable "$TOTAL_RECLAIMABLE")
|
total_hr=$(human_readable "$TOTAL_RECLAIMABLE")
|
||||||
printf "${BOLD}Total auto-reclaimable: ${GREEN}%s${RESET}\n" "$total_hr"
|
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%\%}"
|
used_pct="${used_pct%\%}"
|
||||||
printf "${BOLD}Current usage: %s / %s (%s%%)${RESET}\n" \
|
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
|
if ((TOTAL_RECLAIMABLE > 0)); then
|
||||||
new_used=$(( used_kb - TOTAL_RECLAIMABLE ))
|
new_used=$((used_kb - TOTAL_RECLAIMABLE))
|
||||||
if (( new_used < 0 )); then new_used=0; fi
|
if ((new_used < 0)); then new_used=0; fi
|
||||||
new_pct=$(( new_used * 100 / total_kb ))
|
new_pct=$((new_used * 100 / total_kb))
|
||||||
printf "${BOLD}After cleanup: %s / %s (%d%%)${RESET}\n" \
|
printf "${BOLD}After cleanup: %s / %s (%d%%)${RESET}\n" \
|
||||||
"$(human_readable "$new_used")" "$(human_readable "$total_kb")" "$new_pct"
|
"$(human_readable "$new_used")" "$(human_readable "$total_kb")" "$new_pct"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! $CLEAN; then
|
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
|
fi
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
|||||||
@ -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
|
if [ ! -f "$SCRIPT_DIR/config_secrets.sh" ] && [ -f "/data/local/tmp/focus_mode/config_secrets.sh" ]; then
|
||||||
SCRIPT_DIR="/data/local/tmp/focus_mode"
|
SCRIPT_DIR="/data/local/tmp/focus_mode"
|
||||||
fi
|
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"
|
. "$SCRIPT_DIR/config_secrets.sh"
|
||||||
|
|
||||||
# --- Radius in meters ---
|
# --- Radius in meters ---
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -36,135 +36,135 @@ touch "$DNS_LOG"
|
|||||||
chmod 666 "$DNS_LOG" 2>/dev/null || true
|
chmod 666 "$DNS_LOG" 2>/dev/null || true
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
local ts
|
local ts
|
||||||
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
echo "[$ts] $1" >> "$DNS_LOG"
|
echo "[$ts] $1" >>"$DNS_LOG"
|
||||||
}
|
}
|
||||||
|
|
||||||
rotate_log() {
|
rotate_log() {
|
||||||
local lines
|
local lines
|
||||||
lines="$(wc -l < "$DNS_LOG" 2>/dev/null || echo 0)"
|
lines="$(wc -l <"$DNS_LOG" 2>/dev/null || echo 0)"
|
||||||
if [ "$lines" -gt 500 ]; then
|
if [ "$lines" -gt 500 ]; then
|
||||||
local tmp="$DNS_LOG.tmp"
|
local tmp="$DNS_LOG.tmp"
|
||||||
tail -n 500 "$DNS_LOG" > "$tmp"
|
tail -n 500 "$DNS_LOG" >"$tmp"
|
||||||
mv "$tmp" "$DNS_LOG"
|
mv "$tmp" "$DNS_LOG"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
acquire_lock() {
|
acquire_lock() {
|
||||||
if [ -f "$PIDFILE" ]; then
|
if [ -f "$PIDFILE" ]; then
|
||||||
local old_pid
|
local old_pid
|
||||||
old_pid="$(cat "$PIDFILE")"
|
old_pid="$(cat "$PIDFILE")"
|
||||||
if kill -0 "$old_pid" 2>/dev/null; then
|
if kill -0 "$old_pid" 2>/dev/null; then
|
||||||
local cmdline
|
local cmdline
|
||||||
cmdline="$(tr '\0' ' ' < "/proc/$old_pid/cmdline" 2>/dev/null)"
|
cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)"
|
||||||
if echo "$cmdline" | grep -q "dns_enforcer"; then
|
if echo "$cmdline" | grep -q "dns_enforcer"; then
|
||||||
echo "dns_enforcer already running (PID $old_pid)"
|
echo "dns_enforcer already running (PID $old_pid)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
rm -f "$PIDFILE"
|
rm -f "$PIDFILE"
|
||||||
fi
|
fi
|
||||||
echo $$ > "$PIDFILE"
|
echo $$ >"$PIDFILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Private DNS ----
|
# ---- Private DNS ----
|
||||||
|
|
||||||
ensure_private_dns_off() {
|
ensure_private_dns_off() {
|
||||||
local mode
|
local mode
|
||||||
mode="$(settings get global private_dns_mode 2>/dev/null)"
|
mode="$(settings get global private_dns_mode 2>/dev/null)"
|
||||||
# Possible values: "off", "opportunistic", "hostname", null (default=opportunistic)
|
# Possible values: "off", "opportunistic", "hostname", null (default=opportunistic)
|
||||||
if [ "$mode" != "off" ]; then
|
if [ "$mode" != "off" ]; then
|
||||||
settings put global private_dns_mode off 2>/dev/null
|
settings put global private_dns_mode off 2>/dev/null
|
||||||
log "Private DNS was '$mode' - forced to 'off'"
|
log "Private DNS was '$mode' - forced to 'off'"
|
||||||
fi
|
fi
|
||||||
# Clear any pinned DoT hostname so the "hostname" mode cannot be
|
# Clear any pinned DoT hostname so the "hostname" mode cannot be
|
||||||
# re-enabled silently by Settings UI.
|
# re-enabled silently by Settings UI.
|
||||||
local spec
|
local spec
|
||||||
spec="$(settings get global private_dns_specifier 2>/dev/null)"
|
spec="$(settings get global private_dns_specifier 2>/dev/null)"
|
||||||
if [ -n "$spec" ] && [ "$spec" != "null" ]; then
|
if [ -n "$spec" ] && [ "$spec" != "null" ]; then
|
||||||
settings delete global private_dns_specifier 2>/dev/null
|
settings delete global private_dns_specifier 2>/dev/null
|
||||||
log "Cleared private_dns_specifier (was '$spec')"
|
log "Cleared private_dns_specifier (was '$spec')"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- iptables chain management ----
|
# ---- iptables chain management ----
|
||||||
|
|
||||||
ensure_chain() {
|
ensure_chain() {
|
||||||
local ipt="$1"
|
local ipt="$1"
|
||||||
# Create the chain if missing.
|
# Create the chain if missing.
|
||||||
if ! "$ipt" -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
|
if ! "$ipt" -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
|
||||||
"$ipt" -N "$DNS_IPT_CHAIN" 2>/dev/null || {
|
"$ipt" -N "$DNS_IPT_CHAIN" 2>/dev/null || {
|
||||||
log "ERROR: could not create $ipt chain $DNS_IPT_CHAIN"
|
log "ERROR: could not create $ipt chain $DNS_IPT_CHAIN"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
log "Created $ipt chain $DNS_IPT_CHAIN"
|
log "Created $ipt chain $DNS_IPT_CHAIN"
|
||||||
fi
|
fi
|
||||||
# Remove ALL existing OUTPUT -> chain jumps (handles duplicates from
|
# Remove ALL existing OUTPUT -> chain jumps (handles duplicates from
|
||||||
# previous iptables lock races where -C returned error but -I succeeded).
|
# previous iptables lock races where -C returned error but -I succeeded).
|
||||||
local removed=0
|
local removed=0
|
||||||
while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do
|
while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do
|
||||||
removed=$((removed + 1))
|
removed=$((removed + 1))
|
||||||
done
|
done
|
||||||
# Insert exactly one jump at position 1 of OUTPUT.
|
# Insert exactly one jump at position 1 of OUTPUT.
|
||||||
if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then
|
if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then
|
||||||
if [ "$removed" -gt 1 ]; then
|
if [ "$removed" -gt 1 ]; then
|
||||||
log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN"
|
log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt"
|
log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
fill_chain_v4() {
|
fill_chain_v4() {
|
||||||
# Flush and refill so we always converge to the intended rule set.
|
# Flush and refill so we always converge to the intended rule set.
|
||||||
iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
||||||
# Drop DoT everywhere. This is a narrow port rule - there's no legit
|
# Drop DoT everywhere. This is a narrow port rule - there's no legit
|
||||||
# reason for arbitrary apps to talk 853/tcp on Android.
|
# reason for arbitrary apps to talk 853/tcp on Android.
|
||||||
iptables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
iptables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
||||||
--reject-with tcp-reset 2>/dev/null || true
|
--reject-with tcp-reset 2>/dev/null || true
|
||||||
iptables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
iptables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
||||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||||
|
|
||||||
local ip
|
local ip
|
||||||
for ip in $DNS_DOH_IPV4; do
|
for ip in $DNS_DOH_IPV4; do
|
||||||
[ -z "$ip" ] && continue
|
[ -z "$ip" ] && continue
|
||||||
[ "${ip#\#}" != "$ip" ] && continue
|
[ "${ip#\#}" != "$ip" ] && continue
|
||||||
# Reject 443/tcp (DoH) and 53 (classic DNS) to well-known resolvers.
|
# 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
|
# 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.
|
# (ignoring /etc/resolv.conf) still fall back to the system resolver.
|
||||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
||||||
--reject-with tcp-reset 2>/dev/null || true
|
--reject-with tcp-reset 2>/dev/null || true
|
||||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
||||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
||||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
||||||
--reject-with tcp-reset 2>/dev/null || true
|
--reject-with tcp-reset 2>/dev/null || true
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
fill_chain_v6() {
|
fill_chain_v6() {
|
||||||
ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
||||||
ip6tables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
ip6tables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
||||||
--reject-with tcp-reset 2>/dev/null || true
|
--reject-with tcp-reset 2>/dev/null || true
|
||||||
ip6tables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
ip6tables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
||||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||||
|
|
||||||
local ip
|
local ip
|
||||||
for ip in $DNS_DOH_IPV6; do
|
for ip in $DNS_DOH_IPV6; do
|
||||||
[ -z "$ip" ] && continue
|
[ -z "$ip" ] && continue
|
||||||
[ "${ip#\#}" != "$ip" ] && continue
|
[ "${ip#\#}" != "$ip" ] && continue
|
||||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
||||||
--reject-with tcp-reset 2>/dev/null || true
|
--reject-with tcp-reset 2>/dev/null || true
|
||||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
||||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
||||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
||||||
--reject-with tcp-reset 2>/dev/null || true
|
--reject-with tcp-reset 2>/dev/null || true
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- State-change gate ----
|
# ---- State-change gate ----
|
||||||
@ -179,65 +179,71 @@ fill_chain_v6() {
|
|||||||
# redundant identical rebuilds are skipped.
|
# redundant identical rebuilds are skipped.
|
||||||
|
|
||||||
expected_rule_count() {
|
expected_rule_count() {
|
||||||
# 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP.
|
# 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP.
|
||||||
local n=2 ip
|
local n=2 ip
|
||||||
for ip in "$@"; do
|
for ip in "$@"; do
|
||||||
[ -z "$ip" ] && continue
|
[ -z "$ip" ] && continue
|
||||||
[ "${ip#\#}" != "$ip" ] && continue
|
[ "${ip#\#}" != "$ip" ] && continue
|
||||||
n=$((n + 4))
|
n=$((n + 4))
|
||||||
done
|
done
|
||||||
echo "$n"
|
echo "$n"
|
||||||
}
|
}
|
||||||
|
|
||||||
chain_intact() {
|
chain_intact() {
|
||||||
local ipt="$1" expected="$2" actual
|
local ipt="$1" expected="$2" actual
|
||||||
"$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1 || return 1
|
"$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="$("$ipt" -S "$DNS_IPT_CHAIN" 2>/dev/null | grep -c '^-A')"
|
||||||
[ "$actual" = "$expected" ]
|
[ "$actual" = "$expected" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce_iptables() {
|
enforce_iptables() {
|
||||||
if command -v iptables >/dev/null 2>&1; then
|
if command -v iptables >/dev/null 2>&1; then
|
||||||
if chain_intact iptables "$(expected_rule_count $DNS_DOH_IPV4)"; then
|
# Intentional word split: expected_rule_count() iterates "$@" over the
|
||||||
:
|
# configured IP list. This runs under /system/bin/sh on the phone, where
|
||||||
elif ensure_chain iptables && fill_chain_v4; then
|
# arrays do not exist, so an unquoted expansion is the POSIX way to do it.
|
||||||
log "iptables (v4) DNS chain rebuilt (was missing/tampered)"
|
# shellcheck disable=SC2086
|
||||||
fi
|
if chain_intact iptables "$(expected_rule_count $DNS_DOH_IPV4)"; then
|
||||||
fi
|
:
|
||||||
if command -v ip6tables >/dev/null 2>&1; then
|
elif ensure_chain iptables && fill_chain_v4; then
|
||||||
if chain_intact ip6tables "$(expected_rule_count $DNS_DOH_IPV6)"; then
|
log "iptables (v4) DNS chain rebuilt (was missing/tampered)"
|
||||||
:
|
fi
|
||||||
elif ensure_chain ip6tables && fill_chain_v6; then
|
fi
|
||||||
log "ip6tables (v6) DNS chain rebuilt (was missing/tampered)"
|
if command -v ip6tables >/dev/null 2>&1; then
|
||||||
fi
|
# Intentional word split, as above.
|
||||||
fi
|
# 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() {
|
cleanup() {
|
||||||
# We intentionally leave the iptables chain in place on SIGTERM so
|
# We intentionally leave the iptables chain in place on SIGTERM so
|
||||||
# stopping the enforcer for maintenance does not immediately re-open
|
# stopping the enforcer for maintenance does not immediately re-open
|
||||||
# the DoH hole. `focus_ctl.sh dns-stop` does the explicit teardown.
|
# the DoH hole. `focus_ctl.sh dns-stop` does the explicit teardown.
|
||||||
log "dns_enforcer shutting down"
|
log "dns_enforcer shutting down"
|
||||||
rm -f "$PIDFILE"
|
rm -f "$PIDFILE"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup INT TERM
|
trap cleanup INT TERM
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
acquire_lock
|
acquire_lock
|
||||||
log "dns_enforcer started (PID=$$)"
|
log "dns_enforcer started (PID=$$)"
|
||||||
|
|
||||||
# Initial arm-up
|
# Initial arm-up
|
||||||
ensure_private_dns_off
|
ensure_private_dns_off
|
||||||
enforce_iptables
|
enforce_iptables
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
ensure_private_dns_off
|
ensure_private_dns_off
|
||||||
enforce_iptables
|
enforce_iptables
|
||||||
rotate_log
|
rotate_log
|
||||||
sleep "$DNS_CHECK_INTERVAL"
|
sleep "$DNS_CHECK_INTERVAL"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -13,166 +13,166 @@ PIDFILE="$STATE_DIR/daemon.pid"
|
|||||||
|
|
||||||
# ---- PID lock: exit if already running ----
|
# ---- PID lock: exit if already running ----
|
||||||
acquire_lock() {
|
acquire_lock() {
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
if [ -f "$PIDFILE" ]; then
|
if [ -f "$PIDFILE" ]; then
|
||||||
local old_pid
|
local old_pid
|
||||||
old_pid="$(cat "$PIDFILE")"
|
old_pid="$(cat "$PIDFILE")"
|
||||||
if kill -0 "$old_pid" 2>/dev/null; then
|
if kill -0 "$old_pid" 2>/dev/null; then
|
||||||
# Verify the PID is actually a focus_daemon, not a reused PID
|
# Verify the PID is actually a focus_daemon, not a reused PID
|
||||||
local cmdline
|
local cmdline
|
||||||
cmdline="$(cat /proc/$old_pid/cmdline 2>/dev/null | tr '\0' ' ')"
|
cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)"
|
||||||
if echo "$cmdline" | grep -q "focus_daemon"; then
|
if echo "$cmdline" | grep -q "focus_daemon"; then
|
||||||
echo "Daemon already running (PID $old_pid), exiting."
|
echo "Daemon already running (PID $old_pid), exiting."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Stale or reused pidfile
|
# Stale or reused pidfile
|
||||||
rm -f "$PIDFILE"
|
rm -f "$PIDFILE"
|
||||||
fi
|
fi
|
||||||
echo $$ > "$PIDFILE"
|
echo $$ >"$PIDFILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Logging ----
|
# ---- Logging ----
|
||||||
log() {
|
log() {
|
||||||
local ts
|
local ts
|
||||||
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
echo "[$ts] $1" >> "$LOG_FILE"
|
echo "[$ts] $1" >>"$LOG_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
rotate_log() {
|
rotate_log() {
|
||||||
local lines
|
local lines
|
||||||
lines="$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)"
|
lines="$(wc -l <"$LOG_FILE" 2>/dev/null || echo 0)"
|
||||||
if [ "$lines" -gt "$LOG_MAX_LINES" ]; then
|
if [ "$lines" -gt "$LOG_MAX_LINES" ]; then
|
||||||
local tmp="$LOG_FILE.tmp"
|
local tmp="$LOG_FILE.tmp"
|
||||||
tail -n "$LOG_MAX_LINES" "$LOG_FILE" > "$tmp"
|
tail -n "$LOG_MAX_LINES" "$LOG_FILE" >"$tmp"
|
||||||
mv "$tmp" "$LOG_FILE"
|
mv "$tmp" "$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Build helper files for fast package checks ----
|
# ---- Build helper files for fast package checks ----
|
||||||
|
|
||||||
build_whitelist_file() {
|
build_whitelist_file() {
|
||||||
echo "$WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \
|
echo "$WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' |
|
||||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/whitelist.txt"
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/whitelist.txt"
|
||||||
# Sanity check: the WHITELIST string in config.sh is fragile - any
|
# Sanity check: the WHITELIST string in config.sh is fragile - any
|
||||||
# literal double-quote inside a comment will close the heredoc and
|
# literal double-quote inside a comment will close the heredoc and
|
||||||
# silently truncate the variable. Log the parsed line count so any
|
# silently truncate the variable. Log the parsed line count so any
|
||||||
# future regression is visible in the log, and warn loudly if it
|
# future regression is visible in the log, and warn loudly if it
|
||||||
# falls below a known floor (we always have ~70+ entries).
|
# falls below a known floor (we always have ~70+ entries).
|
||||||
local n
|
local n
|
||||||
n=$(wc -l < "$STATE_DIR/whitelist.txt" 2>/dev/null | tr -d ' ')
|
n=$(wc -l <"$STATE_DIR/whitelist.txt" 2>/dev/null | tr -d ' ')
|
||||||
log "Whitelist parsed: $n entries"
|
log "Whitelist parsed: $n entries"
|
||||||
if [ "${n:-0}" -lt 30 ]; then
|
if [ "${n:-0}" -lt 30 ]; then
|
||||||
log "WARN: whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside WHITELIST string"
|
log "WARN: whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside WHITELIST string"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
build_night_whitelist_file() {
|
build_night_whitelist_file() {
|
||||||
# Strict allow-list used while the night curfew is active (see config.sh
|
# 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.
|
# NIGHT_WHITELIST and is_curfew_now()). Parsed exactly like the day list.
|
||||||
echo "$NIGHT_WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \
|
echo "$NIGHT_WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' |
|
||||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/night_whitelist.txt"
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/night_whitelist.txt"
|
||||||
local n
|
local n
|
||||||
n=$(wc -l < "$STATE_DIR/night_whitelist.txt" 2>/dev/null | tr -d ' ')
|
n=$(wc -l <"$STATE_DIR/night_whitelist.txt" 2>/dev/null | tr -d ' ')
|
||||||
log "Night-curfew whitelist parsed: $n entries"
|
log "Night-curfew whitelist parsed: $n entries"
|
||||||
if [ "${n:-0}" -lt 10 ]; then
|
if [ "${n:-0}" -lt 10 ]; then
|
||||||
log "WARN: night whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside NIGHT_WHITELIST string"
|
log "WARN: night whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside NIGHT_WHITELIST string"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
build_sysprotect_file() {
|
build_sysprotect_file() {
|
||||||
echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' \
|
echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' |
|
||||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/sysprotect.txt"
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/sysprotect.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
# $BLOCKED_SYSTEM_APPS is a static config value (never changes without a
|
# $BLOCKED_SYSTEM_APPS is a static config value (never changes without a
|
||||||
# daemon restart), so - like build_sysprotect_file above - this only needs
|
# daemon restart), so - like build_sysprotect_file above - this only needs
|
||||||
# to run once at startup, not every enable_focus_mode() sweep.
|
# to run once at startup, not every enable_focus_mode() sweep.
|
||||||
build_blocked_sys_file() {
|
build_blocked_sys_file() {
|
||||||
echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \
|
echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' |
|
||||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/blocked_sys.txt"
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/blocked_sys.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
reconcile_disabled_apps() {
|
reconcile_disabled_apps() {
|
||||||
[ -f "$DISABLED_APPS_FILE" ] || return
|
[ -f "$DISABLED_APPS_FILE" ] || return
|
||||||
|
|
||||||
local tmp_disabled="$STATE_DIR/disabled_by_focus.tmp"
|
local tmp_disabled="$STATE_DIR/disabled_by_focus.tmp"
|
||||||
: > "$tmp_disabled"
|
: >"$tmp_disabled"
|
||||||
|
|
||||||
while IFS= read -r pkg; do
|
while IFS= read -r pkg; do
|
||||||
[ -z "$pkg" ] && continue
|
[ -z "$pkg" ] && continue
|
||||||
|
|
||||||
if is_allowed "$pkg"; then
|
if is_allowed "$pkg"; then
|
||||||
pm install-existing --user 0 "$pkg" >/dev/null 2>&1 || true
|
pm install-existing --user 0 "$pkg" >/dev/null 2>&1 || true
|
||||||
pm enable "$pkg" >/dev/null 2>&1 || true
|
pm enable "$pkg" >/dev/null 2>&1 || true
|
||||||
log "Re-enabled allowed app during state reconciliation: $pkg"
|
log "Re-enabled allowed app during state reconciliation: $pkg"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$pkg" >> "$tmp_disabled"
|
echo "$pkg" >>"$tmp_disabled"
|
||||||
done < "$DISABLED_APPS_FILE"
|
done <"$DISABLED_APPS_FILE"
|
||||||
|
|
||||||
mv "$tmp_disabled" "$DISABLED_APPS_FILE"
|
mv "$tmp_disabled" "$DISABLED_APPS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Initialization ----
|
# ---- Initialization ----
|
||||||
init() {
|
init() {
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
touch "$LOG_FILE"
|
touch "$LOG_FILE"
|
||||||
touch "$DISABLED_APPS_FILE"
|
touch "$DISABLED_APPS_FILE"
|
||||||
# Ensure state files are writable (survives reboot / permission drift)
|
# Ensure state files are writable (survives reboot / permission drift)
|
||||||
chmod 666 "$LOG_FILE" "$DISABLED_APPS_FILE" "$PIDFILE" 2>/dev/null
|
chmod 666 "$LOG_FILE" "$DISABLED_APPS_FILE" "$PIDFILE" 2>/dev/null
|
||||||
# Status file must be world-readable (companion app reads it).
|
# Status file must be world-readable (companion app reads it).
|
||||||
# State dir must be world-writable+executable so the companion app can
|
# State dir must be world-writable+executable so the companion app can
|
||||||
# drop the recheck trigger file (it runs as a normal app UID).
|
# drop the recheck trigger file (it runs as a normal app UID).
|
||||||
chmod 777 "$STATE_DIR" 2>/dev/null
|
chmod 777 "$STATE_DIR" 2>/dev/null
|
||||||
|
|
||||||
if [ "$HOME_LAT" = "0.000000" ] && [ "$HOME_LON" = "0.000000" ]; then
|
if [ "$HOME_LAT" = "0.000000" ] && [ "$HOME_LON" = "0.000000" ]; then
|
||||||
log "ERROR: Home coordinates not set! Edit config_secrets.sh first."
|
log "ERROR: Home coordinates not set! Edit config_secrets.sh first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! echo "$HOME_LAT" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then
|
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"
|
log "ERROR: HOME_LAT is invalid ('$HOME_LAT'). Expected decimal degrees in config_secrets.sh"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! echo "$HOME_LON" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then
|
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"
|
log "ERROR: HOME_LON is invalid ('$HOME_LON'). Expected decimal degrees in config_secrets.sh"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build_whitelist_file
|
build_whitelist_file
|
||||||
build_night_whitelist_file
|
build_night_whitelist_file
|
||||||
build_sysprotect_file
|
build_sysprotect_file
|
||||||
build_blocked_sys_file
|
build_blocked_sys_file
|
||||||
refresh_default_handlers
|
refresh_default_handlers
|
||||||
rotate_log
|
rotate_log
|
||||||
|
|
||||||
if [ -f "$MODE_FILE" ]; then
|
if [ -f "$MODE_FILE" ]; then
|
||||||
CURRENT_MODE="$(cat "$MODE_FILE")"
|
CURRENT_MODE="$(cat "$MODE_FILE")"
|
||||||
else
|
else
|
||||||
CURRENT_MODE="normal"
|
CURRENT_MODE="normal"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CURRENT_MODE" = "focus" ]; then
|
if [ "$CURRENT_MODE" = "focus" ]; then
|
||||||
reconcile_disabled_apps
|
reconcile_disabled_apps
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Focus mode daemon started (PID=$$, mode=$CURRENT_MODE, home=$HOME_LAT,$HOME_LON, radius=${RADIUS}m)"
|
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 "Intervals: focus=${CHECK_INTERVAL_FOCUS}s normal=${CHECK_INTERVAL_NORMAL}s"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Location ----
|
# ---- Location ----
|
||||||
get_location() {
|
get_location() {
|
||||||
dumpsys location 2>/dev/null \
|
dumpsys location 2>/dev/null |
|
||||||
| grep -oE '[-]?[0-9]{1,3}\.[0-9]{4,},[-]?[0-9]{1,3}\.[0-9]{4,}' \
|
grep -oE '[-]?[0-9]{1,3}\.[0-9]{4,},[-]?[0-9]{1,3}\.[0-9]{4,}' |
|
||||||
| head -1
|
head -1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Distance Calculation (Haversine via awk) ----
|
# ---- Distance Calculation (Haversine via awk) ----
|
||||||
calc_distance() {
|
calc_distance() {
|
||||||
echo "$1 $2 $3 $4" | awk '{
|
echo "$1 $2 $3 $4" | awk '{
|
||||||
PI = 3.14159265358979323846
|
PI = 3.14159265358979323846
|
||||||
R = 6371000.0
|
R = 6371000.0
|
||||||
lat1 = $1 * PI / 180.0
|
lat1 = $1 * PI / 180.0
|
||||||
@ -195,27 +195,29 @@ calc_distance() {
|
|||||||
# can never strand you behind the strict list — essentials stay reachable
|
# can never strand you behind the strict list — essentials stay reachable
|
||||||
# either way, but the day list is the less-surprising default.
|
# either way, but the day list is the less-surprising default.
|
||||||
_dec() {
|
_dec() {
|
||||||
# Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed
|
# Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed
|
||||||
# as (sometimes invalid) octal by the shell's arithmetic. Portable across
|
# as (sometimes invalid) octal by the shell's arithmetic. Portable across
|
||||||
# ash/mksh; keeps at least one digit so "0000" -> "0".
|
# ash/mksh; keeps at least one digit so "0000" -> "0".
|
||||||
local n="$1"
|
local n="$1"
|
||||||
while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done
|
while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done
|
||||||
printf '%s' "$n"
|
printf '%s' "$n"
|
||||||
}
|
}
|
||||||
|
|
||||||
is_curfew_now() {
|
is_curfew_now() {
|
||||||
local now start end
|
local now start end
|
||||||
now="$(date +%H%M 2>/dev/null)"
|
now="$(date +%H%M 2>/dev/null)"
|
||||||
case "$now" in
|
case "$now" in
|
||||||
''|*[!0-9]*) return 1 ;;
|
'' | *[!0-9]*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
now="$(_dec "$now")"; start="$(_dec "$NIGHT_CURFEW_START")"; end="$(_dec "$NIGHT_CURFEW_END")"
|
now="$(_dec "$now")"
|
||||||
if [ "$start" -le "$end" ]; then
|
start="$(_dec "$NIGHT_CURFEW_START")"
|
||||||
[ "$now" -ge "$start" ] && [ "$now" -lt "$end" ]
|
end="$(_dec "$NIGHT_CURFEW_END")"
|
||||||
else
|
if [ "$start" -le "$end" ]; then
|
||||||
# Window wraps past midnight (e.g. 2300 -> 0500).
|
[ "$now" -ge "$start" ] && [ "$now" -lt "$end" ]
|
||||||
[ "$now" -ge "$start" ] || [ "$now" -lt "$end" ]
|
else
|
||||||
fi
|
# 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
|
# 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
|
# 10s tick for N enabled apps, all recomputing a value that cannot change
|
||||||
# within a single tick. Result is cached in _CURFEW_TICK_RESULT.
|
# within a single tick. Result is cached in _CURFEW_TICK_RESULT.
|
||||||
curfew_active() {
|
curfew_active() {
|
||||||
if [ -n "$_CURFEW_TICK_CACHED" ]; then
|
if [ -n "$_CURFEW_TICK_CACHED" ]; then
|
||||||
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
_CURFEW_TICK_CACHED=1
|
_CURFEW_TICK_CACHED=1
|
||||||
_CURFEW_TICK_RESULT=0
|
_CURFEW_TICK_RESULT=0
|
||||||
if [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] && [ ! -e "$CURFEW_OVERRIDE_FILE" ]; then
|
if [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] && [ ! -e "$CURFEW_OVERRIDE_FILE" ]; then
|
||||||
if [ -e "$CURFEW_FORCE_FILE" ] || is_curfew_now; then
|
if [ -e "$CURFEW_FORCE_FILE" ] || is_curfew_now; then
|
||||||
_CURFEW_TICK_RESULT=1
|
_CURFEW_TICK_RESULT=1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Check if package is allowed (whitelist or system-protected) ----
|
# ---- Check if package is allowed (whitelist or system-protected) ----
|
||||||
is_allowed() {
|
is_allowed() {
|
||||||
local pkg="$1"
|
local pkg="$1"
|
||||||
# During the night curfew, swap the permissive day list for the strict
|
# During the night curfew, swap the permissive day list for the strict
|
||||||
# night list. The sysprotect + default-handler guards below still apply on
|
# night list. The sysprotect + default-handler guards below still apply on
|
||||||
# top of whichever list is active.
|
# top of whichever list is active.
|
||||||
local list="$STATE_DIR/whitelist.txt"
|
local list="$STATE_DIR/whitelist.txt"
|
||||||
if curfew_active; then
|
if curfew_active; then
|
||||||
list="$STATE_DIR/night_whitelist.txt"
|
list="$STATE_DIR/night_whitelist.txt"
|
||||||
fi
|
fi
|
||||||
# Exact match against the active whitelist file
|
# Exact match against the active whitelist file
|
||||||
if grep -qxF "$pkg" "$list" 2>/dev/null; then
|
if grep -qxF "$pkg" "$list" 2>/dev/null; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
# Prefix match against system-protect file
|
# Prefix match against system-protect file
|
||||||
while IFS= read -r prefix; do
|
while IFS= read -r prefix; do
|
||||||
[ -z "$prefix" ] && continue
|
[ -z "$prefix" ] && continue
|
||||||
case "$pkg" in
|
case "$pkg" in
|
||||||
"$prefix"*) return 0 ;;
|
"$prefix"*) return 0 ;;
|
||||||
esac
|
esac
|
||||||
done < "$STATE_DIR/sysprotect.txt"
|
done <"$STATE_DIR/sysprotect.txt"
|
||||||
# Hard-stop guard: refuse to disable any package that is the current
|
# Hard-stop guard: refuse to disable any package that is the current
|
||||||
# default handler for a critical role (Dialer / SMS / Home / Contacts).
|
# default handler for a critical role (Dialer / SMS / Home / Contacts).
|
||||||
# Without this, a misconfigured WHITELIST can disable the default Phone
|
# Without this, a misconfigured WHITELIST can disable the default Phone
|
||||||
# app and Android falls back to com.android.settings/.FallbackHome -
|
# app and Android falls back to com.android.settings/.FallbackHome -
|
||||||
# the persistent "Phone is starting..." screen with broken SystemUI
|
# the persistent "Phone is starting..." screen with broken SystemUI
|
||||||
# gestures (no swipe-up recents). Recovering requires `pm enable` over
|
# gestures (no swipe-up recents). Recovering requires `pm enable` over
|
||||||
# ADB. Treat the guard as last-resort safety net independent of WHITELIST
|
# ADB. Treat the guard as last-resort safety net independent of WHITELIST
|
||||||
# contents so a future config edit can never wipe these out.
|
# contents so a future config edit can never wipe these out.
|
||||||
is_default_handler "$pkg" && return 0
|
is_default_handler "$pkg" && return 0
|
||||||
# The default browser is guarded only OUTSIDE curfew. At night the whole
|
# 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.
|
# point is to disable browsers, so this guard must not re-allow it.
|
||||||
if ! curfew_active \
|
if ! curfew_active &&
|
||||||
&& grep -qxF "$pkg" "$STATE_DIR/default_browser.txt" 2>/dev/null; then
|
grep -qxF "$pkg" "$STATE_DIR/default_browser.txt" 2>/dev/null; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Default handler detection ----
|
# ---- Default handler detection ----
|
||||||
# Refreshed once per focus_daemon tick into $STATE_DIR/default_handlers.txt.
|
# 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.
|
# Each line is a package name. Lookup is a cheap grep against this file.
|
||||||
refresh_default_handlers() {
|
refresh_default_handlers() {
|
||||||
local f="$STATE_DIR/default_handlers.txt"
|
local f="$STATE_DIR/default_handlers.txt"
|
||||||
local tmp="$f.tmp"
|
local tmp="$f.tmp"
|
||||||
: > "$tmp"
|
: >"$tmp"
|
||||||
# Default Home (launcher). resolve-activity prints "Activity Resolver Table:"
|
# Default Home (launcher). resolve-activity prints "Activity Resolver Table:"
|
||||||
# on line 1 and "<pkg>/<.Activity>" on line 2 in --brief mode.
|
# on line 1 and "<pkg>/<.Activity>" on line 2 in --brief mode.
|
||||||
cmd package resolve-activity --brief \
|
cmd package resolve-activity --brief \
|
||||||
-c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null \
|
-c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null |
|
||||||
| awk -F/ 'NR==2 && $1 != "" {print $1}' >> "$tmp"
|
awk -F/ 'NR==2 && $1 != "" {print $1}' >>"$tmp"
|
||||||
# Default Dialer
|
# Default Dialer
|
||||||
local dialer
|
local dialer
|
||||||
dialer="$(cmd telecom get-default-dialer 2>/dev/null | tr -d '[:space:]')"
|
dialer="$(cmd telecom get-default-dialer 2>/dev/null | tr -d '[:space:]')"
|
||||||
[ -n "$dialer" ] && echo "$dialer" >> "$tmp"
|
[ -n "$dialer" ] && echo "$dialer" >>"$tmp"
|
||||||
# Default SMS handler (settings provider key)
|
# Default SMS handler (settings provider key)
|
||||||
local sms
|
local sms
|
||||||
sms="$(settings get secure sms_default_application 2>/dev/null | tr -d '[:space:]')"
|
sms="$(settings get secure sms_default_application 2>/dev/null | tr -d '[:space:]')"
|
||||||
[ -n "$sms" ] && [ "$sms" != "null" ] && echo "$sms" >> "$tmp"
|
[ -n "$sms" ] && [ "$sms" != "null" ] && echo "$sms" >>"$tmp"
|
||||||
# Default input method (active keyboard). Disabling the active IME with
|
# Default input method (active keyboard). Disabling the active IME with
|
||||||
# pm disable-user PERSISTS across reboot; a 1am reboot would then leave no
|
# 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
|
# keyboard to type any recovery command. Protect it day and night so the
|
||||||
# curfew can never lock you out of typing.
|
# curfew can never lock you out of typing.
|
||||||
local ime
|
local ime
|
||||||
ime="$(settings get secure default_input_method 2>/dev/null | cut -d/ -f1)"
|
ime="$(settings get secure default_input_method 2>/dev/null | cut -d/ -f1)"
|
||||||
[ -n "$ime" ] && [ "$ime" != "null" ] && echo "$ime" >> "$tmp"
|
[ -n "$ime" ] && [ "$ime" != "null" ] && echo "$ime" >>"$tmp"
|
||||||
sort -u "$tmp" -o "$f"
|
sort -u "$tmp" -o "$f"
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
|
|
||||||
# Default Browser handler is tracked SEPARATELY and guarded only OUTSIDE
|
# Default Browser handler is tracked SEPARATELY and guarded only OUTSIDE
|
||||||
# the curfew window (see is_allowed). During curfew the whole point is to
|
# the curfew window (see is_allowed). During curfew the whole point is to
|
||||||
# disable browsers, so the default-handler guard must not resurrect them.
|
# disable browsers, so the default-handler guard must not resurrect them.
|
||||||
local bf="$STATE_DIR/default_browser.txt"
|
local bf="$STATE_DIR/default_browser.txt"
|
||||||
cmd package resolve-activity --brief \
|
cmd package resolve-activity --brief \
|
||||||
-a android.intent.action.VIEW -d http://example.com 2>/dev/null \
|
-a android.intent.action.VIEW -d http://example.com 2>/dev/null |
|
||||||
| awk -F/ 'NR==2 && $1 != "" {print $1}' > "$bf.tmp" 2>/dev/null
|
awk -F/ 'NR==2 && $1 != "" {print $1}' >"$bf.tmp" 2>/dev/null
|
||||||
mv "$bf.tmp" "$bf" 2>/dev/null || : > "$bf"
|
mv "$bf.tmp" "$bf" 2>/dev/null || : >"$bf"
|
||||||
}
|
}
|
||||||
|
|
||||||
is_default_handler() {
|
is_default_handler() {
|
||||||
local pkg="$1"
|
local pkg="$1"
|
||||||
grep -qxF "$pkg" "$STATE_DIR/default_handlers.txt" 2>/dev/null
|
grep -qxF "$pkg" "$STATE_DIR/default_handlers.txt" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Focus Mode Control ----
|
# ---- Focus Mode Control ----
|
||||||
|
|
||||||
enable_focus_mode() {
|
enable_focus_mode() {
|
||||||
local first_entry=0
|
local first_entry=0
|
||||||
if [ "$CURRENT_MODE" != "focus" ]; then
|
if [ "$CURRENT_MODE" != "focus" ]; then
|
||||||
first_entry=1
|
first_entry=1
|
||||||
log "ENABLING focus mode - restricting non-whitelisted apps"
|
log "ENABLING focus mode - restricting non-whitelisted apps"
|
||||||
: > "$DISABLED_APPS_FILE"
|
: >"$DISABLED_APPS_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Refresh default-handler list every tick. The user may switch dialer /
|
# Refresh default-handler list every tick. The user may switch dialer /
|
||||||
# SMS / launcher between sweeps; the guard in is_allowed() consults this
|
# SMS / launcher between sweeps; the guard in is_allowed() consults this
|
||||||
# list so a newly-promoted handler is never disabled.
|
# list so a newly-promoted handler is never disabled.
|
||||||
refresh_default_handlers
|
refresh_default_handlers
|
||||||
|
|
||||||
# Blocked system app list is static; built once in init() (see
|
# Blocked system app list is static; built once in init() (see
|
||||||
# build_blocked_sys_file), not rebuilt on every sweep.
|
# build_blocked_sys_file), not rebuilt on every sweep.
|
||||||
local blocked_sys="$STATE_DIR/blocked_sys.txt"
|
local blocked_sys="$STATE_DIR/blocked_sys.txt"
|
||||||
|
|
||||||
# Periodic rescan catches third-party apps the user re-enabled (e.g. via
|
# 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.
|
# Play Store or `pm enable` in a terminal) since the last tick.
|
||||||
# -e = enabled only, so we skip apps that are already disabled.
|
# -e = enabled only, so we skip apps that are already disabled.
|
||||||
local tmp_pkgs="$STATE_DIR/pkg_list.txt"
|
local tmp_pkgs="$STATE_DIR/pkg_list.txt"
|
||||||
pm list packages -3 -e 2>/dev/null | sed 's/^package://' > "$tmp_pkgs"
|
pm list packages -3 -e 2>/dev/null | sed 's/^package://' >"$tmp_pkgs"
|
||||||
local newly_disabled=0
|
local newly_disabled=0
|
||||||
while IFS= read -r pkg; do
|
while IFS= read -r pkg; do
|
||||||
[ -z "$pkg" ] && continue
|
[ -z "$pkg" ] && continue
|
||||||
is_allowed "$pkg" && continue
|
is_allowed "$pkg" && continue
|
||||||
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
||||||
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null \
|
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null ||
|
||||||
|| echo "$pkg" >> "$DISABLED_APPS_FILE"
|
echo "$pkg" >>"$DISABLED_APPS_FILE"
|
||||||
newly_disabled=$((newly_disabled + 1))
|
newly_disabled=$((newly_disabled + 1))
|
||||||
fi
|
fi
|
||||||
done < "$tmp_pkgs"
|
done <"$tmp_pkgs"
|
||||||
rm -f "$tmp_pkgs"
|
rm -f "$tmp_pkgs"
|
||||||
|
|
||||||
# Disable-for-user-0 any blocked system apps (Play Store, browsers,
|
# Disable-for-user-0 any blocked system apps (Play Store, browsers,
|
||||||
# package installer UI, terminal apps).
|
# package installer UI, terminal apps).
|
||||||
# IMPORTANT: We intentionally use pm disable-user (NOT pm uninstall) here.
|
# IMPORTANT: We intentionally use pm disable-user (NOT pm uninstall) here.
|
||||||
# pm uninstall -k --user 0 removes the package from Android's user-0
|
# 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
|
# package registry. If the daemon is killed with SIGKILL during a reboot
|
||||||
# (bypassing the cleanup trap), those packages stay uninstalled across the
|
# (bypassing the cleanup trap), those packages stay uninstalled across the
|
||||||
# reboot. Android's bootloop-protection (MTK and others) then detects
|
# reboot. Android's bootloop-protection (MTK and others) then detects
|
||||||
# missing critical system packages and triggers recovery / factory wipe.
|
# missing critical system packages and triggers recovery / factory wipe.
|
||||||
# pm disable-user leaves the package registered but inactive, so the
|
# pm disable-user leaves the package registered but inactive, so the
|
||||||
# PackageManager scan at next boot succeeds and no wipe occurs.
|
# PackageManager scan at next boot succeeds and no wipe occurs.
|
||||||
while IFS= read -r pkg; do
|
while IFS= read -r pkg; do
|
||||||
[ -z "$pkg" ] && continue
|
[ -z "$pkg" ] && continue
|
||||||
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
||||||
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null \
|
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null ||
|
||||||
|| echo "$pkg" >> "$DISABLED_APPS_FILE"
|
echo "$pkg" >>"$DISABLED_APPS_FILE"
|
||||||
newly_disabled=$((newly_disabled + 1))
|
newly_disabled=$((newly_disabled + 1))
|
||||||
fi
|
fi
|
||||||
done < "$blocked_sys"
|
done <"$blocked_sys"
|
||||||
|
|
||||||
CURRENT_MODE="focus"
|
CURRENT_MODE="focus"
|
||||||
echo "focus" > "$MODE_FILE"
|
echo "focus" >"$MODE_FILE"
|
||||||
|
|
||||||
if [ "$first_entry" -eq 1 ]; then
|
if [ "$first_entry" -eq 1 ]; then
|
||||||
local count
|
local count
|
||||||
count=$(wc -l < "$DISABLED_APPS_FILE" 2>/dev/null || echo 0)
|
count=$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null || echo 0)
|
||||||
log "Focus mode enabled - disabled $count apps"
|
log "Focus mode enabled - disabled $count apps"
|
||||||
elif [ "$newly_disabled" -gt 0 ]; then
|
elif [ "$newly_disabled" -gt 0 ]; then
|
||||||
log "Focus mode re-sweep: re-disabled $newly_disabled apps (re-enabled by user?)"
|
log "Focus mode re-sweep: re-disabled $newly_disabled apps (re-enabled by user?)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
reconcile_disabled_apps
|
reconcile_disabled_apps
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_focus_mode() {
|
disable_focus_mode() {
|
||||||
[ "$CURRENT_MODE" = "normal" ] && return
|
[ "$CURRENT_MODE" = "normal" ] && return
|
||||||
log "DISABLING focus mode - re-enabling apps"
|
log "DISABLING focus mode - re-enabling apps"
|
||||||
|
|
||||||
local count=0
|
local count=0
|
||||||
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
|
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
|
||||||
# Re-enable all disabled apps (both 3rd-party and system apps).
|
# Re-enable all disabled apps (both 3rd-party and system apps).
|
||||||
# Both paths now use pm disable-user, so pm enable is the only
|
# Both paths now use pm disable-user, so pm enable is the only
|
||||||
# restore command needed.
|
# restore command needed.
|
||||||
while IFS= read -r pkg; do
|
while IFS= read -r pkg; do
|
||||||
[ -z "$pkg" ] && continue
|
[ -z "$pkg" ] && continue
|
||||||
pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1))
|
pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1))
|
||||||
done < "$DISABLED_APPS_FILE"
|
done <"$DISABLED_APPS_FILE"
|
||||||
: > "$DISABLED_APPS_FILE"
|
: >"$DISABLED_APPS_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CURRENT_MODE="normal"
|
CURRENT_MODE="normal"
|
||||||
echo "normal" > "$MODE_FILE"
|
echo "normal" >"$MODE_FILE"
|
||||||
log "Focus mode disabled - re-enabled $count apps"
|
log "Focus mode disabled - re-enabled $count apps"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Status snapshot for companion notification app ----
|
# ---- 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,
|
# Fields: mode, lat, lon, distance_m, threshold_m, radius_m, disabled_count,
|
||||||
# last_check_ts (unix), last_check_iso (human).
|
# last_check_ts (unix), last_check_iso (human).
|
||||||
write_status_snapshot() {
|
write_status_snapshot() {
|
||||||
local mode="$1" lat="$2" lon="$3" dist="$4" thr="$5"
|
local mode="$1" lat="$2" lon="$3" dist="$4" thr="$5"
|
||||||
local count iso ts cf ov frc
|
local count iso ts cf ov frc
|
||||||
count="$(wc -l < "$DISABLED_APPS_FILE" 2>/dev/null | tr -d ' ' || echo 0)"
|
count="$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null | tr -d ' ' || echo 0)"
|
||||||
[ -z "$count" ] && count=0
|
[ -z "$count" ] && count=0
|
||||||
ts="$(date +%s)"
|
ts="$(date +%s)"
|
||||||
iso="$(date '+%Y-%m-%d %H:%M:%S')"
|
iso="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
# Curfew state for the companion app: 1/0 so it slots into the existing
|
# Curfew state for the companion app: 1/0 so it slots into the existing
|
||||||
# numeric JSON path. "curfew" = restrictions active now; "curfew_override"
|
# numeric JSON path. "curfew" = restrictions active now; "curfew_override"
|
||||||
# = the escape-hatch file is set (curfew suspended).
|
# = the escape-hatch file is set (curfew suspended).
|
||||||
if curfew_active; then cf=1; else cf=0; fi
|
if curfew_active; then cf=1; else cf=0; fi
|
||||||
if [ -e "$CURFEW_OVERRIDE_FILE" ]; then ov=1; else ov=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
|
# "curfew_force" = the demo/test force file is set (curfew forced on
|
||||||
# regardless of clock). Lets the companion app show Start/Stop demo.
|
# regardless of clock). Lets the companion app show Start/Stop demo.
|
||||||
if [ -e "$CURFEW_FORCE_FILE" ]; then frc=1; else frc=0; fi
|
if [ -e "$CURFEW_FORCE_FILE" ]; then frc=1; else frc=0; fi
|
||||||
local tmp="$STATUS_FILE.tmp"
|
local tmp="$STATUS_FILE.tmp"
|
||||||
# Shell-emitted JSON — keep values numeric where possible, strings quoted.
|
# Shell-emitted JSON — keep values numeric where possible, strings quoted.
|
||||||
{
|
{
|
||||||
printf '{'
|
printf '{'
|
||||||
printf '"mode":"%s",' "$mode"
|
printf '"mode":"%s",' "$mode"
|
||||||
printf '"lat":"%s",' "${lat:-}"
|
printf '"lat":"%s",' "${lat:-}"
|
||||||
printf '"lon":"%s",' "${lon:-}"
|
printf '"lon":"%s",' "${lon:-}"
|
||||||
printf '"distance_m":%s,' "${dist:-null}"
|
printf '"distance_m":%s,' "${dist:-null}"
|
||||||
printf '"threshold_m":%s,' "${thr:-null}"
|
printf '"threshold_m":%s,' "${thr:-null}"
|
||||||
printf '"radius_m":%s,' "$RADIUS"
|
printf '"radius_m":%s,' "$RADIUS"
|
||||||
printf '"disabled_count":%s,' "$count"
|
printf '"disabled_count":%s,' "$count"
|
||||||
printf '"curfew":%s,' "$cf"
|
printf '"curfew":%s,' "$cf"
|
||||||
printf '"curfew_override":%s,' "$ov"
|
printf '"curfew_override":%s,' "$ov"
|
||||||
printf '"curfew_force":%s,' "$frc"
|
printf '"curfew_force":%s,' "$frc"
|
||||||
printf '"last_check_ts":%s,' "$ts"
|
printf '"last_check_ts":%s,' "$ts"
|
||||||
printf '"last_check_iso":"%s"' "$iso"
|
printf '"last_check_iso":"%s"' "$iso"
|
||||||
printf '}\n'
|
printf '}\n'
|
||||||
} > "$tmp" 2>/dev/null || return 0
|
} >"$tmp" 2>/dev/null || return 0
|
||||||
mv "$tmp" "$STATUS_FILE" 2>/dev/null || true
|
mv "$tmp" "$STATUS_FILE" 2>/dev/null || true
|
||||||
chmod 644 "$STATUS_FILE" 2>/dev/null || true
|
chmod 644 "$STATUS_FILE" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Sleep with early-wake on recheck trigger ----
|
# ---- 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"
|
# early. The file can be touched by the companion app (via "Re-check now"
|
||||||
# button) or by `focus_ctl.sh recheck` from a shell.
|
# button) or by `focus_ctl.sh recheck` from a shell.
|
||||||
sleep_with_recheck() {
|
sleep_with_recheck() {
|
||||||
local total="$1"
|
local total="$1"
|
||||||
local elapsed=0
|
local elapsed=0
|
||||||
while [ "$elapsed" -lt "$total" ]; do
|
while [ "$elapsed" -lt "$total" ]; do
|
||||||
if [ -e "$RECHECK_TRIGGER" ]; then
|
if [ -e "$RECHECK_TRIGGER" ]; then
|
||||||
rm -f "$RECHECK_TRIGGER" 2>/dev/null
|
rm -f "$RECHECK_TRIGGER" 2>/dev/null
|
||||||
log "Manual re-check triggered"
|
log "Manual re-check triggered"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
elapsed=$((elapsed + 1))
|
elapsed=$((elapsed + 1))
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Signal handlers ----
|
# ---- Signal handlers ----
|
||||||
cleanup() {
|
cleanup() {
|
||||||
log "Daemon shutting down - re-enabling all apps"
|
log "Daemon shutting down - re-enabling all apps"
|
||||||
disable_focus_mode
|
disable_focus_mode
|
||||||
rm -f "$PIDFILE"
|
rm -f "$PIDFILE"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# HUP is intentionally NOT trapped so the daemon survives ADB disconnects.
|
# HUP is intentionally NOT trapped so the daemon survives ADB disconnects.
|
||||||
@ -490,51 +492,52 @@ trap cleanup INT TERM
|
|||||||
|
|
||||||
# ---- Main Loop ----
|
# ---- Main Loop ----
|
||||||
main() {
|
main() {
|
||||||
acquire_lock
|
acquire_lock
|
||||||
init
|
init
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# Invalidate the per-tick curfew_active() memo (see its definition).
|
# Invalidate the per-tick curfew_active() memo (see its definition).
|
||||||
_CURFEW_TICK_CACHED=""
|
_CURFEW_TICK_CACHED=""
|
||||||
|
|
||||||
location="$(get_location)"
|
location="$(get_location)"
|
||||||
|
|
||||||
if [ -n "$location" ]; then
|
if [ -n "$location" ]; then
|
||||||
lat="$(echo "$location" | cut -d',' -f1)"
|
lat="$(echo "$location" | cut -d',' -f1)"
|
||||||
lon="$(echo "$location" | cut -d',' -f2)"
|
lon="$(echo "$location" | cut -d',' -f2)"
|
||||||
distance="$(calc_distance "$lat" "$lon" "$HOME_LAT" "$HOME_LON")"
|
distance="$(calc_distance "$lat" "$lon" "$HOME_LAT" "$HOME_LON")"
|
||||||
|
|
||||||
if [ "$CURRENT_MODE" = "focus" ]; then
|
if [ "$CURRENT_MODE" = "focus" ]; then
|
||||||
threshold=$((RADIUS + HYSTERESIS))
|
threshold=$((RADIUS + HYSTERESIS))
|
||||||
else
|
else
|
||||||
threshold=$((RADIUS - HYSTERESIS))
|
threshold=$((RADIUS - HYSTERESIS))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$distance" -le "$threshold" ] 2>/dev/null; then
|
if [ "$distance" -le "$threshold" ] 2>/dev/null; then
|
||||||
enable_focus_mode
|
enable_focus_mode
|
||||||
else
|
else
|
||||||
disable_focus_mode
|
disable_focus_mode
|
||||||
fi
|
fi
|
||||||
|
|
||||||
curfew_state="day"; curfew_active && curfew_state="CURFEW"
|
curfew_state="day"
|
||||||
log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE | Curfew: $curfew_state"
|
curfew_active && curfew_state="CURFEW"
|
||||||
write_status_snapshot "$CURRENT_MODE" "$lat" "$lon" "$distance" "$threshold"
|
log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE | Curfew: $curfew_state"
|
||||||
else
|
write_status_snapshot "$CURRENT_MODE" "$lat" "$lon" "$distance" "$threshold"
|
||||||
log "Location unavailable - defaulting to focus mode (restrictions ON)"
|
else
|
||||||
enable_focus_mode
|
log "Location unavailable - defaulting to focus mode (restrictions ON)"
|
||||||
write_status_snapshot "$CURRENT_MODE" "" "" "null" "null"
|
enable_focus_mode
|
||||||
fi
|
write_status_snapshot "$CURRENT_MODE" "" "" "null" "null"
|
||||||
|
fi
|
||||||
|
|
||||||
# Dynamic interval: shorter at home (can charge), longer away (save battery).
|
# Dynamic interval: shorter at home (can charge), longer away (save battery).
|
||||||
# sleep_with_recheck returns early if the companion app requests a recheck.
|
# sleep_with_recheck returns early if the companion app requests a recheck.
|
||||||
if [ "$CURRENT_MODE" = "focus" ]; then
|
if [ "$CURRENT_MODE" = "focus" ]; then
|
||||||
sleep_with_recheck "$CHECK_INTERVAL_FOCUS"
|
sleep_with_recheck "$CHECK_INTERVAL_FOCUS"
|
||||||
else
|
else
|
||||||
sleep_with_recheck "$CHECK_INTERVAL_NORMAL"
|
sleep_with_recheck "$CHECK_INTERVAL_NORMAL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rotate_log
|
rotate_log
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@ -11,13 +11,23 @@ cd "$SCRIPT_DIR"
|
|||||||
|
|
||||||
SDK="${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}"
|
SDK="${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}"
|
||||||
# Pick highest-numbered build-tools directory.
|
# 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.
|
# 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"
|
PLATFORM_JAR="$PLATFORM_DIR/android.jar"
|
||||||
|
|
||||||
[ -d "$BUILD_TOOLS_DIR" ] || { echo "ERROR: build-tools not found under $SDK" >&2; exit 1; }
|
[ -d "$BUILD_TOOLS_DIR" ] || {
|
||||||
[ -f "$PLATFORM_JAR" ] || { echo "ERROR: android.jar not found at $PLATFORM_JAR" >&2; exit 1; }
|
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"
|
AAPT2="$BUILD_TOOLS_DIR/aapt2"
|
||||||
D8="$BUILD_TOOLS_DIR/d8"
|
D8="$BUILD_TOOLS_DIR/d8"
|
||||||
@ -25,7 +35,10 @@ ZIPALIGN="$BUILD_TOOLS_DIR/zipalign"
|
|||||||
APKSIGNER="$BUILD_TOOLS_DIR/apksigner"
|
APKSIGNER="$BUILD_TOOLS_DIR/apksigner"
|
||||||
|
|
||||||
for tool in "$AAPT2" "$D8" "$ZIPALIGN" "$APKSIGNER"; do
|
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
|
done
|
||||||
|
|
||||||
BUILD="$SCRIPT_DIR/build"
|
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) ----
|
# ---- Compile resources (none for now, but aapt2 requires the dir) ----
|
||||||
mkdir -p "$SCRIPT_DIR/res"
|
mkdir -p "$SCRIPT_DIR/res"
|
||||||
if find "$SCRIPT_DIR/res" -type f -print -quit | grep -q .; then
|
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
|
fi
|
||||||
|
|
||||||
# ---- Link resources + manifest into base APK ----
|
# ---- Link resources + manifest into base APK ----
|
||||||
LINK_ARGS=(
|
LINK_ARGS=(
|
||||||
--manifest "$SCRIPT_DIR/AndroidManifest.xml"
|
--manifest "$SCRIPT_DIR/AndroidManifest.xml"
|
||||||
-I "$PLATFORM_JAR"
|
-I "$PLATFORM_JAR"
|
||||||
--java "$BUILD"
|
--java "$BUILD"
|
||||||
--min-sdk-version 29
|
--min-sdk-version 29
|
||||||
--target-sdk-version 35
|
--target-sdk-version 35
|
||||||
--version-code 1
|
--version-code 1
|
||||||
--version-name 1.0.0
|
--version-name 1.0.0
|
||||||
-o "$BUILD/base.apk"
|
-o "$BUILD/base.apk"
|
||||||
)
|
)
|
||||||
# Include any compiled res archives.
|
# Include any compiled res archives.
|
||||||
for rfile in "$BUILD"/compiled-res/*.flat; do
|
for rfile in "$BUILD"/compiled-res/*.flat; do
|
||||||
[ -e "$rfile" ] && LINK_ARGS+=("$rfile")
|
[ -e "$rfile" ] && LINK_ARGS+=("$rfile")
|
||||||
done
|
done
|
||||||
"$AAPT2" link "${LINK_ARGS[@]}"
|
"$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)
|
while IFS= read -r -d '' f; do JAVA_SRCS+=("$f"); done < <(find "$SCRIPT_DIR/java" "$BUILD" -name '*.java' -print0)
|
||||||
|
|
||||||
javac -source 11 -target 11 \
|
javac -source 11 -target 11 \
|
||||||
-classpath "$PLATFORM_JAR" \
|
-classpath "$PLATFORM_JAR" \
|
||||||
-d "$BUILD/classes" \
|
-d "$BUILD/classes" \
|
||||||
"${JAVA_SRCS[@]}"
|
"${JAVA_SRCS[@]}"
|
||||||
|
|
||||||
# ---- Dex ----
|
# ---- Dex ----
|
||||||
CLASS_FILES=()
|
CLASS_FILES=()
|
||||||
@ -80,18 +93,18 @@ cp "$BUILD/base.apk" "$BUILD/unsigned.apk"
|
|||||||
# ---- Sign with debug key (auto-generated on first build) ----
|
# ---- Sign with debug key (auto-generated on first build) ----
|
||||||
KEYSTORE="$SCRIPT_DIR/debug.keystore"
|
KEYSTORE="$SCRIPT_DIR/debug.keystore"
|
||||||
if [ ! -f "$KEYSTORE" ]; then
|
if [ ! -f "$KEYSTORE" ]; then
|
||||||
echo "Generating debug keystore..."
|
echo "Generating debug keystore..."
|
||||||
keytool -genkeypair -v \
|
keytool -genkeypair -v \
|
||||||
-keystore "$KEYSTORE" -storepass android -keypass android \
|
-keystore "$KEYSTORE" -storepass android -keypass android \
|
||||||
-alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \
|
-alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \
|
||||||
-dname "CN=Focus Status Debug, OU=Dev, O=Dev, L=NA, ST=NA, C=NA" \
|
-dname "CN=Focus Status Debug, OU=Dev, O=Dev, L=NA, ST=NA, C=NA" \
|
||||||
>/dev/null 2>&1
|
>/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
"$APKSIGNER" sign \
|
"$APKSIGNER" sign \
|
||||||
--ks "$KEYSTORE" --ks-pass pass:android \
|
--ks "$KEYSTORE" --ks-pass pass:android \
|
||||||
--key-pass pass:android \
|
--key-pass pass:android \
|
||||||
--out "$BUILD/focus_status.apk" \
|
--out "$BUILD/focus_status.apk" \
|
||||||
"$BUILD/aligned.apk"
|
"$BUILD/aligned.apk"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Built: $BUILD/focus_status.apk"
|
echo "Built: $BUILD/focus_status.apk"
|
||||||
|
|||||||
@ -7,13 +7,13 @@ PASS=0
|
|||||||
FAIL=0
|
FAIL=0
|
||||||
|
|
||||||
_t_pass() {
|
_t_pass() {
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
printf ' OK: %s\n' "$1"
|
printf ' OK: %s\n' "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
_t_fail() {
|
_t_fail() {
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
printf ' FAIL: %s\n' "$1"
|
printf ' FAIL: %s\n' "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_TMPDIR="$(mktemp -d)"
|
TEST_TMPDIR="$(mktemp -d)"
|
||||||
@ -25,189 +25,198 @@ mkdir -p "${XDG_STATE_HOME}"
|
|||||||
source "${SCRIPT_DIR}/../adb_common.sh"
|
source "${SCRIPT_DIR}/../adb_common.sh"
|
||||||
|
|
||||||
ADB_MOCK_MODEL=$'Pixel "7";$(rm -rf /)`danger`\nline2'
|
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_MOCK_FINGERPRINT='google/pixel:14/UP1A.231005.007/$evil;`cmd`'
|
||||||
|
|
||||||
adb() {
|
adb() {
|
||||||
if [[ "$#" -eq 1 && "$1" == "devices" ]]; then
|
if [[ "$#" -eq 1 && "$1" == "devices" ]]; then
|
||||||
printf 'List of devices attached\nSERIAL123\tdevice\n'
|
printf 'List of devices attached\nSERIAL123\tdevice\n'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.product.model" ]]; then
|
if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.product.model" ]]; then
|
||||||
printf '%s\r\n' "${ADB_MOCK_MODEL}"
|
printf '%s\r\n' "${ADB_MOCK_MODEL}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.build.fingerprint" ]]; then
|
if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.build.fingerprint" ]]; then
|
||||||
printf '%s\r\n' "${ADB_MOCK_FINGERPRINT}"
|
printf '%s\r\n' "${ADB_MOCK_FINGERPRINT}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "su" && "$5" == "--mount-master" && "$6" == "-c" && "$7" == "echo ok" ]]; then
|
if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "su" && "$5" == "--mount-master" && "$6" == "-c" && "$7" == "echo ok" ]]; then
|
||||||
printf 'ok\n'
|
printf 'ok\n'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'Unexpected adb invocation:' >&2
|
printf 'Unexpected adb invocation:' >&2
|
||||||
printf ' %q' "$@" >&2
|
printf ' %q' "$@" >&2
|
||||||
printf '\n' >&2
|
printf '\n' >&2
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
run_test() {
|
run_test() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
if "$@"; then
|
if "$@"; then
|
||||||
_t_pass "${name}"
|
_t_pass "${name}"
|
||||||
else
|
else
|
||||||
_t_fail "${name}"
|
_t_fail "${name}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
test_box_does_not_crash() {
|
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() {
|
test_check_cooldown_zero_allows() {
|
||||||
LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-zero"
|
LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-zero"
|
||||||
adb_check_cooldown 0 "test_marker"
|
adb_check_cooldown 0 "test_marker"
|
||||||
}
|
}
|
||||||
|
|
||||||
test_check_cooldown_blocks_when_marker_fresh() {
|
test_check_cooldown_blocks_when_marker_fresh() {
|
||||||
LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-block"
|
LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-block"
|
||||||
mkdir -p "${LAST_RUN_DIR}"
|
mkdir -p "${LAST_RUN_DIR}"
|
||||||
date +%s >"${LAST_RUN_DIR}/fresh_marker"
|
date +%s >"${LAST_RUN_DIR}/fresh_marker"
|
||||||
|
|
||||||
if adb_check_cooldown 9999 "fresh_marker"; then
|
if adb_check_cooldown 9999 "fresh_marker"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
test_mark_last_run_creates_marker() {
|
test_mark_last_run_creates_marker() {
|
||||||
LAST_RUN_DIR="${TEST_TMPDIR}/mark-last-run"
|
LAST_RUN_DIR="${TEST_TMPDIR}/mark-last-run"
|
||||||
adb_mark_last_run "run_test"
|
adb_mark_last_run "run_test"
|
||||||
[[ -f "${LAST_RUN_DIR}/run_test" ]]
|
[[ -f "${LAST_RUN_DIR}/run_test" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
test_acquire_lock_creates_lock_directory() {
|
test_acquire_lock_creates_lock_directory() {
|
||||||
LOCK_DIR="${TEST_TMPDIR}/lock-dir-create"
|
LOCK_DIR="${TEST_TMPDIR}/lock-dir-create"
|
||||||
LOCK_FILE=""
|
LOCK_FILE=""
|
||||||
adb_acquire_lock >/dev/null 2>&1
|
adb_acquire_lock >/dev/null 2>&1
|
||||||
[[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]]
|
[[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]]
|
||||||
_adb_release_lock
|
_adb_release_lock
|
||||||
}
|
}
|
||||||
|
|
||||||
test_acquire_lock_removes_stale_lock_directory() {
|
test_acquire_lock_removes_stale_lock_directory() {
|
||||||
LOCK_DIR="${TEST_TMPDIR}/lock-dir-stale"
|
LOCK_DIR="${TEST_TMPDIR}/lock-dir-stale"
|
||||||
LOCK_FILE="${LOCK_DIR}/run_phone.lock"
|
LOCK_FILE="${LOCK_DIR}/run_phone.lock"
|
||||||
mkdir -p "${LOCK_FILE}"
|
mkdir -p "${LOCK_FILE}"
|
||||||
printf '999999\n' >"${LOCK_FILE}/pid"
|
printf '999999\n' >"${LOCK_FILE}/pid"
|
||||||
|
|
||||||
adb_acquire_lock >/dev/null 2>&1
|
adb_acquire_lock >/dev/null 2>&1
|
||||||
[[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]]
|
[[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]]
|
||||||
_adb_release_lock
|
_adb_release_lock
|
||||||
}
|
}
|
||||||
|
|
||||||
test_sanitize_device_string_removes_dangerous_chars() {
|
test_sanitize_device_string_removes_dangerous_chars() {
|
||||||
local sanitized
|
local sanitized
|
||||||
sanitized="$(_sanitize_device_string $'abc DEF-._:/$`";\n')"
|
sanitized="$(_sanitize_device_string $'abc DEF-._:/$`";\n')"
|
||||||
|
|
||||||
[[ "${sanitized}" == 'abc DEF-._:/' ]]
|
[[ "${sanitized}" == 'abc DEF-._:/' ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
test_save_trusted_device_sanitizes_and_quotes() {
|
test_save_trusted_device_sanitizes_and_quotes() {
|
||||||
local trusted_contents
|
local trusted_contents
|
||||||
|
|
||||||
export ADB_SERIAL='SERIAL123$() ;'
|
# Literal payload on purpose - see the note on ADB_MOCK_FINGERPRINT.
|
||||||
adb_save_trusted_device
|
# shellcheck disable=SC2016
|
||||||
|
export ADB_SERIAL='SERIAL123$() ;'
|
||||||
|
adb_save_trusted_device
|
||||||
|
|
||||||
[[ -f "${TRUSTED_DEVICE_FILE}" ]] || return 1
|
[[ -f "${TRUSTED_DEVICE_FILE}" ]] || return 1
|
||||||
trusted_contents="$(cat "${TRUSTED_DEVICE_FILE}")"
|
trusted_contents="$(cat "${TRUSTED_DEVICE_FILE}")"
|
||||||
|
|
||||||
[[ "${trusted_contents}" != *'$('* ]]
|
# Asserting the sanitiser stripped these metacharacters, so the patterns
|
||||||
[[ "${trusted_contents}" != *';'* ]]
|
# must stay literal rather than expand.
|
||||||
[[ "${trusted_contents}" != *'`'* ]]
|
# shellcheck disable=SC2016
|
||||||
|
[[ "${trusted_contents}" != *'$('* ]]
|
||||||
|
[[ "${trusted_contents}" != *';'* ]]
|
||||||
|
[[ "${trusted_contents}" != *'`'* ]]
|
||||||
|
|
||||||
local -a trusted_values=()
|
local -a trusted_values=()
|
||||||
mapfile -t trusted_values < <(
|
mapfile -t trusted_values < <(
|
||||||
bash -c '
|
bash -c '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$1"
|
source "$1"
|
||||||
printf "%s\n" "${TRUSTED_SERIAL:-}" "${TRUSTED_MODEL:-}" "${TRUSTED_FINGERPRINT:-}"
|
printf "%s\n" "${TRUSTED_SERIAL:-}" "${TRUSTED_MODEL:-}" "${TRUSTED_FINGERPRINT:-}"
|
||||||
' bash "${TRUSTED_DEVICE_FILE}"
|
' bash "${TRUSTED_DEVICE_FILE}"
|
||||||
)
|
)
|
||||||
|
|
||||||
[[ "${trusted_values[0]:-}" == 'SERIAL123 ' ]]
|
[[ "${trusted_values[0]:-}" == 'SERIAL123 ' ]]
|
||||||
[[ "${trusted_values[1]:-}" == 'Pixel 7rm -rf /dangerline2' ]]
|
[[ "${trusted_values[1]:-}" == 'Pixel 7rm -rf /dangerline2' ]]
|
||||||
[[ "${trusted_values[2]:-}" == 'google/pixel:14/UP1A.231005.007/evilcmd' ]]
|
[[ "${trusted_values[2]:-}" == 'google/pixel:14/UP1A.231005.007/evilcmd' ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
test_verify_root_uses_root_shell() {
|
test_verify_root_uses_root_shell() {
|
||||||
export ADB_SERIAL='SERIAL123'
|
export ADB_SERIAL='SERIAL123'
|
||||||
adb_verify_root >/dev/null 2>&1
|
adb_verify_root >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
test_select_device_rejects_multiple_devices_even_with_trusted_record() {
|
test_select_device_rejects_multiple_devices_even_with_trusted_record() {
|
||||||
unset ADB_SERIAL
|
unset ADB_SERIAL
|
||||||
|
|
||||||
adb_list_serials() {
|
adb_list_serials() {
|
||||||
printf 'SERIAL123\nSERIAL999\n'
|
printf 'SERIAL123\nSERIAL999\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
|
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
|
||||||
TRUSTED_SERIAL='SERIAL123'
|
TRUSTED_SERIAL='SERIAL123'
|
||||||
TRUSTED_MODEL='Pixel 7rm -rf /dangerline2'
|
TRUSTED_MODEL='Pixel 7rm -rf /dangerline2'
|
||||||
TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd'
|
TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd'
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if (adb_select_device >/dev/null 2>&1); then
|
if (adb_select_device >/dev/null 2>&1); then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
test_verify_trusted_identity_accepts_exact_match() {
|
test_verify_trusted_identity_accepts_exact_match() {
|
||||||
export ADB_SERIAL='SERIAL123'
|
export ADB_SERIAL='SERIAL123'
|
||||||
adb_save_trusted_device
|
adb_save_trusted_device
|
||||||
adb_verify_trusted_identity >/dev/null 2>&1
|
adb_verify_trusted_identity >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
test_verify_trusted_identity_rejects_model_mismatch() {
|
test_verify_trusted_identity_rejects_model_mismatch() {
|
||||||
export ADB_SERIAL='SERIAL123'
|
export ADB_SERIAL='SERIAL123'
|
||||||
adb_save_trusted_device
|
adb_save_trusted_device
|
||||||
|
|
||||||
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
|
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
|
||||||
TRUSTED_SERIAL='SERIAL123'
|
TRUSTED_SERIAL='SERIAL123'
|
||||||
TRUSTED_MODEL='Different Model'
|
TRUSTED_MODEL='Different Model'
|
||||||
TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd'
|
TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd'
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if (adb_verify_trusted_identity >/dev/null 2>&1); then
|
if (adb_verify_trusted_identity >/dev/null 2>&1); then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
test_verify_trusted_identity_rejects_fingerprint_mismatch() {
|
test_verify_trusted_identity_rejects_fingerprint_mismatch() {
|
||||||
export ADB_SERIAL='SERIAL123'
|
export ADB_SERIAL='SERIAL123'
|
||||||
adb_save_trusted_device
|
adb_save_trusted_device
|
||||||
|
|
||||||
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
|
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
|
||||||
TRUSTED_SERIAL='SERIAL123'
|
TRUSTED_SERIAL='SERIAL123'
|
||||||
TRUSTED_MODEL='Pixel 7rm -rf /dangerline2'
|
TRUSTED_MODEL='Pixel 7rm -rf /dangerline2'
|
||||||
TRUSTED_FINGERPRINT='different/fingerprint'
|
TRUSTED_FINGERPRINT='different/fingerprint'
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if (adb_verify_trusted_identity >/dev/null 2>&1); then
|
if (adb_verify_trusted_identity >/dev/null 2>&1); then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
run_test "_box output without crash" test_box_does_not_crash
|
run_test "_box output without crash" test_box_does_not_crash
|
||||||
|
|||||||
@ -12,11 +12,11 @@ SRC="$SCRIPT_DIR/../../tether_enforcer.sh"
|
|||||||
|
|
||||||
RUN="$(mktemp -d)"
|
RUN="$(mktemp -d)"
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [ -n "${PID:-}" ]; then
|
if [ -n "${PID:-}" ]; then
|
||||||
kill "$PID" 2>/dev/null || true
|
kill "$PID" 2>/dev/null || true
|
||||||
wait "$PID" 2>/dev/null || true # let its TERM handler finish before rm
|
wait "$PID" 2>/dev/null || true # let its TERM handler finish before rm
|
||||||
fi
|
fi
|
||||||
rm -rf "$RUN"
|
rm -rf "$RUN"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
mkdir -p "$RUN/bin" "$RUN/state" "$RUN/ipt" "$RUN/settings" "$RUN/calls" "$RUN/app"
|
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") ---
|
# --- fake config.sh in the daemon's own dir (it sources "$SCRIPT_DIR/config.sh") ---
|
||||||
cp "$SRC" "$RUN/app/tether_enforcer.sh"
|
cp "$SRC" "$RUN/app/tether_enforcer.sh"
|
||||||
cat > "$RUN/app/config.sh" <<EOF
|
cat >"$RUN/app/config.sh" <<EOF
|
||||||
export STATE_DIR="$STATE"
|
export STATE_DIR="$STATE"
|
||||||
export MODE_FILE="$STATE/current_mode.txt"
|
export MODE_FILE="$STATE/current_mode.txt"
|
||||||
export TETHER_ENFORCER_ENABLED=1
|
export TETHER_ENFORCER_ENABLED=1
|
||||||
@ -40,7 +40,7 @@ export TETHER_FORCE_FILE="$STATE/tether_force_on"
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# --- stubs ---
|
# --- stubs ---
|
||||||
cat > "$RUN/bin/settings" <<EOF
|
cat >"$RUN/bin/settings" <<EOF
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
db="$RUN/settings/tether_offload_disabled"
|
db="$RUN/settings/tether_offload_disabled"
|
||||||
echo "settings \$*" >> "$RUN/calls/settings.log"
|
echo "settings \$*" >> "$RUN/calls/settings.log"
|
||||||
@ -52,7 +52,7 @@ esac
|
|||||||
exit 0
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
# iptables/ip6tables: minimal chain-state model so chain_intact converges.
|
# iptables/ip6tables: minimal chain-state model so chain_intact converges.
|
||||||
cat > "$RUN/bin/iptables" <<EOF
|
cat >"$RUN/bin/iptables" <<EOF
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
bin="\$(basename "\$0")"; s="$RUN/ipt/\$bin"; mkdir -p "\$s"
|
bin="\$(basename "\$0")"; s="$RUN/ipt/\$bin"; mkdir -p "\$s"
|
||||||
[ "\$1" = "-w" ] && shift 2
|
[ "\$1" = "-w" ] && shift 2
|
||||||
@ -70,54 +70,66 @@ esac
|
|||||||
exit \$?
|
exit \$?
|
||||||
EOF
|
EOF
|
||||||
cp "$RUN/bin/iptables" "$RUN/bin/ip6tables"
|
cp "$RUN/bin/iptables" "$RUN/bin/ip6tables"
|
||||||
printf '#!/usr/bin/env bash\necho "cmd $*" >> "%s/calls/cmd.log"\nexit 0\n' "$RUN" > "$RUN/bin/cmd"
|
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"
|
printf '#!/usr/bin/env bash\necho 13\n' >"$RUN/bin/getprop"
|
||||||
chmod +x "$RUN/bin/"*
|
chmod +x "$RUN/bin/"*
|
||||||
export PATH="$RUN/bin:$PATH"
|
export PATH="$RUN/bin:$PATH"
|
||||||
|
|
||||||
PASS=0; FAIL=0
|
PASS=0
|
||||||
ok() { PASS=$((PASS + 1)); printf ' OK: %s\n' "$1"; }
|
FAIL=0
|
||||||
no() { FAIL=$((FAIL + 1)); printf ' FAIL: %s\n' "$1"; }
|
ok() {
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
printf ' OK: %s\n' "$1"
|
||||||
|
}
|
||||||
|
no() {
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
printf ' FAIL: %s\n' "$1"
|
||||||
|
}
|
||||||
applied() { [ -f "$STATE/tether_applied" ]; }
|
applied() { [ -f "$STATE/tether_applied" ]; }
|
||||||
offload() { cat "$RUN/settings/tether_offload_disabled" 2>/dev/null || echo "<unset>"; }
|
offload() { cat "$RUN/settings/tether_offload_disabled" 2>/dev/null || echo "<unset>"; }
|
||||||
v4jump() { [ -f "$RUN/ipt/iptables/jump" ]; }
|
v4jump() { [ -f "$RUN/ipt/iptables/jump" ]; }
|
||||||
v6jump() { [ -f "$RUN/ipt/ip6tables/jump" ]; }
|
v6jump() { [ -f "$RUN/ipt/ip6tables/jump" ]; }
|
||||||
v4rules() { grep -c '^-A' "$RUN/ipt/iptables/rules" 2>/dev/null || echo 0; }
|
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
|
sleep 2
|
||||||
|
|
||||||
# [A] Away from home -> block OFF
|
# [A] Away from home -> block OFF
|
||||||
applied && no "A: applied while away" || ok "A: not applied while away"
|
if applied; then no "A: applied while away"; else ok "A: not applied while away"; fi
|
||||||
[ "$(offload)" = "<unset>" ] && ok "A: offload untouched away" || no "A: offload changed away"
|
if [ "$(offload)" = "<unset>" ]; then ok "A: offload untouched away"; else no "A: offload changed away"; fi
|
||||||
v4jump && no "A: v4 jump while away" || ok "A: no v4 jump while away"
|
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)
|
# [B] force-on -> block ENGAGES (all three levers)
|
||||||
touch "$STATE/tether_force_on"; sleep 3
|
touch "$STATE/tether_force_on"
|
||||||
applied && ok "B: applied on force" || no "B: not applied on force"
|
sleep 3
|
||||||
[ "$(offload)" = "1" ] && ok "B: offload disabled" || no "B: offload=$(offload)"
|
if applied; then ok "B: applied on force"; else no "B: not applied on force"; fi
|
||||||
v4jump && ok "B: v4 FORWARD jump pinned" || no "B: no v4 jump"
|
if [ "$(offload)" = "1" ]; then ok "B: offload disabled"; else no "B: offload=$(offload)"; fi
|
||||||
v6jump && ok "B: v6 FORWARD jump pinned" || no "B: no v6 jump"
|
if v4jump; then ok "B: v4 FORWARD jump pinned"; else no "B: no v4 jump"; fi
|
||||||
[ "$(v4rules)" = "1" ] && ok "B: single REJECT rule (no rebuild loop)" || no "B: v4 rules=$(v4rules)"
|
if v6jump; then ok "B: v6 FORWARD jump pinned"; else no "B: no v6 jump"; fi
|
||||||
[ "$(softap)" -ge 1 ] && ok "B: stop-softap invoked" || no "B: softap not stopped"
|
if [ "$(v4rules)" = "1" ]; then ok "B: single REJECT rule (no rebuild loop)"; else no "B: v4 rules=$(v4rules)"; fi
|
||||||
[ "$(cat "$STATE/tether_offload.snap" 2>/dev/null)" = "null" ] && ok "B: offload snapshot captured" || no "B: bad snapshot"
|
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
|
# [C] clear force -> block REVERTS
|
||||||
rm -f "$STATE/tether_force_on"; sleep 3
|
rm -f "$STATE/tether_force_on"
|
||||||
applied && no "C: still applied" || ok "C: reverted on force-clear"
|
sleep 3
|
||||||
[ "$(offload)" = "<unset>" ] && ok "C: offload restored" || no "C: offload=$(offload)"
|
if applied; then no "C: still applied"; else ok "C: reverted on force-clear"; fi
|
||||||
v4jump && no "C: v4 jump remains" || ok "C: v4 chain torn down"
|
if [ "$(offload)" = "<unset>" ]; 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
|
# [D] current_mode=focus -> ENGAGES via home gate
|
||||||
echo focus > "$STATE/current_mode.txt"; sleep 3
|
echo focus >"$STATE/current_mode.txt"
|
||||||
applied && ok "D: applied at home (focus)" || no "D: not applied at home"
|
sleep 3
|
||||||
v4jump && ok "D: v4 jump pinned at home" || no "D: no v4 jump at home"
|
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
|
# [E] override -> SUSPENDS even at home
|
||||||
touch "$STATE/tether_override"; sleep 3
|
touch "$STATE/tether_override"
|
||||||
applied && no "E: applied despite override" || ok "E: suspended by override"
|
sleep 3
|
||||||
v4jump && no "E: v4 jump despite override" || ok "E: torn down by override"
|
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"
|
printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
|
||||||
[ "$FAIL" -eq 0 ]
|
[ "$FAIL" -eq 0 ]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user