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


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