Created
April 3, 2026 11:47
-
-
Save kousu/5da679673a5e27041e0655b153f66a75 to your computer and use it in GitHub Desktop.
Insane mofo'ing coprocesses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| declare -A stdins coprocs | |
| dup() { | |
| eval "exec $1>&$2" | |
| # eval "exec $2<&$1" # or ??? are these equivalent?? | |
| # they should both be calling dup2(old_fd, new_fd) in C underneath but .. maybe not? | |
| } | |
| launch() { | |
| title=$1 | |
| coproc { while read event; do echo "$title: $event"; done } & coprocs[$title]=$! | |
| stdins[$title]=$((${#stdins[@]} + 4)) | |
| exec 3<&${COPROC[0]} | |
| eval "exec ${stdins[$title]}>&${COPROC[1]}" # ugly that we need exec here | |
| cat <&3 & # re-mux (without this you won't get any output unless you cat) | |
| # also you can't 1<&${COPROC[0]} directly, even though that would seem to be the most logical.. ? | |
| # weird: | |
| #exec 3<&${COPROC[0]} # works | |
| # exec ${stdins[$title]}>&${COPROC[1]} # fails; it tries to interpret the fd (.e.g "4") as a command to run | |
| #eval "exec ${stdins[$title]}>&${COPROC[1]}" # works | |
| #: ${stdins[$title]}>&${COPROC[1]} # works but has an implicit subshell around it so it's usefless | |
| } | |
| launch A | |
| launch B | |
| launch C | |
| echo "Hello" >&${stdins[A]} | |
| echo "We are not" >&${stdins[B]} | |
| echo "Computer" >&${stdins[C]} | |
| echo "People" >&${stdins[A]} | |
| echo "I am $$" | |
| set -x; ls -l /proc/$$/fd | |
| # bash | |
| bash --norc <&0 >&1 2>&2 4>&4 5>&5 6>&6 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| DEBOUNCE_TIME=$((5 * 60)) # 5 minutes | |
| DEBOUNCE_TIME=7 | |
| debounce() { | |
| # debounce a single stream of events | |
| # this means it takes events and only outputs the *first one* that happens | |
| # TIMEOUT="${1:-60}"; shift | |
| TIMEOUT="${DEBOUNCE_TIME}" | |
| local event | |
| local holding # flag; set when we have an event, reset when we emit it; if we have a series of timeouts in a row, we only output | |
| # we use a flag to distinguish "" from unset | |
| # (there's probably a more bash-ey way to do that? test -u ? idk) | |
| while true; do | |
| if IFS= read -t "$TIMEOUT" -r -d $'\0' new_event; then | |
| : # got an event within the timeout so that means the state is still bouncing; reset the timer and try again | |
| printf "debouncing '%s'\n" "$new_event" >&2 | |
| holding=1 | |
| event="$new_event" | |
| elif [ $? -eq 142 ]; then | |
| # timeout | |
| if [ -n "$holding" ]; then | |
| printf "emitting '%s'\n" "$event" >&2 | |
| printf "%s\0" "$event" | |
| holding="" | |
| event="" | |
| fi | |
| else | |
| # some other error | |
| return 1 | |
| fi | |
| done | |
| } | |
| # dup() { | |
| # eval "exec $1>&$2" | |
| # # eval "exec $2<&$1" # or ??? are these equivalent?? | |
| # # they should both be calling dup2(old_fd, new_fd) in C underneath but .. maybe not? | |
| # } | |
| declare -A stdins coprocs | |
| spawn_debounce() { | |
| event=$1 | |
| coproc { debounce; } & coprocs[$event]=$! | |
| stdins[$event]=$((${#stdins[@]} + 200)) | |
| exec 33<&${COPROC[0]} | |
| eval "exec ${stdins[$event]}>&${COPROC[1]}" # ugly that we need exec here | |
| cat <&33 & # re-mux (without this you won't get any output unless you cat all the individual COPROC[0]'s one by one..which is basically what this does.') | |
| # also you can't 1<&${COPROC[0]} directly, even though that would seem to be the most logical.. ? | |
| # | |
| # TODO: clean up children explicitly | |
| # weird: | |
| #exec 3<&${COPROC[0]} # works | |
| # exec ${stdins[$title]}>&${COPROC[1]} # fails; it tries to interpret the fd (.e.g "4") as a command to run | |
| #eval "exec ${stdins[$title]}>&${COPROC[1]}" # works | |
| #: ${stdins[$title]}>&${COPROC[1]} # works but has an implicit subshell around it so it's usefless | |
| } | |
| demux_and_debounce() { | |
| # this *demuxes* the inputs into distinct types | |
| # and sends each to a distinct coproc | |
| while read -r -d $'\0' dir; do | |
| echo "raw event: $dir" >&2 | |
| if [ -z "${stdins[$dir]}" ]; then | |
| spawn_debounce "$dir" | |
| fi | |
| printf "%s\0" "$dir" >&${stdins[$dir]} | |
| done | |
| } | |
| read_all() { | |
| while read -r -d $'\0' event; do | |
| if OUTPUT=$($1 "$event"); then | |
| echo "read_all: $1 $event: success" >& 2 | |
| printf "%s\0" "$OUTPUT" | |
| else | |
| echo "read_all: $1 $event: failure" >& 2 | |
| fi | |
| done | |
| } | |
| git_worktree() { | |
| # convert inotify events into top level git worktree paths | |
| # rev-parse ALWAYS outputs a trailing newline, even | |
| # perl is the only tool I could find which can strip EXACTLY ONE trailing newline | |
| (set -o pipefail; | |
| git -C "$1" rev-parse --show-toplevel | perl -pe 'chomp if eof') && | |
| printf "\0" | |
| } | |
| git_sync() { | |
| git -C "$1" status | |
| return 0 | |
| echo "syncing $1" >&2 | |
| if git -C "$1" sync; RC=$?; then | |
| RC=$? | |
| echo "success" >&2 | |
| else | |
| echo "err" >&2 | |
| RC=$? | |
| fi | |
| echo "done syncing $1; RC=$RC" >&2 | |
| } | |
| check() { | |
| # verify all the passed dirs are actually git repos | |
| err=0 | |
| for dir in "$@"; do | |
| if ! git -C "$dir" sync --able; then | |
| err=1 | |
| fi | |
| done | |
| if [ "$err" -ne 0 ]; then | |
| exit "$err" | |
| fi | |
| } | |
| check "$@" || exit $? | |
| # TIP: you can NUL-delimit things | |
| # inotifywait -m -e close_write,move,delete --format "%w%f%0" "$@" | while read -r -d $'\0' path; do ... done | |
| # but you can't so easily | |
| inotifywait -r -m -e close_write,move,delete --format "%w%0" "$@" | \ | |
| read_all git_worktree | \ | |
| demux_and_debounce | \ | |
| read_all git_sync | |
| # while read -r -d $'\0' event; do | |
| # echo "got $event" >&2 | |
| # done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment