From d5c02f8459900d2815c9569b6077e478a1d6ad20 Mon Sep 17 00:00:00 2001
From: Narvin Singh <Narvin.A.Singh@gmail.com>
Date: Fri, 1 Jan 2021 13:17:08 -0500
Subject: [PATCH] Feat: Use named pipe instead of signals and sleep

This is a breaking change and will lead to a new major version.
---
 README.md | 20 ++++++++---------
 avdd      | 64 +++++++++++++++++++++++++------------------------------
 avds      | 53 ++++++++++++++++++++++-----------------------
 3 files changed, 65 insertions(+), 72 deletions(-)

diff --git a/README.md b/README.md
index 653a915..c5d5897 100644
--- a/README.md
+++ b/README.md
@@ -9,19 +9,19 @@ The Daemon
 ----------
 
 The daemon creates a modular status bar by updating the X root window name
-when it receives the USR1 signal. It takes an ordered list of modules as a
-parameter, and calls a function in each module to compute a section of the
-status bar. Each section is cached and only recomputed upon request. These
-requests are made by creating empty files with the same names as the modules
-in the `/tmp/avdd` directory, then sending the daemon a USR1 signal.
+when it receives a request on its named pipe. It takes an ordered list of
+modules as a parameter, and calls a function in each module to compute a
+section of the status bar. Each section is cached and only recomputed upon
+request. These requests are made by writing to a named pipe that the daemon
+creates, `/tmp/avdd-fifo`.
 
 The Scheduler
 -------------
 
-The scheduler creates request files in `/tmp/avdd`, then sends the USR1
-signal to the daemon. It can send one signal to update multiple status bar
-sections by creating a request file for each section to update, and can send
-the signal immediately, after some delay, or repeatedly at some interval.
+The scheduler creates requests by writing a module name to the named pipe for
+each module it wants the daemon to run to update that section of the status
+bar. It can send requests immediately, after some delay, or repeatedly at
+some interval.
 
 Installation
 ------------
@@ -38,7 +38,7 @@ Usage
 The following examples can be executed manually or by putting them in, e.g.,
 your `.xinitrc` file. Note the `&` after long-running commands to make them
 run in the background. If the directory that you cloned the appliction into
-is not in your path, be sure to include the path when calling `avdd` or `avds`.
+is not in your path, be sure to specify the path when calling `avdd` or `avds`.
 
 Start the daemon to create a status bar with the default sections, prefix,
 separators, and suffix.
diff --git a/avdd b/avdd
index 702d95f..6cf0853 100755
--- a/avdd
+++ b/avdd
@@ -44,8 +44,7 @@ DEFAULT_SEP_L='| '
 DEFAULT_SEP_R=' '
 DEFAULT_SUF=' '
 MOD_DIR="$(dirname "$0")"/mod
-ACTION_DIR=/tmp/avdd
-ACTION_DIR_LEN=${#ACTION_DIR}
+FIFO=/tmp/avdd-fifo
 
 mod_list="${1-${DEFAULT_MOD_LIST}}"
 pre="${2-${DEFAULT_PRE}}"
@@ -96,43 +95,38 @@ draw_status() {
 # Draw the initial status
 draw_status
 
-# For each file in the action directory, remove the file, and if a module
-# for the action is cached, call the module function and update the cache. If
-# any cache entries were updated, redraw the status.
-process_signal () {
-  local -a action_paths
-  local action_path mod is_changed
-  readarray -d '' action_paths< \
-    <(find "${ACTION_DIR}" -maxdepth 1 -type f -exec rm -f {} + -print0)
-  for action_path in "${action_paths[@]}"; do
-    mod="${action_path:$((ACTION_DIR_LEN + 1))}"
-    if [[ -v stat_cache[${mod}] ]]; then
-      stat_cache["${mod}"]="$(eval "$(mod_to_fn "${mod}")")"
-      is_changed=1
+# If the module value is in the cache, indicating that the module controls
+# part of the status bar, execute the module function and redraw the status
+# bar if that part of the status bar has changed
+process_cmd () {
+  local -r mod="$1"
+  if [[ -v stat_cache[${mod}] ]]; then
+    local -r new_val="$(eval "$(mod_to_fn "${mod}")")"
+    if [[ "${new_val}" != stat_cache["${mod}"] ]]; then
+      stat_cache["${mod}"]="${new_val}"
+      draw_status
     fi
-  done
-  if [[ -v is_changed ]]; then draw_status; fi
+  fi
 }
 
-# Begin trapping signals
-mkdir -p "${ACTION_DIR}"
-trap process_signal SIGUSR1
+# Setup the named pipe to receive commands
+if [[ ! -p "${FIFO}" ]]; then mkfifo "${FIFO}"; fi
+trap "rm -f ${FIFO}" EXIT
 
-# Wait for signals efficiently. In a loop begin a long-running sleep command
-# in the background, then wait on it. If we trap a signal before the wait is
-# over and sleep is still running, trap will call process_signal, then code
-# execution will resume at the line after the wait statement. So on that line
-# we kill the (probably) still running sleep command so they don't pile up,
-# and loop to sleep and wait for the next signal. If we don't trap a signal
-# during the long running sleep, then the wait ends, we try to kill the
-# sleep command that has already exited, so it doesn't matter, and loop to
-# sleep and wait again. Note that we don't make the sleep too long because
-# if the daemon is killed, the sleep will become an orphaned process until
-# the sleep period elapses.
+# Each time the pipe is emptied out, the inner while loop will finish, so
+# wrap it in an infinte loop to keep blocking until there is data on the pipe
 while :; do
-  sleep 30m &
-  sleep_pid="$!"
-  wait "${sleep_pid}"
-  kill "${sleep_pid}" 2>/dev/null
+  while read -r cmd; do
+    case "${cmd}" in
+      res_quit)
+        exit 0
+        ;;
+      res_*)
+        ;;
+      *)
+        process_cmd "${cmd}"
+        ;;
+    esac
+  done < "${FIFO}"
 done
 
diff --git a/avds b/avds
index 3ec128d..0a914e5 100755
--- a/avds
+++ b/avds
@@ -1,43 +1,44 @@
 #!/bin/bash
 
 USAGE="
-USAGE: avds <action> [<when>] [<repeat>]
+USAGE: avds <mod_list> [<when>] [<repeat>]
 
-        action  A comma or space separated list of actions to associate with the
-                update signal.
+        mod_list
+                A comma or space separated list of modules to request that
+                the daemon execute.
 
         when    The integer number of milliseconds to wait before sending the
-                update signal, or one of the following values:
-                  - m to send the update signal at the top of the next minute
-                  - h to send the update signal at the top of the next hour
-                  - d to send the update signal at the top of the next day
-                If not present, the update signal will be sent immediately.
+                request, or one of the following values:
+                  - m to send the update request at the top of the next minute
+                  - h to send the update request at the top of the next hour
+                  - d to send the update request at the top of the next day
+                If not present, the request will be sent immediately.
 
-        repeat  If present, the update signal will be sent repeatedly according
+        repeat  If present, the request will be sent repeatedly according
                 to <when>.
 
 EXAMPLES:
 
-        If the daemon interprets the actions 'vol' and 'bl' to mean update the
-        volume and backlight statuses, respectively, send a signal to
-        immediately update both of those statuses.
+        If there are volume and backlight modules named 'vol' and 'bl' that
+        update the volume and backlight statuses, send a requst to update
+        both of those statuses immediately.
 
                 avds 'vol,bl'
 
-        If the daemon interprets the actions 'cpu' and 'mem' to mean update the
-        cpu and memory usage statuses, respectively, send a signal to update
-        both of those statuses every 5 seconds.
+        If there are cpu and memory usage modules named 'cpu' and 'mem'
+        that update the cpu and memory usage statuses, send a requst to
+        update both of those statuses every 5 seconds.
 
                 avds 'cpu,mem' 5000 1
 
-        If the daemon interprets the actions 'bat' and 'dt' to mean update the
-        battery and date/time statuses, respectively, send a signal to update
-        both of those statuses at the top of every minute.
+        If there are battery and date/time modules named 'bat' and 'dt'
+        that update the battery and date/time statuses, send a requst to
+        update both of those statuses at the top of every minute.
 
                 avds 'bat,dt' m true
 "
 DAEMON=avdd
-ACTION_DIR=/tmp/"${DAEMON}"
+FIFO=/tmp/"${DAEMON}"-fifo
 
 # Convert integer milliseconds to floating point seconds
 ms_to_s () {
@@ -50,7 +51,7 @@ if [[ "$#" -lt 1 || "$#" -gt 3 ]]; then
   exit 128
 fi
 
-IFS=', ' read -r -a actions <<< "$1"
+IFS=', ' read -r -a mods <<< "$1"
 when="${2:-0}"
 repeat="$3"
 
@@ -59,12 +60,12 @@ if [[ ! "${when}" =~ ^[0-9]+|[mhd]$ ]]; then
   exit 128
 fi
 
-# Send the signal if this is the first run or if repeat is on
+# Write to the pipe if this is the first run or if repeat is on
 first_run=1
 while [[ "${first_run}" -eq 1 || -n "${repeat}" ]]; do
   first_run=0
 
-  # Sleep until it's time to send the signal
+  # Sleep until it's time to write to the pipe
   if [[ "${when}" != '0' ]]; then
     if [[ "${when}" =~ ^[0-9]+$ ]]; then
       sleep "$(ms_to_s "${when}")"
@@ -87,13 +88,11 @@ while [[ "${first_run}" -eq 1 || -n "${repeat}" ]]; do
     fi
   fi
 
-  # Create the signal data and send the signal to the daemon
-  daemon_pid="$(pgrep --newest --exact "${DAEMON}")"
-  if [[ -z "${daemon_pid}" ]]; then
+  # Write each command to the pipe
+  if [[ ! -p "${FIFO}" ]]; then
     printf 'The daemon %s is not running\n' "${DAEMON}" 1>&2
     exit 1
   fi
-  for action in "${actions[@]}"; do touch "${ACTION_DIR}/${action}"; done
-  kill -USR1 "${daemon_pid}"
+  for mod in "${mods[@]}"; do printf '%s\n' "${mod}" >> "${FIFO}"; done
 done