#!/bin/bash # Daemon for accepting TAME commands (compilers, linker, etc) # # Copyright (C) 2014-2021 Ryan Specialty Group, LLC. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## set -euo pipefail declare mypath; mypath=$( dirname "$( readlink -f "$0" )" ) readonly mypath declare -ri EX_RUNNING=1 declare -ri EX_USAGE=64 # incorrect usage; sysexits.h declare -ri EX_CANTCREAT=73 # cannot create file; sysexits.h # number of seconds of output silence before runners are considered unused # and are subject to termination (see stall-monitor) declare -ri TAMED_STALL_SECONDS="${TAMED_STALL_SECONDS:-1}" # id of process that indirectly spawned tamed (default $PPID) declare -ri TAMED_SPAWNER_PID="${TAMED_SPAWNER_PID:-$PPID}" # options to pass to JVM via dslc declare -r TAMED_JAVA_OPTS="${TAMED_JAVA_OPTS:-}" export JAVA_OPTS="$TAMED_JAVA_OPTS" # set by `main', global for `cleanup' declare root= # Create FIFOs for runner # # The FIFOs are intended to be attached to stderr and stdout # of the runner and will be created relative to the given # root path ROOT. # # If a FIFO cannot be created, exit with EX_CANTCREAT. mkfifos() { local -r root="${1?Missing root path}" mkdir -p "$root" # note that there's no stderr; see `add-runner' for n in 0 1; do rm -f "$root-$n" mkfifo -m 0600 "$root/$n" || { echo "fatal: failed to create FIFO at $root/n" exit $EX_CANTCREAT } done # keep FIFOs open so we don't get EOF from writers tail -f >"$root/0" & } # Spawn a new runner using the next available runner id # # See `spawn-runner' for more information. spawn-next-runner() { local -r root="${1?Missing root path}" # get the next available id local -ri id=$( < "$root/maxid" ) spawn-runner "$(( id + 1 ))" "$root" } # Spawn a runner # # A new runner is created by spawning dslc and attaching # new FIFOs under the given id ID relative to the given # run path ROOT. The PID of the runner will be stored # alongside the FIFOs in a pidfile `pid'. spawn-runner() { local -ri id="${1?Missing id}" local -r root="${2?Missing root run path}" local -r base="$root/$id" mkfifos "$base" # flag as available (the client will manipulate these) echo 0 > "$base/busy" # monitor runner usage and kill when inactive stall-monitor "$base" & # loop to restart runner in case of crash while true; do declare -i job=0 trap 'kill -INT $job' HUP "$mypath/dslc" < "$base/0" &> "$base/1" & job=$! declare -i status=0 wait -n 2>/dev/null || status=$? echo "warning: runner $id exited with code $status; restarting" >&2 done & echo "$!" > "$base/pid" # we assume that this is the new largest runner id echo "$id" > "$root/maxid" echo "runner $id ($!): $base" } # Kill runner at BASE when it becomes inactive for TAMED_STALL_SECONDS # seconds # # This monitors the modification time on the stdout FIFO. stdin does not # need to be monitored since dslc immediately echoes back commands it # receives. # # dslc is pretty chatty at the time of writing this, so TAMED_STALL_SECONDS # can easily be <=30s even for large packages. This may need to change in # the future if it becomes too much less chatty. Increase that environment # variable if runners stall unexpectedly in the middle of builds. # # If the id of the spawning process has been provided then we will never # consider ourselves to be stalled if that process is still running. This # prevents, for example, tamed from killing itself while a parent make # process is still running. stall-monitor() { local -r base="${1?Missing base}" # monitor output FIFO modification time while true; do local -i since last since=$( date +%s ) sleep "$TAMED_STALL_SECONDS" last=$( stat -c%Y "$base/1" ) # keep waiting if there has been activity since $since test "$last" -le "$since" || continue spawner-dead || continue # no activity; kill local -r pid=$( cat "$base/pid" ) kill "$pid" wait "$pid" 2>/dev/null # this stall subprocess is no longer needed break done } # Check to see if the spawning process has died # # If no spawning process was provided, then this always returns a zero # status. Otherwise, it returns whether the given pid is _not_ running. spawner-dead() { test "$TAMED_SPAWNER_PID" -gt 0 || return 0 ! ps "$TAMED_SPAWNER_PID" &>/dev/null } # Exit if tamed is already running at path ROOT # # If tamed is already running at ROOT, exit with status # EX_RUNNING; otherwise, do nothing except output a warning # if a stale pid file exists. abort-if-running() { local -r root="${1?Missing root rundir}" local -ri pid=$( cat "$root/pid" 2>/dev/null ) test "$pid" -gt 0 || return 0 ! ps "$pid" &>/dev/null || { echo "fatal: tamed is already running at $root (pid $pid)!" exit $EX_RUNNING } test -z "$pid" || { echo "warning: clearing stale tamed (pid $pid)" } } # Kill running tamed at path ROOT # # If no pidfile is found at ROOT, do nothing. This sends a # signal only to the parent tamed process, _not_ individual # runners; the target tamed is expected to clean up itself. # Consequently, if a tamed terminated abnormally without # cleaning up, this will not solve that problem. kill-running() { local -r root="${1?Missing root}" local -r pid=$( cat "$root"/pid 2>/dev/null ) test -n "$pid" || return 0 echo "killing tamed at $root ($pid)..." kill "$pid" } # Clean up child processes before exit # # This should be called before exit (perhaps by a trap). Kills # the entire process group. # # Do not attach this to a SIGTERM trap or it will infinitely # recurse. cleanup() { rm -rf "$root" kill 0 } # Output usage information and exit usage() { cat < "$root/pid" # start with a single runner; we'll spawn more if requested spawn-runner 0 "$root" trap "spawn-next-runner '$root'" USR1 # wait for runners to complete or for a signal to be received by this # process that terminates `wait' while true; do wait -n || { status=$? # ignore USR1 if [ $status -ne 138 ]; then exit $status fi } done } main "$@"