0

I face a situation that features Bash scripts on a Linux installation that asynchronously run system tools with a delay, via a

( sleep 10 ; some_command ) &

construct. The scripts that perform these delayed commands cannot be changed, and they are triggered by system processes outside my direct control.

I need to detect this some_command "about to happen" from another bash script. The name of the command is one of a few known alternatives (e.g. systemctl), and I only need to detect if it has been scheduled to run after the timeout implemented by sleep.

I can identify any "active" sleep calls via ps and from there on their parent PIDs. However, the parent PID belongs to the script featuring the sub-shell call, not the sub-shell itself.

Is there any way to retrieve

  • the PID of a sub-shell by knowing the currently running command (in this example, sleep), and from there
  • the complete command-list passed to that sub-shell

with command-line tools?

5
  • What is some_command? Could you replace it with a wrapper script that somehow notifies your other script? Or is there some expected result of some_command like a modified file? What is the purpose of the delayed execution? This seems to be a strange approach. Commented Jan 29 at 17:38
  • use the same technique as described here unix.stackexchange.com/a/701313/72304 Commented Jan 29 at 17:39
  • "The scripts ... cannot be changed", but you could copy them elsewhere and revise them, and change the PATH. Commented Jan 29 at 19:19
  • @Bodo This is about augmenting the capabilities of historically-grown systems "in production use". It may be that this is the only way, but if possible I want to keep modifications of the existing scripts to a minimum. Commented Jan 30 at 11:21
  • @AdminBee It is not clear from the question if some_command is one of the scripts you don't want to modify. It might be possible to use wrapper scripts without modifying the existing files by placing the replacements into a directory that comes first in PATH, e.g. /usr/local/bin/some_command as a wrapper for /usr/bin/some_command assuming that /usr/local/bin precedes /usr/bin. Commented Jan 31 at 12:31

3 Answers 3

2

You'd need to ask the shell process to tell it to you. Could be done with gdb for instance.

$ ps -fH
UID          PID    PPID  C STIME TTY          TIME CMD
chazelas    5148    5145  0 17:31 pts/2    00:00:00 /bin/zsh
chazelas    8142    5148  0 17:49 pts/2    00:00:00   bash -c (sleep 1h; echo A) & (sleep 1h; echo B) & sleep 1h
chazelas    8143    8142  0 17:49 pts/2    00:00:00     bash -c (sleep 1h; echo A) & (sleep 1h; echo B) & sleep 1h
chazelas    8145    8143  0 17:49 pts/2    00:00:00       sleep 1h
chazelas    8144    8142  0 17:49 pts/2    00:00:00     bash -c (sleep 1h; echo A) & (sleep 1h; echo B) & sleep 1h
chazelas    8147    8144  0 17:49 pts/2    00:00:00       sleep 1h
chazelas    8146    8142  0 17:49 pts/2    00:00:00     sleep 1h
chazelas    8503    5148  0 17:50 pts/2    00:00:00   ps -fH
$ gdb --pid 8143 =bash
[...]
(gdb) bt
#0  0x00007fee157668d3 in __GI___wait4 (pid=pid@entry=-1, stat_loc=stat_loc@entry=0x7ffd395961b0, options=options@entry=0, usage=usage@entry=0x0) at ../sysdeps/unix/sysv/linux/wait4.c:30
#1  0x00007fee15766a27 in __GI___waitpid (pid=pid@entry=-1, stat_loc=stat_loc@entry=0x7ffd395961b0, options=options@entry=0) at ./posix/waitpid.c:38
#2  0x0000561fe33573a2 in waitchld (block=block@entry=1, wpid=8145) at .././jobs.c:3805
#3  0x0000561fe3358b9a in wait_for (pid=8145, flags=flags@entry=0) at .././jobs.c:2980
#4  0x0000561fe334422b in execute_command_internal (command=command@entry=0x561ff269de90, asynchronous=asynchronous@entry=0, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1,
    fds_to_close=fds_to_close@entry=0x561ff269e6d0) at .././execute_cmd.c:911
#5  0x0000561fe33443d9 in execute_command (command=0x561ff269de90) at .././execute_cmd.c:413
#6  0x0000561fe3346307 in execute_connection (command=0x561ff269e060, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x561ff269e580) at .././execute_cmd.c:2757
#7  0x0000561fe3340ec4 in execute_command_internal (command=command@entry=0x561ff269e060, asynchronous=asynchronous@entry=0, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1,
    fds_to_close=fds_to_close@entry=0x561ff269e580) at .././execute_cmd.c:1040
#8  0x0000561fe33449e8 in execute_in_subshell (command=0x561ff269e0b0, asynchronous=0, asynchronous@entry=1, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1,
    fds_to_close=fds_to_close@entry=0x561ff269e580) at .././execute_cmd.c:1721
#9  0x0000561fe3340a11 in execute_command_internal (command=command@entry=0x561ff269e0b0, asynchronous=asynchronous@entry=1, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1,
    fds_to_close=fds_to_close@entry=0x561ff269e580) at .././execute_cmd.c:678
#10 0x0000561fe334624f in execute_connection (command=0x561ff269e3e0, asynchronous=1, pipe_in=-1, pipe_out=-1, fds_to_close=0x561ff269e580) at .././execute_cmd.c:2726
#11 0x0000561fe3340ec4 in execute_command_internal (command=command@entry=0x561ff269e3e0, asynchronous=asynchronous@entry=1, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1,
    fds_to_close=fds_to_close@entry=0x561ff269e580) at .././execute_cmd.c:1040
#12 0x0000561fe334624f in execute_connection (command=0x561ff269e550, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x561ff269e580) at .././execute_cmd.c:2726
#13 0x0000561fe3340ec4 in execute_command_internal (command=<optimized out>, asynchronous=asynchronous@entry=0, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1,
    fds_to_close=fds_to_close@entry=0x561ff269e580) at .././execute_cmd.c:1040
#14 0x0000561fe339e189 in parse_and_execute (string=<optimized out>, from_file=from_file@entry=0x561fe33f00a4 "-c", flags=flags@entry=20) at ../.././builtins/evalstring.c:539
#15 0x0000561fe3328fda in run_one_command (command=0x7ffd39598d99 "(sleep 1h; echo A) & (sleep 1h; echo B) & sleep 1h") at .././shell.c:1473
#16 0x0000561fe3327aa2 in main (argc=3, argv=0x7ffd39596db8, env=0x7ffd39596dd8) at .././shell.c:763
(gdb) frame 8
#8  0x0000561fe33449e8 in execute_in_subshell (command=0x561ff269e0b0, asynchronous=0, asynchronous@entry=1, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1,
    fds_to_close=fds_to_close@entry=0x561ff269e580) at .././execute_cmd.c:1721
1721    in .././execute_cmd.c
(gdb) p make_command_string(command)
$1 = 0x561ff269e680 "( sleep 1h; echo A )"
(gdb) detach
Detaching from program: /usr/bin/bash, process 8143
[Inferior 1 (process 8143) detached]
(gdb) exit

That process is running the subshell that will eventually run echo A.

2
  • That's a good idea; unfortunately I cannot install developer tools like gdb on the system in question. But it may be possible to extract the script's path from ps -o args output and the cwd link in /proc and then use grep to search for the line of the script containing sleep. If that line also contains some_command it's a hit. A dirty hack of course, and if the same script uses sleep elsewhere, I'm out of luck. Commented Jan 30 at 11:33
  • subshells will have the same command line in ps output as that of their parent (of the process in their ancestry that executed the shell interpreter); process cmdline is usually only updated upon execve() (I don't know of any shell that lets you change it, even those that let you set $0 like zsh). Commented Jan 30 at 11:56
2

You could set PS4 and set -x to enable tracing to get the filename and linenumber as each line is executed. For example, target file run has:

#!/bin/bash
echo start "$*"
( sleep "$*" ; date ) &
wait
echo done
[ 1 = "$1" ] && ./run 2

We run it from this code:

rm -f fifo
mkfifo fifo
( while read file lno rest
  do    sed -n -e "${lno}{s/^/--> /p;q}" <"$file"
  done <fifo 
) &
exec 5>fifo
export BASH_XTRACEFD=5
export PS4=' $BASH_SOURCE $LINENO '
bash -x ./run 1

which creates a fifo into which bash -x writes, from PS4, the filename and linenumber and the command being executed. The while ... sed prints that line from the file, with a --> prefix. The result is

start 1
--> echo start "$*"
--> wait
--> ( sleep "$*" ; date ) &
Wed Jan 29 20:14:39 CET 2025
done
--> ( sleep "$*" ; date ) &
start 2
--> echo done
--> [ 1 = "$1" ] && ./run 2
--> [ 1 = "$1" ] && ./run 2
Wed Jan 29 20:14:41 CET 2025
done

So you can keep this trace output in a file and grep through it for the command following sleep on the same source line.

1
  • That's actually an interesting approach. Unfortunately (and I admit I forgot to mention that), I do not call these other scripts that contain the ( sleep 10 ; some_command ) & construct. They are triggered by some automated processes, and I cannot change the invocation used there. Commented Jan 30 at 11:45
1

You could run your script as:

env 'BASH_FUNC_sleep%%=() {(exec -a "line $BASH_LINENO: sleep" sleep "$@")}' your-script

Which exports a sleep wrapper function which calls sleep with its argv[0] changed to line n: sleep, where n will be the line number in the script where sleep is invoked.

Then for example:

$ cat myscript
#! /bin/bash -
(sleep 1h; echo foo) &
(sleep 1h; echo bar) &
(sleep 1h; echo baz) &
wait
$ env 'BASH_FUNC_sleep%%=() {(exec -a "line $BASH_LINENO: sleep" sleep "$@")}' bash ./myscript &
[1] 33603
$ ps -fH
UID          PID    PPID  C STIME TTY          TIME CMD
chazelas   15065    4733  0 07:42 pts/5    00:00:01 zsh
chazelas   33603   15065  0 14:53 pts/5    00:00:00   bash ./myscript
chazelas   33604   33603  0 14:53 pts/5    00:00:00     bash ./myscript
chazelas   33606   33604  0 14:53 pts/5    00:00:00       line 2: sleep 1h
chazelas   33605   33603  0 14:53 pts/5    00:00:00     bash ./myscript
chazelas   33608   33605  0 14:53 pts/5    00:00:00       line 3: sleep 1h
chazelas   33607   33603  0 14:53 pts/5    00:00:00     bash ./myscript
chazelas   33609   33607  0 14:53 pts/5    00:00:00       line 4: sleep 1h
chazelas   33623   15065  0 14:53 pts/5    00:00:00   ps -fH

Then you can tell which one each of those bash subshells are from the argv[0] of their sleep child.

For shells other than bash, you can do something similar by putting the function definition in a file that you get the shell to source on startup.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.