├── discover-vnc.sh └── README.md /discover-vnc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | trap '{ 4 | # this block gets called before exit 5 | if [ -z "$out" ]; then 6 | echo "No hosts with VNC enabled found." 7 | exit 0 8 | fi 9 | # some time consuming calulations might be done here 10 | printf "%s\n" "${out[@]}" 11 | echo "${#out[@]} host(s) found." 12 | }' EXIT 13 | 14 | out=(); i=0 15 | while read -r line; do 16 | i=`expr $i + 1` 17 | if [ $i -lt 5 ]; then continue; fi # skip the header lines 18 | 19 | out+=("$line") 20 | 21 | # break if no more items will follow (e.g. Flags != 3) 22 | if [ $(echo $line | cut -d ' ' -f 3) -ne '3' ]; then 23 | break 24 | fi 25 | done < <((sleep 0.5; pgrep -q dns-sd && kill -13 $(pgrep dns-sd)) & # kill quickly if trapped 26 | dns-sd -B _rfb._tcp) 27 | 28 | # kill dns-sd child process 29 | pgrep -q dns-sd && kill -13 $(pgrep dns-sd) 30 | exit 0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taming non-terminating Bash processes 2 | 3 | > "[This is] bash voodoo magic.. the same magic that gave us shellshock." — [Hacker News](https://news.ycombinator.com/item?id=8578072) 4 | 5 | A couple of months ago I found myself hacking on a sophisticated workflow for the brand new [Alfred 2](http://www.alfredapp.com/) - a powerful replacement for Spotlight on OS X. This major release enabled support for populating Alfred's results using scripting languages. 6 | 7 | My goal was to make screen sharing with Alfred a quick and painless endeavor. The user would enter "vnc" to get a list of available hosts with VNC enabled to choose from. The workflow should run on every OS X device without installing any kind of 3rd party software and leaving no side-effects — a simple Bash script should be perfect. 8 | 9 | That's when I entered the world of non-terminating Bash processes. 10 | 11 | ## Discovering network services from the command line 12 | 13 | Hunting for a way to discover network services from the command line, I ended up with a tool called `dns-sd`. 14 | 15 | ``` 16 | $ whatis dns-sd 17 | dns-sd(1) - Multicast DNS (mDNS) & DNS Service Discovery (DNS-SD) Test Tool 18 | ``` 19 | 20 | The [man page](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dns-sd.1.html) revealed that `dns-sd -B type domain` will "browse for instances of service type in domain". After some research I figured out that the service type for VNC is *\_rfb.\_tcp*. 21 | 22 | ``` 23 | # /etc/services contains a list of service types mapped to ports and protocols: 24 | $ cat /etc/services | grep vnc 25 | rfb 5900/tcp vnc-server # VNC Server 26 | rfb 5900/udp vnc-server # VNC Server 27 | ``` 28 | 29 | In case you're wondering, *rfb* stands for *remote framebuffer*. Running `dns-sd -B _rfb._tcp` and... 30 | 31 | ``` 32 | Browsing for _rfb._tcp 33 | DATE: ---Mon 04 Nov 2013--- 34 | 11:43:44.909 ...STARTING... 35 | Timestamp A/R Flags if Domain Service Type Instance Name 36 | 11:43:44.910 Add 3 4 local. _rfb._tcp. Brainbug 37 | 11:43:44.910 Add 2 4 local. _rfb._tcp. Tesla 38 | ``` 39 | 40 | Bingo, that's it! 41 | 42 | ## The never-ending loop 43 | 44 | The problem is that `dns-sd -B` never terminates. It will continue to display changes in network services forever until you interrupt it (e.g Ctrl+c). 45 | 46 | ```bash 47 | #!/bin/bash 48 | while read -r line; do # trapped in the loop 49 | echo $line 50 | done < <(dns-sd -B _rfb._tcp) 51 | 52 | echo "This is never gonna be displayed." 53 | ``` 54 | 55 | We have to break out of the loop at some point. Digging a little bit further unveils that `dns-sd` will send a "3" in the "Flags" column if there's more to display (see output above). In any other case there will be a different value, so let's skip the header and check for the flag in the subsequent lines. 56 | 57 | ```bash 58 | #!/bin/bash 59 | i=0 60 | while read -r line; do 61 | i=`expr $i + 1` 62 | if [ $i -lt 5 ]; then continue; fi # skip the header lines 63 | 64 | echo $line 65 | 66 | # break if no more items will follow (e.g. Flags != 3) 67 | if [ $(echo $line | cut -d ' ' -f 3) -ne '3' ]; then 68 | break 69 | fi 70 | done < <(dns-sd -B _rfb._tcp) 71 | 72 | echo "This _is_ displayed." 73 | ``` 74 | 75 | This breaks out of the loop but `dns-sd` continues to run in a subshell (`<(dns-sd -B _rfb._tcp)`) even if the parent process exits. If we don't kill it manually the process will run forever in the background. 76 | 77 | ## Kill the children 78 | 79 | Nothing simpler than that. Let's just kill the child process before exiting the script. 80 | 81 | ```bash 82 | #!/bin/bash 83 | i=0 84 | while read -r line; do 85 | i=`expr $i + 1` 86 | if [ $i -lt 5 ]; then continue; fi # skip the header lines 87 | 88 | echo $line 89 | 90 | # break if no more items will follow (e.g. Flags != 3) 91 | if [ $(echo $line | cut -d ' ' -f 3) -ne '3' ]; then 92 | break 93 | fi 94 | done < <(dns-sd -B _rfb._tcp) 95 | 96 | # kill child processes 97 | kill -9 $(pgrep dns-sd) # SIGINT is not enough, let's send SIGKILL 98 | ``` 99 | 100 | Success! No more background processes after exit. However, there's this nasty problem with SIGKILL's verbose nature. 101 | 102 | ``` 103 | $ ./discover-vnc.sh # contains the code above 104 | 13:07:12.542 Add 3 4 local. _rfb._tcp. Brainbug 105 | 13:07:12.542 Add 2 4 local. _rfb._tcp. Tesla 106 | [1] 58181 killed ./discover-vnc.sh 107 | ``` 108 | 109 | It's crucial to suppress this line. Doing some research on the topic revealed the following: 110 | 111 | > "If a pipeline in a shell script is killed by a signal other than SIGINT or SIGPIPE, the shell reports it. People generally want to know when their processes are killed. It's 112 | independent of job control." 113 | > — [Chet Ramey on the Bash mailing list](http://lists.gnu.org/archive/html/bug-bash/2006-09/msg00073.html) 114 | 115 | SIGKILL is too verbose, SIGINT is too soft, let's hope that SIGPIPE (-13) will do the trick. 116 | 117 | ```bash 118 | #!/bin/bash 119 | i=0 120 | while read -r line; do 121 | i=`expr $i + 1` 122 | if [ $i -lt 5 ]; then continue; fi # skip the header lines 123 | 124 | echo $line 125 | 126 | # break if no more items will follow (e.g. Flags != 3) 127 | if [ $(echo $line | cut -d ' ' -f 3) -ne '3' ]; then 128 | break 129 | fi 130 | done < <(dns-sd -B _rfb._tcp) 131 | 132 | # kill child processes 133 | kill -13 $(pgrep dns-sd) # SIGPIPE to the rescue 134 | ``` 135 | 136 | It does the trick. The child process gets killed while the termination message is suppressed: 137 | 138 | ``` 139 | $ ./discover-vnc.sh # contains the code above 140 | 13:07:12.542 Add 3 4 local. _rfb._tcp. Brainbug 141 | 13:07:12.542 Add 2 4 local. _rfb._tcp. Tesla 142 | 143 | $ ps aux |grep dns-sd 144 | # no matching processes found 145 | ``` 146 | 147 | There's still one more problem to solve. If there's no VNC service available `dns-sd` won't return a line for us to check the Flags column for value != 3, therefore the loop will never break and the script will run forever. 148 | 149 | ## Still trapped in the loop 150 | 151 | The nature of `dns-sd` prevents us from breaking the loop in this case but if there are results they are returned almost instantly (they're probably kept in memory). Due to this fact we can assume that after a couple of hundred milliseconds there won't be any results any time soon and we can kill the script after a short period. To achieve this we use `sleep` as a timer followed by a `kill`. 152 | 153 | ```bash 154 | #!/bin/bash 155 | i=0 156 | while read -r line; do 157 | i=`expr $i + 1` 158 | if [ $i -lt 5 ]; then continue; fi # skip the header lines 159 | 160 | echo $line 161 | 162 | # break if no more items will follow (e.g. Flags != 3) 163 | if [ $(echo $line | cut -d ' ' -f 3) -ne '3' ]; then 164 | break 165 | fi 166 | done < <((sleep 0.5; pgrep -q dns-sd && kill -13 $(pgrep dns-sd)) & # kill quickly if trapped 167 | dns-sd -B _rfb._tcp) 168 | 169 | # kill child processes 170 | pgrep -q dns-sd && kill -13 $(pgrep dns-sd) 171 | ``` 172 | 173 | A new child process (`(sleep 0.5; pgrep -q dns-sd && kill -13 $(pgrep dns-sd)) &`) is now running in the background followed by the `dns-sd` process. After 500ms it sends a SIGPIPE and the script will exit, no matter what. It's important to remember that any code after the loop is not executed in this case, as the script is terminated while still being trapped in the loop. 174 | 175 | ## Do some work before termination 176 | 177 | For being able to run code before exiting the script we can define a `trap`. This is helpful if we want to run some more logic on the results which might take longer than 500ms and would be brutally killed by our timer. 178 | 179 | ```bash 180 | #!/bin/bash 181 | 182 | trap '{ 183 | # this block gets called before exit 184 | if [ -z "$out" ]; then 185 | echo "No hosts with VNC enabled found." 186 | exit 0 187 | fi 188 | # some time consuming calulations might be done here 189 | printf "%s\n" "${out[@]}" 190 | echo "${#out[@]} host(s) found." 191 | }' EXIT 192 | 193 | out=(); i=0 194 | while read -r line; do 195 | i=`expr $i + 1` 196 | if [ $i -lt 5 ]; then continue; fi # skip the header lines 197 | 198 | out+=("$line") 199 | 200 | # break if no more items will follow (e.g. Flags != 3) 201 | if [ $(echo $line | cut -d ' ' -f 3) -ne '3' ]; then 202 | break 203 | fi 204 | done < <((sleep 0.5; pgrep -q dns-sd && kill -13 $(pgrep dns-sd)) & # kill quickly if trapped 205 | dns-sd -B _rfb._tcp) 206 | 207 | # kill child processes 208 | pgrep -q dns-sd && kill -13 $(pgrep dns-sd) 209 | exit 0 210 | ``` 211 | 212 | At this point we're done. To run the above script paste the following line into your terminal: 213 | 214 | ``` 215 | bash <(curl -s https://raw.githubusercontent.com/pstadler/non-terminating-bash-processes/master/discover-vnc.sh) 216 | ``` 217 | 218 | This approach is being used in the following projects: 219 | 220 | - [Screen Sharing for Alfred](https://github.com/pstadler/alfred-screensharing) — Connect to a host in Alfred with automatic network discovery. 221 | - [Mount Network Shares with Alfred](https://github.com/pstadler/alfred-mount) — Use Alfred to connect to your network shares with ease. 222 | - [AirControl](https://github.com/AdRoll/AirControl) — Controlling AirPlay Display Mirroring from the command line. 223 | 224 | ## Conclusion 225 | 226 | Advanced Bash scripting can cause nasty hacks and unexpected side-effects but there's always a way to work around them. Many ways lead to rome and there could be a more sane way to achieve the same. Make sure to check out this [discussion](https://news.ycombinator.com/item?id=8577729) on Hacker News. 227 | 228 | Please get in touch with me if you have any questions or suggestions related to this topic. You can find me on [Twitter](https://twitter.com/pstadler) and [GitHub](https://github.com/pstadler). 229 | --------------------------------------------------------------------------------