When a program is launched (by one of the exec(3)
family of system calls), it inherits the environment (i.e., shell variables export
ed) and the open files from the parent. What is done when launching a program is a fork(2)
, the child sets up the environment and files, then exec(3)
s the new program. When a shell does this, STDIN, STDOUT and STDERR are connected to the terminal. What any graphic launcher does is up to it, but is should connect them to /dev/null
(where should keyboard input come from, and where should output go to?).
If a program launched like that in turn calls exec(3)
, it is as explained above. system(3)
is a bit more complex, as it spawns a shell to do command line parsing and so on, and that shell then exec(3)
s the command. But the mechanics is the same: Files are inherited, as is the environment.
Another form of this problem occurs with long running applications whose logs are periodically rotated. Even if you move the original log (e.g., mv log.txt log.1
) and replace it immediately with a file of the same name before any actual logging occurs, if the process is holding the file open, it will either end up writing to log.1
(because that may still be the open inode) or to nothing.
A common way to deal with this (the system logger itself works this way) is to implement a signal handler in the process which will close and reopen its logs. Then, when ever you want to move or clear (by deleting) the log, send that signal to the process immediately afterward.
Here's a simple demonstration for bash -- forgive my cruddy shell skills (but if you are going to edit this for best practices, etc., please make sure you understand the functionality first and test your revision before you edit):
#!/bin/bash
trap sighandler INT
function sighandler () {
touch log.txt
exec &> log.txt
}
echo $BASHPID
exec &> log.txt
count=0;
while [ $count -lt 60 ]; do
echo "$BASHPID Count is now $count"
sleep 2
((count++))
done
Start this by forking into the background:
> ./test.sh &
12356
Notice it reports its PID to the terminal and then begins logging to log.txt
.
You now have 2 minutes to play around. Wait a few seconds and try:
> mv log.txt log.1 && kill -s 2 12356
Just plain kill -2 12356
may work for you here too. Signal 2 is SIGINT (it's also what Ctrl-C does, so you could try this in the foreground and move or remove the logfile from another terminal), which the trap
should trap. To check;
> cat log.1
12356 Count is now 0
12356 Count is now 1
12356 Count is now 2
12356 Count is now 3
12356 Count is now 4
12356 Count is now 5
12356 Count is now 6
12356 Count is now 7
12356 Count is now 8
12356 Count is now 9
12356 Count is now 10
12356 Count is now 11
12356 Count is now 12
12356 Count is now 13
12356 Count is now 14
Now let's see if it is still writing to a log.txt
even though we moved it:
> cat log.txt
12356 Count is now 15
12356 Count is now 16
12356 Count is now 17
12356 Count is now 18
12356 Count is now 19
12356 Count is now 20
12356 Count is now 21
Notice it kept going right where it left off. If you don't want to keep the record simply clear the log by deleting it
> rm -f log.txt && kill -s 2 12356
Check:
> cat log.txt
12356 Count is now 29
12356 Count is now 30
12356 Count is now 31
12356 Count is now 32
12356 Count is now 33
12356 Count is now 34
12356 Count is now 35
12356 Count is now 36
Still going.
You can't do this in a shell script for an executed subprocess, unfortunately, because if it is in the foreground, bash's own signal handlers (trap
s) are suspended, and if you fork it into the background, you can't reassign its output. I.e., this is something you have to implement in your application.
However...
If you can't modify the application (e.g., because you did not write it), I have a CLI utility you can use as an intermediary. You could also implement a simple version of this in a script which serves as a pipe to the log:
#!/bin/bash
trap sighandler INT
function sighandler () {
touch log.txt
exec 1> log.txt
}
echo "$0 $BASHPID"
exec 1> log.txt
count=0;
while read; do
echo $REPLY
done
Let's call this pipetrap.sh
. Now we need a separate program to test with, mimicking the application you want to log:
#!/bin/bash
count=0
while [ $count -lt 60 ]; do
echo "$BASHPID Count is now $count"
sleep 2
((count++))
done
That will be test.sh
:
> (./test.sh | ./pipetrap.sh) &
./pipetrap.sh 15859
These are two separate processes with separate PIDs. To clear test.sh
's output, which is being funnelled through pipetrap.sh
:
> rm -f log.txt && kill -s 2 15859
Check:
>cat log.txt
15858 Count is now 6
15858 Count is now 7
15858 Count is now 8
15858, test.sh
, is still running and its output is being logged. In this case, no modifications to the application are needed.
Best Answer
Many (most?) programs, and practically all C or C++ programs, 'fully' buffer stdout when it is a pipe or disk file, or in general not
isattty()
, and any output still in the buffer and not flushed to the OS when you kill it is lost. Programs normally do NOT do this buffering for stderr, which is why that works.In general use
stdbuf -oL program ...
or possibly-o0
. Some programs have their own private options e.g. GNU sed has-u/--unbuffered
.Alternatively use something that runs the program under a pty and thus turns off buffering, including
script
,screen
,expect
or its simplified formunbuffer
, orssh -t
.For more details and options see Turn off buffering in pipe