mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
block_compulsive_opening: split into lib/ under the 250-line cap
705 -> 182 lines, with state, the launch gate, install/uninstall and reporting moved into cco_state.sh, cco_wrapper.sh, cco_install.sh and cco_report.sh. The entry script keeps its exact deployed path and stays a real file there, not a symlink: the app wrappers already installed on disk hard-code /usr/local/bin/block-compulsive-opening.sh, and the pacman hook invokes it. _CCO_LIB_DIR probes for lib/ beside the script and falls back to the script's own directory, so the repo tree and the flat /usr/local/bin deploy layout both work; both branches were exercised. install_all deploys the libs and uninstall_all removes them in this same commit, so there is never a deployed copy that is split but missing its libs -- the failure that had to be repaired for install_leechblock. The libs deploy flat into /usr/local/bin rather than into /usr/local/share/digital_wellbeing/lib, because install_leechblock_payload does an rm -rf mirror of that directory and would delete them. Verified beyond the usual gates: a line-multiset check reports zero missing functional lines, and all three heredoc bodies -- the generated app wrapper, the generated pacman hook and the help text -- are byte-identical, which verify_shell_split.sh cannot check. status output matches the still-monolithic deployed copy byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
baeb1119d0
commit
f5edebae4a
@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Split block_compulsive_opening.sh under the 250-line cap",
|
||||
"objective": "Bring linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh (705 lines) under the repo-wide 250-line file cap by moving its state, wrapper, install and reporting concerns into four sourced libs, preserving behaviour exactly and keeping the deployed /usr/local/bin entry path intact so already-installed app wrappers and the pacman hook continue to work.",
|
||||
"acceptance_criteria": [
|
||||
"The entry script and all four lib/cco_*.sh files are each at most 250 lines.",
|
||||
"A line-multiset check against the pre-split file reports zero missing functional lines.",
|
||||
"All three heredoc bodies -- the generated app wrapper, the generated pacman hook, and the help text -- are byte-identical to the pre-split versions, since they define deployed artifacts that verify_shell_split.sh cannot inspect.",
|
||||
"status output is byte-identical to the still-monolithic deployed copy, and both exit 0.",
|
||||
"The script runs correctly in BOTH layouts: lib/ beside the script (repo) and libs flat beside the script (deployed), and fails closed with exit 1 when a lib is missing.",
|
||||
"install_all deploys the libs and uninstall_all removes them, in this same commit, so the deployed copy is never split-without-libs.",
|
||||
"shellcheck passes without -x and shfmt -w is a no-op on all five files, with no suppression added."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Shell test coverage for the new libs -- no suite exists for this script; the split is verified behaviourally and textually.",
|
||||
"Changing the deployed entry path or converting it to a symlink -- installed app wrappers hard-code /usr/local/bin/block-compulsive-opening.sh.",
|
||||
"Deploying the libs under /usr/local/share/digital_wellbeing/lib, which install_leechblock_payload mirrors with rm -rf.",
|
||||
"Any change to the app timeout policy, the hour-bucket semantics, or the managed app list."
|
||||
],
|
||||
"verifier": "bash meta/scripts/check_file_length.sh --all for the cap; a python3 line-multiset diff and per-heredoc body diffs against HEAD for content equivalence; status output compared against the pre-split deployed copy; a flat-layout run plus a deleted-lib run for the two _CCO_LIB_DIR branches and the fail-closed guard; shellcheck, shfmt idempotence and bash -n on all five files; pre-commit run --files on the staged set."
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
{
|
||||
"intent": "Bring block_compulsive_opening.sh (705 lines) under the 250-line cap by moving its state, wrapper, install and reporting logic into four sourced libs, without changing behaviour and without breaking the already-installed app wrappers that hard-code the deployed script's path.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh: 705 -> 182 lines",
|
||||
"New: lib/cco_install.sh (238), lib/cco_report.sh (102), lib/cco_state.sh (149), lib/cco_wrapper.sh (120)",
|
||||
"install_all deploys the libs in the same commit, so there is no window where the deployed copy is split but its libs are missing",
|
||||
"Non-goal: shell coverage for the new libs; no test suite exists for this script"
|
||||
],
|
||||
"changes": [
|
||||
"Split into four libs by concern: cco_state.sh (hour-bucket state files), cco_wrapper.sh (the launch gate), cco_install.sh (install/uninstall, the generated app wrapper and the pacman hook), cco_report.sh (status and help).",
|
||||
"The entry script keeps its exact deployed path, /usr/local/bin/block-compulsive-opening.sh, and stays a real file there rather than a symlink. Already-installed app wrappers on disk hard-code that path, and the pacman hook at 96-restore-pkg-wrappers.hook invokes it; a symlink would also break dirname \"${BASH_SOURCE[0]}\" resolution.",
|
||||
"_CCO_LIB_DIR probes for lib/ beside the script (repo tree) and falls back to the script's own directory (the flat /usr/local/bin deploy layout). Both branches are exercised below.",
|
||||
"install_all copies the four libs flat into /usr/local/bin alongside the entry script, and uninstall_all removes them so no orphans are stranded. The copy runs even when the script is already at install_path, so a lib changed upstream still reaches the deployed copy that `rewrap-quiet` executes.",
|
||||
"A guard before the source lines fails closed with an explicit message when any lib is unreadable, rather than dying on a bare `source` error.",
|
||||
"Ran shfmt -w on the entry script, which removed a block of ~26 consecutive blank lines. No associative-array keys were affected: unlike install_leechblock.sh, this file's hyphenated keys (\"signal-desktop\") were already quoted, so the shfmt arithmetic-parsing bug does not apply here. Verified by grep after the run."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash meta/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "Over-cap list went from 4 files to 3; block_compulsive_opening.sh no longer listed. All five files 182/238/149/120/102 lines, under 250."
|
||||
},
|
||||
{
|
||||
"command": "python3 line-multiset check of entry+4 libs against HEAD's block_compulsive_opening.sh",
|
||||
"result": "pass",
|
||||
"evidence": "MISSING functional lines: 0. Every non-comment, non-blank line from the pre-split file is accounted for across the five files."
|
||||
},
|
||||
{
|
||||
"command": "Heredoc body extraction and diff for all three heredocs",
|
||||
"result": "pass",
|
||||
"evidence": "This is the check verify_shell_split.sh cannot perform, because it cannot see inside a heredoc body. WRAPPER_EOF (5 lines, the generated app wrapper), HOOK_EOF (14 lines, the generated pacman hook) and EOF (26 lines, the help text) are each byte-identical between HEAD and the split. The two deployed artifacts this script generates are therefore provably unchanged."
|
||||
},
|
||||
{
|
||||
"command": "bash block_compulsive_opening.sh status vs /usr/local/bin/block-compulsive-opening.sh status",
|
||||
"result": "pass",
|
||||
"evidence": "Byte-identical output, both exit 0. The deployed copy is still the pre-split monolith and sha256-identical to HEAD, so this compares the split against the real pre-split behaviour rather than against itself. `help` differs only in the $0 invocation path, as expected."
|
||||
},
|
||||
{
|
||||
"command": "Flat-layout test: entry script + libs copied into one directory (the deployed shape)",
|
||||
"result": "pass",
|
||||
"evidence": "status runs correctly with exit 0, confirming the _CCO_LIB_DIR fallback branch works in the /usr/local/bin layout, not just the repo tree."
|
||||
},
|
||||
{
|
||||
"command": "Missing-lib guard: delete cco_report.sh from the flat layout and re-run",
|
||||
"result": "pass",
|
||||
"evidence": "Fails closed with 'Error: missing library cco_report.sh in <dir>' and exit code 1 (measured directly, not through a pipeline). A partially-deployed install cannot look successful to the pacman hook."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck (no -x) + shfmt idempotence + bash -n on all five files",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck rc=0 with no suppressions; shfmt -w is a no-op on all five, so the pre-commit auto-format-and-re-stage cannot alter them; all five parse."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The deployed copy at /usr/local/bin/block-compulsive-opening.sh is still the pre-split monolith and keeps working untouched; nothing breaks until someone runs `sudo block-compulsive-opening.sh install`, which now deploys the libs too. Verified that the pacman hook's `rewrap-quiet` path does not call install_all, so the hook cannot trigger a partial deploy.",
|
||||
"The libs deploy flat into /usr/local/bin rather than into /usr/local/share/digital_wellbeing/lib. That is deliberate: install_leechblock_payload does an `rm -rf` mirror of the latter, which would delete these libs on the next pacman-wrapper install.",
|
||||
"No test suite exists for this script, so coverage is 0% and the verification above is behavioural and textual rather than test-driven."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit; block_compulsive_opening.sh returns to a single 705-line file and lib/cco_*.sh disappear.",
|
||||
"If the split had already been deployed, re-run `sudo block-compulsive-opening.sh install` from the reverted tree to restore the monolith and remove the deployed libs.",
|
||||
"Validate with: block-compulsive-opening.sh status (expect exit 0 and the per-app status table)."
|
||||
]
|
||||
}
|
||||
@ -9,6 +9,48 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# The phases live in four libs. They are found in TWO layouts, because this
|
||||
# script runs from both:
|
||||
#
|
||||
# repo: <here>/lib/cco_*.sh
|
||||
# deployed: /usr/local/bin/cco_*.sh (flat, beside the entry script)
|
||||
#
|
||||
# install_all copies the libs flat next to the installed entry script rather
|
||||
# than creating /usr/local/bin/lib, matching how pacman_lock_lib.sh and
|
||||
# heavy_job_lock.sh are already deployed there.
|
||||
#
|
||||
# readlink -f because /usr/bin/<app> wrappers exec this through its absolute
|
||||
# path and the installed copy may itself be reached via a symlink.
|
||||
#
|
||||
# This fails CLOSED: a missing lib aborts. The pacman fail-open pattern is
|
||||
# wrong here — a half-loaded blocker that silently stops blocking is the exact
|
||||
# failure this tool exists to prevent, and unlike pacman you do not need this
|
||||
# script to repair it.
|
||||
_CCO_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||
if [[ -d "$_CCO_DIR/lib" ]]; then
|
||||
_CCO_LIB_DIR="$_CCO_DIR/lib"
|
||||
else
|
||||
_CCO_LIB_DIR="$_CCO_DIR"
|
||||
fi
|
||||
|
||||
for _cco_lib in cco_state cco_wrapper cco_install cco_report; do
|
||||
if [[ ! -r "$_CCO_LIB_DIR/${_cco_lib}.sh" ]]; then
|
||||
echo "Error: missing library ${_cco_lib}.sh in $_CCO_LIB_DIR" >&2
|
||||
echo "The installation is incomplete; re-run: sudo $0 install" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
unset _cco_lib
|
||||
|
||||
# shellcheck source=lib/cco_state.sh
|
||||
. "$_CCO_LIB_DIR/cco_state.sh"
|
||||
# shellcheck source=lib/cco_wrapper.sh
|
||||
. "$_CCO_LIB_DIR/cco_wrapper.sh"
|
||||
# shellcheck source=lib/cco_install.sh
|
||||
. "$_CCO_LIB_DIR/cco_install.sh"
|
||||
# shellcheck source=lib/cco_report.sh
|
||||
. "$_CCO_LIB_DIR/cco_report.sh"
|
||||
|
||||
# Send desktop notification (inlined from common.sh to avoid dependency issues
|
||||
# when script is installed to /usr/local/bin)
|
||||
notify() {
|
||||
@ -79,571 +121,6 @@ declare -A PROCESS_MATCH=(
|
||||
["discord"]="$_cco_home/.config/discord/"
|
||||
)
|
||||
|
||||
# Ensure state directory exists
|
||||
ensure_state_dir() {
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Log message with timestamp
|
||||
log_message() {
|
||||
local msg
|
||||
msg="$(printf '%(%Y-%m-%d %H:%M:%S)T' -1) - $1"
|
||||
echo "$msg" >&2
|
||||
echo "$msg" >>"$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get current hour key (YYYY-MM-DD-HH format)
|
||||
get_hour_key() {
|
||||
printf '%(%Y-%m-%d-%H)T' -1
|
||||
}
|
||||
|
||||
# Get state file path for an app
|
||||
get_state_file() {
|
||||
local app="$1"
|
||||
echo "$STATE_DIR/${app}.lastopen"
|
||||
}
|
||||
|
||||
# Check if app was already opened this hour
|
||||
was_opened_this_hour() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
return 0 # Was opened this hour
|
||||
fi
|
||||
fi
|
||||
return 1 # Not opened this hour
|
||||
}
|
||||
|
||||
# Record app opening
|
||||
record_opening() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
echo "$current_hour" >"$state_file"
|
||||
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
|
||||
}
|
||||
|
||||
# Block app and notify
|
||||
block_app() {
|
||||
local app="$1"
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)"
|
||||
|
||||
# Send notification using common library
|
||||
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
|
||||
}
|
||||
|
||||
# Get real binary path for an app
|
||||
get_real_binary() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
|
||||
# Check if wrapper is installed (original moved to .orig)
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
# Wrapper installed, return the actual executable
|
||||
echo "$real_binary"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get running state file path for an app (tracks PID and start time)
|
||||
get_running_file() {
|
||||
local app="$1"
|
||||
echo "$STATE_DIR/${app}.running"
|
||||
}
|
||||
|
||||
# Clean up stale running state (process no longer running)
|
||||
# Uses process-name matching so Electron apps that fork don't appear stale.
|
||||
cleanup_stale_running_state() {
|
||||
local app="$1"
|
||||
local running_file
|
||||
running_file=$(get_running_file "$app")
|
||||
|
||||
if [[ ! -f $running_file ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Match on PROCESS_MATCH when the exec target is a launcher that exec()s away
|
||||
# (see the PROCESS_MATCH map); otherwise the exec target is the process.
|
||||
local real_binary="${PROCESS_MATCH[$app]:-${REAL_BINARIES[$app]}}"
|
||||
|
||||
# Check if any process matching the real binary is still running
|
||||
if ! is_app_running "$real_binary"; then
|
||||
log_message "CLEANUP: Stale running state for $app (no matching processes found)"
|
||||
rm -f "$running_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if app is running by process name (handles Electron apps that fork)
|
||||
is_app_running() {
|
||||
local real_binary="$1"
|
||||
pgrep -f "$real_binary" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Kill all processes matching the real binary path
|
||||
kill_app() {
|
||||
local real_binary="$1"
|
||||
pkill -f "$real_binary" 2>/dev/null || true
|
||||
sleep 2
|
||||
pkill -9 -f "$real_binary" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Check if auto-close is suspended for an app today
|
||||
is_autoclose_suspended() {
|
||||
local app="$1"
|
||||
local today
|
||||
today=$(printf '%(%Y-%m-%d)T' -1)
|
||||
local suspend_file="$STATE_DIR/${app}.suspend-autoclose"
|
||||
|
||||
if [[ -f $suspend_file ]]; then
|
||||
local suspend_date
|
||||
suspend_date=$(cat "$suspend_file" 2>/dev/null || echo "")
|
||||
if [[ $suspend_date == "$today" ]]; then
|
||||
return 0 # Suspended for today
|
||||
else
|
||||
# Stale suspend file, clean up
|
||||
rm -f "$suspend_file"
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Launch app with auto-close timer
|
||||
launch_with_timer() {
|
||||
local app="$1"
|
||||
local real_binary="$2"
|
||||
shift 2
|
||||
|
||||
# $real_binary is what we EXEC; $proc_match is what we look for in the process
|
||||
# table. They are usually identical, but NOT when the exec target is a launcher
|
||||
# that exec()s away: discord's /usr/bin/discord.orig replaces itself with
|
||||
# ~/.config/discord/<app>/Discord, so matching/killing on the launcher path
|
||||
# would find nothing — the auto-close daemon would think the app exited
|
||||
# instantly and the time limit would silently never fire.
|
||||
local proc_match="${PROCESS_MATCH[$app]:-$real_binary}"
|
||||
|
||||
# Check if auto-close is suspended for today
|
||||
if is_autoclose_suspended "$app"; then
|
||||
log_message "LAUNCHED: $app (auto-close suspended for today)"
|
||||
exec "$real_binary" "$@"
|
||||
fi
|
||||
|
||||
# Use per-app timeout if set, otherwise fall back to global default
|
||||
local timeout_minutes="${APP_TIMEOUT_MINUTES[$app]:-$AUTO_CLOSE_TIMEOUT_MINUTES}"
|
||||
local warning_seconds=$(((timeout_minutes - AUTO_CLOSE_WARNING_MINUTES) * 60))
|
||||
local running_file
|
||||
running_file=$(get_running_file "$app")
|
||||
|
||||
# Launch the app in background
|
||||
"$real_binary" "$@" &
|
||||
local app_pid=$!
|
||||
|
||||
# Give Electron apps time to fork before we start polling
|
||||
sleep 2
|
||||
|
||||
# Record state (FORK-FREE: use printf %s for timestamp)
|
||||
echo "$app_pid $(printf '%(%s)T' -1)" >"$running_file"
|
||||
log_message "LAUNCHED: $app with PID $app_pid (auto-close in ${timeout_minutes}m)"
|
||||
|
||||
# Spawn the auto-close daemon in a completely detached subshell
|
||||
# Uses process-name matching so it works for Electron apps that fork on launch
|
||||
(
|
||||
# Detach from terminal
|
||||
exec </dev/null >/dev/null 2>&1
|
||||
|
||||
# Wait for warning time
|
||||
sleep "$warning_seconds"
|
||||
|
||||
# Check if still running before warning (by process name, not PID)
|
||||
if is_app_running "$proc_match"; then
|
||||
# Send warning notification
|
||||
notify-send -u critical -t 30000 "⏰ $app Closing Soon" \
|
||||
"Session will end in ${AUTO_CLOSE_WARNING_MINUTES} minutes. Save your work!" 2>/dev/null || true
|
||||
else
|
||||
# Process already exited
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait remaining time
|
||||
sleep $((AUTO_CLOSE_WARNING_MINUTES * 60))
|
||||
|
||||
# Check if still running (by process name)
|
||||
if is_app_running "$proc_match"; then
|
||||
# Send final notification
|
||||
notify-send -u critical -t 5000 "🚫 $app Session Ended" \
|
||||
"Time's up! Closing $app now." 2>/dev/null || true
|
||||
|
||||
# Kill all matching processes (handles forked Electron children)
|
||||
kill_app "$proc_match"
|
||||
|
||||
printf '%(%Y-%m-%d %H:%M:%S)T - AUTO-CLOSED: %s after %dm\n' -1 "$app" "${timeout_minutes}" >>"$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
) &
|
||||
disown
|
||||
|
||||
# Wait for the app to exit by polling process name.
|
||||
# Electron apps fork immediately so waiting on $app_pid would return too soon.
|
||||
while is_app_running "$proc_match"; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Clean up running state
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
|
||||
log_message "EXITED: $app"
|
||||
}
|
||||
|
||||
# Main wrapper function - called when wrapping app launches
|
||||
wrapper_main() {
|
||||
local app="$1"
|
||||
shift
|
||||
|
||||
ensure_state_dir
|
||||
|
||||
local real_binary
|
||||
if ! real_binary=$(get_real_binary "$app"); then
|
||||
log_message "ERROR: Real binary not found for $app"
|
||||
echo "Error: Real binary for $app not found. Was the installer run?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up stale running state from previous crashes
|
||||
cleanup_stale_running_state "$app"
|
||||
|
||||
if was_opened_this_hour "$app"; then
|
||||
block_app "$app"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
record_opening "$app"
|
||||
|
||||
# Launch with auto-close timer (replaces direct exec)
|
||||
launch_with_timer "$app" "$real_binary" "$@"
|
||||
}
|
||||
|
||||
# Install wrapper for a specific app
|
||||
install_wrapper() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
|
||||
# Check if already wrapped: .orig must exist AND current file must be our wrapper
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
if grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then
|
||||
echo " ✓ $app already wrapped"
|
||||
return 0
|
||||
else
|
||||
# .orig exists but wrapper was overwritten (e.g. by package update)
|
||||
echo " ↻ $app wrapper was overwritten, re-installing..."
|
||||
rm -f "${wrapper_path}.orig"
|
||||
# Fall through to re-install
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if wrapper location exists (file or symlink)
|
||||
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
|
||||
echo " ⚠ $app not installed ($wrapper_path not found)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if real binary exists.
|
||||
# Special case: when the exec target IS this wrapper's own backup
|
||||
# ("${wrapper_path}.orig") the app has no separate system binary — the
|
||||
# launcher itself is the real thing, and the wrap below CREATES the .orig by
|
||||
# moving it aside. Requiring it to exist beforehand is a chicken-and-egg that
|
||||
# could never pass. The wrapper_path check above already proved it installed.
|
||||
if [[ $real_binary != "${wrapper_path}.orig" && ! -x $real_binary ]]; then
|
||||
echo " ⚠ $app real binary not found ($real_binary)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Installing wrapper for $app..."
|
||||
|
||||
# Handle symlinks: save the symlink itself, not the target
|
||||
if [[ -L $wrapper_path ]]; then
|
||||
local link_target
|
||||
link_target=$(readlink "$wrapper_path")
|
||||
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
|
||||
# Remove symlink and create .orig that stores the link target info
|
||||
echo "SYMLINK:$link_target" >"${wrapper_path}.orig"
|
||||
rm "$wrapper_path"
|
||||
else
|
||||
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
|
||||
mv "$wrapper_path" "${wrapper_path}.orig"
|
||||
fi
|
||||
|
||||
echo " Creating wrapper at $wrapper_path"
|
||||
cat >"$wrapper_path" <<WRAPPER_EOF
|
||||
#!/bin/bash
|
||||
# Auto-generated wrapper for $app - blocks compulsive opening
|
||||
# Real binary: $real_binary
|
||||
# Original script: ${wrapper_path}.orig
|
||||
exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
chmod +x "$wrapper_path"
|
||||
echo " ✓ $app wrapper installed"
|
||||
}
|
||||
|
||||
# Uninstall wrapper for a specific app
|
||||
uninstall_wrapper() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
|
||||
if [[ ! -f "${wrapper_path}.orig" ]]; then
|
||||
echo " ⚠ $app wrapper not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Removing wrapper for $app..."
|
||||
rm -f "$wrapper_path"
|
||||
|
||||
# Check if it was a symlink (stored as SYMLINK:target in .orig)
|
||||
local orig_content
|
||||
orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "")
|
||||
if [[ $orig_content == SYMLINK:* ]]; then
|
||||
local link_target="${orig_content#SYMLINK:}"
|
||||
echo " Restoring symlink $wrapper_path -> $link_target"
|
||||
ln -s "$link_target" "$wrapper_path"
|
||||
rm "${wrapper_path}.orig"
|
||||
else
|
||||
echo " Restoring original file"
|
||||
mv "${wrapper_path}.orig" "$wrapper_path"
|
||||
fi
|
||||
echo " ✓ $app restored"
|
||||
}
|
||||
|
||||
# Install all wrappers
|
||||
install_all() {
|
||||
echo "Installing compulsive opening blockers..."
|
||||
echo ""
|
||||
|
||||
# Install main script to /usr/local/bin
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
local install_path="/usr/local/bin/block-compulsive-opening.sh"
|
||||
|
||||
if [[ $script_path != "$install_path" ]]; then
|
||||
echo "Installing main script to $install_path..."
|
||||
cp "$script_path" "$install_path"
|
||||
chmod +x "$install_path"
|
||||
echo "✓ Main script installed"
|
||||
else
|
||||
echo "Main script already at $install_path"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Install wrappers for each app
|
||||
local installed=0
|
||||
for app in "${!APPS[@]}"; do
|
||||
if install_wrapper "$app"; then
|
||||
((installed++)) || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Installation complete. $installed app(s) wrapped."
|
||||
echo ""
|
||||
echo "Each app can now only be opened once per hour."
|
||||
echo "State files stored in: $STATE_DIR"
|
||||
echo "Logs stored in: $LOG_FILE"
|
||||
|
||||
# Install pacman hook to re-wrap after package updates
|
||||
install_pacman_hook
|
||||
}
|
||||
|
||||
# Install pacman hook to re-install wrappers after package updates
|
||||
install_pacman_hook() {
|
||||
local hook_dir="/etc/pacman.d/hooks"
|
||||
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
|
||||
|
||||
echo ""
|
||||
echo "Installing pacman hook..."
|
||||
|
||||
mkdir -p "$hook_dir"
|
||||
|
||||
cat >"$hook_file" <<'HOOK_EOF'
|
||||
[Trigger]
|
||||
Operation = Upgrade
|
||||
Operation = Install
|
||||
Type = Package
|
||||
Target = beeper
|
||||
Target = signal-desktop
|
||||
Target = discord
|
||||
|
||||
[Action]
|
||||
Description = Re-installing compulsive opening blockers after package update
|
||||
When = PostTransaction
|
||||
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
|
||||
HOOK_EOF
|
||||
|
||||
chmod 644 "$hook_file"
|
||||
echo "✓ Pacman hook installed: $hook_file"
|
||||
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
|
||||
}
|
||||
|
||||
# Uninstall pacman hook
|
||||
uninstall_pacman_hook() {
|
||||
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
|
||||
if [[ -f $hook_file ]]; then
|
||||
rm -f "$hook_file"
|
||||
echo "✓ Pacman hook removed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Quietly re-wrap apps (for pacman hook - no interactive output)
|
||||
rewrap_quiet() {
|
||||
log_message "REWRAP: Pacman hook triggered, re-installing wrappers"
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
|
||||
# Re-wrap if wrapper is missing or was overwritten by a package update
|
||||
if [[ ! -f "${wrapper_path}.orig" ]] || ! grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then
|
||||
log_message "REWRAP: $app wrapper missing or overwritten, re-installing"
|
||||
rm -f "${wrapper_path}.orig"
|
||||
install_wrapper "$app" >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
log_message "REWRAP: Complete"
|
||||
}
|
||||
|
||||
# Uninstall all wrappers
|
||||
uninstall_all() {
|
||||
echo "Removing compulsive opening blockers..."
|
||||
echo ""
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
uninstall_wrapper "$app" || true
|
||||
done
|
||||
|
||||
rm -f "/usr/local/bin/block-compulsive-opening.sh"
|
||||
|
||||
# Remove pacman hook
|
||||
uninstall_pacman_hook
|
||||
|
||||
echo ""
|
||||
echo "Uninstallation complete."
|
||||
}
|
||||
|
||||
# Show status of all apps
|
||||
show_status() {
|
||||
ensure_state_dir
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
echo "Compulsive Opening Blocker Status"
|
||||
echo "=================================="
|
||||
echo "Current hour: $current_hour"
|
||||
echo ""
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local status="not opened this hour"
|
||||
local icon="○"
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
status="already opened (blocked until next hour)"
|
||||
icon="●"
|
||||
else
|
||||
status="last opened: $last_hour"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if wrapped
|
||||
local wrapped="not installed"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
wrapped="wrapped"
|
||||
elif [[ -f $wrapper_path ]]; then
|
||||
wrapped="installed (not wrapped)"
|
||||
fi
|
||||
|
||||
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "State directory: $STATE_DIR"
|
||||
}
|
||||
|
||||
# Reset state for an app (allow opening again)
|
||||
reset_app() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
rm -f "$state_file"
|
||||
echo "Reset $app - can be opened again this hour"
|
||||
log_message "RESET: $app state cleared by user"
|
||||
else
|
||||
echo "$app was not marked as opened"
|
||||
fi
|
||||
}
|
||||
|
||||
# Clear all state
|
||||
reset_all() {
|
||||
ensure_state_dir
|
||||
rm -f "$STATE_DIR"/*.lastopen
|
||||
echo "All apps reset - can be opened again this hour"
|
||||
log_message "RESET: All app states cleared by user"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
cat <<EOF
|
||||
Block Compulsive Opening Script
|
||||
================================
|
||||
|
||||
Limits messaging apps to one launch per hour to reduce compulsive checking.
|
||||
|
||||
Usage: $0 [command] [args]
|
||||
|
||||
Commands:
|
||||
install - Install wrappers for all apps (requires root)
|
||||
uninstall - Remove all wrappers (requires root)
|
||||
status - Show current status of all apps
|
||||
reset <app> - Reset an app to allow opening again this hour
|
||||
reset-all - Reset all apps
|
||||
wrapper <app> [args] - Run as wrapper for an app (internal use)
|
||||
help - Show this help message
|
||||
|
||||
Managed Apps:
|
||||
beeper - Beeper messaging client
|
||||
signal-desktop - Signal messenger
|
||||
discord - Discord chat
|
||||
|
||||
Examples:
|
||||
sudo $0 install # Install all wrappers
|
||||
$0 status # Check which apps were opened this hour
|
||||
$0 reset discord # Allow Discord to be opened again
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main entry point
|
||||
main() {
|
||||
case "${1:-help}" in
|
||||
|
||||
@ -0,0 +1,238 @@
|
||||
#!/bin/bash
|
||||
# Install/uninstall of the per-app wrappers, the flat lib deployment and the
|
||||
# pacman rewrap hook for block_compulsive_opening.sh.
|
||||
# Sourced by the entry script; inherits its strict mode and globals.
|
||||
|
||||
# Install wrapper for a specific app
|
||||
install_wrapper() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
|
||||
# Check if already wrapped: .orig must exist AND current file must be our wrapper
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
if grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then
|
||||
echo " ✓ $app already wrapped"
|
||||
return 0
|
||||
else
|
||||
# .orig exists but wrapper was overwritten (e.g. by package update)
|
||||
echo " ↻ $app wrapper was overwritten, re-installing..."
|
||||
rm -f "${wrapper_path}.orig"
|
||||
# Fall through to re-install
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if wrapper location exists (file or symlink)
|
||||
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
|
||||
echo " ⚠ $app not installed ($wrapper_path not found)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if real binary exists.
|
||||
# Special case: when the exec target IS this wrapper's own backup
|
||||
# ("${wrapper_path}.orig") the app has no separate system binary — the
|
||||
# launcher itself is the real thing, and the wrap below CREATES the .orig by
|
||||
# moving it aside. Requiring it to exist beforehand is a chicken-and-egg that
|
||||
# could never pass. The wrapper_path check above already proved it installed.
|
||||
if [[ $real_binary != "${wrapper_path}.orig" && ! -x $real_binary ]]; then
|
||||
echo " ⚠ $app real binary not found ($real_binary)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Installing wrapper for $app..."
|
||||
|
||||
# Handle symlinks: save the symlink itself, not the target
|
||||
if [[ -L $wrapper_path ]]; then
|
||||
local link_target
|
||||
link_target=$(readlink "$wrapper_path")
|
||||
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
|
||||
# Remove symlink and create .orig that stores the link target info
|
||||
echo "SYMLINK:$link_target" >"${wrapper_path}.orig"
|
||||
rm "$wrapper_path"
|
||||
else
|
||||
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
|
||||
mv "$wrapper_path" "${wrapper_path}.orig"
|
||||
fi
|
||||
|
||||
echo " Creating wrapper at $wrapper_path"
|
||||
cat >"$wrapper_path" <<WRAPPER_EOF
|
||||
#!/bin/bash
|
||||
# Auto-generated wrapper for $app - blocks compulsive opening
|
||||
# Real binary: $real_binary
|
||||
# Original script: ${wrapper_path}.orig
|
||||
exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
chmod +x "$wrapper_path"
|
||||
echo " ✓ $app wrapper installed"
|
||||
}
|
||||
|
||||
# Uninstall wrapper for a specific app
|
||||
uninstall_wrapper() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
|
||||
if [[ ! -f "${wrapper_path}.orig" ]]; then
|
||||
echo " ⚠ $app wrapper not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Removing wrapper for $app..."
|
||||
rm -f "$wrapper_path"
|
||||
|
||||
# Check if it was a symlink (stored as SYMLINK:target in .orig)
|
||||
local orig_content
|
||||
orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "")
|
||||
if [[ $orig_content == SYMLINK:* ]]; then
|
||||
local link_target="${orig_content#SYMLINK:}"
|
||||
echo " Restoring symlink $wrapper_path -> $link_target"
|
||||
ln -s "$link_target" "$wrapper_path"
|
||||
rm "${wrapper_path}.orig"
|
||||
else
|
||||
echo " Restoring original file"
|
||||
mv "${wrapper_path}.orig" "$wrapper_path"
|
||||
fi
|
||||
echo " ✓ $app restored"
|
||||
}
|
||||
|
||||
# Install all wrappers
|
||||
install_all() {
|
||||
echo "Installing compulsive opening blockers..."
|
||||
echo ""
|
||||
|
||||
# Install main script to /usr/local/bin
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
local install_path="/usr/local/bin/block-compulsive-opening.sh"
|
||||
|
||||
if [[ $script_path != "$install_path" ]]; then
|
||||
echo "Installing main script to $install_path..."
|
||||
cp "$script_path" "$install_path"
|
||||
chmod +x "$install_path"
|
||||
echo "✓ Main script installed"
|
||||
else
|
||||
echo "Main script already at $install_path"
|
||||
fi
|
||||
|
||||
# The libs travel with the entry script, FLAT beside it rather than in a
|
||||
# lib/ subdirectory — the same shape as /usr/local/bin/pacman_lock_lib.sh
|
||||
# and heavy_job_lock.sh. The entry script probes for lib/ first and falls
|
||||
# back to its own directory, so both layouts work.
|
||||
#
|
||||
# This must run even when the script is already at $install_path: a lib
|
||||
# added or changed upstream still has to reach /usr/local/bin, and
|
||||
# `rewrap-quiet` from the pacman hook runs the DEPLOYED copy, which would
|
||||
# otherwise keep sourcing stale libs.
|
||||
local lib_src="$_CCO_LIB_DIR"
|
||||
local lib_dest="/usr/local/bin"
|
||||
if [[ $lib_src != "$lib_dest" ]]; then
|
||||
echo "Installing libraries to $lib_dest..."
|
||||
local lib
|
||||
for lib in cco_state cco_wrapper cco_install cco_report; do
|
||||
cp "$lib_src/${lib}.sh" "$lib_dest/${lib}.sh"
|
||||
chmod +x "$lib_dest/${lib}.sh"
|
||||
done
|
||||
echo "✓ Libraries installed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Install wrappers for each app
|
||||
local installed=0
|
||||
for app in "${!APPS[@]}"; do
|
||||
if install_wrapper "$app"; then
|
||||
((installed++)) || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Installation complete. $installed app(s) wrapped."
|
||||
echo ""
|
||||
echo "Each app can now only be opened once per hour."
|
||||
echo "State files stored in: $STATE_DIR"
|
||||
echo "Logs stored in: $LOG_FILE"
|
||||
|
||||
# Install pacman hook to re-wrap after package updates
|
||||
install_pacman_hook
|
||||
}
|
||||
|
||||
# Install pacman hook to re-install wrappers after package updates
|
||||
install_pacman_hook() {
|
||||
local hook_dir="/etc/pacman.d/hooks"
|
||||
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
|
||||
|
||||
echo ""
|
||||
echo "Installing pacman hook..."
|
||||
|
||||
mkdir -p "$hook_dir"
|
||||
|
||||
cat >"$hook_file" <<'HOOK_EOF'
|
||||
[Trigger]
|
||||
Operation = Upgrade
|
||||
Operation = Install
|
||||
Type = Package
|
||||
Target = beeper
|
||||
Target = signal-desktop
|
||||
Target = discord
|
||||
|
||||
[Action]
|
||||
Description = Re-installing compulsive opening blockers after package update
|
||||
When = PostTransaction
|
||||
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
|
||||
HOOK_EOF
|
||||
|
||||
chmod 644 "$hook_file"
|
||||
echo "✓ Pacman hook installed: $hook_file"
|
||||
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
|
||||
}
|
||||
|
||||
# Uninstall pacman hook
|
||||
uninstall_pacman_hook() {
|
||||
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
|
||||
if [[ -f $hook_file ]]; then
|
||||
rm -f "$hook_file"
|
||||
echo "✓ Pacman hook removed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Quietly re-wrap apps (for pacman hook - no interactive output)
|
||||
rewrap_quiet() {
|
||||
log_message "REWRAP: Pacman hook triggered, re-installing wrappers"
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
|
||||
# Re-wrap if wrapper is missing or was overwritten by a package update
|
||||
if [[ ! -f "${wrapper_path}.orig" ]] || ! grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then
|
||||
log_message "REWRAP: $app wrapper missing or overwritten, re-installing"
|
||||
rm -f "${wrapper_path}.orig"
|
||||
install_wrapper "$app" >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
log_message "REWRAP: Complete"
|
||||
}
|
||||
|
||||
# Uninstall all wrappers
|
||||
uninstall_all() {
|
||||
echo "Removing compulsive opening blockers..."
|
||||
echo ""
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
uninstall_wrapper "$app" || true
|
||||
done
|
||||
|
||||
rm -f "/usr/local/bin/block-compulsive-opening.sh"
|
||||
|
||||
# The libs install_all copied flat beside the entry script go with it;
|
||||
# leaving them behind would strand four orphans in /usr/local/bin.
|
||||
local lib
|
||||
for lib in cco_state cco_wrapper cco_install cco_report; do
|
||||
rm -f "/usr/local/bin/${lib}.sh"
|
||||
done
|
||||
|
||||
# Remove pacman hook
|
||||
uninstall_pacman_hook
|
||||
|
||||
echo ""
|
||||
echo "Uninstallation complete."
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# Status reporting and reset commands for block_compulsive_opening.sh.
|
||||
# Sourced by the entry script; inherits its strict mode and globals.
|
||||
|
||||
# Show status of all apps
|
||||
show_status() {
|
||||
ensure_state_dir
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
echo "Compulsive Opening Blocker Status"
|
||||
echo "=================================="
|
||||
echo "Current hour: $current_hour"
|
||||
echo ""
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local status="not opened this hour"
|
||||
local icon="○"
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
status="already opened (blocked until next hour)"
|
||||
icon="●"
|
||||
else
|
||||
status="last opened: $last_hour"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if wrapped
|
||||
local wrapped="not installed"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
wrapped="wrapped"
|
||||
elif [[ -f $wrapper_path ]]; then
|
||||
wrapped="installed (not wrapped)"
|
||||
fi
|
||||
|
||||
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "State directory: $STATE_DIR"
|
||||
}
|
||||
|
||||
# Reset state for an app (allow opening again)
|
||||
reset_app() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
rm -f "$state_file"
|
||||
echo "Reset $app - can be opened again this hour"
|
||||
log_message "RESET: $app state cleared by user"
|
||||
else
|
||||
echo "$app was not marked as opened"
|
||||
fi
|
||||
}
|
||||
|
||||
# Clear all state
|
||||
reset_all() {
|
||||
ensure_state_dir
|
||||
rm -f "$STATE_DIR"/*.lastopen
|
||||
echo "All apps reset - can be opened again this hour"
|
||||
log_message "RESET: All app states cleared by user"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
cat <<EOF
|
||||
Block Compulsive Opening Script
|
||||
================================
|
||||
|
||||
Limits messaging apps to one launch per hour to reduce compulsive checking.
|
||||
|
||||
Usage: $0 [command] [args]
|
||||
|
||||
Commands:
|
||||
install - Install wrappers for all apps (requires root)
|
||||
uninstall - Remove all wrappers (requires root)
|
||||
status - Show current status of all apps
|
||||
reset <app> - Reset an app to allow opening again this hour
|
||||
reset-all - Reset all apps
|
||||
wrapper <app> [args] - Run as wrapper for an app (internal use)
|
||||
help - Show this help message
|
||||
|
||||
Managed Apps:
|
||||
beeper - Beeper messaging client
|
||||
signal-desktop - Signal messenger
|
||||
discord - Discord chat
|
||||
|
||||
Examples:
|
||||
sudo $0 install # Install all wrappers
|
||||
$0 status # Check which apps were opened this hour
|
||||
$0 reset discord # Allow Discord to be opened again
|
||||
|
||||
EOF
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
# State, timing and process helpers for block_compulsive_opening.sh.
|
||||
# Sourced by the entry script; inherits its strict mode, its notify() helper
|
||||
# and the STATE_DIR/LOG_FILE/APPS/REAL_BINARIES/PROCESS_MATCH globals it
|
||||
# defines.
|
||||
|
||||
# Ensure state directory exists
|
||||
ensure_state_dir() {
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Log message with timestamp
|
||||
log_message() {
|
||||
local msg
|
||||
msg="$(printf '%(%Y-%m-%d %H:%M:%S)T' -1) - $1"
|
||||
echo "$msg" >&2
|
||||
echo "$msg" >>"$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get current hour key (YYYY-MM-DD-HH format)
|
||||
get_hour_key() {
|
||||
printf '%(%Y-%m-%d-%H)T' -1
|
||||
}
|
||||
|
||||
# Get state file path for an app
|
||||
get_state_file() {
|
||||
local app="$1"
|
||||
echo "$STATE_DIR/${app}.lastopen"
|
||||
}
|
||||
|
||||
# Check if app was already opened this hour
|
||||
was_opened_this_hour() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
return 0 # Was opened this hour
|
||||
fi
|
||||
fi
|
||||
return 1 # Not opened this hour
|
||||
}
|
||||
|
||||
# Record app opening
|
||||
record_opening() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
echo "$current_hour" >"$state_file"
|
||||
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
|
||||
}
|
||||
|
||||
# Block app and notify
|
||||
block_app() {
|
||||
local app="$1"
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)"
|
||||
|
||||
# Send notification using common library
|
||||
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
|
||||
}
|
||||
|
||||
# Get real binary path for an app
|
||||
get_real_binary() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
|
||||
# Check if wrapper is installed (original moved to .orig)
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
# Wrapper installed, return the actual executable
|
||||
echo "$real_binary"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get running state file path for an app (tracks PID and start time)
|
||||
get_running_file() {
|
||||
local app="$1"
|
||||
echo "$STATE_DIR/${app}.running"
|
||||
}
|
||||
|
||||
# Clean up stale running state (process no longer running)
|
||||
# Uses process-name matching so Electron apps that fork don't appear stale.
|
||||
cleanup_stale_running_state() {
|
||||
local app="$1"
|
||||
local running_file
|
||||
running_file=$(get_running_file "$app")
|
||||
|
||||
if [[ ! -f $running_file ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Match on PROCESS_MATCH when the exec target is a launcher that exec()s away
|
||||
# (see the PROCESS_MATCH map); otherwise the exec target is the process.
|
||||
local real_binary="${PROCESS_MATCH[$app]:-${REAL_BINARIES[$app]}}"
|
||||
|
||||
# Check if any process matching the real binary is still running
|
||||
if ! is_app_running "$real_binary"; then
|
||||
log_message "CLEANUP: Stale running state for $app (no matching processes found)"
|
||||
rm -f "$running_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if app is running by process name (handles Electron apps that fork)
|
||||
is_app_running() {
|
||||
local real_binary="$1"
|
||||
pgrep -f "$real_binary" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Kill all processes matching the real binary path
|
||||
kill_app() {
|
||||
local real_binary="$1"
|
||||
pkill -f "$real_binary" 2>/dev/null || true
|
||||
sleep 2
|
||||
pkill -9 -f "$real_binary" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Check if auto-close is suspended for an app today
|
||||
is_autoclose_suspended() {
|
||||
local app="$1"
|
||||
local today
|
||||
today=$(printf '%(%Y-%m-%d)T' -1)
|
||||
local suspend_file="$STATE_DIR/${app}.suspend-autoclose"
|
||||
|
||||
if [[ -f $suspend_file ]]; then
|
||||
local suspend_date
|
||||
suspend_date=$(cat "$suspend_file" 2>/dev/null || echo "")
|
||||
if [[ $suspend_date == "$today" ]]; then
|
||||
return 0 # Suspended for today
|
||||
else
|
||||
# Stale suspend file, clean up
|
||||
rm -f "$suspend_file"
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
# App-launch path for block_compulsive_opening.sh: the auto-close timer and the
|
||||
# wrapper entry that the generated /usr/bin/<app> shims exec into.
|
||||
# Sourced by the entry script; inherits its strict mode and globals.
|
||||
|
||||
# Launch app with auto-close timer
|
||||
launch_with_timer() {
|
||||
local app="$1"
|
||||
local real_binary="$2"
|
||||
shift 2
|
||||
|
||||
# $real_binary is what we EXEC; $proc_match is what we look for in the process
|
||||
# table. They are usually identical, but NOT when the exec target is a launcher
|
||||
# that exec()s away: discord's /usr/bin/discord.orig replaces itself with
|
||||
# ~/.config/discord/<app>/Discord, so matching/killing on the launcher path
|
||||
# would find nothing — the auto-close daemon would think the app exited
|
||||
# instantly and the time limit would silently never fire.
|
||||
local proc_match="${PROCESS_MATCH[$app]:-$real_binary}"
|
||||
|
||||
# Check if auto-close is suspended for today
|
||||
if is_autoclose_suspended "$app"; then
|
||||
log_message "LAUNCHED: $app (auto-close suspended for today)"
|
||||
exec "$real_binary" "$@"
|
||||
fi
|
||||
|
||||
# Use per-app timeout if set, otherwise fall back to global default
|
||||
local timeout_minutes="${APP_TIMEOUT_MINUTES[$app]:-$AUTO_CLOSE_TIMEOUT_MINUTES}"
|
||||
local warning_seconds=$(((timeout_minutes - AUTO_CLOSE_WARNING_MINUTES) * 60))
|
||||
local running_file
|
||||
running_file=$(get_running_file "$app")
|
||||
|
||||
# Launch the app in background
|
||||
"$real_binary" "$@" &
|
||||
local app_pid=$!
|
||||
|
||||
# Give Electron apps time to fork before we start polling
|
||||
sleep 2
|
||||
|
||||
# Record state (FORK-FREE: use printf %s for timestamp)
|
||||
echo "$app_pid $(printf '%(%s)T' -1)" >"$running_file"
|
||||
log_message "LAUNCHED: $app with PID $app_pid (auto-close in ${timeout_minutes}m)"
|
||||
|
||||
# Spawn the auto-close daemon in a completely detached subshell
|
||||
# Uses process-name matching so it works for Electron apps that fork on launch
|
||||
(
|
||||
# Detach from terminal
|
||||
exec </dev/null >/dev/null 2>&1
|
||||
|
||||
# Wait for warning time
|
||||
sleep "$warning_seconds"
|
||||
|
||||
# Check if still running before warning (by process name, not PID)
|
||||
if is_app_running "$proc_match"; then
|
||||
# Send warning notification
|
||||
notify-send -u critical -t 30000 "⏰ $app Closing Soon" \
|
||||
"Session will end in ${AUTO_CLOSE_WARNING_MINUTES} minutes. Save your work!" 2>/dev/null || true
|
||||
else
|
||||
# Process already exited
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait remaining time
|
||||
sleep $((AUTO_CLOSE_WARNING_MINUTES * 60))
|
||||
|
||||
# Check if still running (by process name)
|
||||
if is_app_running "$proc_match"; then
|
||||
# Send final notification
|
||||
notify-send -u critical -t 5000 "🚫 $app Session Ended" \
|
||||
"Time's up! Closing $app now." 2>/dev/null || true
|
||||
|
||||
# Kill all matching processes (handles forked Electron children)
|
||||
kill_app "$proc_match"
|
||||
|
||||
printf '%(%Y-%m-%d %H:%M:%S)T - AUTO-CLOSED: %s after %dm\n' -1 "$app" "${timeout_minutes}" >>"$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
) &
|
||||
disown
|
||||
|
||||
# Wait for the app to exit by polling process name.
|
||||
# Electron apps fork immediately so waiting on $app_pid would return too soon.
|
||||
while is_app_running "$proc_match"; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Clean up running state
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
|
||||
log_message "EXITED: $app"
|
||||
}
|
||||
|
||||
# Main wrapper function - called when wrapping app launches
|
||||
wrapper_main() {
|
||||
local app="$1"
|
||||
shift
|
||||
|
||||
ensure_state_dir
|
||||
|
||||
local real_binary
|
||||
if ! real_binary=$(get_real_binary "$app"); then
|
||||
log_message "ERROR: Real binary not found for $app"
|
||||
echo "Error: Real binary for $app not found. Was the installer run?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up stale running state from previous crashes
|
||||
cleanup_stale_running_state "$app"
|
||||
|
||||
if was_opened_this_hour "$app"; then
|
||||
block_app "$app"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
record_opening "$app"
|
||||
|
||||
# Launch with auto-close timer (replaces direct exec)
|
||||
launch_with_timer "$app" "$real_binary" "$@"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user