Use the following shell function:
relay () (
#!/bin/sh
sink="${1:-/dev/ttyS0}"
exec 4<&0 2>/dev/null
while :; do
cat 3<"$sink" >/proc/self/fd/3
<&4 cat >/dev/null & sleep 2; kill -s PIPE "$!" || exit
done
)
like this:
my_daemon | relay
What looks like a shebang (#!/bin/sh
) means nothing inside the function. It's just an indication what shell the code is for. If you want to build a script, not a function, save the body of the function in an executable file and then the shebang will matter.
There are few tricks:
Solutions that use cat >/dev/ttyS0
or similar redirection are potentially flawed. I assume your words "the serial device gets recreated" mean there is a time window when /dev/ttyS0
does not exist. I also assume the user running the code (possibly root) may be able to create files in /dev/
. If >/dev/ttyS0
happens when there is no such file, a regular file will be created. You can test like ! [ -e /dev/ttyS0 ]
, but the file may disappear after the test and before >/dev/ttyS0
.
For this reason I use cat 3<"$sink" >/proc/self/fd/3
, where $sink
is /dev/ttyS0
by default. The first redirection tries to open the file for reading; the second redirection tries to redirect stdout to the same file. The trick is the second redirection never creates a new file.
An alternative would be to arrange things, so the user has access to /dev/ttyS0
, but cannot create files in /dev/
. In this case the trick would not be needed and solutions using >/dev/ttyS0
should be safe. It may be a valid approach if you cannot use /proc/self/fd/3
for whatever reason. The first cat
would be just:
cat >"$sink"
The purpose of the first cat is to send data to the sink. The redirections may fail or the sink may disappear eventually. Here comes the second cat
. The purpose of the second cat
is to discard data when there is no sink. If /dev/ttyS0
should be recreated immediately, we wouldn't need the second cat
. But I'm not sure if the pathname is guaranteed to reappear very fast in your case. I guess when there is no /dev/ttyS0
, you want my_daemon
to continue rather than to block. The second cat
lets it continue.
A simple cat >/dev/null
is not a good idea, it could run indefinitely even after /dev/ttyS0
gets recreated. The trick is to run it asynchronously and kill it after few seconds. Then the code loops and the first cat
tries to open the sink again.
<&4
is needed because of the trick. When job control is disabled (and it is by default in a subshell our function is, or in a script), commands terminated with &
get their stdin redirected to /dev/null
or an equivalent file. Thanks to prior exec 4<&0
and this <&4
, the second cat
can read from the stdin of the function anyway.
kill
will (most likely) fail if the second cat
is no more after sleep 2
. This will happen if my_daemon
exits. kill … || exit
is a trick to detect EOF condition. E.g. date | relay
should terminate, although after some delay because of sleep
. Without the trick the code would loop indefinitely.
In case of EOF it may happen the PID of the second cat
gets reused and kill
targets the wrong process. AFAIK in Linux PIDs are allocated sequentially; it's unlikely the sequence wraps around and gets to the same PID in about two seconds. The trick assumes that if the second cat
exits early because of EOF, kill
will see no such process
and fail, thus exit
will happen.
The default $sink
is /dev/ttyS0
, still you can use another pathname by specifying it as the first command line argument (e.g. … | relay foo
). If you want to test on a regular file, remember that removing a file when it's open does not destroy it. In my tests I used set -x
to see what happens and then Ctrl+d to easily terminate (the first) cat
on demand.
My testbed: Linux kernel 5.15.0, sh
(ash
) from Busybox 1.30.1.
strace
on "my_daemon", what error/result is actually causing the exit?nohup
help?nohup
orsetpgrp
do not help. From how I understand it, it's that when you enterexit
then the process gets restarted per what is written ininittab
file (mind you it's a Busybox-based system) which then causes this behaviour. – phk Sep 01 '22 at 13:51my_daemon
are discarded when/dev/ttyS0
vanishes? (2) In case/dev/ttyS0
is not recreated for "long" time andmy_daemon
tries to write, should it (a) exit? (b) block? (c) be able to write anyway (but what it writes should be discarded)? – Kamil Maciorowski Sep 02 '22 at 13:43syslog
so it starts dropping log messages when systemd decides for some asinine reason that it gets "too many" is probably the greatest sin of systemd - other than its fundamental flaw of being a brain-dead, horribly-implemented, unreliable solution in search of a problem in the first place. – Andrew Henle Sep 03 '22 at 15:38getty
internally, I could try leaving out the<id>
id field but I am afraid of breaking things. – phk Sep 09 '22 at 11:32