Возможно ли найти полный список команд, который будет выполняться в под-оболочке?

Вопрос или проблема

У меня возникла ситуация с Bash-скриптами в системе Linux, которые асинхронно вызывают отложенные команды через

( sleep 10 ; some_command ) &

. Скрипты, выполняющие эти отложенные команды, не могут быть изменены.

Мне нужно обнаружить эту команду some_command, “которая вот-вот произойдет”, из другого Bash-скрипта. Имя команды известно, мне нужно только обнаружить, была ли она запланирована для выполнения после таймаута, реализованного с помощью sleep.

Я могу определить любые “активные” вызовы sleep через ps и оттуда их родительские PID. Однако, родительский PID принадлежит скрипту, который вызывает под-оболочку, а не самой под-оболочке.

Существует ли какой-либо способ получить

  • PID под-оболочки, зная выполняемую команду (в данном примере, sleep), и оттуда
  • полный список команд, переданных этой под-оболочке

с помощью командных инструментов?

Вам нужно попросить процесс оболочки рассказать вам об этом. Это можно сделать с помощью gdb, например.

$ 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

Этот процесс выполняет под-оболочку, которая в конечном итоге выполнит echo A.

Вы можете установить PS4 и set -x, чтобы включить трассировку и получить имя файла и номер строки при выполнении каждой строки.
Например, целевой файл run содержит:

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

Мы запускаем его из этого кода:

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

что создает fifo, в который bash -x записывает, из PS4, имя файла и номер строки и выполняемую команду. Команда while ... sed печатает эту строку из файла с префиксом -->. Результат:

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

Таким образом, вы можете хранить этот вывод трассировки в файле и искать в нем команду, следующую за sleep на той же исходной строке.

.

Ответ или решение

В современных системах, особенно при работе с командными оболочками Bash в ОС Linux, возникает множество задач, связанных с управлением процессами и пониманием того, что именно выполняется в данный момент. Один из таких случаев — отслеживание и выявление команд, которые установлены для выполнения внутри подчинённых оболочек, особенно если они запускаются асинхронно или с задержкой.

Теория

Подобные сценарии часто возникают, когда сотрудники ИТ-департамента сталкиваются с необходимостью отладки сложных скриптов, которые нельзя изменять напрямую. Часто это происходит на серверах или в продуктах, код которых принадлежит другим командам или внешним поставщикам. Задача существенно усложняется, если подсистема использует вложенные конструкции, например, компенсация возрастающих таймаутов или выполнение команд, зависящих от временных задержек.

Для выявления команд, которые планируются к исполнению, прежде всего, нужно понимать, что каждая подкоманда Bash — это отдельный процесс в системе. Когда мы видим компонент, такой как ( sleep 10; some_command ) &, мы имеем дело с командой, исполняемой внутри подсистемы, которая получает собственный PID.

Пример

Предположим, у нас есть следующая строка кода:

( sleep 10; some_command ) &

Конструкция sleep задерживает выполнение some_command. Далее, чтобы отследить выполнение этой команды, можно использовать различные инструменты, такие как ps, чтобы мониторить sleep, извлекая его родительский процесс и анализируя процессное древо. Однако ps может отображать родительский процесс, являющийся оболочкой, а не содержимое самой подкоманды.

Одно из решений, предложенных опытными инженерами, заключается в использовании отладчика gdb для извлечения информации из работающих процессов. Например, подключаясь к PID процесса Bash, который исполняет подсистему, можно следить за состоянием и вызовами функций, извлекая строки команд:

gdb --pid <PID_процесса>

После некоторых манипуляций в gdb, возможно, удастся извлечь команду, запланированную подсистемой.

Применение

Практическое применение этой информации может включать автоматизацию задач по мониторингу планируемых команд. Рассмотрим способ с использованием переменной отладки PS4 и возможности запуска Bash в режиме -x для трассировки команд. Это позволяет автоматически сохранять хронологию команд в файл или другой выходной поток, анализируя после этого содержимое для нужных строк:

  1. Создайте FIFO-файл для записи отладочной информации (mkfifo fifo).
  2. Настройте переменные окружения: BASH_XTRACEFD для указания файла или потока для вывода отладочной информации и PS4 для формата этой информации.
  3. Запустите Bash-скрипт в режиме отладки.

Полученные данные можно сохранять и анализировать, ведь в них будут присутствовать не только команды, но и путь к файлу скрипта и номера строк. Это дает возможность четко определить, где и какие команды были запланированы для выполнения.

Резюмируя, необходимо отметить, что решения этой проблемы требуют комплексного подхода, начиная от понимания структуры процесса в Linux до использования отладочных инструментов. В частности, использование gdb и возможностей Bash может дать глубокий взгляд на выполнение команд, что, в свою очередь, позволит своевременно реагировать на потенциальные проблемы или аномалии в работе системных скриптов.

Оцените материал
Добавить комментарий

Капча загружается...