From 36c348eed3e658c07047a600d4609d379b6cd086 Mon Sep 17 00:00:00 2001 From: Whisperity Date: Wed, 5 Jun 2024 17:36:49 +0200 Subject: [PATCH] feat: Report available system memory under `dcc_free_mem` This patch adds a trivial wrapper HTTP server, written in Python, that intercepts the requests going towards the `--stats-port` of the running `distccd` server. It relays the request verbatim at first, and if the response is a valid statistics message, extends it at the bottom with a line dcc_free_mem 16384 MB where the numeric value is obtained from `free`'s output under **Available** system memory. --- .github/workflows/docker-image.yml | 87 +++++++- Dockerfile | 32 +-- etc/logrotate.d/distcc | 10 - etc/logrotate.d/{cron => systemlogs} | 4 +- etc/skel/.config/htop/htoprc | 7 + usr/local/sbin/container-main.sh | 227 ++++++++++++++------ usr/local/sbin/install-compilers.sh | 9 +- usr/local/share/dcc_free_mem/stat_server.py | 218 +++++++++++++++++++ 8 files changed, 496 insertions(+), 98 deletions(-) delete mode 100644 etc/logrotate.d/distcc rename etc/logrotate.d/{cron => systemlogs} (70%) create mode 100644 etc/skel/.config/htop/htoprc create mode 100755 usr/local/share/dcc_free_mem/stat_server.py diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 6e880b7..dd49cd8 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -34,12 +34,20 @@ jobs: mv -v ./shellcheck-v*/shellcheck "./shellcheck" rm -rv ./shellcheck-v* ./shellcheck --version + - name: "Install PyCodeStyle" + run: | + sudo apt-get install -y --no-install-recommends \ + python3-pycodestyle - name: "ShellCheck" run: | ./shellcheck \ --color=always \ etc/cron.*/* \ usr/local/*bin/* + - name: "PyCodeStyle" + run: | + python3 -m pycodestyle \ + . build: name: "Build ${{ github.event_name == 'workflow_dispatch' && inputs.release && 'and Deploy' || '' }} Docker Image" @@ -54,16 +62,85 @@ jobs: with: load: true tags: "distcc-docker:${{ env.RUNS_ON }}" - - name: "Simple test" + - name: "Test starting the container" run: | docker run \ --init \ --rm \ "distcc-docker:${{ env.RUNS_ON }}" \ - --jobs $(nproc) \ - -- \ - bash -c \ - "set -x; ps fauxw; /usr/bin/ls -alh .; /usr/bin/ls -alh /var/log; cat /var/log/*; exit;" + --jobs $(nproc) \ + -- \ + bash -c \ + "set -x; ps fauxw; /usr/bin/ls -alh .; /usr/bin/ls -alh /var/log; cat /var/log/*; exit;" + - name: "Test that \"dcc_free_mem\" is reported" + run: | + set -ex + + docker run \ + --detach \ + --init \ + --name "distcc-1" \ + --publish "3632:3632/tcp" \ + --publish "3633:3633/tcp" \ + --rm \ + "distcc-docker:${{ env.RUNS_ON }}" \ + --jobs $(nproc) \ + -- \ + bash -c "set -x; sleep 120; exit;" + + sleep 10 + STAT_RESULT="$(curl "http://localhost:3633" | grep "dcc_free_mem")" + if [ -z "$STAT_RESULT" ]; then + echo "Server did not report 'dcc_free_mem'!" >&2 + exit 1 + fi + - name: "Test compilation in the container" + run: | + set -ex + + sudo apt-get update -y + sudo apt-get install distcc g++ --no-install-recommends + sudo update-distcc-symlinks + + docker run \ + --detach \ + --init \ + --name "distcc-1" \ + --publish "3632:3632/tcp" \ + --publish "3633:3633/tcp" \ + --rm \ + "distcc-docker:${{ env.RUNS_ON }}" \ + --jobs $(nproc) \ + -- \ + bash -c "set -x; sleep 120; exit;" + + sleep 10 + curl "http://localhost:3633" + + echo "int main() { return MY_EXIT_CODE; }" >> main.cpp + + DISTCC_HOSTS="127.0.0.1:3632/$(nproc),lzo" \ + distcc /usr/bin/g++ \ + -D MY_EXIT_CODE=42 \ + -c \ + ./main.cpp \ + -o main.o + + rm -v main.cpp + + curl "http://localhost:3633" + + docker kill "distcc-1" + + /usr/bin/g++ main.o -o main + + set +e + + ./main + if [ "$?" -ne 42 ]; then + echo "Unexpected error code obtained!" >&2 + exit 1 + fi - name: "Log in to GitHub Container Registry (GHCR)" if: "github.ref == 'refs/heads/master' && github.event_name == 'workflow_dispatch' && inputs.release" uses: "docker/login-action@v3" diff --git a/Dockerfile b/Dockerfile index 8667168..de47181 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ FROM ubuntu:20.04 MAINTAINER Whisperity -RUN export DEBIAN_FRONTEND=noninteractive && \ +RUN \ + export DEBIAN_FRONTEND=noninteractive && \ set -x && \ apt-get update -y && \ apt-get install -y --no-install-recommends \ @@ -15,6 +16,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ lsb-release \ locales \ logrotate \ + python3 \ wget \ && \ apt-get purge -y --auto-remove && \ @@ -24,7 +26,8 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ mkdir -pv "/var/log/" -RUN export DEBIAN_FRONTEND=noninteractive; \ +RUN \ + export DEBIAN_FRONTEND=noninteractive; \ sed -i -e "s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/" "/etc/locale.gen" && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG="en_US.UTF-8" @@ -34,11 +37,11 @@ ENV \ LC_ALL="en_US.UTF-8" +COPY usr/local/sbin/install-compilers.sh /usr/local/sbin/install-compilers.sh + # Set to non-zero if the compilers should only be installed into the container, # and not immediately baked into the image itself. ARG LAZY_COMPILERS=0 - -COPY usr/local/sbin/install-compilers.sh /usr/local/sbin/install-compilers.sh RUN \ if [ "x$LAZY_COMPILERS" = "x0" ]; \ then \ @@ -52,25 +55,26 @@ RUN \ fi +COPY etc/ /etc/ +COPY usr/ /usr/ + + ARG USERNAME="distcc" -RUN echo "Creating service user: $USERNAME ..." >&2 && \ - mkdir -pv "/var/lib/distcc/" && \ - chmod -Rv 755 "/var/lib/distcc" && \ +RUN \ + cp -av "/etc/skel/." "/root/" && \ + echo "Creating service user: $USERNAME ..." >&2 && \ useradd "$USERNAME" \ - --system \ + --create-home \ --comment "DistCC service" \ + --home-dir "/var/lib/distcc/" \ --shell "/bin/bash" \ - --home-dir "/var/lib/distcc/" && \ - cp -v "/root/.bashrc" "/root/.profile" "/var/lib/distcc/" && \ + --system \ + && \ chown -Rv "$USERNAME":"$USERNAME" "/var/lib/distcc" && \ echo "$USERNAME" > "/var/lib/distcc/distcc.user" && \ chmod -v 444 "/var/lib/distcc/distcc.user" -COPY etc/ /etc/ -COPY usr/ /usr/ - - # Expose the DistCC server's normal job and statistics subservice port. # Custom ports to be used on the host machine should be managed via Docker. EXPOSE \ diff --git a/etc/logrotate.d/distcc b/etc/logrotate.d/distcc deleted file mode 100644 index a676559..0000000 --- a/etc/logrotate.d/distcc +++ /dev/null @@ -1,10 +0,0 @@ -/var/log/distccd.log { - daily - rotate 30 - - compress - copytruncate - delaycompress - missingok - notifempty -} diff --git a/etc/logrotate.d/cron b/etc/logrotate.d/systemlogs similarity index 70% rename from etc/logrotate.d/cron rename to etc/logrotate.d/systemlogs index f3604f0..99a9ade 100644 --- a/etc/logrotate.d/cron +++ b/etc/logrotate.d/systemlogs @@ -1,6 +1,6 @@ -/var/log/cron.log { +/var/log/*.log { daily - rotate 30 + rotate 7 compress copytruncate diff --git a/etc/skel/.config/htop/htoprc b/etc/skel/.config/htop/htoprc new file mode 100644 index 0000000..4b56f44 --- /dev/null +++ b/etc/skel/.config/htop/htoprc @@ -0,0 +1,7 @@ +hide_kernel_threads=1 +hide_userland_threads=1 +highlight_base_name=1 +highlight_megabytes=1 +shadow_other_users=0 +show_program_path=0 +tree_view=1 diff --git a/usr/local/sbin/container-main.sh b/usr/local/sbin/container-main.sh index 45583da..29a31cf 100755 --- a/usr/local/sbin/container-main.sh +++ b/usr/local/sbin/container-main.sh @@ -5,13 +5,21 @@ # # shellcheck disable=SC2317 +CRON_PIDF="/run/crond.pid" CRON_RUNNING=0 -DISTCC_LOGF="/var/log/distccd.log" -DISTCC_PIDF="/run/distccd.pid" -DISTCC_RUNNING=0 +DISTCCD_LOGF="/var/log/distccd.log" +DISTCCD_PIDF="/run/distccd.pid" +DISTCCD_PORT=3632 +DISTCCD_PORT_STATS=3634 +DISTCCD_STATS_HACK_PIDF="/run/distccd-dcc_free_mem.pid" +DISTCCD_STATS_HACK_ACCESS_LOG="/var/log/access.log" +DISTCCD_STATS_HACK_ERROR_LOG="/var/log/error.log" +DISTCCD_STATS_HACK_PORT=3633 +DISTCCD_STATS_HACK_RUNNING=0 +DISTCCD_RUNNING=0 +DISTCCD_TAIL_PID=0 DISTCC_USER="$(cat /var/lib/distcc/distcc.user)" -DISTCC_TAIL_PID=0 -HAS_COMPILERS_INSTALLED_FILE="/var/lib/.distcc-compilers-done" +HAS_COMPILERS_INSTALLED_FILE="/var/lib/distcc-compilers-done" SYSTEM_LOGF="/var/log/syslog" @@ -32,7 +40,7 @@ Usage: -j J | --jobs J Run distcc server with J worker processes. Default: $JOBS - -n J | --nice N Run distcc server with extra N ninceness. + -n J | --nice N Run distcc server with extra N niceness. Default: $NICE --startup-timeout S Wait S seconds for the server to start. Default: $STARTUP_TIMEOUT @@ -46,10 +54,10 @@ USAGE # "getopt". while [ $# -gt 0 ]; do - OPT="$1" + opt="$1" shift 1 - case "$OPT" in + case "$opt" in --help|-h) usage exit 0 @@ -74,7 +82,7 @@ while [ $# -gt 0 ]; do break ;; *) - echo "ERROR: Unexpected argument: '$OPT'" >&2 + echo "ERROR: Unexpected argument: '$opt'" >&2 exit 2 ;; esac @@ -122,14 +130,15 @@ function _check_and_install_compilers() { _syslog "_" $$ "Failed to find compilers. Installing..." /usr/local/sbin/install-compilers.sh - local RC=$? + local -ri rc=$? - if [ $RC -ne 0 ]; then + if [ $rc -ne 0 ]; then _syslog "_" $$ "! Failed to install compilers after failed detection" - echo "[!!!] ALERT! This container did not succeed installing compilers!" >&2 + echo "[!!!] ALERT! This container did not succeed installing" \ + "compilers!" >&2 echo " The DistCC service will not be appropriately usable!" >&2 - return $RC + return $rc fi fi @@ -141,8 +150,9 @@ function _check_and_install_compilers() { # Helper "system" daemons. function std_cron() { start-stop-daemon \ - --pidfile "/run/crond.pid" \ + --verbose \ --exec "$(which cron)" \ + --pidfile "$CRON_PIDF" \ "$@" return $? } @@ -150,14 +160,14 @@ function std_cron() { function start_cron() { echo "[+++] Starting cron..." >&2 std_cron --start - _syslog "cron" "$(cat "/run/crond.pid")" "Cron daemon started." + _syslog "cron" "$(cat "$CRON_PIDF")" "Cron daemon started." CRON_RUNNING=1 return $? } function stop_cron() { echo "[---] Stopping cron..." >&2 - _syslog "cron" "$(cat "/run/crond.pid")" "Cron daemon stopping..." + _syslog "cron" "$(cat "$CRON_PIDF")" "Cron daemon stopping..." std_cron --stop CRON_RUNNING=0 return $? @@ -167,69 +177,150 @@ function stop_cron() { # DistCC service daemon. function std_distccd() { start-stop-daemon \ - --pidfile "$DISTCC_PIDF" \ + --verbose \ + --pidfile "$DISTCCD_PIDF" \ "$@" return $? } function start_distccd() { # Start the DistCC server normally. - echo "[+++] Starting distcc..." >&2 + echo "[+++] Starting distccd..." >&2 - touch "$DISTCC_LOGF" "$DISTCC_PIDF" - chown "$DISTCC_USER":"$DISTCC_USER" "$DISTCC_LOGF" "$DISTCC_PIDF" - chmod 0644 "$DISTCC_LOGF" "$DISTCC_PIDF" + touch "$DISTCCD_LOGF" "$DISTCCD_PIDF" + chown "$DISTCC_USER":"$DISTCC_USER" "$DISTCCD_LOGF" "$DISTCCD_PIDF" + chmod 0644 "$DISTCCD_LOGF" "$DISTCCD_PIDF" - start-stop-daemon --start \ + start-stop-daemon \ + --verbose \ + --start \ --exec "$(which distccd)" \ -- \ --daemon \ --user "$DISTCC_USER" \ - --listen "0.0.0.0" \ --allow "0.0.0.0/0" \ - --port "3632" \ + --listen "0.0.0.0" \ + --log-file "$DISTCCD_LOGF" \ + --log-level info \ --jobs "$JOBS" \ --nice "$NICE" \ + --pid-file "$DISTCCD_PIDF" \ + --port "$DISTCCD_PORT" \ --stats \ - --stats-port "3633" \ - --pid-file "$DISTCC_PIDF" \ - --log-file "$DISTCC_LOGF" \ - --log-level info - local RC=$? - if [ $RC -ne 0 ]; then - return $RC + --stats-port "$DISTCCD_PORT_STATS" + local -i rc=$? + if [ "$rc" -ne 0 ]; then + return $rc fi # Wait for DistCC to start properly. /usr/local/libexec/wait-for \ --quiet \ --timeout="$STARTUP_TIMEOUT" \ - "http://0.0.0.0:3633" \ + "http://0.0.0.0:$DISTCCD_PORT_STATS" \ -- \ - echo "[^:)] distcc up!" >&2 - RC=$? - if [ "$RC" -ne 0 ]; then - echo "[:'(] distcc failed to start after $STARTUP_TIMEOUT seconds!" >&2 + echo "[^:)] distccd up!" >&2 + rc=$? + if [ "$rc" -ne 0 ]; then + echo "[:'(] distccd failed to start after $STARTUP_TIMEOUT seconds!" >&2 _syslog "_" $$ "DistCC daemon failed to start!" - DISTCC_RUNNING=0 + DISTCCD_RUNNING=0 else - local DISTCC_PID - DISTCC_PID="$(cat $DISTCC_PIDF)" + local -i distccd_pid + distccd_pid="$(cat $DISTCCD_PIDF)" - _syslog "distccd" "$DISTCC_PID" "DistCC daemon started." - _syslog "distccd" "$DISTCC_PID" "DistCC running $JOBS workers..." - DISTCC_RUNNING=1 + _syslog "distccd" "$distccd_pid" "DistCC daemon started." + _syslog "distccd" "$distccd_pid" "DistCC running $JOBS workers..." + DISTCCD_RUNNING=1 fi - return $RC + return $rc } function stop_distccd() { - echo "[---] Stopping distcc..." >&2 - _syslog "distccd" "$(cat $DISTCC_PIDF)" "DistCC daemon stopping..." - std_distccd --stop \ + echo "[---] Stopping distccd..." >&2 + _syslog "distccd" "$(cat "$DISTCCD_PIDF")" "DistCC daemon stopping..." + std_distccd \ + --stop \ + --remove-pidfile \ + --user "$DISTCC_USER" + DISTCCD_RUNNING=0 + return $? +} + + +# DistCC --stats "dcc_free_mem" hack response transformer daemon. +function std_distccd_dcc_free_mem() { + start-stop-daemon \ + --verbose \ + --pidfile "$DISTCCD_STATS_HACK_PIDF" \ + "$@" + return $? +} + +function start_distccd_free_mem_server() { + # Start the DistCC server free memory reporting transformer. + echo "[+++] Starting distccd dcc_free_mem ..." >&2 + + touch "$DISTCCD_STATS_HACK_ACCESS_LOG" "$DISTCCD_STATS_HACK_ERROR_LOG" \ + "$DISTCCD_STATS_HACK_PIDF" + chown "$DISTCC_USER":"$DISTCC_USER" \ + "$DISTCCD_STATS_HACK_ACCESS_LOG" "$DISTCCD_STATS_HACK_ERROR_LOG" \ + "$DISTCCD_STATS_HACK_PIDF" + chmod 0644 \ + "$DISTCCD_STATS_HACK_ACCESS_LOG" "$DISTCCD_STATS_HACK_ERROR_LOG" \ + "$DISTCCD_STATS_HACK_PIDF" + + std_distccd_dcc_free_mem \ + --start \ + --background \ + --chuid "$DISTCC_USER" \ + --exec "$(which python3)" \ + --make-pidfile \ + -- \ + "/usr/local/share/dcc_free_mem/stat_server.py" \ + --access-log "$DISTCCD_STATS_HACK_ACCESS_LOG" \ + --error-log "$DISTCCD_STATS_HACK_ERROR_LOG" \ + --system-log "$SYSTEM_LOGF" \ + "$DISTCCD_STATS_HACK_PORT" \ + "$DISTCCD_PORT_STATS" + local -i rc=$? + if [ "$rc" -ne 0 ]; then + return $rc + fi + + # Wait for DistCC to start properly. + /usr/local/libexec/wait-for \ + --quiet \ + --timeout="$STARTUP_TIMEOUT" \ + "http://0.0.0.0:$DISTCCD_STATS_HACK_PORT" \ + -- \ + echo "[^:)] distccd dcc_free_mem up!" >&2 + rc=$? + if [ "$rc" -ne 0 ]; then + echo "[:'(] distccd dcc_free_mem failed to start after" \ + "$STARTUP_TIMEOUT seconds!" >&2 + _syslog "_" $$ "DistCC dcc_free_mem failed to start!" + DISTCCD_STATS_HACK_RUNNING=0 + else + local distccd_free_mem_pid + distccd_free_mem_pid="$(cat "$DISTCCD_STATS_HACK_PIDF")" + + _syslog "distccd-dcc_free_mem" "$distccd_free_mem_pid" \ + "DistCC dcc_free_mem started." + DISTCCD_STATS_HACK_RUNNING=1 + fi + return $rc +} + +function stop_distccd_dcc_free_mem() { + echo "[---] Stopping distccd dcc_free_mem ..." >&2 + _syslog "distccd-dcc_free_mem" "$(cat "$DISTCCD_STATS_HACK_PIDF")" \ + "DistCC dcc_free_mem stopping..." + std_distccd_dcc_free_mem \ + --stop \ --remove-pidfile \ --user "$DISTCC_USER" - DISTCC_RUNNING=0 + DISTCCD_STATS_HACK_RUNNING=0 return $? } @@ -240,17 +331,22 @@ function atexit() { stty echoctl fi - if [ "$DISTCC_TAIL_PID" -ne 0 ]; then + local -i sig + if [ "$DISTCCD_TAIL_PID" -ne 0 ]; then if [ $# -eq 1 ] && [ "$1" -ne 0 ]; then - SIG=$1 + sig=$1 else - SIG=9 + sig=9 fi - kill -"$SIG" "$DISTCC_TAIL_PID" - DISTCC_TAIL_PID=0 + kill -"$sig" "$DISTCCD_TAIL_PID" + DISTCCD_TAIL_PID=0 fi - if [ "$DISTCC_RUNNING" -ne 0 ]; then + if [ "$DISTCCD_STATS_HACK_RUNNING" -ne 0 ]; then + stop_distccd_dcc_free_mem + fi + + if [ "$DISTCCD_RUNNING" -ne 0 ]; then stop_distccd fi @@ -296,9 +392,9 @@ function custom_command() { _syslog "sudo" "" "root : COMMAND=$*" "$@" fi - local RETURN_CODE=$? - echo "[---] Custom command '$1' exited with '$RETURN_CODE'" >&2 - return $RETURN_CODE + local -ri rc=$? + echo "[---] Custom command '$1' exited with '$rc'" >&2 + return $rc } @@ -322,9 +418,13 @@ fi start_cron start_distccd +start_distccd_free_mem_server if [ "$EXEC_CUSTOM" -eq 1 ]; then - if [ "$DISTCC_RUNNING" -ne 0 ]; then - echo "[...] distcc service is running." >&2 + if [ "$DISTCCD_RUNNING" -ne 0 ]; then + echo "[...] distccd service is running." >&2 + fi + if [ "$DISTCCD_STATS_HACK_RUNNING" -ne 0 ]; then + echo "[...] distccd dcc_free_mem is running." >&2 fi custom_command "$@" @@ -334,7 +434,8 @@ if [ "$EXEC_CUSTOM" -eq 1 ]; then _syslog "_" $$ "Shutting down (custom command exited)..." _exit $EXIT_CODE else - if [ "$DISTCC_RUNNING" -eq 0 ]; then + if [ "$DISTCCD_RUNNING" -eq 0 ] \ + || [ "$DISTCCD_STATS_HACK_RUNNING" -eq 0 ]; then atexit _syslog "_" $$ "Shutting down (distcc failed to start)..." _exit 1 @@ -356,14 +457,14 @@ fi # Just keep the main script alive as long as the DistCC server is alive... su --pty --login "$DISTCC_USER" --command \ - "tail -f /var/log/distccd.log --pid $(cat "$DISTCC_PIDF")" 2>"/dev/null" & \ -DISTCC_TAIL_PID=$! -wait $DISTCC_TAIL_PID + "tail -f /var/log/distccd.log --pid $(cat "$DISTCCD_PIDF")" 2>"/dev/null" & \ +DISTCCD_TAIL_PID=$! +wait $DISTCCD_TAIL_PID # Do something loud if the process has terminated "organically". echo "[!!!] distcc service process terminated!" >&2 -DISTCC_TAIL_PID=0 +DISTCCD_TAIL_PID=0 sleep 1 atexit 15 diff --git a/usr/local/sbin/install-compilers.sh b/usr/local/sbin/install-compilers.sh index 4b85be7..6f87062 100755 --- a/usr/local/sbin/install-compilers.sh +++ b/usr/local/sbin/install-compilers.sh @@ -3,10 +3,11 @@ # # Install LTS compiler versions into the image (or the container). -COMPILERS_INSTALLED_STAMP="/var/lib/.distcc-compilers-done" +COMPILERS_INSTALLED_STAMP="/var/lib/distcc-compilers-done" if [ -f "$COMPILERS_INSTALLED_STAMP" ]; then - echo "[^:)] The compilers are already installed appropriately, skipping..." >&2 + echo "[^:)] The compilers are already installed appropriately," \ + "skipping..." >&2 exit 0 else echo "[...] Installing C, C++ compilers..." >&2 @@ -62,7 +63,7 @@ if [[ "$OS_RELEASE" == "focal" ]]; then | cut -d ':' -f 6 \ | cut -d ' ' -f 3)" apt-key adv --keyserver "hkp://keyserver.ubuntu.com:80" --recv-keys "$KEY" - mv "/etc/apt/trusted.gpg" "/etc/apt/keyrings/ubuntu-toolchain-r.gpg" + mv -v "/etc/apt/trusted.gpg" "/etc/apt/keyrings/ubuntu-toolchain-r.gpg" apt-get update -y @@ -88,5 +89,5 @@ fi update-distcc-symlinks touch "$COMPILERS_INSTALLED_STAMP" -chmod 444 "$COMPILERS_INSTALLED_STAMP" +chmod -v 444 "$COMPILERS_INSTALLED_STAMP" exit 0 diff --git a/usr/local/share/dcc_free_mem/stat_server.py b/usr/local/share/dcc_free_mem/stat_server.py new file mode 100755 index 0000000..b7e667d --- /dev/null +++ b/usr/local/share/dcc_free_mem/stat_server.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +""" +Implements a transformating wrapper over the requests otherwise served by +``distccd(1)``'s ``--stats`` server, otherwise known as a "man-in-the-middle" +(MITM) access. +This Python script injects the ``dcc_free_mem`` statistical variable into the +output, with which clients can obtain the currently available system memory +that could be consumeable by spawned compiler jobs, as returned by ``free(1)``. +""" +import argparse +import datetime +import http +import http.server +import os +import socket +import subprocess +import sys +import urllib.request +from typing import List, Optional, Union, cast + + +def argument_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=""" +Serve `dcc_free_mem` alongside the reported statistics of a 'distccd' server. +""", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument("listen_port", + metavar="listen_port", + type=int, + help=""" +The TCP port to listen on where the extended statistics will be reported. +""" + ) + + parser.add_argument("stats_port", + metavar="PORT", + type=int, + default=3633, + help=""" +The statistics server's port number, as was passed to "distccd +--stats-port=PORT". +""" + ) + + parser.add_argument("--access-log", + type=str, + default="/var/log/access.log", + help=""" +Path to the webserver's output "access log" file. +""") + + parser.add_argument("--error-log", + type=str, + default="/var/log/error.log", + help=""" +Path to the webserver's output "error log" file. +""") + + parser.add_argument("--system-log", + type=str, + default="/var/log/syslog", + help=""" +Path to the standard "syslog" file. +""") + + return parser + + +def _syslog(file: str, message: str, facility: str, pid: Optional[int] = None): + """ + Logs one line to the "emulated" system log. + """ + date = datetime.datetime.now().strftime("%b %_d %H:%M:%S") + hostname = socket.gethostname() + if not pid: + pid = os.getpid() + pid_str = f"[{pid}]".format(pid) + log_line = f"{date} {hostname} {facility}{pid_str}: {message}" + + try: + with open(file, 'a') as io: + io.writelines([log_line]) + except Exception: + print(log_line, file=sys.stderr) + + +class DistCCStatsMITMRequestHandler(http.server.BaseHTTPRequestHandler): + def log_request(self, + code: Union[int, str] = '-', + size: Union[int, str] = '-'): + if isinstance(code, http.HTTPStatus): + code = code.value + self.log_access('"%s" %s %s', self.requestline, str(code), str(size)) + + def log_access(self, format: str, *args, **kwargs): + log = cast(DistCCStatsMITMHTTPServer, self.server).access_log + return self.log(format, log, *args, **kwargs) + + def log_message(self, format: str, *args): + log = cast(DistCCStatsMITMHTTPServer, self.server).error_log + return self.log(format, log, *args) + + def log(self, format: str, file: str, *args): + log_line = "%s - - [%s] %s\n" % \ + (self.address_string(), self.log_date_time_string(), format % args) + + try: + with open(file, 'a') as io: + io.writelines([log_line]) + except Exception: + log = cast(DistCCStatsMITMHTTPServer, self.server).system_log + _syslog(log, + f"{self.__class__.__name__} failed to log message " + f"\"{log_line}\" to file \"{file}\"", + "dcc_free_mem") + print(log_line, file=sys.stderr) + + def do_GET(self): + stats_port = cast(DistCCStatsMITMHTTPServer, self.server).stats_port + + try: + with urllib.request.urlopen(f"http://0.0.0.0:{stats_port}") \ + as response: + response_data = response.read().decode() + code = response.code + headers = response.headers + except Exception: + import traceback + return self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, + "An exception occurred when querying the " + f"--stats server at :{stats_port}", + traceback.format_exc()) + + def _mimic_response_headers(size: Union[str, int] = '-'): + self.log_request(code, size) + self.send_response_only(code) + for header, value in headers.items(): + self.send_header(header, value) + self.end_headers() + + if not response_data \ + or "" not in response_data \ + or "" not in response_data: + self.log_error( + '"%s" %s %d %s', + self.requestline, + http.HTTPStatus.INTERNAL_SERVER_ERROR.value, + len(response_data), + f"--stats at :{stats_port} returned empty or invalid response") + + _mimic_response_headers(size=0) + self.wfile.write(''.encode()) + return + + dcc_free_mem: Optional[int] = None + try: + free_result = subprocess. \ + check_output(["free", "--mebi", "--wide"]). \ + decode().splitlines() + for line in free_result: + if line.startswith("Mem:"): + values = list(filter(bool, line.split(' '))) + dcc_free_mem = int(values[-1]) + + if dcc_free_mem is None: + raise ValueError( + "No 'Mem:' line found in the output of `free`") + except Exception: + import traceback + return self.send_error( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + "An exception occurred when querying `free`", + traceback.format_exc()) + + response_data_lines: List[str] = response_data.splitlines() + response_data_lines.insert(-1, "dcc_free_mem %d MB" % dcc_free_mem) + response_data_lines.append('') + response_to_send = '\n'.join(response_data_lines).encode() + + _mimic_response_headers(size=len(response_to_send)) + self.wfile.write(response_to_send) + + +class DistCCStatsMITMHTTPServer(http.server.HTTPServer): + def __init__(self, server_address, bind_and_activate, stats_port: int, + access_log: str, error_log: str, system_log: str): + super().__init__(server_address, + DistCCStatsMITMRequestHandler, + bind_and_activate) + self.stats_port = stats_port + self.access_log = access_log + self.error_log = error_log + self.system_log = system_log + + +def main(args: argparse.Namespace) -> int: + server = DistCCStatsMITMHTTPServer( + server_address=("0.0.0.0", args.listen_port), + bind_and_activate=True, + stats_port=args.stats_port, + access_log=args.access_log, + error_log=args.error_log, + system_log=args.system_log + ) + + server.serve_forever() + + return 0 + + +if __name__ == "__main__": + opts = argument_parser() + args = opts.parse_args() + sys.exit(main(args) or 0)