mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 12:47:47 +01:00
focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer
The largest file in phone_focus_mode, and the last one blocking deploy.sh:
splitting it grows deploy.sh's two hardcoded lists, so those lists are only
final now. All 55 functions moved verbatim.
Three while-read loops were audited before extraction. Two call pm in their
bodies and moved untouched. The third, iter_whitelist_packages, is fed by a
pipe from printf and reads nothing from stdin -- already safe, and now said
so in ctl_usage.sh's header so the distinction is not lost by a later editor
who has only read the warning in daemon_apps.sh.
136 assertions and 22 mutations, all killed. Three survived the first run
and every one was a defect in MY OWN tests, not in the subject:
- cmd_status carries a THIRD copy of the Haversine, and I had asserted
only that it printed "some number". A wrong Earth radius sailed through.
Now the value is checked against a seeded fix ~4.3 km from home.
- the two whitelist filters were each tested with input the OTHER filter
also rejects, so either could be deleted with nothing failing. Fixed
with a punctuation-free undotted token (only the dot check rejects it)
and a dotted token carrying an illegal character (only the charset check
does), making each independently observable.
That last investigation turned up dead code: the `#*)` case arm is provably
equivalent, because `set -- $line` makes a comment's first token "#", which
the dot check already rejects. Removed rather than covered by a test that
cannot fail.
Coverage is NOT 100% and the real numbers are in the evidence: 41.79% to
92.98% across the eight, up from 5-22%. Unlike the daemon split, most of
what remains is genuinely untested -- the per-enforcer status blocks that
print mount state, APK hashes and offload settings, each needing its own
device fixture. The mutations prove the assertions that exist are load-
bearing; they do not prove those branches work.
jscpd checked in a clean worktree at 1.57%: eight files each carrying a
near-identical start/stop/status trio is exactly what that gate is for, so
the tests drive the trios through tables instead of repeating them.
Two drift hazards recorded, not fixed: ctl_is_curfew_now duplicates
daemon_location.sh's is_curfew_now (pinned by an agreement test across every
boundary minute), and cmd_status's awk is the third Haversine.
Over-cap count 17 -> 16. deploy.sh is the last one here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
7dc3c94160
commit
3ddc13f671
21
docs/superpowers/contracts/focus-ctl-split-2026-08-17.json
Normal file
21
docs/superpowers/contracts/focus-ctl-split-2026-08-17.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "Split focus_ctl.sh, the largest file in phone_focus_mode",
|
||||
"objective": "Take focus_ctl.sh from 1091 lines to under the repo's 250-line cap by extracting one library per enforcer, and give the extracted logic its first tests. This must land BEFORE deploy.sh is split, because it grows deploy.sh's two hardcoded lists.",
|
||||
"acceptance_criteria": [
|
||||
"bash meta/scripts/check_file_length.sh --all no longer lists phone_focus_mode/focus_ctl.sh, and no new file is over the cap.",
|
||||
"Every stdin-draining while-read loop is identified before extraction and moved verbatim; none is converted to a pipe.",
|
||||
"meta/scripts/verify_shell_split.sh reports every function moved verbatim.",
|
||||
"All eight siblings are added to BOTH of deploy.sh's hardcoded lists, and the lists stay in exact correspondence.",
|
||||
"A mutation spec lands in meta/scripts/fixtures/mutations/ and kills every mutation, including the escape-hatch commands and the third copy of the Haversine.",
|
||||
"Any mutation that proves equivalent is resolved by deleting the dead code, not by weakening the mutation or adding a test that cannot fail.",
|
||||
"shellcheck passes on each touched file individually; sh -n passes on every phone-side script.",
|
||||
"jscpd stays under its 2% threshold, measured in a clean worktree so vendored .venv does not pollute the figure."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Deploying to the phone or verifying on-device.",
|
||||
"Splitting deploy.sh, which is the next item and must come after this one.",
|
||||
"De-duplicating ctl_is_curfew_now against daemon_location.sh's is_curfew_now, or the third Haversine copy in cmd_status — both are recorded as drift hazards for a later session.",
|
||||
"Reaching 100% line coverage on the cmd_* subcommands, which would require modelling far more device behaviour than the split itself warrants."
|
||||
],
|
||||
"verifier": "bash phone_focus_mode/lib/tests/test_ctl_libs.sh (expects 136 passed, 0 failed), plus python3 meta/scripts/mutate_shell.py meta/scripts/fixtures/mutations/ctl_libs.json at 22 killed / 0 not killed, plus check_file_length.sh --all showing 16 files, plus deploy-list parity, plus jscpd under 2%, plus pre-commit run --files on the touched files."
|
||||
}
|
||||
71
docs/superpowers/evidence/focus-ctl-split-2026-08-17.json
Normal file
71
docs/superpowers/evidence/focus-ctl-split-2026-08-17.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"intent": "Bring focus_ctl.sh, at 1091 lines the largest file in phone_focus_mode, under the 250-line cap by extracting one library per enforcer, and give the extracted logic its first tests.",
|
||||
"scope": [
|
||||
"phone_focus_mode/focus_ctl.sh - 1091 lines down to 171",
|
||||
"phone_focus_mode/ctl_{usage,daemon,hosts,dns,launcher,workout,curfew,tether}.sh - new, one per enforcer plus the help text and the daemon's own commands",
|
||||
"phone_focus_mode/deploy.sh - all eight added to both hardcoded lists",
|
||||
"phone_focus_mode/lib/tests/ - a harness, three case files and one entry point, all new",
|
||||
"meta/scripts/fixtures/mutations/ctl_libs.json - new: 22 reproducible mutations",
|
||||
"Non-goal: any device deploy, and any de-duplication of the two drift hazards recorded under risks."
|
||||
],
|
||||
"changes": [
|
||||
"Ran the stdin-drain audit before extracting. focus_ctl.sh has three while-read loops; two call pm in their bodies and moved verbatim. The third (iter_whitelist_packages) is fed by a pipe from printf and reads nothing from stdin, so it was already safe -- noted in ctl_usage.sh's header so the distinction is not lost.",
|
||||
"Checked for globals written inside functions: none. The seven *_PIDFILE paths were assigned in the entry script and read only in their libraries, so each moved to the file that reads it (SC2034).",
|
||||
"Placed all eight as flat siblings and added them to both deploy lists, then checked the lists correspond exactly in both directions.",
|
||||
"Built a device stub layer covering pm, am, settings, dumpsys, svc, sqlite3, getprop, content, sleep, setsid and both iptables binaries, plus a pinned clock. The harness sources the REAL config.sh for defaults and redirects only the paths, so the fixture cannot drift from the values the subcommands read.",
|
||||
"Deleted a provably equivalent filter in iter_whitelist_packages. Its `\\#*)` case arm cannot be observed: `set -- $line` splits on whitespace, so a comment's first token is always '#', which the dot check then rejects. A mutation removing it survived against every input tried, so the dead arm was removed rather than covered by a test that would pass either way."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash phone_focus_mode/lib/tests/test_ctl_libs.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Results: 136 passed, 0 failed. None of this code had a test before."
|
||||
},
|
||||
{
|
||||
"command": "python3 meta/scripts/mutate_shell.py meta/scripts/fixtures/mutations/ctl_libs.json",
|
||||
"result": "pass",
|
||||
"evidence": "22 killed, 0 not killed. Three survived the first run and all three were defects in my own assertions: the Haversine in cmd_status was only checked for printing 'some number' (a wrong Earth radius passed), and the two whitelist filters were tested with inputs that each OTHER filter also rejected. Fixed by asserting the actual distance (~4300 m for the seeded fix) and by using a punctuation-free undotted token and a dotted token carrying an illegal character, so each filter is independently observable."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/verify_shell_split.sh 7dc3c94 phone_focus_mode/focus_ctl.sh phone_focus_mode/focus_ctl.sh phone_focus_mode/ctl_*.sh",
|
||||
"result": "pass",
|
||||
"evidence": "IDENTICAL: all 55 top-level functions moved verbatim, before the one equivalent-code deletion described above."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/shell_coverage.sh phone_focus_mode/lib/tests/test_ctl_libs.sh <each of the eight>",
|
||||
"result": "fail",
|
||||
"evidence": "NOT 100%, and these are mostly REAL gaps rather than measurement artifacts: ctl_tether 92.98%, ctl_dns 89.29%, ctl_daemon 64.86%, ctl_curfew 59.26%, ctl_hosts 58.46%, ctl_usage 55.81%, ctl_launcher 47.67%, ctl_workout 41.79%. Up from 5-22% before the tests. What remains uncovered is the per-enforcer status reporting -- the long echo blocks that print mount state, APK hashes, offload settings and database rows -- each of which needs its own device fixture. The exception is cmd_status's embedded awk, which is the same kcov-cannot-attribute class as daemon_location.sh and is proven executed by the Haversine mutation above."
|
||||
},
|
||||
{
|
||||
"command": "jscpd, the pre-commit hook's exact invocation, in a clean HEAD worktree with these files applied",
|
||||
"result": "pass",
|
||||
"evidence": "1.57%, under the 2% threshold. Checked because eight files each carrying a near-identical start/stop/status trio is exactly what that gate exists to catch. The tests drive those trios through tables rather than repeating them, for the same reason."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck on each touched file individually (no -x); sh -n on every phone-side script",
|
||||
"result": "pass",
|
||||
"evidence": "All fifteen clean standalone. Six SC1073/SC1072 errors came from a comment line beginning with the word 'shellcheck', which the linter parsed as a directive; reworded rather than disabled. Several SC2034s were fixtures nothing read, and were deleted."
|
||||
},
|
||||
{
|
||||
"command": "deploy-list parity in both directions",
|
||||
"result": "pass",
|
||||
"evidence": "comm returns empty both ways; the eight siblings appear in the push list and the cp list. deploy.sh's lists are now final, which is what unblocks splitting it next."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "16 over-cap files, down from 17. focus_ctl.sh is 171, the eight siblings 100-198, and the five test files 20-250. deploy.sh at 875 is now the only phone_focus_mode file left over the cap."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"COVERAGE IS BELOW 100% ON ALL EIGHT LIBRARIES and the numbers above are the measured ones. Unlike the daemon split, most of the shortfall is genuinely untested code -- the status-reporting blocks -- not a kcov artifact. The 22 mutations prove the assertions that exist are load-bearing; they do not prove the untested branches work.",
|
||||
"DRIFT HAZARD: ctl_is_curfew_now in ctl_curfew.sh is a verbatim duplicate of is_curfew_now in daemon_location.sh. The suite asserts the two agree across every boundary minute, so a one-sided edit fails rather than silently making the daemon and its control script disagree about whether it is night. The duplication itself remains.",
|
||||
"DRIFT HAZARD: cmd_status carries a THIRD copy of the Haversine, after daemon_location.sh's calc_distance and focus_daemon's original. Only this copy and calc_distance are tested; the three are not checked against each other.",
|
||||
"UNVERIFIED ON DEVICE. This is the fifth enforcer-family split of the session with no deploy behind it, and focus_ctl.sh now sources eight files -- a partial push leaves the control script unusable, which is how one recovers from the others misbehaving."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit. focus_ctl.sh returns to a single 1091-line file, over the cap and untested, and deploy.sh stops pushing the eight siblings.",
|
||||
"After rollback, run sh -n phone_focus_mode/focus_ctl.sh expecting exit 0, and confirm grep -c 'ctl_' phone_focus_mode/deploy.sh returns 0.",
|
||||
"A rollback AFTER a deploy leaves eight stale files in $REMOTE_DIR; they are inert once nothing sources them."
|
||||
]
|
||||
}
|
||||
138
meta/scripts/fixtures/mutations/ctl_libs.json
Normal file
138
meta/scripts/fixtures/mutations/ctl_libs.json
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
"test": "phone_focus_mode/lib/tests/test_ctl_libs.sh",
|
||||
"timeout": 300,
|
||||
"mutations": [
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_curfew.sh",
|
||||
"find": "[ \"$now\" -ge \"$start\" ] || [ \"$now\" -lt \"$end\" ]",
|
||||
"replace": "[ \"$now\" -ge \"$start\" ] && [ \"$now\" -lt \"$end\" ]",
|
||||
"describe": "break the midnight-wrapping curfew window"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_curfew.sh",
|
||||
"find": "case \"$now\" in '' | *[!0-9]*) return 1 ;; esac",
|
||||
"replace": "case \"$now\" in '' | *[!0-9]*) return 0 ;; esac",
|
||||
"describe": "fail CLOSED on a broken clock instead of open"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_curfew.sh",
|
||||
"find": "while [ \"${n#0}\" != \"$n\" ] && [ \"${#n}\" -gt 1 ]; do n=\"${n#0}\"; done",
|
||||
"replace": ":",
|
||||
"describe": "stop stripping leading zeros (octal parse)"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_curfew.sh",
|
||||
"find": "\ttouch \"$CURFEW_FORCE_FILE\"",
|
||||
"replace": "\t:",
|
||||
"describe": "curfew-test-on stops creating the force file"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_curfew.sh",
|
||||
"find": "cmd_curfew_off() {\n\ttouch \"$CURFEW_OVERRIDE_FILE\"",
|
||||
"replace": "cmd_curfew_off() {\n\t:",
|
||||
"describe": "curfew-off stops creating the 2am escape hatch"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_curfew.sh",
|
||||
"find": "cmd_curfew_on() {\n\trm -f \"$CURFEW_OVERRIDE_FILE\"",
|
||||
"replace": "cmd_curfew_on() {\n\t:",
|
||||
"describe": "curfew-on stops clearing the override"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_tether.sh",
|
||||
"find": "\ttouch \"$TETHER_FORCE_FILE\"",
|
||||
"replace": "\t:",
|
||||
"describe": "tether-test-on stops creating the force file"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_tether.sh",
|
||||
"find": "cmd_tether_test_off() {\n\trm -f \"$TETHER_FORCE_FILE\"",
|
||||
"replace": "cmd_tether_test_off() {\n\t:",
|
||||
"describe": "tether-test-off stops clearing the force file"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_dns.sh",
|
||||
"find": "if iptables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then",
|
||||
"replace": "if ! iptables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then",
|
||||
"describe": "invert the v4 DNS chain presence check"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_dns.sh",
|
||||
"find": "if ip6tables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then",
|
||||
"replace": "if ! ip6tables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then",
|
||||
"describe": "invert the v6 DNS chain presence check"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_dns.sh",
|
||||
"find": "${mode:-<unset>}",
|
||||
"replace": "${mode}",
|
||||
"describe": "render an unset private DNS mode as blank"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_hosts.sh",
|
||||
"find": "cmd_hosts_start() {\n\tlocal pid\n\tpid=\"$(hosts_enforcer_pid)\"\n\tif [ -n \"$pid\" ]; then",
|
||||
"replace": "cmd_hosts_start() {\n\tlocal pid\n\tpid=\"$(hosts_enforcer_pid)\"\n\tif [ -z \"$pid\" ]; then",
|
||||
"describe": "invert the hosts already-running guard"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_workout.sh",
|
||||
"find": "if [ ! -x \"$WORKOUT_SQLITE3_BIN\" ]; then",
|
||||
"replace": "if [ -x \"$WORKOUT_SQLITE3_BIN\" ]; then",
|
||||
"describe": "invert the sqlite3 precondition"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_daemon.sh",
|
||||
"find": "local mode=\"unknown\"",
|
||||
"replace": "local mode=\"\"",
|
||||
"describe": "report an absent mode as blank rather than unknown"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_daemon.sh",
|
||||
"find": "echo \"Location: unavailable\"",
|
||||
"replace": ":",
|
||||
"describe": "stay silent when no GPS fix is available"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_daemon.sh",
|
||||
"find": "R=6371000",
|
||||
"replace": "R=1",
|
||||
"describe": "wrong Earth radius in cmd_status's Haversine (awk kcov cannot see)"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_daemon.sh",
|
||||
"find": "pm enable \"$pkg\" >/dev/null 2>&1 && count=$((count + 1))",
|
||||
"replace": "count=$((count + 1))",
|
||||
"describe": "count apps as restored without enabling them"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_daemon.sh",
|
||||
"find": "\t\t: >\"$DISABLED_APPS_FILE\"",
|
||||
"replace": "\t\t:",
|
||||
"describe": "leave the disabled record in place after restoring"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_daemon.sh",
|
||||
"find": "\ttouch \"$RECHECK_TRIGGER\"",
|
||||
"replace": "\t:",
|
||||
"describe": "recheck stops writing the trigger"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_daemon.sh",
|
||||
"find": "echo \"Daemon not running - start it first with: focus_ctl.sh start\"\n\t\treturn 1",
|
||||
"replace": "echo \"Daemon not running - start it first with: focus_ctl.sh start\"",
|
||||
"describe": "recheck continues after reporting no daemon"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_usage.sh",
|
||||
"find": "\t\t*.*) ;;",
|
||||
"replace": "\t\t*) ;;",
|
||||
"describe": "accept non-dotted tokens as package names"
|
||||
},
|
||||
{
|
||||
"file": "phone_focus_mode/ctl_usage.sh",
|
||||
"find": "\t\tcase \"$pkg\" in\n\t\t*[!A-Za-z0-9._]*) continue ;;\n\t\tesac",
|
||||
"replace": "\t\t:",
|
||||
"describe": "accept packages containing illegal characters"
|
||||
}
|
||||
]
|
||||
}
|
||||
152
phone_focus_mode/ctl_curfew.sh
Executable file
152
phone_focus_mode/ctl_curfew.sh
Executable file
@ -0,0 +1,152 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_curfew.sh — focus_ctl.sh's subcommands for the night-curfew enforcer:
|
||||
# status, start, stop and log.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one
|
||||
# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh
|
||||
# globals these read. The pidfile path below lives here rather than in
|
||||
# the entry script because this file is its only reader (SC2034), and
|
||||
# the linter runs without -x, so each file stands alone.
|
||||
|
||||
CURFEW_PIDFILE="$STATE_DIR/curfew_enforcer.pid"
|
||||
|
||||
curfew_enforcer_pid() {
|
||||
if [ -f "$CURFEW_PIDFILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$CURFEW_PIDFILE")"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Replicates focus_daemon.sh::is_curfew_now for status display.
|
||||
_ctl_dec() {
|
||||
local n="$1"
|
||||
while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done
|
||||
printf '%s' "$n"
|
||||
}
|
||||
|
||||
ctl_is_curfew_now() {
|
||||
local now start end
|
||||
now="$(date +%H%M 2>/dev/null)"
|
||||
case "$now" in '' | *[!0-9]*) return 1 ;; esac
|
||||
now="$(_ctl_dec "$now")"
|
||||
start="$(_ctl_dec "$NIGHT_CURFEW_START")"
|
||||
end="$(_ctl_dec "$NIGHT_CURFEW_END")"
|
||||
if [ "$start" -le "$end" ]; then
|
||||
[ "$now" -ge "$start" ] && [ "$now" -lt "$end" ]
|
||||
else
|
||||
[ "$now" -ge "$start" ] || [ "$now" -lt "$end" ]
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_curfew_status() {
|
||||
local pid
|
||||
pid="$(curfew_enforcer_pid)"
|
||||
echo "=== Night Curfew Status ==="
|
||||
echo "Enabled: ${NIGHT_CURFEW_ENABLED}"
|
||||
echo "Window: ${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END} (now $(date +%H%M))"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Enforcer: RUNNING (PID $pid)"
|
||||
else
|
||||
echo "Enforcer: STOPPED"
|
||||
fi
|
||||
ctl_is_curfew_now && echo "Within window: YES" || echo "Within window: no"
|
||||
[ -e "$CURFEW_FORCE_FILE" ] && echo "Forced ON: YES (demo/test active)"
|
||||
[ -e "$CURFEW_OVERRIDE_FILE" ] && echo "Override: YES (curfew SUSPENDED)"
|
||||
if [ -f "$MODE_FILE" ]; then
|
||||
echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)"
|
||||
fi
|
||||
[ -e "$CURFEW_ENFORCER_STATE" ] && echo "Applied now: YES (grayscale/DND locked)" ||
|
||||
echo "Applied now: no"
|
||||
echo "Grayscale: ${CURFEW_GRAYSCALE_ENABLED} DND: ${CURFEW_DND_ENABLED} Net: ${CURFEW_NET_ENABLED}"
|
||||
if iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then
|
||||
echo "iptables $CURFEW_NET_IPT_CHAIN: $(iptables -S "$CURFEW_NET_IPT_CHAIN" 2>/dev/null | wc -l) rules"
|
||||
else
|
||||
echo "iptables $CURFEW_NET_IPT_CHAIN: absent (net curfew not applied)"
|
||||
fi
|
||||
if [ -f "$STATE_DIR/night_whitelist.txt" ]; then
|
||||
echo "Night whitelist: $(wc -l <"$STATE_DIR/night_whitelist.txt" | tr -d ' ') apps allowed"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_curfew_start() {
|
||||
local pid
|
||||
pid="$(curfew_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Curfew enforcer already running (PID $pid)"
|
||||
return
|
||||
fi
|
||||
setsid sh "$SCRIPT_DIR/curfew_enforcer.sh" </dev/null >/dev/null 2>&1 &
|
||||
sleep 2
|
||||
pid="$(curfew_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Curfew enforcer started (PID $pid)"
|
||||
else
|
||||
echo "ERROR: curfew enforcer failed to start. Check log: $CURFEW_ENFORCER_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_curfew_stop() {
|
||||
local pid
|
||||
pid="$(curfew_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
kill "$pid" 2>/dev/null
|
||||
echo "Curfew enforcer stopped (PID $pid) - daytime state restored"
|
||||
else
|
||||
echo "Curfew enforcer not running"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test hook: force curfew ACTIVE regardless of clock/location so the whole
|
||||
# stack can be validated during the day. Daemon re-checks within one tick.
|
||||
cmd_curfew_test_on() {
|
||||
touch "$CURFEW_FORCE_FILE"
|
||||
chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true
|
||||
rm -f "$CURFEW_OVERRIDE_FILE"
|
||||
touch "$RECHECK_TRIGGER" 2>/dev/null || true
|
||||
echo "Curfew FORCED ON. App sweep + enforcer will engage within a few seconds."
|
||||
echo "Validate: open mBank (works), keyboard (works), Firefox (gone). Then: curfew-test-off"
|
||||
}
|
||||
|
||||
cmd_curfew_test_off() {
|
||||
rm -f "$CURFEW_FORCE_FILE"
|
||||
touch "$RECHECK_TRIGGER" 2>/dev/null || true
|
||||
echo "Curfew force cleared. Back to clock-based behaviour."
|
||||
}
|
||||
|
||||
# Demo mode = the same force mechanism as the test hook, worded for an
|
||||
# on-demand demo. The companion app's Start/Stop demo button drives the same
|
||||
# force file. Easy off: curfew-demo-off (or tap "Stop demo curfew").
|
||||
cmd_curfew_demo_on() {
|
||||
touch "$CURFEW_FORCE_FILE"
|
||||
chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true
|
||||
rm -f "$CURFEW_OVERRIDE_FILE"
|
||||
touch "$RECHECK_TRIGGER" 2>/dev/null || true
|
||||
echo "Demo curfew STARTED - full curfew engaged now (apps, grayscale, DND, net)."
|
||||
echo "Stop any time with: curfew-demo-off (or tap 'Stop demo curfew')"
|
||||
}
|
||||
|
||||
cmd_curfew_demo_off() {
|
||||
rm -f "$CURFEW_FORCE_FILE"
|
||||
touch "$RECHECK_TRIGGER" 2>/dev/null || true
|
||||
echo "Demo curfew STOPPED - back to clock-based behaviour."
|
||||
}
|
||||
|
||||
# Escape hatch: suspend curfew now (the 2am 'let me out' button). Survives
|
||||
# until you re-arm. Reachable on-device only via ADB (this command) unless a
|
||||
# root file-manager/terminal has been added to NIGHT_WHITELIST.
|
||||
cmd_curfew_off() {
|
||||
touch "$CURFEW_OVERRIDE_FILE"
|
||||
chmod 666 "$CURFEW_OVERRIDE_FILE" 2>/dev/null || true
|
||||
touch "$RECHECK_TRIGGER" 2>/dev/null || true
|
||||
echo "Curfew SUSPENDED (override set: $CURFEW_OVERRIDE_FILE). Re-arm with: curfew-on"
|
||||
}
|
||||
|
||||
cmd_curfew_on() {
|
||||
rm -f "$CURFEW_OVERRIDE_FILE"
|
||||
touch "$RECHECK_TRIGGER" 2>/dev/null || true
|
||||
echo "Curfew re-armed (override cleared)."
|
||||
}
|
||||
198
phone_focus_mode/ctl_daemon.sh
Executable file
198
phone_focus_mode/ctl_daemon.sh
Executable file
@ -0,0 +1,198 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_daemon.sh — focus_ctl.sh's subcommands for the main focus daemon:
|
||||
# start, stop, status, enable, disable, recheck, log and the companion
|
||||
# notification status.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory.
|
||||
# Sourced by focus_ctl.sh, which owns log() and the config.sh globals these
|
||||
# read. $PIDFILE lives here rather than in the entry script because this file
|
||||
# is its only reader (SC2034), and the linter runs without -x so each file
|
||||
# stands alone.
|
||||
|
||||
PIDFILE="$STATE_DIR/daemon.pid"
|
||||
|
||||
# Helper to check if daemon is running
|
||||
daemon_pid() {
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$PIDFILE")"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
local pid
|
||||
pid="$(daemon_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Daemon already running (PID $pid)"
|
||||
return
|
||||
fi
|
||||
setsid sh "$SCRIPT_DIR/focus_daemon.sh" </dev/null >/dev/null 2>&1 &
|
||||
sleep 2
|
||||
pid="$(daemon_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Daemon started (PID $pid)"
|
||||
else
|
||||
echo "ERROR: Daemon failed to start. Check log: $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
local pid
|
||||
pid="$(daemon_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "Daemon not running"
|
||||
# Clean up stale pidfile if present
|
||||
rm -f "$PIDFILE"
|
||||
else
|
||||
kill -TERM "$pid"
|
||||
echo "Daemon stopped (sent SIGTERM to PID $pid)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local pid
|
||||
pid="$(daemon_pid)"
|
||||
local mode="unknown"
|
||||
[ -f "$MODE_FILE" ] && mode="$(cat "$MODE_FILE")"
|
||||
|
||||
echo "=== Focus Mode Status ==="
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Daemon: RUNNING (PID $pid)"
|
||||
else
|
||||
echo "Daemon: STOPPED"
|
||||
fi
|
||||
echo "Mode: $mode"
|
||||
echo "Home: $HOME_LAT, $HOME_LON (radius: ${RADIUS}m)"
|
||||
echo ""
|
||||
|
||||
# Show current location if available
|
||||
location="$(dumpsys location 2>/dev/null |
|
||||
grep -oE 'Location\[.*[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' |
|
||||
grep -oE '[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' |
|
||||
head -1)"
|
||||
|
||||
if [ -n "$location" ]; then
|
||||
lat="$(echo "$location" | cut -d',' -f1)"
|
||||
lon="$(echo "$location" | cut -d',' -f2)"
|
||||
dist="$(echo "$lat $lon $HOME_LAT $HOME_LON" | awk '{
|
||||
PI=3.14159265358979; R=6371000
|
||||
a1=$1*PI/180; o1=$2*PI/180
|
||||
a2=$3*PI/180; o2=$4*PI/180
|
||||
da=a2-a1; dlon=o2-o1
|
||||
x=sin(da/2)^2+cos(a1)*cos(a2)*sin(dlon/2)^2
|
||||
printf "%d", R*2*atan2(sqrt(x),sqrt(1-x))
|
||||
}')"
|
||||
echo "Location: $lat, $lon"
|
||||
echo "Distance: ${dist}m from home"
|
||||
else
|
||||
echo "Location: unavailable"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
|
||||
echo "=== Apps disabled by focus mode ==="
|
||||
cat "$DISABLED_APPS_FILE"
|
||||
else
|
||||
echo "No apps currently disabled by focus mode"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_enable() {
|
||||
# Disable daemon temporarily, force focus
|
||||
echo "Forcing focus mode ON..."
|
||||
. "$SCRIPT_DIR/config.sh"
|
||||
|
||||
# Source common functions - inline here for standalone use
|
||||
: >"$STATE_DIR/disabled_by_focus.txt"
|
||||
local count=0
|
||||
for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do
|
||||
# Check whitelist
|
||||
whitelisted=0
|
||||
for w in $(iter_whitelist_packages); do
|
||||
w_clean="$(echo "$w" | tr -d '[:space:]')"
|
||||
[ -z "$w_clean" ] && continue
|
||||
[ "$pkg" = "$w_clean" ] && {
|
||||
whitelisted=1
|
||||
break
|
||||
}
|
||||
done
|
||||
[ "$whitelisted" -eq 1 ] && continue
|
||||
|
||||
# Check system protection
|
||||
protected=0
|
||||
for prefix in $SYSTEM_NEVER_DISABLE; do
|
||||
prefix_clean="$(echo "$prefix" | tr -d '[:space:]')"
|
||||
[ -z "$prefix_clean" ] && continue
|
||||
case "$pkg" in
|
||||
"$prefix_clean"*)
|
||||
protected=1
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[ "$protected" -eq 1 ] && continue
|
||||
|
||||
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
||||
echo "$pkg" >>"$STATE_DIR/disabled_by_focus.txt"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
done
|
||||
echo "focus" >"$MODE_FILE"
|
||||
echo "Done: disabled $count apps"
|
||||
}
|
||||
|
||||
cmd_recheck() {
|
||||
# Write the trigger file; the daemon's sleep_with_recheck() will pick it
|
||||
# up within ~1 second and perform an immediate location check.
|
||||
if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
|
||||
echo "Daemon not running - start it first with: focus_ctl.sh start"
|
||||
return 1
|
||||
fi
|
||||
touch "$RECHECK_TRIGGER"
|
||||
chmod 666 "$RECHECK_TRIGGER" 2>/dev/null || true
|
||||
echo "Recheck requested. Tail the log to see the next reading:"
|
||||
echo " tail -f $LOG_FILE"
|
||||
}
|
||||
|
||||
cmd_notif_status() {
|
||||
if [ -f "$STATUS_FILE" ]; then
|
||||
echo "=== $STATUS_FILE ==="
|
||||
cat "$STATUS_FILE"
|
||||
echo
|
||||
else
|
||||
echo "No status snapshot yet (daemon has not written $STATUS_FILE)."
|
||||
fi
|
||||
if command -v dumpsys >/dev/null 2>&1; then
|
||||
echo "=== Companion app state ==="
|
||||
dumpsys package com.kuhy.focusstatus 2>/dev/null | grep -E 'enabled=|installed=|userId=' | head -5 || true
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_disable() {
|
||||
echo "Forcing focus mode OFF..."
|
||||
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
|
||||
local count=0
|
||||
while IFS= read -r pkg; do
|
||||
[ -z "$pkg" ] && continue
|
||||
pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1))
|
||||
done <"$DISABLED_APPS_FILE"
|
||||
: >"$DISABLED_APPS_FILE"
|
||||
echo "Done: re-enabled $count apps"
|
||||
else
|
||||
echo "No apps to re-enable"
|
||||
fi
|
||||
echo "normal" >"$MODE_FILE"
|
||||
}
|
||||
|
||||
cmd_log() {
|
||||
local lines="${1:-50}"
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
tail -n "$lines" "$LOG_FILE"
|
||||
else
|
||||
echo "Log file not found: $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
100
phone_focus_mode/ctl_dns.sh
Executable file
100
phone_focus_mode/ctl_dns.sh
Executable file
@ -0,0 +1,100 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_dns.sh — focus_ctl.sh's subcommands for the DNS enforcer:
|
||||
# status, start, stop and log.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one
|
||||
# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh
|
||||
# globals these read. The pidfile path below lives here rather than in
|
||||
# the entry script because this file is its only reader (SC2034), and
|
||||
# the linter runs without -x, so each file stands alone.
|
||||
|
||||
DNS_PIDFILE="$STATE_DIR/dns_enforcer.pid"
|
||||
|
||||
dns_enforcer_pid() {
|
||||
if [ -f "$DNS_PIDFILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$DNS_PIDFILE")"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dns_status() {
|
||||
local pid
|
||||
pid="$(dns_enforcer_pid)"
|
||||
echo "=== DNS Enforcer Status ==="
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Daemon: RUNNING (PID $pid)"
|
||||
else
|
||||
echo "Daemon: STOPPED"
|
||||
fi
|
||||
local mode spec
|
||||
mode="$(settings get global private_dns_mode 2>/dev/null)"
|
||||
spec="$(settings get global private_dns_specifier 2>/dev/null)"
|
||||
echo "private_dns_mode: ${mode:-<unset>}"
|
||||
echo "private_dns_specifier: ${spec:-<unset>}"
|
||||
if iptables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
|
||||
local v4rules
|
||||
v4rules="$(iptables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)"
|
||||
echo "iptables $DNS_IPT_CHAIN: $v4rules rules"
|
||||
else
|
||||
echo "iptables $DNS_IPT_CHAIN: MISSING"
|
||||
fi
|
||||
if ip6tables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
|
||||
local v6rules
|
||||
v6rules="$(ip6tables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)"
|
||||
echo "ip6tables $DNS_IPT_CHAIN: $v6rules rules"
|
||||
else
|
||||
echo "ip6tables $DNS_IPT_CHAIN: MISSING"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dns_start() {
|
||||
local pid
|
||||
pid="$(dns_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "DNS enforcer already running (PID $pid)"
|
||||
return
|
||||
fi
|
||||
setsid sh "$SCRIPT_DIR/dns_enforcer.sh" </dev/null >/dev/null 2>&1 &
|
||||
sleep 2
|
||||
pid="$(dns_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "DNS enforcer started (PID $pid)"
|
||||
else
|
||||
echo "ERROR: DNS enforcer failed to start. Check log: $DNS_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dns_stop() {
|
||||
local pid
|
||||
pid="$(dns_enforcer_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "DNS enforcer not running"
|
||||
rm -f "$DNS_PIDFILE"
|
||||
else
|
||||
kill -TERM "$pid"
|
||||
echo "DNS enforcer stopped (sent SIGTERM to PID $pid)"
|
||||
fi
|
||||
# Explicit teardown of the iptables chain so maintenance work can
|
||||
# use DoH. The enforcer itself leaves the chain intact on TERM to
|
||||
# keep the block closed between periodic re-applies.
|
||||
iptables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true
|
||||
iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || true
|
||||
iptables -X "$DNS_IPT_CHAIN" 2>/dev/null || true
|
||||
ip6tables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true
|
||||
ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || true
|
||||
ip6tables -X "$DNS_IPT_CHAIN" 2>/dev/null || true
|
||||
echo "iptables chain $DNS_IPT_CHAIN removed"
|
||||
}
|
||||
|
||||
cmd_dns_log() {
|
||||
local lines="${1:-50}"
|
||||
if [ -f "$DNS_LOG" ]; then
|
||||
tail -n "$lines" "$DNS_LOG"
|
||||
else
|
||||
echo "DNS enforcer log not found: $DNS_LOG"
|
||||
fi
|
||||
}
|
||||
118
phone_focus_mode/ctl_hosts.sh
Executable file
118
phone_focus_mode/ctl_hosts.sh
Executable file
@ -0,0 +1,118 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_hosts.sh — focus_ctl.sh's subcommands for the hosts-blocklist enforcer:
|
||||
# status, start, stop and log.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one
|
||||
# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh
|
||||
# globals these read. The pidfile path below lives here rather than in
|
||||
# the entry script because this file is its only reader (SC2034), and
|
||||
# the linter runs without -x, so each file stands alone.
|
||||
|
||||
HOSTS_PIDFILE="$STATE_DIR/hosts_enforcer.pid"
|
||||
|
||||
hosts_enforcer_pid() {
|
||||
if [ -f "$HOSTS_PIDFILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$HOSTS_PIDFILE")"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_hosts_status() {
|
||||
local pid
|
||||
pid="$(hosts_enforcer_pid)"
|
||||
echo "=== Hosts Enforcer Status ==="
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Daemon: RUNNING (PID $pid)"
|
||||
else
|
||||
echo "Daemon: STOPPED"
|
||||
fi
|
||||
echo "Canonical: $HOSTS_CANONICAL"
|
||||
echo "Target: $HOSTS_TARGET"
|
||||
if grep -qE "[[:space:]]${HOSTS_TARGET}[[:space:]]" /proc/self/mounts 2>/dev/null; then
|
||||
# A mount exists on the target path, but on Android the OEM sometimes
|
||||
# already mounts its own hosts file here. Trust the sha check below.
|
||||
echo "Mount: present (integrity check below tells us if ours)"
|
||||
else
|
||||
echo "Mount: NOT mounted (unprotected)"
|
||||
fi
|
||||
if [ -f "$HOSTS_CANONICAL" ]; then
|
||||
local expected actual
|
||||
expected="$(cat "$HOSTS_SHA_FILE" 2>/dev/null)"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual="$(sha256sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')"
|
||||
else
|
||||
actual="$(md5sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')"
|
||||
fi
|
||||
echo "Expected: ${expected:-<none>}"
|
||||
echo "Actual: ${actual:-<unreadable>}"
|
||||
if [ -n "$expected" ] && [ "$expected" = "$actual" ]; then
|
||||
echo "Integrity: OK"
|
||||
else
|
||||
echo "Integrity: MISMATCH"
|
||||
fi
|
||||
else
|
||||
echo "Canonical hosts file missing - run deploy.sh"
|
||||
fi
|
||||
# Magisk Systemless Hosts module protection state.
|
||||
local module_dir="/data/adb/modules/hosts"
|
||||
if [ -d "$module_dir" ]; then
|
||||
local lock_state="UNLOCKED (Magisk app can disable!)"
|
||||
if lsattr -d "$module_dir" 2>/dev/null | awk '{print $1}' | grep -q i; then
|
||||
lock_state="LOCKED (chattr +i)"
|
||||
fi
|
||||
echo "Magisk dir: $module_dir [$lock_state]"
|
||||
local marker_warn=""
|
||||
for marker in disable remove update; do
|
||||
if [ -e "$module_dir/$marker" ]; then
|
||||
marker_warn="$marker_warn $marker"
|
||||
fi
|
||||
done
|
||||
if [ -n "$marker_warn" ]; then
|
||||
echo "WARN: Magisk markers present:$marker_warn (will be auto-removed by hosts_enforcer)"
|
||||
fi
|
||||
else
|
||||
echo "Magisk dir: <missing - module not installed>"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_hosts_start() {
|
||||
local pid
|
||||
pid="$(hosts_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Hosts enforcer already running (PID $pid)"
|
||||
return
|
||||
fi
|
||||
setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" </dev/null >/dev/null 2>&1 &
|
||||
sleep 2
|
||||
pid="$(hosts_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Hosts enforcer started (PID $pid)"
|
||||
else
|
||||
echo "ERROR: hosts enforcer failed to start. Check log: $HOSTS_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_hosts_stop() {
|
||||
local pid
|
||||
pid="$(hosts_enforcer_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "Hosts enforcer not running"
|
||||
rm -f "$HOSTS_PIDFILE"
|
||||
return
|
||||
fi
|
||||
kill -TERM "$pid"
|
||||
echo "Hosts enforcer stopped (sent SIGTERM to PID $pid)"
|
||||
}
|
||||
|
||||
cmd_hosts_log() {
|
||||
local lines="${1:-50}"
|
||||
if [ -f "$HOSTS_LOG" ]; then
|
||||
tail -n "$lines" "$HOSTS_LOG"
|
||||
else
|
||||
echo "Hosts enforcer log not found: $HOSTS_LOG"
|
||||
fi
|
||||
}
|
||||
154
phone_focus_mode/ctl_launcher.sh
Executable file
154
phone_focus_mode/ctl_launcher.sh
Executable file
@ -0,0 +1,154 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_launcher.sh — focus_ctl.sh's subcommands for the minimalist-launcher enforcer:
|
||||
# status, start, stop and log.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one
|
||||
# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh
|
||||
# globals these read. The pidfile path below lives here rather than in
|
||||
# the entry script because this file is its only reader (SC2034), and
|
||||
# the linter runs without -x, so each file stands alone.
|
||||
|
||||
LAUNCHER_PIDFILE="$STATE_DIR/launcher_enforcer.pid"
|
||||
DISABLED_COMPETITORS_FILE="$STATE_DIR/disabled_competitors.txt"
|
||||
|
||||
launcher_enforcer_pid() {
|
||||
if [ -f "$LAUNCHER_PIDFILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$LAUNCHER_PIDFILE")"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_launcher_snapshot() {
|
||||
# Find the APK path for the currently-installed launcher and copy it
|
||||
# to LAUNCHER_APK. Also capture the current HOME activity component.
|
||||
local apk_path
|
||||
apk_path="$(pm path "$LAUNCHER_PACKAGE" 2>/dev/null | head -1 | sed 's/^package://')"
|
||||
if [ -z "$apk_path" ] || [ ! -f "$apk_path" ]; then
|
||||
echo "ERROR: $LAUNCHER_PACKAGE is not installed. Install it once via Aurora/Play Store, then rerun this command."
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "$(dirname "$LAUNCHER_APK")"
|
||||
chattr -i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true
|
||||
cp "$apk_path" "$LAUNCHER_APK" || return 1
|
||||
chmod 644 "$LAUNCHER_APK"
|
||||
sha256sum "$LAUNCHER_APK" | awk '{print $1}' >"$LAUNCHER_SHA_FILE"
|
||||
chmod 644 "$LAUNCHER_SHA_FILE"
|
||||
|
||||
# Resolve the current HOME activity (or the launcher's default activity
|
||||
# if it isn't yet the default).
|
||||
local component
|
||||
component="$(cmd package resolve-activity --brief \
|
||||
-c android.intent.category.HOME \
|
||||
-a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}')"
|
||||
if [ -z "$component" ] || [ "${component%%/*}" != "$LAUNCHER_PACKAGE" ]; then
|
||||
# Fall back to the launcher's MAIN/LAUNCHER activity
|
||||
component="$(cmd package resolve-activity --brief \
|
||||
-c android.intent.category.LAUNCHER \
|
||||
-a android.intent.action.MAIN "$LAUNCHER_PACKAGE" 2>/dev/null |
|
||||
awk 'NR==2{print}')"
|
||||
fi
|
||||
if [ -z "$component" ]; then
|
||||
echo "ERROR: could not resolve HOME activity for $LAUNCHER_PACKAGE"
|
||||
return 1
|
||||
fi
|
||||
echo "$component" >"$LAUNCHER_ACTIVITY_FILE"
|
||||
chmod 644 "$LAUNCHER_ACTIVITY_FILE"
|
||||
|
||||
# Make snapshot immutable so even root-in-a-terminal can't overwrite
|
||||
# it without first running `chattr -i`.
|
||||
chattr +i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true
|
||||
|
||||
echo "Snapshot saved:"
|
||||
echo " APK: $LAUNCHER_APK ($(wc -c <"$LAUNCHER_APK") bytes)"
|
||||
echo " SHA256: $(cat "$LAUNCHER_SHA_FILE")"
|
||||
echo " Activity: $component"
|
||||
}
|
||||
|
||||
cmd_launcher_status() {
|
||||
local pid
|
||||
pid="$(launcher_enforcer_pid)"
|
||||
echo "=== Launcher Enforcer Status ==="
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Daemon: RUNNING (PID $pid)"
|
||||
else
|
||||
echo "Daemon: STOPPED"
|
||||
fi
|
||||
echo "Package: $LAUNCHER_PACKAGE"
|
||||
if pm path "$LAUNCHER_PACKAGE" >/dev/null 2>&1; then
|
||||
echo "Installed: YES ($(pm path "$LAUNCHER_PACKAGE" | head -1))"
|
||||
else
|
||||
echo "Installed: NO"
|
||||
fi
|
||||
local desired actual
|
||||
desired="$(cat "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null)"
|
||||
actual="$(cmd package resolve-activity --brief \
|
||||
-c android.intent.category.HOME -a android.intent.action.MAIN \
|
||||
2>/dev/null | awk 'NR==2{print}')"
|
||||
echo "Expected: ${desired:-<not armed - run launcher-snapshot>}"
|
||||
echo "Actual: ${actual:-<unresolved>}"
|
||||
if [ -n "$desired" ] && [ "$desired" = "$actual" ]; then
|
||||
echo "Default: OK (pinned)"
|
||||
else
|
||||
echo "Default: MISMATCH"
|
||||
fi
|
||||
echo "Snapshot: $LAUNCHER_APK"
|
||||
if [ -f "$LAUNCHER_APK" ]; then
|
||||
echo "Snapshot size: $(wc -c <"$LAUNCHER_APK") bytes"
|
||||
fi
|
||||
if [ -s "$DISABLED_COMPETITORS_FILE" ]; then
|
||||
echo "Disabled competitors:"
|
||||
sed 's/^/ - /' "$DISABLED_COMPETITORS_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_launcher_start() {
|
||||
local pid
|
||||
pid="$(launcher_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Launcher enforcer already running (PID $pid)"
|
||||
return
|
||||
fi
|
||||
setsid sh "$SCRIPT_DIR/launcher_enforcer.sh" </dev/null >/dev/null 2>&1 &
|
||||
sleep 2
|
||||
pid="$(launcher_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Launcher enforcer started (PID $pid)"
|
||||
else
|
||||
echo "ERROR: launcher enforcer failed to start. Check log: $LAUNCHER_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_launcher_stop() {
|
||||
local pid
|
||||
pid="$(launcher_enforcer_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "Launcher enforcer not running"
|
||||
rm -f "$LAUNCHER_PIDFILE"
|
||||
else
|
||||
kill -TERM "$pid"
|
||||
echo "Launcher enforcer stopped (sent SIGTERM to PID $pid)"
|
||||
fi
|
||||
# Re-enable any competitors we disabled so the device is usable if the
|
||||
# enforcer is intentionally stopped (e.g. during maintenance).
|
||||
if [ -s "$DISABLED_COMPETITORS_FILE" ]; then
|
||||
while read -r pkg; do
|
||||
[ -z "$pkg" ] && continue
|
||||
pm enable --user 0 "$pkg" >/dev/null 2>&1 &&
|
||||
echo "Re-enabled competing launcher: $pkg"
|
||||
done <"$DISABLED_COMPETITORS_FILE"
|
||||
: >"$DISABLED_COMPETITORS_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_launcher_log() {
|
||||
local lines="${1:-50}"
|
||||
if [ -f "$LAUNCHER_LOG" ]; then
|
||||
tail -n "$lines" "$LAUNCHER_LOG"
|
||||
else
|
||||
echo "Launcher enforcer log not found: $LAUNCHER_LOG"
|
||||
fi
|
||||
}
|
||||
104
phone_focus_mode/ctl_tether.sh
Executable file
104
phone_focus_mode/ctl_tether.sh
Executable file
@ -0,0 +1,104 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_tether.sh — focus_ctl.sh's subcommands for the tethering enforcer:
|
||||
# status, start, stop and log.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one
|
||||
# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh
|
||||
# globals these read. The pidfile path below lives here rather than in
|
||||
# the entry script because this file is its only reader (SC2034), and
|
||||
# the linter runs without -x, so each file stands alone.
|
||||
|
||||
TETHER_PIDFILE="$STATE_DIR/tether_enforcer.pid"
|
||||
|
||||
tether_enforcer_pid() {
|
||||
if [ -f "$TETHER_PIDFILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$TETHER_PIDFILE")"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_tether_status() {
|
||||
local pid
|
||||
pid="$(tether_enforcer_pid)"
|
||||
echo "=== Hotspot / Tethering Block Status ==="
|
||||
echo "Enabled: ${TETHER_ENFORCER_ENABLED}"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Enforcer: RUNNING (PID $pid)"
|
||||
else
|
||||
echo "Enforcer: STOPPED"
|
||||
fi
|
||||
if [ -f "$MODE_FILE" ]; then
|
||||
echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)"
|
||||
fi
|
||||
[ -e "$TETHER_FORCE_FILE" ] && echo "Forced ON: YES (test active)"
|
||||
[ -e "$TETHER_OVERRIDE_FILE" ] && echo "Override: YES (block SUSPENDED)"
|
||||
[ -e "$TETHER_ENFORCER_STATE" ] && echo "Applied now: YES (offload off + FORWARD reject)" ||
|
||||
echo "Applied now: no"
|
||||
local off
|
||||
off="$(settings get global "$TETHER_OFFLOAD_KEY" 2>/dev/null)"
|
||||
echo "tether_offload_disabled: ${off:-<unset>}"
|
||||
if iptables -C FORWARD -j "$TETHER_IPT_CHAIN" >/dev/null 2>&1; then
|
||||
echo "iptables $TETHER_IPT_CHAIN: pinned to FORWARD ($(iptables -S "$TETHER_IPT_CHAIN" 2>/dev/null | grep -c '^-A') rule)"
|
||||
else
|
||||
echo "iptables $TETHER_IPT_CHAIN: absent (block not applied)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_tether_start() {
|
||||
local pid
|
||||
pid="$(tether_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Tether enforcer already running (PID $pid)"
|
||||
return
|
||||
fi
|
||||
setsid sh "$SCRIPT_DIR/tether_enforcer.sh" </dev/null >/dev/null 2>&1 &
|
||||
sleep 2
|
||||
pid="$(tether_enforcer_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Tether enforcer started (PID $pid)"
|
||||
else
|
||||
echo "ERROR: tether enforcer failed to start. Check log: $TETHER_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_tether_stop() {
|
||||
local pid
|
||||
pid="$(tether_enforcer_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "Tether enforcer not running"
|
||||
rm -f "$TETHER_PIDFILE"
|
||||
else
|
||||
kill -TERM "$pid"
|
||||
echo "Tether enforcer stopped (sent SIGTERM to PID $pid)"
|
||||
fi
|
||||
# Explicit teardown so maintenance can tether even if the daemon was already
|
||||
# dead (its own TERM handler reverts too; this is the belt-and-suspenders).
|
||||
iptables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true
|
||||
iptables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true
|
||||
iptables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true
|
||||
ip6tables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true
|
||||
ip6tables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true
|
||||
ip6tables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true
|
||||
echo "FORWARD chain $TETHER_IPT_CHAIN removed (tethering restored)"
|
||||
}
|
||||
|
||||
cmd_tether_log() { tail -n "${1:-50}" "$TETHER_LOG" 2>/dev/null || echo "No tether log yet."; }
|
||||
|
||||
# Test hook: force the block ACTIVE regardless of location so the full stack can
|
||||
# be validated during the day with a real second phone on the hotspot.
|
||||
cmd_tether_test_on() {
|
||||
touch "$TETHER_FORCE_FILE"
|
||||
chmod 666 "$TETHER_FORCE_FILE" 2>/dev/null || true
|
||||
rm -f "$TETHER_OVERRIDE_FILE"
|
||||
echo "Tether block FORCED ON. Enforcer engages within ${TETHER_CHECK_INTERVAL}s."
|
||||
echo "Validate: second phone on the hotspot loses internet; this phone stays online."
|
||||
}
|
||||
|
||||
cmd_tether_test_off() {
|
||||
rm -f "$TETHER_FORCE_FILE"
|
||||
echo "Tether force cleared. Back to focus-mode-gated behaviour."
|
||||
}
|
||||
144
phone_focus_mode/ctl_usage.sh
Executable file
144
phone_focus_mode/ctl_usage.sh
Executable file
@ -0,0 +1,144 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_usage.sh — focus_ctl.sh's help text and the two commands that report on
|
||||
# the whitelist: list-apps and whitelist.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory.
|
||||
# Sourced by focus_ctl.sh, which owns the config.sh globals these read.
|
||||
#
|
||||
# NOTE on iter_whitelist_packages: its `while read` loop is fed by a pipe from
|
||||
# printf, and nothing in the body reads stdin, so the pipe is safe here. That
|
||||
# is NOT true of the pm-driven loops elsewhere in this repo -- see the warning
|
||||
# in daemon_apps.sh before converting any redirect-driven loop to a pipe.
|
||||
|
||||
# Emit one valid package name per line from WHITELIST.
|
||||
# This strips comments/blank lines from the multi-line quoted string and avoids
|
||||
# treating heading text (e.g. "---") as package tokens.
|
||||
iter_whitelist_packages() {
|
||||
printf '%s\n' "$WHITELIST" | while IFS= read -r line; do
|
||||
line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||
# Blank lines only. A comment needs no case of its own: `set -- $line`
|
||||
# below splits on whitespace, so a comment's first token is always "#",
|
||||
# which the dot check then rejects. A dedicated `\#*)` arm here was
|
||||
# provably equivalent -- a mutation removing it could not be
|
||||
# distinguished by any input -- and equivalent code is a claim of a
|
||||
# check that is not actually being made.
|
||||
case "$line" in
|
||||
"") continue ;;
|
||||
esac
|
||||
|
||||
# Keep first token only; ignore any inline prose if present.
|
||||
# Intentional word split to grab the first token. This runs under
|
||||
# /system/bin/sh on the phone, where arrays do not exist, so an
|
||||
# unquoted expansion is the POSIX way to do it.
|
||||
# shellcheck disable=SC2086
|
||||
set -- $line
|
||||
pkg="$1"
|
||||
|
||||
# Package names are dot-delimited identifiers.
|
||||
case "$pkg" in
|
||||
*.*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
case "$pkg" in
|
||||
*[!A-Za-z0-9._]*) continue ;;
|
||||
esac
|
||||
|
||||
echo "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: focus_ctl.sh <command>"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start - Start the focus mode daemon"
|
||||
echo " stop - Stop the daemon and re-enable all apps"
|
||||
echo " status - Show current mode, location and disabled apps"
|
||||
echo " enable - Force focus mode on (regardless of location)"
|
||||
echo " disable - Force focus mode off (regardless of location)"
|
||||
echo " log - Show daemon log"
|
||||
echo " list-apps - List all non-whitelisted third-party apps"
|
||||
echo " whitelist - List currently whitelisted packages"
|
||||
echo " restart - Restart the daemon"
|
||||
echo " hosts-status - Show hosts enforcer state (mount + hash)"
|
||||
echo " hosts-start - Start the hosts enforcer daemon"
|
||||
echo " hosts-stop - Stop the hosts enforcer daemon"
|
||||
echo " hosts-log - Show hosts enforcer log"
|
||||
echo " dns-status - Show DNS enforcer state (Private DNS + iptables)"
|
||||
echo " dns-start - Start the DNS enforcer daemon"
|
||||
echo " dns-stop - Stop the DNS enforcer daemon (removes iptables chain)"
|
||||
echo " dns-log - Show DNS enforcer log"
|
||||
echo " launcher-status - Show launcher enforcer state"
|
||||
echo " launcher-start - Start the launcher enforcer daemon"
|
||||
echo " launcher-stop - Stop the launcher enforcer daemon"
|
||||
echo " launcher-log - Show launcher enforcer log"
|
||||
echo " launcher-snapshot - Back up currently-installed launcher APK"
|
||||
echo " workout-status - Show StrongLifts workout-detection state"
|
||||
echo " workout-start - Start the workout detector daemon"
|
||||
echo " workout-stop - Stop the workout detector daemon (sets flag=0)"
|
||||
echo " workout-log - Show workout detector log"
|
||||
echo " recheck - Nudge the daemon to perform a fresh location check now"
|
||||
echo " curfew-status - Show night-curfew + enforcer state"
|
||||
echo " curfew-start - Start the curfew enforcer (grayscale/DND/net)"
|
||||
echo " curfew-stop - Stop it and restore daytime display/DND"
|
||||
echo " curfew-log - Show curfew enforcer log"
|
||||
echo " curfew-test-on - Force curfew ACTIVE now (daytime validation)"
|
||||
echo " curfew-test-off - Clear the test force"
|
||||
echo " curfew-demo-on - Start a demo: full curfew now, easy one-tap off"
|
||||
echo " curfew-demo-off - Stop the demo"
|
||||
echo " curfew-off - Escape hatch: suspend curfew now (2am opt-out)"
|
||||
echo " curfew-on - Re-arm curfew (clear the override)"
|
||||
echo " tether-status - Show hotspot/tethering-block state"
|
||||
echo " tether-start - Start the tether enforcer (FORWARD block + offload off)"
|
||||
echo " tether-stop - Stop it and restore tethering (teardown FORWARD chain)"
|
||||
echo " tether-log - Show tether enforcer log"
|
||||
echo " tether-test-on - Force the tether block ACTIVE now (daytime validation)"
|
||||
echo " tether-test-off - Clear the tether force"
|
||||
echo " notif-status - Show companion status-notification details"
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_list_apps() {
|
||||
echo "=== Third-party apps NOT in whitelist ==="
|
||||
for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do
|
||||
whitelisted=0
|
||||
for w in $(iter_whitelist_packages); do
|
||||
w="$(echo "$w" | tr -d '[:space:]')"
|
||||
[ -z "$w" ] && continue
|
||||
[ "$pkg" = "$w" ] && {
|
||||
whitelisted=1
|
||||
break
|
||||
}
|
||||
done
|
||||
if [ "$whitelisted" -eq 0 ]; then
|
||||
# Check if currently disabled by focus mode
|
||||
if grep -qF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null; then
|
||||
echo " [BLOCKED] $pkg"
|
||||
else
|
||||
echo " [active] $pkg"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "=== Whitelisted apps ==="
|
||||
for w in $(iter_whitelist_packages); do
|
||||
w="$(echo "$w" | tr -d '[:space:]')"
|
||||
[ -z "$w" ] && continue
|
||||
echo " [allowed] $w"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_whitelist() {
|
||||
echo "=== Whitelisted packages ==="
|
||||
for w in $(iter_whitelist_packages); do
|
||||
w="$(echo "$w" | tr -d '[:space:]')"
|
||||
[ -z "$w" ] && continue
|
||||
# Check if installed
|
||||
if pm list packages "$w" 2>/dev/null | grep -qF "$w"; then
|
||||
echo " [installed] $w"
|
||||
else
|
||||
echo " [not found] $w"
|
||||
fi
|
||||
done
|
||||
}
|
||||
122
phone_focus_mode/ctl_workout.sh
Executable file
122
phone_focus_mode/ctl_workout.sh
Executable file
@ -0,0 +1,122 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ctl_workout.sh — focus_ctl.sh's subcommands for the workout detector:
|
||||
# status, start, stop and log.
|
||||
#
|
||||
# A flat sibling, not lib/: deploy.sh pushes phone scripts into one
|
||||
# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh
|
||||
# globals these read. The pidfile path below lives here rather than in
|
||||
# the entry script because this file is its only reader (SC2034), and
|
||||
# the linter runs without -x, so each file stands alone.
|
||||
|
||||
WORKOUT_PIDFILE="$STATE_DIR/workout_detector.pid"
|
||||
|
||||
workout_detector_pid() {
|
||||
if [ -f "$WORKOUT_PIDFILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$WORKOUT_PIDFILE")"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_workout_status() {
|
||||
local pid
|
||||
pid="$(workout_detector_pid)"
|
||||
echo "=== Workout Detector Status ==="
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Daemon: RUNNING (PID $pid)"
|
||||
else
|
||||
echo "Daemon: STOPPED"
|
||||
fi
|
||||
echo "Package: $WORKOUT_TRIGGER_PACKAGE"
|
||||
if pm path "$WORKOUT_TRIGGER_PACKAGE" >/dev/null 2>&1; then
|
||||
echo "Installed: YES"
|
||||
else
|
||||
echo "Installed: NO (detector will always report inactive)"
|
||||
fi
|
||||
echo "sqlite3: $WORKOUT_SQLITE3_BIN"
|
||||
if [ -x "$WORKOUT_SQLITE3_BIN" ]; then
|
||||
echo "sqlite3 ver: $("$WORKOUT_SQLITE3_BIN" -version 2>/dev/null | awk '{print $1}')"
|
||||
else
|
||||
echo "sqlite3 ver: <missing or not executable — detector cannot query DB>"
|
||||
fi
|
||||
echo "DB path: $WORKOUT_DB_PATH"
|
||||
if [ -f "$WORKOUT_DB_PATH" ]; then
|
||||
echo "DB present: YES"
|
||||
else
|
||||
echo "DB present: NO"
|
||||
fi
|
||||
echo "Poll interval: ${WORKOUT_DETECTOR_INTERVAL}s"
|
||||
local flag="<unset>"
|
||||
if [ -f "$WORKOUT_ACTIVE_FILE" ]; then
|
||||
flag="$(cat "$WORKOUT_ACTIVE_FILE" 2>/dev/null)"
|
||||
fi
|
||||
case "$flag" in
|
||||
1) echo "Workout flag: 1 (workout IN PROGRESS → YouTube hosts UNBLOCKED)" ;;
|
||||
0) echo "Workout flag: 0 (no workout → YouTube hosts BLOCKED)" ;;
|
||||
*) echo "Workout flag: '$flag' (treated as 0, fail-closed)" ;;
|
||||
esac
|
||||
# Live one-shot query so the user can see ground truth without waiting
|
||||
# for the next poll cycle. Best-effort — never fails the status command.
|
||||
if [ -x "$WORKOUT_SQLITE3_BIN" ] && [ -f "$WORKOUT_DB_PATH" ]; then
|
||||
local live_count
|
||||
live_count="$("$WORKOUT_SQLITE3_BIN" "file:${WORKOUT_DB_PATH}?mode=ro" \
|
||||
"SELECT COUNT(*) FROM workouts WHERE start>0 AND (finish IS NULL OR finish=0);" \
|
||||
2>/dev/null)"
|
||||
echo "Live DB query: in-progress workouts = ${live_count:-<query failed>}"
|
||||
fi
|
||||
if [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then
|
||||
echo "Workout hosts: $HOSTS_CANONICAL_WORKOUT ($(wc -l <"$HOSTS_CANONICAL_WORKOUT" 2>/dev/null) lines)"
|
||||
else
|
||||
echo "Workout hosts: <missing — deploy.sh must regenerate it>"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_workout_start() {
|
||||
local pid
|
||||
pid="$(workout_detector_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Workout detector already running (PID $pid)"
|
||||
return
|
||||
fi
|
||||
if [ ! -x "$WORKOUT_SQLITE3_BIN" ]; then
|
||||
echo "ERROR: $WORKOUT_SQLITE3_BIN missing or not executable. Re-run deploy.sh."
|
||||
return 1
|
||||
fi
|
||||
setsid sh "$SCRIPT_DIR/workout_detector.sh" </dev/null >/dev/null 2>&1 &
|
||||
sleep 2
|
||||
pid="$(workout_detector_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "Workout detector started (PID $pid)"
|
||||
else
|
||||
echo "ERROR: Workout detector failed to start. Check log: $WORKOUT_DETECTOR_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_workout_stop() {
|
||||
local pid
|
||||
pid="$(workout_detector_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "Workout detector not running"
|
||||
rm -f "$WORKOUT_PIDFILE"
|
||||
else
|
||||
kill -TERM "$pid"
|
||||
echo "Workout detector stopped (sent SIGTERM to PID $pid)"
|
||||
fi
|
||||
# Fail-closed on manual stop: write 0 so the hosts enforcer reverts to
|
||||
# the full-block canonical and YouTube goes back to being blocked.
|
||||
printf '0\n' >"$WORKOUT_ACTIVE_FILE" 2>/dev/null || true
|
||||
chmod 666 "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true
|
||||
echo "workout_active flag forced to 0"
|
||||
}
|
||||
|
||||
cmd_workout_log() {
|
||||
local lines="${1:-50}"
|
||||
if [ -f "$WORKOUT_DETECTOR_LOG" ]; then
|
||||
tail -n "$lines" "$WORKOUT_DETECTOR_LOG"
|
||||
else
|
||||
echo "Workout detector log not found: $WORKOUT_DETECTOR_LOG"
|
||||
fi
|
||||
}
|
||||
@ -382,6 +382,14 @@ do_deploy() {
|
||||
adb_cmd push "$DEPLOY_DIR/daemon_state.sh" "/data/local/tmp/focus_stage/daemon_state.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/daemon_apps.sh" "/data/local/tmp/focus_stage/daemon_apps.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_usage.sh" "/data/local/tmp/focus_stage/ctl_usage.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_daemon.sh" "/data/local/tmp/focus_stage/ctl_daemon.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_hosts.sh" "/data/local/tmp/focus_stage/ctl_hosts.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_dns.sh" "/data/local/tmp/focus_stage/ctl_dns.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_launcher.sh" "/data/local/tmp/focus_stage/ctl_launcher.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_workout.sh" "/data/local/tmp/focus_stage/ctl_workout.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_curfew.sh" "/data/local/tmp/focus_stage/ctl_curfew.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/ctl_tether.sh" "/data/local/tmp/focus_stage/ctl_tether.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/hosts_magisk.sh" "/data/local/tmp/focus_stage/hosts_magisk.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/hosts_mount.sh" "/data/local/tmp/focus_stage/hosts_mount.sh"
|
||||
@ -511,6 +519,14 @@ do_deploy() {
|
||||
adb_root "cp /data/local/tmp/focus_stage/daemon_state.sh $REMOTE_DIR/daemon_state.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/daemon_apps.sh $REMOTE_DIR/daemon_apps.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/focus_ctl.sh $REMOTE_DIR/focus_ctl.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_usage.sh $REMOTE_DIR/ctl_usage.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_daemon.sh $REMOTE_DIR/ctl_daemon.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_hosts.sh $REMOTE_DIR/ctl_hosts.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_dns.sh $REMOTE_DIR/ctl_dns.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_launcher.sh $REMOTE_DIR/ctl_launcher.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_workout.sh $REMOTE_DIR/ctl_workout.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_curfew.sh $REMOTE_DIR/ctl_curfew.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/ctl_tether.sh $REMOTE_DIR/ctl_tether.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/hosts_enforcer.sh $REMOTE_DIR/hosts_enforcer.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/hosts_magisk.sh $REMOTE_DIR/hosts_magisk.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/hosts_mount.sh $REMOTE_DIR/hosts_mount.sh"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
250
phone_focus_mode/lib/tests/ctl_cases_commands.sh
Executable file
250
phone_focus_mode/lib/tests/ctl_cases_commands.sh
Executable file
@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/ctl_cases_commands.sh — assertions for focus_ctl.sh's subcommands,
|
||||
# against the device stubs in test_ctl_libs.sh.
|
||||
#
|
||||
# The start/stop/status trio is the same shape in all seven enforcer
|
||||
# libraries, so it is driven through tables rather than seven near-identical
|
||||
# blocks: the duplication is in the subject, and repeating it here would add
|
||||
# nothing and trip the jscpd gate. The per-enforcer specifics that tables
|
||||
# cannot express get their own cases below.
|
||||
set -euo pipefail
|
||||
|
||||
# Run a subcommand for its side effects only. These are CLI entry points whose
|
||||
# exit status is not part of their contract, and an unguarded nonzero return
|
||||
# aborts the whole file under set -e.
|
||||
_run_quiet() {
|
||||
"$1" >/dev/null 2>&1 || return 0
|
||||
}
|
||||
|
||||
# _start_case <cmd> <pid-fn> <pidfile-var> <label>
|
||||
#
|
||||
# A start command must refuse when already running, and must report the
|
||||
# failure rather than claim success when the daemon does not come up. The
|
||||
# stubbed setsid never actually starts anything, so "came up" is modelled by
|
||||
# the pidfile appearing.
|
||||
_start_case() {
|
||||
local cmd="$1" pidvar="$2" label="$3"
|
||||
local file="${!pidvar}"
|
||||
|
||||
printf '%s\n' "$$" >"${file}"
|
||||
_reset_calls
|
||||
case "$(${cmd} || true)" in
|
||||
*"already running"*) _t_pass "${label} refuses to start a second copy" ;;
|
||||
*) _t_fail "${label} must not start when already running" ;;
|
||||
esac
|
||||
if _calls | grep -q "^setsid"; then
|
||||
_t_fail "${label} launched a daemon that was already running"
|
||||
else
|
||||
_t_pass "${label} launches nothing when already running"
|
||||
fi
|
||||
|
||||
rm -f "${file}"
|
||||
_reset_calls
|
||||
case "$(${cmd} 2>&1 || true)" in
|
||||
*ERROR*) _t_pass "${label} reports a daemon that failed to come up" ;;
|
||||
*) _t_fail "${label} must report a failed start, not claim success" ;;
|
||||
esac
|
||||
if _calls | grep -q "^setsid"; then
|
||||
_t_pass "${label} does try to launch when nothing is running"
|
||||
else
|
||||
_t_fail "${label} should have launched a daemon"
|
||||
fi
|
||||
}
|
||||
|
||||
# _stop_case <cmd> <pidfile-var> <label>
|
||||
#
|
||||
# The live pid must NOT be $$: these commands call `kill -TERM "$pid"`, so
|
||||
# seeding the test's own pid makes the suite signal itself and die silently
|
||||
# mid-run. A disposable background process is used instead.
|
||||
_stop_case() {
|
||||
local cmd="$1" pidvar="$2" label="$3"
|
||||
local file="${!pidvar}" victim=""
|
||||
|
||||
rm -f "${file}"
|
||||
_reset_calls
|
||||
case "$(${cmd} 2>&1 || true)" in
|
||||
*"not running"*) _t_pass "${label} reports nothing to stop" ;;
|
||||
*) _t_fail "${label} must report that nothing is running" ;;
|
||||
esac
|
||||
|
||||
/usr/bin/sleep 30 &
|
||||
victim=$!
|
||||
printf '%s\n' "${victim}" >"${file}"
|
||||
_reset_calls
|
||||
_run_quiet "${cmd}"
|
||||
# kill is a shell builtin, so no PATH stub can observe it. The effect is
|
||||
# what is asserted: the victim must no longer be alive afterwards.
|
||||
/usr/bin/sleep 0.2
|
||||
if kill -0 "${victim}" 2>/dev/null; then
|
||||
_t_fail "${label} left the running daemon alive"
|
||||
kill "${victim}" 2>/dev/null || true
|
||||
else
|
||||
_t_pass "${label} signals the running daemon"
|
||||
fi
|
||||
wait "${victim}" 2>/dev/null || true
|
||||
rm -f "${file}"
|
||||
}
|
||||
|
||||
# _status_case <cmd> <pidfile-var> <label>
|
||||
_status_case() {
|
||||
local cmd="$1" pidvar="$2" label="$3"
|
||||
local file="${!pidvar}"
|
||||
|
||||
rm -f "${file}"
|
||||
case "$(${cmd} 2>&1 || true)" in
|
||||
*STOPPED*) _t_pass "${label} reports STOPPED with no daemon" ;;
|
||||
*) _t_fail "${label} must report STOPPED when nothing is running" ;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "$$" >"${file}"
|
||||
case "$(${cmd} 2>&1 || true)" in
|
||||
*RUNNING*) _t_pass "${label} reports RUNNING with a live daemon" ;;
|
||||
*) _t_fail "${label} must report RUNNING when the daemon is up" ;;
|
||||
esac
|
||||
rm -f "${file}"
|
||||
}
|
||||
|
||||
# --- the four enforcers whose trio is uniform -------------------------------
|
||||
|
||||
_start_case cmd_hosts_start HOSTS_PIDFILE "hosts start"
|
||||
_stop_case cmd_hosts_stop HOSTS_PIDFILE "hosts stop"
|
||||
_status_case cmd_hosts_status HOSTS_PIDFILE "hosts status"
|
||||
|
||||
_start_case cmd_dns_start DNS_PIDFILE "dns start"
|
||||
_stop_case cmd_dns_stop DNS_PIDFILE "dns stop"
|
||||
_status_case cmd_dns_status DNS_PIDFILE "dns status"
|
||||
|
||||
_start_case cmd_launcher_start LAUNCHER_PIDFILE "launcher start"
|
||||
_stop_case cmd_launcher_stop LAUNCHER_PIDFILE "launcher stop"
|
||||
_status_case cmd_launcher_status LAUNCHER_PIDFILE "launcher status"
|
||||
|
||||
_start_case cmd_workout_start WORKOUT_PIDFILE "workout start"
|
||||
|
||||
# The workout detector needs the sqlite3 binary deploy.sh pushes alongside it.
|
||||
# Missing, it must refuse rather than launch a detector that cannot read the
|
||||
# StrongLifts database.
|
||||
rm -f "${WORKOUT_PIDFILE}"
|
||||
_saved_sqlite="${WORKOUT_SQLITE3_BIN}"
|
||||
WORKOUT_SQLITE3_BIN="${RUN}/state/no-such-sqlite3"
|
||||
_reset_calls
|
||||
case "$(cmd_workout_start 2>&1 || true)" in
|
||||
*"missing or not executable"*) _t_pass "workout start refuses without sqlite3" ;;
|
||||
*) _t_fail "workout start must refuse when sqlite3 is missing" ;;
|
||||
esac
|
||||
if _calls | grep -q "^setsid"; then
|
||||
_t_fail "workout start launched a detector with no sqlite3"
|
||||
else
|
||||
_t_pass "workout start launches nothing without sqlite3"
|
||||
fi
|
||||
WORKOUT_SQLITE3_BIN="${_saved_sqlite}"
|
||||
_stop_case cmd_workout_stop WORKOUT_PIDFILE "workout stop"
|
||||
|
||||
_start_case cmd_curfew_start CURFEW_PIDFILE "curfew start"
|
||||
_stop_case cmd_curfew_stop CURFEW_PIDFILE "curfew stop"
|
||||
|
||||
_start_case cmd_tether_start TETHER_PIDFILE "tether start"
|
||||
_stop_case cmd_tether_stop TETHER_PIDFILE "tether stop"
|
||||
_status_case cmd_tether_status TETHER_PIDFILE "tether status"
|
||||
|
||||
_start_case cmd_start PIDFILE "daemon start"
|
||||
_stop_case cmd_stop PIDFILE "daemon stop"
|
||||
|
||||
# --- dns status: the chain report -------------------------------------------
|
||||
|
||||
# A missing firewall chain is the whole point of the check, so it must be
|
||||
# reported as MISSING rather than as a zero-rule chain.
|
||||
rm -f "${DNS_PIDFILE}"
|
||||
_clear_chain iptables
|
||||
_clear_chain ip6tables
|
||||
out="$(cmd_dns_status 2>&1)"
|
||||
case "${out}" in
|
||||
*"iptables ${DNS_IPT_CHAIN}: MISSING"*) _t_pass "dns status reports a missing v4 chain" ;;
|
||||
*) _t_fail "dns status must report a missing v4 chain" ;;
|
||||
esac
|
||||
case "${out}" in
|
||||
*"ip6tables ${DNS_IPT_CHAIN}: MISSING"*) _t_pass "dns status reports a missing v6 chain" ;;
|
||||
*) _t_fail "dns status must report a missing v6 chain" ;;
|
||||
esac
|
||||
|
||||
_seed_chain iptables "$(printf -- '-A one\n-A two\n-A three')"
|
||||
_seed_chain ip6tables "-A one"
|
||||
out="$(cmd_dns_status 2>&1)"
|
||||
case "${out}" in
|
||||
*"iptables ${DNS_IPT_CHAIN}: 3 rules"*) _t_pass "dns status counts the v4 rules" ;;
|
||||
*) _t_fail "dns status must count the v4 rules" ;;
|
||||
esac
|
||||
case "${out}" in
|
||||
*"ip6tables ${DNS_IPT_CHAIN}: 1 rules"*) _t_pass "dns status counts the v6 rules" ;;
|
||||
*) _t_fail "dns status must count the v6 rules" ;;
|
||||
esac
|
||||
|
||||
# Private DNS is the setting the enforcer exists to hold down, so an unset
|
||||
# value must read as <unset> rather than as an empty line.
|
||||
_seed_out settings ""
|
||||
case "$(cmd_dns_status 2>&1)" in
|
||||
*"private_dns_mode: <unset>"*) _t_pass "dns status renders an unset mode explicitly" ;;
|
||||
*) _t_fail "dns status must render an unset private DNS mode" ;;
|
||||
esac
|
||||
rm -f "${DEV}/out_settings"
|
||||
|
||||
# --- curfew status and the override/force hooks -----------------------------
|
||||
|
||||
_set_now "0000"
|
||||
rm -f "${CURFEW_PIDFILE}" "${CURFEW_OVERRIDE_FILE}" "${CURFEW_FORCE_FILE}"
|
||||
|
||||
cmd_curfew_test_on >/dev/null 2>&1
|
||||
if [[ -e "${CURFEW_FORCE_FILE}" ]]; then
|
||||
_t_pass "curfew-test-on creates the force file"
|
||||
else
|
||||
_t_fail "curfew-test-on must create the force file"
|
||||
fi
|
||||
|
||||
cmd_curfew_test_off >/dev/null 2>&1
|
||||
if [[ -e "${CURFEW_FORCE_FILE}" ]]; then
|
||||
_t_fail "curfew-test-off left the force file behind"
|
||||
else
|
||||
_t_pass "curfew-test-off clears the force file"
|
||||
fi
|
||||
|
||||
# The escape hatch: this is the 2am opt-out, so it must actually take effect.
|
||||
cmd_curfew_off >/dev/null 2>&1
|
||||
if [[ -e "${CURFEW_OVERRIDE_FILE}" ]]; then
|
||||
_t_pass "curfew-off creates the override that suspends the curfew"
|
||||
else
|
||||
_t_fail "curfew-off must create the override file"
|
||||
fi
|
||||
|
||||
cmd_curfew_on >/dev/null 2>&1
|
||||
if [[ -e "${CURFEW_OVERRIDE_FILE}" ]]; then
|
||||
_t_fail "curfew-on left the override in place"
|
||||
else
|
||||
_t_pass "curfew-on clears the override and re-arms the curfew"
|
||||
fi
|
||||
|
||||
# --- tether test hooks ------------------------------------------------------
|
||||
|
||||
rm -f "${TETHER_FORCE_FILE}"
|
||||
cmd_tether_test_on >/dev/null 2>&1
|
||||
if [[ -e "${TETHER_FORCE_FILE}" ]]; then
|
||||
_t_pass "tether-test-on creates the force file"
|
||||
else
|
||||
_t_fail "tether-test-on must create the force file"
|
||||
fi
|
||||
|
||||
cmd_tether_test_off >/dev/null 2>&1
|
||||
if [[ -e "${TETHER_FORCE_FILE}" ]]; then
|
||||
_t_fail "tether-test-off left the force file behind"
|
||||
else
|
||||
_t_pass "tether-test-off clears the force file"
|
||||
fi
|
||||
|
||||
# --- usage ------------------------------------------------------------------
|
||||
|
||||
usage_text="$(usage 2>&1)"
|
||||
for sub in start stop status hosts-status dns-status launcher-status \
|
||||
workout-status curfew-status tether-status curfew-off; do
|
||||
case "${usage_text}" in
|
||||
*"${sub}"*) _t_pass "usage documents ${sub}" ;;
|
||||
*) _t_fail "usage must document ${sub}" ;;
|
||||
esac
|
||||
done
|
||||
144
phone_focus_mode/lib/tests/ctl_cases_curfew.sh
Executable file
144
phone_focus_mode/lib/tests/ctl_cases_curfew.sh
Executable file
@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/ctl_cases_curfew.sh — assertions for the pure logic in the ctl
|
||||
# libraries: the zero-stripping helper, the night-curfew window, and the
|
||||
# seven pidfile helpers.
|
||||
#
|
||||
# Sourced by test_ctl_libs.sh after ctl_libs_harness.sh, which owns the stubs,
|
||||
# the subjects and the PASS/FAIL counters this file adds to.
|
||||
set -euo pipefail
|
||||
|
||||
# --- _ctl_dec ---------------------------------------------------------------
|
||||
|
||||
# Zero-padded times must not reach shell arithmetic as octal: "0830" is not a
|
||||
# valid octal number and would abort the comparison mid-check.
|
||||
_t_eq "830" "$(_ctl_dec 0830)" "_ctl_dec strips the leading zero from 0830"
|
||||
_t_eq "500" "$(_ctl_dec 0500)" "_ctl_dec strips the leading zero from 0500"
|
||||
_t_eq "0" "$(_ctl_dec 0000)" "_ctl_dec keeps a single digit for midnight"
|
||||
_t_eq "2300" "$(_ctl_dec 2300)" "_ctl_dec leaves an unpadded time alone"
|
||||
_t_eq "9" "$(_ctl_dec 0009)" "_ctl_dec strips several leading zeros"
|
||||
|
||||
# --- ctl_is_curfew_now ------------------------------------------------------
|
||||
|
||||
# The window wraps past midnight, which a naive start<=now<end comparison gets
|
||||
# exactly backwards: it would report curfew all day and never at night.
|
||||
NIGHT_CURFEW_START="2300"
|
||||
NIGHT_CURFEW_END="0500"
|
||||
|
||||
for t in 2300 2359 0000 0430; do
|
||||
_set_now "${t}"
|
||||
if ctl_is_curfew_now; then
|
||||
_t_pass "curfew is open at ${t} inside the wrapping window"
|
||||
else
|
||||
_t_fail "curfew should be open at ${t}"
|
||||
fi
|
||||
done
|
||||
|
||||
for t in 0500 1200 2259; do
|
||||
_set_now "${t}"
|
||||
if ! ctl_is_curfew_now; then
|
||||
_t_pass "curfew is closed at ${t} outside the wrapping window"
|
||||
else
|
||||
_t_fail "curfew should be closed at ${t}"
|
||||
fi
|
||||
done
|
||||
|
||||
# A same-day window includes its start and excludes its end.
|
||||
NIGHT_CURFEW_START="0900"
|
||||
NIGHT_CURFEW_END="1700"
|
||||
|
||||
_set_now "0900"
|
||||
if ctl_is_curfew_now; then
|
||||
_t_pass "a same-day window includes its start minute"
|
||||
else
|
||||
_t_fail "0900 should be inside a 0900-1700 window"
|
||||
fi
|
||||
|
||||
_set_now "1700"
|
||||
if ! ctl_is_curfew_now; then
|
||||
_t_pass "a same-day window excludes its end minute"
|
||||
else
|
||||
_t_fail "1700 should be outside a 0900-1700 window"
|
||||
fi
|
||||
|
||||
_set_now "1200"
|
||||
if ctl_is_curfew_now; then
|
||||
_t_pass "a same-day window is open midway through"
|
||||
else
|
||||
_t_fail "1200 should be inside a 0900-1700 window"
|
||||
fi
|
||||
|
||||
NIGHT_CURFEW_START="2300"
|
||||
NIGHT_CURFEW_END="0500"
|
||||
_t_eq "2300-0500" "${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END}" \
|
||||
"the wrapping window is restored for the clock cases"
|
||||
|
||||
# A broken clock must fail OPEN: reporting permanent curfew because `date`
|
||||
# misbehaved would lock the phone down with no way to see why.
|
||||
for bad in "not-a-time" "" "12:00"; do
|
||||
_set_now "${bad}"
|
||||
if ! ctl_is_curfew_now; then
|
||||
_t_pass "a malformed clock ('${bad}') reads as no curfew"
|
||||
else
|
||||
_t_fail "a malformed clock ('${bad}') must not enable the curfew"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- the two copies must agree ---------------------------------------------
|
||||
|
||||
# daemon_location.sh carries its own is_curfew_now. The phone's daemon and its
|
||||
# control script disagreeing about whether it is night is a silent, confusing
|
||||
# failure, so the copies are compared directly rather than trusted to match.
|
||||
_curfew_table() {
|
||||
local answers="" t=""
|
||||
for t in 2259 2300 2359 0000 0430 0500 1200; do
|
||||
_set_now "${t}"
|
||||
if "$1"; then answers="${answers}1"; else answers="${answers}0"; fi
|
||||
done
|
||||
printf '%s' "${answers}"
|
||||
}
|
||||
|
||||
ctl_answers="$(_curfew_table ctl_is_curfew_now)"
|
||||
|
||||
(
|
||||
# Sourced in a subshell: daemon_location.sh defines is_curfew_now and
|
||||
# curfew_active, and only the former is wanted here.
|
||||
# shellcheck source=../../daemon_location.sh
|
||||
. "${PHONE_DIR}/daemon_location.sh"
|
||||
_curfew_table is_curfew_now
|
||||
) >"${RUN}/state/daemon_answers"
|
||||
|
||||
_t_eq "${ctl_answers}" "$(cat "${RUN}/state/daemon_answers")" \
|
||||
"ctl_is_curfew_now and daemon is_curfew_now agree on every boundary"
|
||||
# 2259 closed, 2300/2359/0000/0430 open, 0500 closed (end is exclusive),
|
||||
# 1200 closed. Spelled out so the agreement check above cannot pass by
|
||||
# both copies being wrong in the same way.
|
||||
_t_eq "0111100" "${ctl_answers}" "the shared window answers match the 2300-0500 spec"
|
||||
|
||||
# --- the seven pid helpers --------------------------------------------------
|
||||
|
||||
# One table rather than seven near-identical blocks: the duplication is in the
|
||||
# subject, and repeating it here would add nothing and trip the jscpd gate.
|
||||
_pid_case() {
|
||||
local fn="$1" var="$2"
|
||||
local file="${RUN}/state/${fn}.pid"
|
||||
# printf -v assigns to a name held in a variable without eval, which the
|
||||
# repo's shell rules forbid.
|
||||
printf -v "${var}" '%s' "${file}"
|
||||
|
||||
rm -f "${file}"
|
||||
_t_eq "" "$(${fn})" "${fn} is empty with no pidfile"
|
||||
|
||||
printf '999999\n' >"${file}"
|
||||
_t_eq "" "$(${fn})" "${fn} is empty when the recorded pid is dead"
|
||||
|
||||
printf '%s\n' "$$" >"${file}"
|
||||
_t_eq "$$" "$(${fn})" "${fn} returns a live pid"
|
||||
}
|
||||
|
||||
_pid_case hosts_enforcer_pid HOSTS_PIDFILE
|
||||
_pid_case dns_enforcer_pid DNS_PIDFILE
|
||||
_pid_case launcher_enforcer_pid LAUNCHER_PIDFILE
|
||||
_pid_case workout_detector_pid WORKOUT_PIDFILE
|
||||
_pid_case curfew_enforcer_pid CURFEW_PIDFILE
|
||||
_pid_case tether_enforcer_pid TETHER_PIDFILE
|
||||
_pid_case daemon_pid PIDFILE
|
||||
178
phone_focus_mode/lib/tests/ctl_cases_daemon.sh
Executable file
178
phone_focus_mode/lib/tests/ctl_cases_daemon.sh
Executable file
@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/ctl_cases_daemon.sh — assertions for focus_ctl.sh's own commands:
|
||||
# status, disable, recheck, log, notif-status and the whitelist iterator.
|
||||
#
|
||||
# Sourced by test_ctl_libs.sh after ctl_cases_commands.sh, which owns the
|
||||
# table helpers and the counters. Split out to stay under the 250-line cap.
|
||||
set -euo pipefail
|
||||
|
||||
# --- cmd_status --------------------------------------------------------------
|
||||
|
||||
# The location block is the only part that talks to the device; the rest is a
|
||||
# report of files the tests control.
|
||||
rm -f "${PIDFILE}"
|
||||
printf 'focus\n' >"${MODE_FILE}"
|
||||
printf 'com.a\ncom.b\n' >"${DISABLED_APPS_FILE}"
|
||||
_seed_out dumpsys 'last location=Location[fused 52.2297,21.0122 hAcc=12]'
|
||||
out="$(cmd_status 2>&1 || true)"
|
||||
|
||||
case "${out}" in
|
||||
*"Mode: focus"*) _t_pass "status reports the recorded mode" ;;
|
||||
*) _t_fail "status must report the mode from MODE_FILE" ;;
|
||||
esac
|
||||
case "${out}" in
|
||||
*"Location: 52.2297, 21.0122"*) _t_pass "status reports the current fix" ;;
|
||||
*) _t_fail "status must report the current location" ;;
|
||||
esac
|
||||
# The distance is the THIRD copy of the Haversine in this repo. Asserting only
|
||||
# that some number is printed lets a wrong Earth radius through, so the value
|
||||
# is checked: HOME_LAT/HOME_LON are Warsaw and the seeded fix is ~4.3 km away.
|
||||
case "${out}" in
|
||||
*"Distance: "*"m from home"*) _t_pass "status reports a distance from home" ;;
|
||||
*) _t_fail "status must report the distance from home" ;;
|
||||
esac
|
||||
status_dist="$(printf '%s\n' "${out}" | sed -n 's/^Distance: \([0-9]*\)m from home$/\1/p')"
|
||||
if [[ "${status_dist}" -ge 4000 && "${status_dist}" -le 4600 ]]; then
|
||||
_t_pass "status computes a correct distance (${status_dist} m)"
|
||||
else
|
||||
_t_fail "status distance should be ~4300 m, got '${status_dist}'"
|
||||
fi
|
||||
case "${out}" in
|
||||
*com.a*) _t_pass "status lists the apps focus mode disabled" ;;
|
||||
*) _t_fail "status must list the disabled apps" ;;
|
||||
esac
|
||||
|
||||
# No fix available must say so rather than print an empty distance.
|
||||
rm -f "${DEV}/out_dumpsys"
|
||||
case "$(cmd_status 2>&1 || true)" in
|
||||
*"Location: unavailable"*) _t_pass "status reports an unavailable location" ;;
|
||||
*) _t_fail "status must report when no fix is available" ;;
|
||||
esac
|
||||
|
||||
# An empty disabled list reads as a sentence, not as blank output.
|
||||
: >"${DISABLED_APPS_FILE}"
|
||||
case "$(cmd_status 2>&1 || true)" in
|
||||
*"No apps currently disabled"*) _t_pass "status states when nothing is disabled" ;;
|
||||
*) _t_fail "status must state when nothing is disabled" ;;
|
||||
esac
|
||||
|
||||
# An unset mode file is reported as unknown, not as an empty field.
|
||||
rm -f "${MODE_FILE}"
|
||||
case "$(cmd_status 2>&1 || true)" in
|
||||
*"Mode: unknown"*) _t_pass "status reports an unknown mode explicitly" ;;
|
||||
*) _t_fail "status must report an unknown mode explicitly" ;;
|
||||
esac
|
||||
|
||||
# --- cmd_disable -------------------------------------------------------------
|
||||
|
||||
# The restore pass must re-enable every recorded package and then clear the
|
||||
# record, or the next run would try to re-enable them all again.
|
||||
printf 'com.one\ncom.two\n' >"${DISABLED_APPS_FILE}"
|
||||
_reset_calls
|
||||
_run_quiet cmd_disable
|
||||
_t_eq "2" "$(_calls | grep -c '^pm enable')" "disable re-enables every recorded app"
|
||||
if [[ -s "${DISABLED_APPS_FILE}" ]]; then
|
||||
_t_fail "disable left the record of disabled apps in place"
|
||||
else
|
||||
_t_pass "disable clears the record once everything is restored"
|
||||
fi
|
||||
|
||||
: >"${DISABLED_APPS_FILE}"
|
||||
case "$(cmd_disable 2>&1 || true)" in
|
||||
*"No apps to re-enable"*) _t_pass "disable states when there is nothing to restore" ;;
|
||||
*) _t_fail "disable must state when there is nothing to restore" ;;
|
||||
esac
|
||||
|
||||
# --- cmd_recheck / cmd_log / cmd_notif_status --------------------------------
|
||||
|
||||
# A recheck with no daemon has nothing to wake, so it must refuse rather than
|
||||
# leave a trigger file that nothing will ever consume.
|
||||
rm -f "${RECHECK_TRIGGER}" "${PIDFILE}"
|
||||
case "$(cmd_recheck 2>&1 || true)" in
|
||||
*"not running"*) _t_pass "recheck refuses when the daemon is not running" ;;
|
||||
*) _t_fail "recheck must refuse when the daemon is not running" ;;
|
||||
esac
|
||||
if [[ -e "${RECHECK_TRIGGER}" ]]; then
|
||||
_t_fail "recheck left a trigger nothing will consume"
|
||||
else
|
||||
_t_pass "recheck writes no trigger when the daemon is down"
|
||||
fi
|
||||
|
||||
/usr/bin/sleep 30 &
|
||||
_recheck_victim=$!
|
||||
printf '%s\n' "${_recheck_victim}" >"${PIDFILE}"
|
||||
_run_quiet cmd_recheck
|
||||
if [[ -e "${RECHECK_TRIGGER}" ]]; then
|
||||
_t_pass "recheck touches the trigger the daemon polls for"
|
||||
else
|
||||
_t_fail "recheck must create the trigger file"
|
||||
fi
|
||||
kill "${_recheck_victim}" 2>/dev/null || true
|
||||
wait "${_recheck_victim}" 2>/dev/null || true
|
||||
rm -f "${PIDFILE}"
|
||||
|
||||
printf 'line one\nline two\n' >"${LOG_FILE}"
|
||||
case "$(cmd_log 2>&1 || true)" in
|
||||
*"line two"*) _t_pass "log shows the daemon log" ;;
|
||||
*) _t_fail "log must show the daemon log" ;;
|
||||
esac
|
||||
|
||||
printf '{"mode":"focus"}\n' >"${STATUS_FILE}"
|
||||
case "$(cmd_notif_status 2>&1 || true)" in
|
||||
*focus*) _t_pass "notif-status shows the companion snapshot" ;;
|
||||
*) _t_fail "notif-status must show the status snapshot" ;;
|
||||
esac
|
||||
|
||||
# --- iter_whitelist_packages -------------------------------------------------
|
||||
|
||||
# The list is a multi-line quoted string with comments and section headings;
|
||||
# only real package names may come out, or the sweep would try to disable a
|
||||
# heading like "---".
|
||||
_saved_whitelist="${WHITELIST}"
|
||||
# The undotted token and the comment must each be caught by their OWN filter,
|
||||
# so the fixture separates them: "# com.commented.out" is a comment whose
|
||||
# first token IS dotted and IS charset-clean, so only the comment check can
|
||||
# reject it. "not-a-package" is undotted, so only the dot check rejects it.
|
||||
# A fixture without both leaves each filter deletable with no test failing.
|
||||
WHITELIST="
|
||||
# a comment
|
||||
|
||||
# --- a section heading ---
|
||||
# com.commented.out
|
||||
com.real.one
|
||||
com.real.two
|
||||
notapackage
|
||||
com.bad!name
|
||||
com.real.three trailing prose
|
||||
"
|
||||
_t_eq "com.real.one com.real.two com.real.three" \
|
||||
"$(iter_whitelist_packages | tr '\n' ' ' | sed 's/ $//')" \
|
||||
"iter_whitelist_packages emits only dotted package names"
|
||||
|
||||
# The two rejections are asserted separately, because the combined list above
|
||||
# passes even if only one filter is doing any work. Captured ONCE here, before
|
||||
# the fixture is replaced below: re-running the function after that would
|
||||
# assert against a list holding neither an undotted token nor a comment, and
|
||||
# would pass with both filters deleted.
|
||||
whitelist_out="$(iter_whitelist_packages)"
|
||||
case "${whitelist_out}" in
|
||||
*"notapackage"*) _t_fail "an undotted token was emitted as a package" ;;
|
||||
*) _t_pass "an undotted token is rejected by the dot check" ;;
|
||||
esac
|
||||
case "${whitelist_out}" in
|
||||
*"com.bad!name"*) _t_fail "a package with illegal characters was emitted" ;;
|
||||
*) _t_pass "an illegal character is rejected by the charset check" ;;
|
||||
esac
|
||||
case "${whitelist_out}" in
|
||||
*"com.commented.out"*) _t_fail "a commented-out package name was emitted" ;;
|
||||
*) _t_pass "a comment line is rejected even when its token looks valid" ;;
|
||||
esac
|
||||
# A comment whose first token IS dotted would slip past the dot check alone,
|
||||
# so the comment filter has to be doing the work on its own.
|
||||
WHITELIST="
|
||||
# com.commented.out is deliberately disabled
|
||||
com.real.only
|
||||
"
|
||||
_t_eq "com.real.only" "$(iter_whitelist_packages | tr '\n' ' ' | sed 's/ $//')" \
|
||||
"a commented-out package name is not emitted"
|
||||
WHITELIST="${_saved_whitelist}"
|
||||
165
phone_focus_mode/lib/tests/ctl_libs_harness.sh
Executable file
165
phone_focus_mode/lib/tests/ctl_libs_harness.sh
Executable file
@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/ctl_libs_harness.sh — the fake device behind the tests for the
|
||||
# libraries split out of focus_ctl.sh.
|
||||
#
|
||||
# Sourced, not executed. The subcommands drive the phone through real binaries
|
||||
# (pm, iptables, settings, setsid, ...), so the stubs go on PATH. The real
|
||||
# config.sh supplies every default so this fixture cannot drift from the
|
||||
# values the subcommands actually read; only the paths are redirected.
|
||||
set -euo pipefail
|
||||
|
||||
# TESTS_DIR is declared readonly by the entry point that sources this file.
|
||||
readonly PHONE_DIR="${TESTS_DIR}/../.."
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
_t_pass() {
|
||||
PASS=$((PASS + 1))
|
||||
printf ' OK: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_fail() {
|
||||
FAIL=$((FAIL + 1))
|
||||
printf ' FAIL: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_eq() {
|
||||
local want="$1" got="$2" what="$3"
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (want '${want}', got '${got}')"
|
||||
fi
|
||||
}
|
||||
|
||||
RUN="$(mktemp -d)"
|
||||
trap 'rm -rf "${RUN}"' EXIT
|
||||
mkdir -p "${RUN}/bin" "${RUN}/state"
|
||||
|
||||
readonly DEV="${RUN}/state"
|
||||
|
||||
# `date` answers from a pinned clock so the window cases are deterministic
|
||||
# rather than dependent on when the suite happens to run.
|
||||
cat >"${RUN}/bin/date" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
if [[ -f "${DEV}/now" && "$1" == "+%H%M" ]]; then
|
||||
cat "${DEV}/now"
|
||||
exit 0
|
||||
fi
|
||||
exec /usr/bin/date "$@"
|
||||
STUB
|
||||
chmod +x "${RUN}/bin/date"
|
||||
|
||||
# --- device stubs -----------------------------------------------------------
|
||||
#
|
||||
# Every cmd_* subcommand drives the phone through one of these. They log each
|
||||
# call so a command's effect is assertable, and each honours a fail_<name>
|
||||
# flag so the "not running" / "chain missing" branches are reachable.
|
||||
#
|
||||
# setsid is the important one: the start commands launch a real daemon with
|
||||
# it. Stubbing it keeps the suite from spawning enforcers on this machine,
|
||||
# and lets a test decide whether the launch "worked" by seeding the pidfile.
|
||||
_mk_stub() {
|
||||
cat >"${RUN}/bin/$1" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf '%s %s\n' "$1" "\$*" >>"\${DEV}/calls.log"
|
||||
[[ -f "\${DEV}/fail_$1" ]] && exit 1
|
||||
[[ -f "\${DEV}/out_$1" ]] && cat "\${DEV}/out_$1"
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "${RUN}/bin/$1"
|
||||
}
|
||||
|
||||
for _s in pm am settings dumpsys svc sqlite3 getprop content kill sleep setsid; do
|
||||
_mk_stub "$_s"
|
||||
done
|
||||
|
||||
# iptables/ip6tables also model chain existence, so the MISSING branches and
|
||||
# the rule counts in the status commands are both reachable.
|
||||
cat >"${RUN}/bin/iptables" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
bin="$(basename "$0")"
|
||||
printf '%s %s\n' "$bin" "$*" >>"${DEV}/calls.log"
|
||||
case "$1" in
|
||||
-L) [[ -f "${DEV}/chain_${bin}" ]] && exit 0 || exit 1 ;;
|
||||
-S) [[ -f "${DEV}/chain_${bin}" ]] && cat "${DEV}/chain_${bin}" || exit 1 ;;
|
||||
esac
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "${RUN}/bin/iptables"
|
||||
cp "${RUN}/bin/iptables" "${RUN}/bin/ip6tables"
|
||||
|
||||
export DEV
|
||||
export PATH="${RUN}/bin:${PATH}"
|
||||
|
||||
_calls() { cat "${DEV}/calls.log" 2>/dev/null || printf ''; }
|
||||
_reset_calls() { : >"${DEV}/calls.log"; }
|
||||
_fail_op() { touch "${DEV}/fail_$1"; }
|
||||
_clear_fail() { rm -f "${DEV}/fail_$1"; }
|
||||
_seed_out() { printf '%s\n' "$2" >"${DEV}/out_$1"; }
|
||||
_seed_chain() { printf '%s\n' "${2:--A rule}" >"${DEV}/chain_$1"; }
|
||||
_clear_chain() { rm -f "${DEV}/chain_$1"; }
|
||||
|
||||
_set_now() { printf '%s\n' "$1" >"${DEV}/now"; }
|
||||
|
||||
# --- ambient definitions the libraries expect from focus_ctl.sh ------------
|
||||
|
||||
log() { :; }
|
||||
|
||||
# The real config.sh supplies every default, so this fixture cannot drift
|
||||
# from the values the subcommands actually read. FOCUS_MODE_SCRIPT_DIR points
|
||||
# its SCRIPT_DIR at the checkout; the paths are then redirected at a temp dir
|
||||
# so nothing here touches /data/local/tmp.
|
||||
FOCUS_MODE_SCRIPT_DIR="${PHONE_DIR}"
|
||||
export FOCUS_MODE_SCRIPT_DIR
|
||||
# shellcheck source=../../config.sh
|
||||
. "${PHONE_DIR}/config.sh"
|
||||
|
||||
STATE_DIR="${RUN}/state"
|
||||
LOG_FILE="${STATE_DIR}/focus.log"
|
||||
MODE_FILE="${STATE_DIR}/mode"
|
||||
STATUS_FILE="${STATE_DIR}/status.json"
|
||||
DISABLED_APPS_FILE="${STATE_DIR}/disabled.txt"
|
||||
DISABLED_COMPETITORS_FILE="${STATE_DIR}/competitors.txt"
|
||||
RECHECK_TRIGGER="${STATE_DIR}/trigger_recheck"
|
||||
CURFEW_OVERRIDE_FILE="${STATE_DIR}/curfew_override"
|
||||
CURFEW_FORCE_FILE="${STATE_DIR}/curfew_force"
|
||||
CURFEW_ENFORCER_STATE="${STATE_DIR}/curfew_applied"
|
||||
CURFEW_ENFORCER_LOG="${STATE_DIR}/curfew.log"
|
||||
TETHER_FORCE_FILE="${STATE_DIR}/tether_force"
|
||||
TETHER_OVERRIDE_FILE="${STATE_DIR}/tether_override"
|
||||
TETHER_ENFORCER_STATE="${STATE_DIR}/tether_applied"
|
||||
TETHER_LOG="${STATE_DIR}/tether.log"
|
||||
HOSTS_LOG="${STATE_DIR}/hosts.log"
|
||||
HOSTS_CANONICAL="${STATE_DIR}/hosts.canonical"
|
||||
HOSTS_SHA_FILE="${STATE_DIR}/hosts.sha256"
|
||||
HOSTS_TARGET="${STATE_DIR}/hosts_target"
|
||||
DNS_LOG="${STATE_DIR}/dns.log"
|
||||
LAUNCHER_LOG="${STATE_DIR}/launcher.log"
|
||||
LAUNCHER_ACTIVITY_FILE="${STATE_DIR}/launcher.activity"
|
||||
LAUNCHER_APK="${STATE_DIR}/launcher.apk"
|
||||
LAUNCHER_SHA_FILE="${STATE_DIR}/launcher.sha256"
|
||||
WORKOUT_DETECTOR_LOG="${STATE_DIR}/workout.log"
|
||||
WORKOUT_ACTIVE_FILE="${STATE_DIR}/workout_active"
|
||||
WORKOUT_DB_PATH="${STATE_DIR}/stronglifts.db"
|
||||
WORKOUT_SQLITE3_BIN="${RUN}/bin/sqlite3"
|
||||
NIGHT_CURFEW_START="2300"
|
||||
NIGHT_CURFEW_END="0500"
|
||||
|
||||
# shellcheck source=../../ctl_curfew.sh
|
||||
. "${PHONE_DIR}/ctl_curfew.sh"
|
||||
# shellcheck source=../../ctl_hosts.sh
|
||||
. "${PHONE_DIR}/ctl_hosts.sh"
|
||||
# shellcheck source=../../ctl_dns.sh
|
||||
. "${PHONE_DIR}/ctl_dns.sh"
|
||||
# shellcheck source=../../ctl_launcher.sh
|
||||
. "${PHONE_DIR}/ctl_launcher.sh"
|
||||
# shellcheck source=../../ctl_workout.sh
|
||||
. "${PHONE_DIR}/ctl_workout.sh"
|
||||
# shellcheck source=../../ctl_tether.sh
|
||||
. "${PHONE_DIR}/ctl_tether.sh"
|
||||
# shellcheck source=../../ctl_daemon.sh
|
||||
. "${PHONE_DIR}/ctl_daemon.sh"
|
||||
# shellcheck source=../../ctl_usage.sh
|
||||
. "${PHONE_DIR}/ctl_usage.sh"
|
||||
22
phone_focus_mode/lib/tests/test_ctl_libs.sh
Executable file
22
phone_focus_mode/lib/tests/test_ctl_libs.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unit tests for the libraries split out of focus_ctl.sh, against the fake
|
||||
# device in ctl_libs_harness.sh. No real phone is needed.
|
||||
#
|
||||
# This is the only entry point; the cases live in sourced files purely to stay
|
||||
# under the 250-line cap, so one coverage command measures every subject.
|
||||
set -euo pipefail
|
||||
|
||||
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly TESTS_DIR
|
||||
|
||||
# shellcheck source=ctl_libs_harness.sh
|
||||
. "${TESTS_DIR}/ctl_libs_harness.sh"
|
||||
# shellcheck source=ctl_cases_curfew.sh
|
||||
. "${TESTS_DIR}/ctl_cases_curfew.sh"
|
||||
# shellcheck source=ctl_cases_commands.sh
|
||||
. "${TESTS_DIR}/ctl_cases_commands.sh"
|
||||
# shellcheck source=ctl_cases_daemon.sh
|
||||
. "${TESTS_DIR}/ctl_cases_daemon.sh"
|
||||
|
||||
printf '\nResults: %d passed, %d failed\n' "${PASS}" "${FAIL}"
|
||||
[[ "${FAIL}" -eq 0 ]]
|
||||
Loading…
Reference in New Issue
Block a user