mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 12:07:52 +01:00
Split music_parallelism.sh into an entry script and three libs
412 lines -> 135, with the nine functions in lib/music_detect.sh (133), lib/music_monitor.sh (91) and lib/music_status.sh (85). find_music_services and kill_music_services stay together: the first writes MUSIC_FOUND_PROCESS/MUSIC_FOUND_WINDOW and the second reads them, so splitting between them would thread mutable state across files. The test suite caught what the static checks could not. The harness copies the script into a temp worktree to stub common.sh, and copied exactly one file -- so after the split every source line failed and the script died before any behaviour under test ran. Teaching it to copy lib/ too is a harness fix under decision 8: no assertion changed, the same five checks run, exit 0 before and after. bash -n also earned its keep: the first splice dropped notify_user's closing brace, and shellcheck reported that only as SC1094 "parsing of sourced file failed" against the entry. One rename, not a suppression: monitor_loop's local `music_services` became `active_services` after shellcheck flagged SC2153 against the MUSIC_SERVICES config array now visible in the same file. 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
41cc0451c9
commit
d889c8e655
@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Split music_parallelism.sh (412 lines) into an entry script and three libs",
|
||||
"objective": "Bring linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh under the repo-wide 250-line cap by moving its nine functions into lib/music_{detect,monitor,status}.sh, with the existing regression test proving behaviour is unchanged.",
|
||||
"acceptance_criteria": [
|
||||
"music_parallelism.sh and each new lib/music_*.sh file is at most 250 lines.",
|
||||
"shellcheck reports zero findings across the entry script and all three libs, at the repo's no-severity-filter setting, with no suppression added.",
|
||||
"linux_configuration/tests/test_music_parallelism.sh exits 0 and runs the same five named checks as before the split.",
|
||||
"The set of functions defined across the entry and the libs is identical to the nine defined at HEAD, each defined exactly once.",
|
||||
"Every executable line of the original survives, except the three carrying the deliberate music_services -> active_services rename.",
|
||||
"MUSIC_FOUND_PROCESS and MUSIC_FOUND_WINDOW, which find_music_services writes and kill_music_services reads, do not cross a file boundary.",
|
||||
"No lib sources another lib; the entry sources all three, after the configuration arrays they read.",
|
||||
"No assertion in the test is changed, deleted, skipped or weakened; the only permitted test edit is making the libs reachable from the temp worktree."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Changing any detection pattern, interval, or the set of monitored music services or focus apps.",
|
||||
"Converting the $0-based SCRIPT_DIR to ${BASH_SOURCE[0]}; nothing sources this script, so the existing form is correct here.",
|
||||
"Touching the common.sh dual-path source or the /usr/local/lib fallback."
|
||||
],
|
||||
"verifier": "bash linux_configuration/tests/test_music_parallelism.sh (expects exit 0 and the same five checks), shellcheck over the entry plus all three libs (expects zero findings), bash -n on each, bash ~/utils/scripts/check_file_length.sh over the four files (expects none listed), and a sorted line-set diff of the original against entry+libs showing only the deliberate rename."
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
{
|
||||
"intent": "Bring music_parallelism.sh under the 250-line cap by moving its nine functions into three sourced libs, leaving its behaviour and its test outcomes unchanged.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh (412 -> 135 lines)",
|
||||
"New: lib/music_{detect,monitor,status}.sh",
|
||||
"linux_configuration/tests/test_music_parallelism.sh -- harness only, so the libs reach the temp worktree",
|
||||
"Non-goal: changing any assertion, interval, or detection pattern"
|
||||
],
|
||||
"changes": [
|
||||
"Moved wait_seconds, contains_music_process, find_music_services and kill_music_services into lib/music_detect.sh. The first two of those four share the MUSIC_FOUND_PROCESS / MUSIC_FOUND_WINDOW pair, so they stay in one file and no mutable state crosses a boundary.",
|
||||
"Moved notify_user, instant_monitor_loop and monitor_loop into lib/music_monitor.sh; notify_user is called only by those two loops.",
|
||||
"Moved show_status and show_usage into lib/music_status.sh.",
|
||||
"The entry script keeps the header, the common.sh dual-path source, all configuration arrays and the command dispatch case, and sources the three libs after the configuration they read.",
|
||||
"Renamed monitor_loop's local music_services to active_services. shellcheck flagged the local as a possible misspelling of the MUSIC_SERVICES config array (SC2153) once both appeared in the same file; renaming the purely internal local is the fix, not a suppression.",
|
||||
"Taught the test harness to copy the script's lib/ directory into its temp worktree alongside the script. No assertion, expectation or check was changed."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_music_parallelism.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Exit 0, the same five checks as before the split, by the same names: stable-focus backoff, conflict retry interval, idle interval, conflict-path xdotool deduplication, monitor-loop xdotool deduplication. Baseline was captured before any edit and matches."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck music_parallelism.sh lib/music_*.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Zero findings across all four files at the repo's no-severity-filter setting, with no suppression added."
|
||||
},
|
||||
{
|
||||
"command": "bash -n on the entry and each lib",
|
||||
"result": "pass",
|
||||
"evidence": "All four parse. This check earned its keep: the first splice dropped notify_user's closing brace, and shellcheck reported it only as SC1094 'parsing of sourced file failed' on the entry, which reads like a lint nicety rather than a broken file."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh",
|
||||
"result": "pass",
|
||||
"evidence": "entry 135, detect 133, monitor 91, status 85. None of the four appears in the violation list."
|
||||
},
|
||||
{
|
||||
"command": "diff of the function set against HEAD",
|
||||
"result": "pass",
|
||||
"evidence": "Identical: all nine functions preserved, each defined exactly once across the four files. No lib sources another; the entry sources all three."
|
||||
},
|
||||
{
|
||||
"command": "sorted line-set diff of HEAD:music_parallelism.sh against entry+libs concatenated",
|
||||
"result": "pass",
|
||||
"evidence": "The only differences are the three lines carrying the deliberate music_services -> active_services rename, and the three added source lines. No executable line lost."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The test harness previously copied one file; it now copies a directory too. If a future split adds a lib elsewhere the same fix will be needed again, but the failure is loud -- the source line aborts the script and every check fails, which is how this one was caught.",
|
||||
"The entry keeps the existing $0-based SCRIPT_DIR rather than ${BASH_SOURCE[0]}. Safe here because nothing sources music_parallelism.sh: it is invoked as a command by its dispatch case, and grep over linux_configuration finds no other reference than the test.",
|
||||
"The script has a /usr/local/lib/common.sh fallback for its shared library, but no installed copy of the script itself exists (checked /usr/local/bin), so the new libs are only ever resolved from the repo."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; music_parallelism.sh returns to its 412-line single-file form, the three libs disappear, and the test harness stops copying a lib/ directory that no longer exists.",
|
||||
"After rollback, re-run bash linux_configuration/tests/test_music_parallelism.sh and confirm the same five checks still pass."
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
#!/bin/bash
|
||||
# Music-service detection and termination, plus the shared wait helper.
|
||||
#
|
||||
# Sourced by music_parallelism.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and reads
|
||||
# the interval, MUSIC_* and PROC_ROOT configuration set above the source line.
|
||||
#
|
||||
# find_music_services and kill_music_services stay in one file: the
|
||||
# MUSIC_FOUND_PROCESS / MUSIC_FOUND_WINDOW pair the first sets and the second
|
||||
# reads would otherwise cross a file boundary.
|
||||
|
||||
wait_seconds() {
|
||||
local timeout_s=$1
|
||||
local start_ts end_ts elapsed_s remaining_s
|
||||
|
||||
if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then
|
||||
printf '%s\n' "$timeout_s" >>"$MUSIC_PARALLELISM_TEST_WAIT_LOG"
|
||||
if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then
|
||||
exit 99
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf -v start_ts '%(%s)T' -1
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
printf -v end_ts '%(%s)T' -1
|
||||
|
||||
elapsed_s=$((end_ts - start_ts))
|
||||
if ((elapsed_s < timeout_s)); then
|
||||
remaining_s=$((timeout_s - elapsed_s))
|
||||
sleep "$remaining_s"
|
||||
fi
|
||||
}
|
||||
|
||||
contains_music_process() {
|
||||
local comm_file comm_lower token_lower
|
||||
|
||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||
[[ -r $comm_file ]] || continue
|
||||
read -r comm_lower <"$comm_file" || continue
|
||||
comm_lower=${comm_lower,,}
|
||||
|
||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls)
|
||||
find_music_services() {
|
||||
local found_services=()
|
||||
MUSIC_FOUND_PROCESS=0
|
||||
MUSIC_FOUND_WINDOW=0
|
||||
|
||||
# Check processes using /proc (fork-free)
|
||||
if contains_music_process; then
|
||||
MUSIC_FOUND_PROCESS=1
|
||||
found_services+=("music process")
|
||||
fi
|
||||
|
||||
# Check windows (use optimized is_focus_app_running logic: single xdotool regex call)
|
||||
if command -v xdotool &>/dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then
|
||||
if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &>/dev/null 2>&1; then
|
||||
MUSIC_FOUND_WINDOW=1
|
||||
found_services+=("music service (window)")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#found_services[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${found_services[@]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Kill music services
|
||||
kill_music_services() {
|
||||
local use_cached_detection="${1:-0}"
|
||||
local killed=false
|
||||
local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com'
|
||||
local should_check_windows=1
|
||||
local should_check_processes=1
|
||||
|
||||
if [[ $use_cached_detection -eq 1 ]]; then
|
||||
should_check_windows=$MUSIC_FOUND_WINDOW
|
||||
should_check_processes=$MUSIC_FOUND_PROCESS
|
||||
fi
|
||||
|
||||
# Close browser tabs for web-based music services via one xdotool search
|
||||
if [[ $should_check_windows -eq 1 ]] && command -v xdotool &>/dev/null; then
|
||||
local windows wid
|
||||
windows=$(xdotool search --name "$window_pattern" 2>/dev/null || true)
|
||||
for wid in $windows; do
|
||||
[[ -n $wid ]] || continue
|
||||
xdotool windowclose "$wid" 2>/dev/null || true
|
||||
killed=true
|
||||
done
|
||||
fi
|
||||
|
||||
# Kill app processes with /proc scan + builtin kill (fork-free in hot path)
|
||||
if [[ $should_check_processes -eq 1 ]]; then
|
||||
local comm_file pid comm_lower token_lower
|
||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||
[[ -r $comm_file ]] || continue
|
||||
read -r comm_lower <"$comm_file" || continue
|
||||
comm_lower=${comm_lower,,}
|
||||
pid=${comm_file#"$PROC_ROOT"/}
|
||||
pid=${pid%%/*}
|
||||
|
||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||
if [[ $PROC_ROOT != "/proc" ]]; then
|
||||
# Test mode (fake proc tree): mark as killed without signaling host PIDs.
|
||||
killed=true
|
||||
elif kill -9 "$pid" 2>/dev/null; then
|
||||
killed=true
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
if $killed; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
# The two monitoring loops: the instant (adaptive-interval) loop and the
|
||||
# fixed-interval fallback loop.
|
||||
#
|
||||
# Sourced by music_parallelism.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and calls
|
||||
# the detection helpers in music_detect.sh, which the entry sources first.
|
||||
|
||||
# Send notification to user
|
||||
notify_user() {
|
||||
local focus_app="$1"
|
||||
local message="Music stopped - focus mode active ($focus_app detected)"
|
||||
|
||||
# Try to send desktop notification
|
||||
if command -v notify-send &>/dev/null; then
|
||||
notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_message "$message"
|
||||
}
|
||||
|
||||
# Instant monitoring loop - uses polling at high frequency ONLY when focus app is detected
|
||||
# When focus app active: checks every 0.5s. When idle: checks every 3s. Reduces fork overhead.
|
||||
# OPTIMIZATION: Single batched pgrep call instead of multiple separate calls
|
||||
instant_monitor_loop() {
|
||||
local next_enforcement_ts=0
|
||||
local current_ts=0
|
||||
local focus_app=""
|
||||
local sleep_interval="$IDLE_CHECK_INTERVAL"
|
||||
|
||||
log_message "=== Music Parallelism INSTANT Monitor Started ==="
|
||||
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
||||
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
|
||||
log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${ACTIVE_NO_MUSIC_INTERVAL}s stable-focus, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown"
|
||||
|
||||
while true; do
|
||||
if focus_app=$(is_focus_app_running 2>/dev/null); then
|
||||
current_ts=$(get_timestamp)
|
||||
if ((current_ts >= next_enforcement_ts)); then
|
||||
if find_music_services >/dev/null 2>&1; then
|
||||
if kill_music_services 1; then
|
||||
notify_user "$focus_app"
|
||||
log_message "INSTANT KILL: Music services terminated"
|
||||
sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL"
|
||||
fi
|
||||
else
|
||||
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
||||
fi
|
||||
next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN))
|
||||
else
|
||||
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
||||
fi
|
||||
else
|
||||
next_enforcement_ts=0
|
||||
sleep_interval="$IDLE_CHECK_INTERVAL"
|
||||
fi
|
||||
|
||||
wait_seconds "$sleep_interval"
|
||||
done
|
||||
}
|
||||
|
||||
# Main monitoring loop
|
||||
monitor_loop() {
|
||||
log_message "=== Music Parallelism Monitor Started ==="
|
||||
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
||||
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
|
||||
log_message "Music services monitored: ${MUSIC_SERVICES[*]}"
|
||||
log_message "Check interval: ${CHECK_INTERVAL}s"
|
||||
|
||||
while true; do
|
||||
# Check if a focus app is running
|
||||
local focus_app
|
||||
if focus_app=$(is_focus_app_running); then
|
||||
# Focus app detected, check for music services
|
||||
# Named to avoid colliding with the MUSIC_SERVICES config array,
|
||||
# which this file also reads.
|
||||
local active_services
|
||||
if active_services=$(find_music_services); then
|
||||
log_message "Conflict detected: Focus app '$focus_app' running with music services"
|
||||
log_message "Active music services: $active_services"
|
||||
|
||||
# Kill the music services
|
||||
if kill_music_services 1; then
|
||||
notify_user "$focus_app"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
wait_seconds "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# The status report and the usage text.
|
||||
#
|
||||
# Sourced by music_parallelism.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and reads
|
||||
# the interval and MUSIC_* configuration set above the source line.
|
||||
|
||||
# Show status
|
||||
show_status() {
|
||||
echo "Music Parallelism Monitor Status"
|
||||
echo "================================="
|
||||
echo ""
|
||||
|
||||
echo "Focus Applications (window-based detection):"
|
||||
local focus_running=false
|
||||
|
||||
# Check windows (OPTIMIZED: single xdotool call with combined regex)
|
||||
if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then
|
||||
local regex
|
||||
printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}"
|
||||
regex="${regex%|}" # strip trailing |
|
||||
if xdotool search --name "$regex" &>/dev/null 2>&1; then
|
||||
echo " ✓ Focus window detected"
|
||||
focus_running=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check processes using shared /proc-based helper (fork-free)
|
||||
if is_focus_app_running >/dev/null 2>&1; then
|
||||
echo " ✓ Focus process running"
|
||||
focus_running=true
|
||||
fi
|
||||
|
||||
if ! $focus_running; then
|
||||
echo " (none detected)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Music Services:"
|
||||
local music_running=false
|
||||
if music_services=$(find_music_services 2>/dev/null); then
|
||||
echo "$music_services" | while read -r svc; do
|
||||
echo " ♪ $svc (RUNNING)"
|
||||
done
|
||||
music_running=true
|
||||
fi
|
||||
if ! $music_running; then
|
||||
echo " (none detected)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if $focus_running && $music_running; then
|
||||
echo "⚠️ CONFLICT: Focus app and music running together!"
|
||||
echo " Music would be killed in monitoring mode."
|
||||
elif $focus_running; then
|
||||
echo "✓ Focus mode active (no music playing)"
|
||||
elif $music_running; then
|
||||
echo "✓ Music playing (no focus app detected - this is fine)"
|
||||
else
|
||||
echo "✓ Idle (nothing detected)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
echo "Music Parallelism Prevention Script"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
echo "Usage: $0 [command]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)"
|
||||
echo " instant - Instant monitoring (${FAST_CHECK_INTERVAL}s active / ${IDLE_CHECK_INTERVAL}s idle)"
|
||||
echo " status - Show current status of focus apps and music services"
|
||||
echo " kill - Immediately kill all music services"
|
||||
echo " help - Show this help message"
|
||||
echo ""
|
||||
echo "Description:"
|
||||
echo " This script prevents multitasking between focus work and music."
|
||||
echo " When a focus application (VS Code, Steam, etc.) is detected"
|
||||
echo " alongside a music streaming service, the music is stopped."
|
||||
echo ""
|
||||
echo " Music is allowed when no focus apps are running."
|
||||
echo ""
|
||||
}
|
||||
@ -99,288 +99,11 @@ readonly IDLE_CHECK_INTERVAL=30
|
||||
MUSIC_FOUND_PROCESS=0
|
||||
MUSIC_FOUND_WINDOW=0
|
||||
|
||||
wait_seconds() {
|
||||
local timeout_s=$1
|
||||
local start_ts end_ts elapsed_s remaining_s
|
||||
|
||||
if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then
|
||||
printf '%s\n' "$timeout_s" >>"$MUSIC_PARALLELISM_TEST_WAIT_LOG"
|
||||
if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then
|
||||
exit 99
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf -v start_ts '%(%s)T' -1
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
printf -v end_ts '%(%s)T' -1
|
||||
|
||||
elapsed_s=$((end_ts - start_ts))
|
||||
if ((elapsed_s < timeout_s)); then
|
||||
remaining_s=$((timeout_s - elapsed_s))
|
||||
sleep "$remaining_s"
|
||||
fi
|
||||
}
|
||||
|
||||
contains_music_process() {
|
||||
local comm_file comm_lower token_lower
|
||||
|
||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||
[[ -r $comm_file ]] || continue
|
||||
read -r comm_lower <"$comm_file" || continue
|
||||
comm_lower=${comm_lower,,}
|
||||
|
||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls)
|
||||
find_music_services() {
|
||||
local found_services=()
|
||||
MUSIC_FOUND_PROCESS=0
|
||||
MUSIC_FOUND_WINDOW=0
|
||||
|
||||
# Check processes using /proc (fork-free)
|
||||
if contains_music_process; then
|
||||
MUSIC_FOUND_PROCESS=1
|
||||
found_services+=("music process")
|
||||
fi
|
||||
|
||||
# Check windows (use optimized is_focus_app_running logic: single xdotool regex call)
|
||||
if command -v xdotool &>/dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then
|
||||
if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &>/dev/null 2>&1; then
|
||||
MUSIC_FOUND_WINDOW=1
|
||||
found_services+=("music service (window)")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#found_services[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${found_services[@]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Kill music services
|
||||
kill_music_services() {
|
||||
local use_cached_detection="${1:-0}"
|
||||
local killed=false
|
||||
local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com'
|
||||
local should_check_windows=1
|
||||
local should_check_processes=1
|
||||
|
||||
if [[ $use_cached_detection -eq 1 ]]; then
|
||||
should_check_windows=$MUSIC_FOUND_WINDOW
|
||||
should_check_processes=$MUSIC_FOUND_PROCESS
|
||||
fi
|
||||
|
||||
# Close browser tabs for web-based music services via one xdotool search
|
||||
if [[ $should_check_windows -eq 1 ]] && command -v xdotool &>/dev/null; then
|
||||
local windows wid
|
||||
windows=$(xdotool search --name "$window_pattern" 2>/dev/null || true)
|
||||
for wid in $windows; do
|
||||
[[ -n $wid ]] || continue
|
||||
xdotool windowclose "$wid" 2>/dev/null || true
|
||||
killed=true
|
||||
done
|
||||
fi
|
||||
|
||||
# Kill app processes with /proc scan + builtin kill (fork-free in hot path)
|
||||
if [[ $should_check_processes -eq 1 ]]; then
|
||||
local comm_file pid comm_lower token_lower
|
||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||
[[ -r $comm_file ]] || continue
|
||||
read -r comm_lower <"$comm_file" || continue
|
||||
comm_lower=${comm_lower,,}
|
||||
pid=${comm_file#"$PROC_ROOT"/}
|
||||
pid=${pid%%/*}
|
||||
|
||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||
if [[ $PROC_ROOT != "/proc" ]]; then
|
||||
# Test mode (fake proc tree): mark as killed without signaling host PIDs.
|
||||
killed=true
|
||||
elif kill -9 "$pid" 2>/dev/null; then
|
||||
killed=true
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
if $killed; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Send notification to user
|
||||
notify_user() {
|
||||
local focus_app="$1"
|
||||
local message="Music stopped - focus mode active ($focus_app detected)"
|
||||
|
||||
# Try to send desktop notification
|
||||
if command -v notify-send &>/dev/null; then
|
||||
notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_message "$message"
|
||||
}
|
||||
|
||||
# Instant monitoring loop - uses polling at high frequency ONLY when focus app is detected
|
||||
# When focus app active: checks every 0.5s. When idle: checks every 3s. Reduces fork overhead.
|
||||
# OPTIMIZATION: Single batched pgrep call instead of multiple separate calls
|
||||
instant_monitor_loop() {
|
||||
local next_enforcement_ts=0
|
||||
local current_ts=0
|
||||
local focus_app=""
|
||||
local sleep_interval="$IDLE_CHECK_INTERVAL"
|
||||
|
||||
log_message "=== Music Parallelism INSTANT Monitor Started ==="
|
||||
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
||||
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
|
||||
log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${ACTIVE_NO_MUSIC_INTERVAL}s stable-focus, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown"
|
||||
|
||||
while true; do
|
||||
if focus_app=$(is_focus_app_running 2>/dev/null); then
|
||||
current_ts=$(get_timestamp)
|
||||
if ((current_ts >= next_enforcement_ts)); then
|
||||
if find_music_services >/dev/null 2>&1; then
|
||||
if kill_music_services 1; then
|
||||
notify_user "$focus_app"
|
||||
log_message "INSTANT KILL: Music services terminated"
|
||||
sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL"
|
||||
fi
|
||||
else
|
||||
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
||||
fi
|
||||
next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN))
|
||||
else
|
||||
sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL"
|
||||
fi
|
||||
else
|
||||
next_enforcement_ts=0
|
||||
sleep_interval="$IDLE_CHECK_INTERVAL"
|
||||
fi
|
||||
|
||||
wait_seconds "$sleep_interval"
|
||||
done
|
||||
}
|
||||
|
||||
# Main monitoring loop
|
||||
monitor_loop() {
|
||||
log_message "=== Music Parallelism Monitor Started ==="
|
||||
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
|
||||
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
|
||||
log_message "Music services monitored: ${MUSIC_SERVICES[*]}"
|
||||
log_message "Check interval: ${CHECK_INTERVAL}s"
|
||||
|
||||
while true; do
|
||||
# Check if a focus app is running
|
||||
local focus_app
|
||||
if focus_app=$(is_focus_app_running); then
|
||||
# Focus app detected, check for music services
|
||||
local music_services
|
||||
if music_services=$(find_music_services); then
|
||||
log_message "Conflict detected: Focus app '$focus_app' running with music services"
|
||||
log_message "Active music services: $music_services"
|
||||
|
||||
# Kill the music services
|
||||
if kill_music_services 1; then
|
||||
notify_user "$focus_app"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
wait_seconds "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# Show status
|
||||
show_status() {
|
||||
echo "Music Parallelism Monitor Status"
|
||||
echo "================================="
|
||||
echo ""
|
||||
|
||||
echo "Focus Applications (window-based detection):"
|
||||
local focus_running=false
|
||||
|
||||
# Check windows (OPTIMIZED: single xdotool call with combined regex)
|
||||
if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then
|
||||
local regex
|
||||
printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}"
|
||||
regex="${regex%|}" # strip trailing |
|
||||
if xdotool search --name "$regex" &>/dev/null 2>&1; then
|
||||
echo " ✓ Focus window detected"
|
||||
focus_running=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check processes using shared /proc-based helper (fork-free)
|
||||
if is_focus_app_running >/dev/null 2>&1; then
|
||||
echo " ✓ Focus process running"
|
||||
focus_running=true
|
||||
fi
|
||||
|
||||
if ! $focus_running; then
|
||||
echo " (none detected)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Music Services:"
|
||||
local music_running=false
|
||||
if music_services=$(find_music_services 2>/dev/null); then
|
||||
echo "$music_services" | while read -r svc; do
|
||||
echo " ♪ $svc (RUNNING)"
|
||||
done
|
||||
music_running=true
|
||||
fi
|
||||
if ! $music_running; then
|
||||
echo " (none detected)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if $focus_running && $music_running; then
|
||||
echo "⚠️ CONFLICT: Focus app and music running together!"
|
||||
echo " Music would be killed in monitoring mode."
|
||||
elif $focus_running; then
|
||||
echo "✓ Focus mode active (no music playing)"
|
||||
elif $music_running; then
|
||||
echo "✓ Music playing (no focus app detected - this is fine)"
|
||||
else
|
||||
echo "✓ Idle (nothing detected)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
echo "Music Parallelism Prevention Script"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
echo "Usage: $0 [command]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)"
|
||||
echo " instant - Instant monitoring (${FAST_CHECK_INTERVAL}s active / ${IDLE_CHECK_INTERVAL}s idle)"
|
||||
echo " status - Show current status of focus apps and music services"
|
||||
echo " kill - Immediately kill all music services"
|
||||
echo " help - Show this help message"
|
||||
echo ""
|
||||
echo "Description:"
|
||||
echo " This script prevents multitasking between focus work and music."
|
||||
echo " When a focus application (VS Code, Steam, etc.) is detected"
|
||||
echo " alongside a music streaming service, the music is stopped."
|
||||
echo ""
|
||||
echo " Music is allowed when no focus apps are running."
|
||||
echo ""
|
||||
}
|
||||
# Sourced after the configuration above, which they read. music_detect.sh comes
|
||||
# first: the monitor loops call its helpers.
|
||||
source "$SCRIPT_DIR/lib/music_detect.sh"
|
||||
source "$SCRIPT_DIR/lib/music_monitor.sh"
|
||||
source "$SCRIPT_DIR/lib/music_status.sh"
|
||||
|
||||
# Main
|
||||
case "${1:-instant}" in
|
||||
|
||||
@ -49,6 +49,11 @@ WORKTREE_LIB="$WORKTREE/scripts/lib/common.sh"
|
||||
mkdir -p "$(dirname "$WORKTREE_SCRIPT")" "$(dirname "$WORKTREE_LIB")" "$BIN_DIR"
|
||||
cp "$TARGET_SCRIPT" "$WORKTREE_SCRIPT"
|
||||
|
||||
# The script sources its own libs from $SCRIPT_DIR/lib/. They must travel into
|
||||
# the worktree beside it, or every source line fails and the script exits before
|
||||
# any behaviour under test runs.
|
||||
cp -r "$(dirname "$TARGET_SCRIPT")/lib" "$(dirname "$WORKTREE_SCRIPT")/lib"
|
||||
|
||||
# Fail loudly if the copy cannot reach the stub, rather than letting the script
|
||||
# fall through to the real library and produce a misleading behavioural failure.
|
||||
STUB_FROM_SCRIPT="$(dirname "$WORKTREE_SCRIPT")/../../lib/common.sh"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user