Загрузка данных
sudo tee /usr/local/bin/watch-gitlab-builds-detail >/dev/null <<'EOF'
#!/usr/bin/env bash
set -uo pipefail
INTERVAL="${1:-3}"
LOG_LINES="${LOG_LINES:-10}"
EVENT_LINES="${EVENT_LINES:-8}"
PROCESS_LINES="${PROCESS_LINES:-8}"
if ! [[ "${INTERVAL}" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
echo "Использование: $0 [интервал_обновления_в_секундах]"
exit 1
fi
FRAME_FILE="$(mktemp)"
SUDO_KEEPALIVE_PID=""
cleanup()
{
if [ -n "${SUDO_KEEPALIVE_PID}" ]; then
kill "${SUDO_KEEPALIVE_PID}" 2>/dev/null || true
fi
rm -f "${FRAME_FILE}"
# Показать курсор.
printf '\033[?25h'
# Вернуться из альтернативного экрана терминала.
printf '\033[?1049l'
}
trap cleanup EXIT
trap 'exit 0' INT TERM
# Использовать docker напрямую либо через sudo.
if docker info >/dev/null 2>&1; then
DOCKER=(docker)
else
sudo -v || exit 1
DOCKER=(sudo docker)
# Поддерживать sudo-сессию во время долгой сборки.
(
while true; do
sudo -n true 2>/dev/null || exit
sleep 50
done
) &
SUDO_KEEPALIVE_PID="$!"
fi
explain_stage()
{
local stage="$1"
case "${stage}" in
*" Downloading")
echo "Скачиваются исходники или архив пакета."
;;
*" Extracting")
echo "Распаковываются исходники пакета."
;;
*" Patching")
echo "Применяются патчи Buildroot."
;;
*" Configuring")
echo "Пакет настраивается перед компиляцией."
;;
*" Building")
echo "Идёт компиляция пакета."
;;
*" Installing to host directory")
echo "Пакет устанавливается в host-инструменты Buildroot."
;;
*" Installing to staging directory")
echo "Библиотеки и заголовки устанавливаются в staging/sysroot."
;;
*" Installing to target")
echo "Файлы устанавливаются в целевую rootfs."
;;
*" Fixing libtool files")
echo "Buildroot исправляет служебные пути libtool."
;;
*" Finalizing host directory")
echo "Завершается подготовка host-каталога."
;;
*" Finalizing target directory")
echo "Завершается подготовка целевой rootfs."
;;
*" Generating filesystem image"*|*" Generating root filesystem"*)
echo "Формируется итоговый образ файловой системы."
;;
*" Executing post-image script"*)
echo "Выполняется финальный post-image скрипт."
;;
*)
echo "Выполняется указанный этап Buildroot."
;;
esac
}
# Перейти в альтернативный экран терминала.
printf '\033[?1049h'
# Скрыть курсор.
printf '\033[?25l'
while true; do
# Автоматически найти все активные build-контейнеры GitLab Runner.
mapfile -t CONTAINERS < <(
"${DOCKER[@]}" ps \
--format '{{.ID}}|{{.Names}}|{{.Status}}' \
2>/dev/null |
awk -F '|' '$2 ~ /^runner-.*-build$/'
)
WIDTH="$(tput cols 2>/dev/null || printf '160')"
if ! [[ "${WIDTH}" =~ ^[0-9]+$ ]]; then
WIDTH=160
fi
# Новый экран сначала полностью собирается в файле.
: > "${FRAME_FILE}"
{
printf 'GitLab Runner build monitor — detailed mode\n'
printf 'Time: %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')"
printf 'Refresh: %s seconds\n' "${INTERVAL}"
printf 'Active build containers: %d\n' "${#CONTAINERS[@]}"
printf '%s\n' \
'================================================================================'
if [ "${#CONTAINERS[@]}" -eq 0 ]; then
echo
echo "Нет активных сборок GitLab Runner."
echo "Ожидание новой job..."
else
for CONTAINER_INFO in "${CONTAINERS[@]}"; do
IFS='|' read -r \
CID \
CONTAINER_NAME \
CONTAINER_STATUS \
<<< "${CONTAINER_INFO}"
ENVIRONMENT="$(
"${DOCKER[@]}" inspect \
--format '{{range .Config.Env}}{{println .}}{{end}}' \
"${CID}" \
2>/dev/null || true
)"
# Контейнер мог завершиться между docker ps и inspect.
[ -n "${ENVIRONMENT}" ] || continue
JOB_NAME="$(
sed -n 's/^CI_JOB_NAME=//p' \
<<< "${ENVIRONMENT}" |
head -n1
)"
JOB_ID="$(
sed -n 's/^CI_JOB_ID=//p' \
<<< "${ENVIRONMENT}" |
head -n1
)"
PIPELINE_ID="$(
sed -n 's/^CI_PIPELINE_ID=//p' \
<<< "${ENVIRONMENT}" |
head -n1
)"
PROJECT_DIR="$(
sed -n 's/^CI_PROJECT_DIR=//p' \
<<< "${ENVIRONMENT}" |
head -n1
)"
OUTPUT_DIR="$(
sed -n 's/^OUTPUT_DIR=//p' \
<<< "${ENVIRONMENT}" |
head -n1
)"
JOB_NAME="${JOB_NAME:-unknown}"
JOB_ID="${JOB_ID:-unknown}"
PIPELINE_ID="${PIPELINE_ID:-unknown}"
LOG_FILE=""
if [ -n "${OUTPUT_DIR}" ]; then
if [[ "${OUTPUT_DIR}" = /* ]]; then
LOG_FILE="${OUTPUT_DIR}/build.log"
elif [ -n "${PROJECT_DIR}" ]; then
LOG_FILE="${PROJECT_DIR}/${OUTPUT_DIR}/build.log"
fi
fi
# Резервный поиск самого свежего build.log.
if [ -z "${LOG_FILE}" ]; then
LOG_FILE="$(
"${DOCKER[@]}" exec "${CID}" sh -lc '
base="${CI_PROJECT_DIR:-/builds}"
find "${base}/output" \
-maxdepth 3 \
-type f \
-name build.log \
-printf "%T@ %p\n" \
2>/dev/null |
sort -nr |
head -n1 |
cut -d" " -f2-
' 2>/dev/null || true
)"
fi
STATS="$(
"${DOCKER[@]}" stats \
--no-stream \
--format \
'CPU={{.CPUPerc}} MEM={{.MemUsage}} NET={{.NetIO}} BLOCK={{.BlockIO}} PIDS={{.PIDs}}' \
"${CID}" \
2>/dev/null || true
)"
echo
printf 'JOB: %s\n' "${JOB_NAME}"
printf 'Job ID: %s Pipeline ID: %s\n' \
"${JOB_ID}" \
"${PIPELINE_ID}"
printf 'CID: %s\n' "${CID}"
printf 'Container: %s\n' "${CONTAINER_NAME}"
printf 'Status: %s\n' "${CONTAINER_STATUS}"
printf 'Resources: %s\n' "${STATS:-unavailable}"
printf 'Output: %s\n' "${OUTPUT_DIR:-not set}"
printf 'Log: %s\n' "${LOG_FILE:-not found}"
printf '%s\n' \
'--------------------------------------------------------------------------------'
if [ -n "${LOG_FILE}" ]; then
CURRENT_STAGE="$(
"${DOCKER[@]}" exec "${CID}" sh -lc '
log="$1"
if [ -f "${log}" ]; then
grep "^>>>" "${log}" |
tail -n1
fi
' sh "${LOG_FILE}" \
2>/dev/null || true
)"
LAST_PROGRESS="$(
"${DOCKER[@]}" exec "${CID}" sh -lc '
log="$1"
if [ -f "${log}" ]; then
grep -E "\[[0-9]+/[0-9]+\]" "${log}" |
tail -n1
fi
' sh "${LOG_FILE}" \
2>/dev/null || true
)"
LOG_INFO="$(
"${DOCKER[@]}" exec "${CID}" sh -lc '
log="$1"
if [ -f "${log}" ]; then
modified=$(stat -c "%Y" "${log}")
size=$(stat -c "%s" "${log}")
now=$(date +%s)
age=$((now - modified))
echo "Size=${size} bytes, updated ${age} seconds ago"
fi
' sh "${LOG_FILE}" \
2>/dev/null || true
)"
BUILD_TIME_LOG="${LOG_FILE%/build.log}/build/build-time.log"
echo "CURRENT BUILDROOT STAGE:"
if [ -n "${CURRENT_STAGE}" ]; then
printf '%s\n' "${CURRENT_STAGE}" |
cut -c "1-${WIDTH}"
printf 'Что происходит: %s\n' \
"$(explain_stage "${CURRENT_STAGE}")"
else
echo "Этап ещё не определён."
fi
if [ -n "${LOG_INFO}" ]; then
printf 'Состояние лога: %s\n' "${LOG_INFO}"
fi
if [ -n "${LAST_PROGRESS}" ]; then
echo
echo "LAST INTERNAL PROGRESS:"
printf '%s\n' "${LAST_PROGRESS}" |
cut -c "1-${WIDTH}"
fi
echo
echo "LAST BUILDROOT EVENTS:"
"${DOCKER[@]}" exec "${CID}" sh -lc '
timing="$1"
lines="$2"
if [ -f "${timing}" ]; then
tail -n "${lines}" "${timing}"
else
echo "build-time.log ещё не создан"
fi
' sh \
"${BUILD_TIME_LOG}" \
"${EVENT_LINES}" \
2>/dev/null |
cut -c "1-${WIDTH}" || true
echo
printf 'LAST %s LOG LINES:\n' "${LOG_LINES}"
"${DOCKER[@]}" exec "${CID}" sh -lc '
log="$1"
lines="$2"
if [ -f "${log}" ]; then
tail -n "${lines}" "${log}"
else
echo "build.log ещё не создан"
fi
' sh \
"${LOG_FILE}" \
"${LOG_LINES}" \
2>/dev/null |
sed 's/\r$//' |
cut -c "1-${WIDTH}" || true
else
echo "build.log пока не найден."
fi
echo
echo "TOP PROCESSES:"
"${DOCKER[@]}" exec "${CID}" sh -lc '
count="$1"
ps -eo \
pid,ppid,etime,%cpu,%mem,args \
--sort=-%cpu \
2>/dev/null |
head -n "${count}"
' sh "${PROCESS_LINES}" \
2>/dev/null |
cut -c "1-${WIDTH}" || true
echo
echo "ERROR SUMMARY:"
if [ -n "${LOG_FILE}" ]; then
"${DOCKER[@]}" exec "${CID}" sh -lc '
log="$1"
if [ -f "${log}" ]; then
pattern="(^|[[:space:]])(error:|fatal:|failed|no space left on device|make.*[*][*][*])"
count=$(
grep -Eic \
"${pattern}" \
"${log}" ||
true
)
echo "Detected error-like lines: ${count}"
if [ "${count}" -gt 0 ]; then
grep -Ei \
"${pattern}" \
"${log}" |
tail -n3
fi
else
echo "Log unavailable"
fi
' sh "${LOG_FILE}" \
2>/dev/null |
cut -c "1-${WIDTH}" || true
else
echo "Log unavailable"
fi
printf '%s\n' \
'================================================================================'
done
fi
} > "${FRAME_FILE}"
# Старый экран остаётся видимым, пока новый кадр собирается.
# После подготовки кадр выводится целиком.
printf '\033[H'
cat "${FRAME_FILE}"
# Удалить остаток предыдущего, более длинного кадра.
printf '\033[J'
sleep "${INTERVAL}"
done
EOF