Загрузка данных


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