Загрузка данных
sudo tee /usr/local/bin/watch-gitlab-builds-smooth >/dev/null <<'EOF'
#!/usr/bin/env bash
set -uo pipefail
INTERVAL="${1:-3}"
LOG_LINES="${LOG_LINES:-12}"
PROCESS_LINES="${PROCESS_LINES:-7}"
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
# Перейти в альтернативный экран и скрыть курсор.
printf '\033[?1049h'
printf '\033[?25l'
while true; do
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 — smooth 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.
if [ -z "${ENVIRONMENT}" ]; then
continue
fi
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
)"
if [ -n "${CURRENT_STAGE}" ]; then
echo "CURRENT BUILDROOT STAGE:"
printf '%s\n' "${CURRENT_STAGE}" |
cut -c "1-${WIDTH}"
else
echo "CURRENT BUILDROOT STAGE: ещё не определён"
fi
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,etime,%cpu,%mem,comm \
--sort=-%cpu 2>/dev/null |
head -n "${count}"
' sh "${PROCESS_LINES}" 2>/dev/null |
cut -c "1-${WIDTH}" || true
printf '%s\n' '================================================================================'
done
fi
} > "${FRAME_FILE}"
# Старый экран остаётся видимым, пока новый кадр полностью готовится.
# Затем готовый кадр выводится одним обновлением.
printf '\033[H'
cat "${FRAME_FILE}"
# Удалить остаток предыдущего, более длинного кадра.
printf '\033[J'
sleep "${INTERVAL}"
done
EOF
sudo chmod +x /usr/local/bin/watch-gitlab-builds-smooth