sahinakkaya.dev/_posts/2023-01-16-hot-reloading-with-trap-and-kill.md

244 lines
6.7 KiB
Markdown
Raw Permalink Normal View History

2023-01-16 04:58:43 +01:00
---
title: "Hot-Reload Long Running Shell Scripts (feat. trap / kill)"
date: 2023-01-16 00:48:08 +0300
2024-08-04 20:13:42 +02:00
classes: wide
2023-01-16 04:58:43 +01:00
tags: trap kill linux
---
## `trap` them and `kill` them!
There is a beautiful command in Linux called [`trap`](https://man7.org/linux/man-pages/man1/trap.1p.html) which *trap*s signals and let you run specific commands when they invoked. There is also good ol' [`kill`](https://man7.org/linux/man-pages/man1/kill.1.html) command which not only kills processes but allows you to specify a signal to send. By combining these two, you can run specific functions from your scripts any time!
### Basic Example
Let's start by creating something very simple and build up from there. Create a script with the following contents:
```bash
#!/bin/bash
echo "My pid is $$. Send me SIGUSR1!"
func() {
echo "Got SIGUSR1"
}
2023-01-16 05:35:55 +01:00
# here we are telling that run 'func' when USR1 signal is
# received. You can run anything. Combine commands with ; etc.
2023-01-16 04:58:43 +01:00
trap "func" USR1
# The while loop is important here otherwise our script will exit
# before we manage to get a chance to send a signal.
while true ; do
echo "waiting SIGUSR1"
sleep 1
done
```
Now make it executable and run it:
```bash
chmod +x trap_example
./trap_example
My pid is 2811137. Send me SIGUSR1!
waiting SIGUSR1
waiting SIGUSR1
waiting SIGUSR1
waiting SIGUSR1
waiting SIGUSR1
```
Open another terminal and send your signal with `kill` to the specified pid.
```bash
kill -s USR1 2811137
```
You should receive `"Got SIGUSR!"` from the other process. That's it! Now, imagine you write whatever thing you want to execute in `func` and then you can simply `kill -s ...` anytime and as many times you want!
Let's move the while loop into the `func` and add some variables so you can see how powerful this is.
```bash
#!/bin/bash
echo "My pid is $$. Send me SIGUSR1!"
func() {
i=1
while true ; do
echo "i: $i"
i=$(( i + 1 ))
sleep 1
done
}
trap "echo 'Got SIGUSR1!'; func" USR1
# we need to call the function once, otherwise script
# will exit before we manage to send a signal
func
```
Now run the script and send `SIGUSR1`. Here is the result:
```bash
./trap_example
My pid is 2880704. Send me SIGUSR1!
i: 1
i: 2
i: 3
i: 4
i: 5
i: 6
i: 7
Got SIGUSR1!
i: 1
i: 2
i: 3
i: 4
i: 5
Got SIGUSR1!
i: 1
i: 2
^C
```
Isn't this neat?
### More useful example
Let's imagine you have multiple long running (infinite loops basically) scripts and you want to restart them without manually searching for their pid's and killing them. `trap` is for the rescue, again! <sup>[*](## "Yeah, I know you can run a systemd service if you want but I think it is an overkill for this situation. Plus, I don't like dealing with them.")</sup> This command is awesome.
Without further ado, let's get started. Create a script called `script1` with the following contents:
```bash
#!/bin/bash
# file: script1
i=1
while true ; do
echo "Hello from $0. i is $i"
i=$(( i + 1 ))
sleep 1
done
```
And symlink it to another name just for fun:
```bash
chmod +x script1
ln -s script1 script2
```
Now we can pretend they are two different scripts as their outputs differ:
```bash
./script1
Hello from ./script1. i is 1
Hello from ./script1. i is 2
Hello from ./script1. i is 3
Hello from ./script1. i is 4
^C
./script2
Hello from ./script2. i is 1
Hello from ./script2. i is 2
Hello from ./script2. i is 3
^C
```
Finally, create the main script which will start child scripts and restart them on our signals:
```bash
#!/bin/bash
echo "My pid is $$. You know what to do ( ͡° ͜ʖ ͡°)"
echo "You can also kill me with 'kill -s INT -\`pgrep -f `basename $0`\`'"
2023-01-16 04:58:43 +01:00
pids=() # we will store the pid's of child scripts here
scripts_to_be_executed=("./script1" "./script2")
kill_childs(){ # wow, this sounded wild
for pid in "${pids[@]}"
do
echo killing "$pid"
# -P: kill all the processes whose parent process is 'pid'
# see how we are creating processes below
pkill -P "$pid"
2023-01-16 04:58:43 +01:00
done
pids=()
}
# kill childs and restart all the scripts
restart_scripts(){
kill_childs
# for each script in the list
for script in "${scripts_to_be_executed[@]}"
do
# Run the script and store its pid.
# note the '&' at the end of command. Without it the script will
# block until its execution is finished. Also we are putting it
# into braces because we want to create a "process group" so that
# we can kill all its children later by specifying parent pid
# (useful if you have pipes (|) or other &'s in your script!)
($script) &
pids+=("$!")
2023-01-16 04:58:43 +01:00
done
}
# we will restart_scripts with SIGUSR1 signal
trap 'echo "restarting scripts"; restart_scripts' USR1
# we will kill all the childs and exit the main script with SIGINT
# which is same signal as when you press <Control-C> on your terminal
trap 'echo exiting; kill_childs; exit' INT
# run the function once
restart_scripts
# infinite loop, otherwise main script will exit before we send signal.
# remember, we started child processes with '&' so they won't block this script
while true; do
sleep 1
done
```
Now, you can run your main script and reload your child scripts any time with `killall main_script -USR1`
Here is an example run:
```
./trap_multiple
My pid is 3124123. You know what to do ( ͡° ͜ʖ ͡°)
You can also kill me with 'kill -s INT -`pgrep -f trap_multiple`'
2023-01-16 04:58:43 +01:00
Hello from ./script1. i is 1
Hello from ./script2. i is 1
Hello from ./script2. i is 2
Hello from ./script1. i is 2
Hello from ./script2. i is 3
Hello from ./script1. i is 3
restarting scripts
killing 3124125
killing 3124126
Hello from ./script1. i is 1
Hello from ./script2. i is 1
Hello from ./script2. i is 2
Hello from ./script1. i is 2
Hello from ./script2. i is 3
Hello from ./script1. i is 3
Hello from ./script2. i is 4
Hello from ./script1. i is 4
restarting scripts
killing 3124304
killing 3124305
Hello from ./script1. i is 1
Hello from ./script2. i is 1
Hello from ./script1. i is 2
Hello from ./script2. i is 2
^Cexiting
killing 3124875
killing 3124876
```
### Final words
2023-01-16 05:35:55 +01:00
I think I am started to getting obsessed with `trap` command because it has such a good name and purpose. FOSS people are really on another level when it comes to naming. Here is another good one:
2023-01-16 04:58:43 +01:00
> \- How can you see the contents of a file? <br>
\+ You *`cat`* it. <br>
\- What if you want to see them in reverse order? <br>
\+ You *`tac`* it. <br>
No, it is not just a joke. Try it... Man I love Gnoo slash Linux.
2023-01-16 05:35:55 +01:00
Anyway, I hope now you know how to `trap` and `kill`. Next week I will explain how to `unzip; strip; touch; finger; grep; mount; fsck; more; yes; fsck; fsck; umount; clean; sleep` <nobr>( ͡° ͜ʖ ͡°)</nobr>. <sup>[*](## "jk :D")</sup>