. "$BST_LIB" # BST-TIMEOUT: 900 # autossh 1.4g (meta-networking recipes-connectivity) -- the supervisor that # runs ssh, watches it, and runs it again when it dies or stops carrying # traffic. One 1821-line C file over libc plus poll(2); it links no library, # authenticates nothing and encrypts nothing. Everything it does is fork(2), # execvp(2), a loopback socket, and waiting to be told the child is gone. # # WHAT THIS SUITE IS BUILT AROUND. autossh splits into two halves that fail for # different reasons on this arm, and a merged verdict would hide both: # # * THE START HALF -- building ssh's argv (including the -L/-R pair that -M # synthesises), binding the monitor's echo socket on loopback, fork(2) and # execvp(2) of the ssh named at configure time or by $AUTOSSH_PATH. All of # it is ordinary and all of it is reachable here without a single signal, # so every start-half case below asserts THE ARTIFACT -- what the forked # child recorded, and what is actually accepting connections -- while # autossh is still running, and never autossh's exit status. # * THE WATCH HALF -- ssh_watch() arms alarm(2) and calls pause(2), and only # a delivered SIGCHLD or SIGALRM gets it out; only then does it # waitpid(WNOHANG) and decide whether to restart. Signals on this arm are # delivered at the next command dispatch, so a process BLOCKED waiting for # one never receives it (brintos/linux#21), and a blocking wait on a live # child never returns (brintos/linux#74). pause(2) is exactly that block. # # So the restart behaviour the package exists for is expected to be RED here, # in the platform and not in the port, and it is MEASURED rather than assumed: # every watch-half case runs the real scenario under its own wall-clock bound # and reports what came back. Each flips to green by itself on the day #21 # closes, which is the point of running it. # # THE TICKET'S NAMED CRITERION -- a local forward to a loopback sshd, data # across the forwarded port, then kill the ssh child and see the forward come # back -- is run against a REAL openssh sshd on 127.0.0.1 and scored, never # skipped. Its first half is blocked in the SERVER by a gap already tracked and # nothing to do with this port: sshd greets, and its privilege-separation # preauth child then fatals because chroot(2) is unwired (brintos/linux#58, and # the permanent reds that own it in 88-openssh.test). The second half is # blocked again, in the watch half above. Both are run anyway and attributed at # the case. # # WHAT THAT LEAVES AS REAL-PEER EVIDENCE, and it is the strongest this image # can produce for this port: autossh drives the image's OWN /usr/bin/ssh at a # live sshd, with the forward requested, and the ssh client gets far enough to # be refused by the server rather than by anything in autossh. require autossh autossh/present D=${TMPDIR:-/tmp}/bst-autossh.$$ rm -rf "$D"; mkdir -p "$D" GREET="tclsh $BST_DATA/openssh-greeting.tcl" MPORT=22140 # -M monitor port; autossh listens on MPORT+1 FPORT=22142 # the local end of the forward under test SPORT=22144 # the loopback sshd # --- one case's environment, and nothing of the previous case's --------------- # autossh is configured entirely through $AUTOSSH_*, and the cases below differ # from each other ONLY in those variables -- the gate case in particular is # defined by AUTOSSH_GATETIME being absent, which an inherited value from the # case above would silently destroy. An empty value is not the same as an unset # one either: autossh xerrlog()s on AUTOSSH_GATETIME="" and never starts. So # each case clears the whole surface and then exports exactly what it needs. aclear() { unset AUTOSSH_PATH AUTOSSH_GATETIME AUTOSSH_MAXSTART AUTOSSH_LOGFILE \ AUTOSSH_LOGLEVEL AUTOSSH_POLL AUTOSSH_FIRST_POLL AUTOSSH_PORT \ AUTOSSH_DEBUG AUTOSSH_MAXLIFETIME AUTOSSH_PIDFILE AUTOSSH_MESSAGE } # --- a bounded run ------------------------------------------------------------ # Every case that lets autossh fork carries its own wall-clock bound, and # neither obvious way of writing one exists here: timeout(1) bounds nothing on # this arm (it signals, and signal delivery does not work -- brintos/linux#21), # and `wait` on a running child never returns (brintos/linux#74). This is the # runner's own watchdog shape applied per case: run behind a completion # sentinel, poll /proc/uptime with builtins only, kill -9 on overrun -- an # explicit SIGKILL does reap a syscall-blocked process even where delivery to a # handler does not. # # Sets AS_RC and AS_OUT. AS_RC is the literal string TIMEOUT when the bound # tripped; every caller distinguishes that from a status, because "autossh # never returned" and "autossh returned the wrong number" are different # findings, and on this arm the first is the expected one. # # BRINTOS-WORKAROUND: brintos/linux#21 revisit -- this watchdog exists because # `timeout N cmd` bounds nothing here. When #21 closes every `ab` call collapses # to a `timeout` and this goes away -- revisit rather than remove, because the # sentinel poll is serviceable regardless. # BRINTOS-WORKAROUND: brintos/linux#74 revisit -- the reason a bound is needed # at all rather than merely tidy: autossh's own pause(2)/waitpid(2) pair never # wakes here, so a case with no bound sits for the suite's whole 900 s. ab() { # ab _abb=$1; shift rm -f "$D/.rc" "$D/.out" { "$@" "$D/.rc"; } > "$D/.out" 2>&1 & _abpid=$! _abwait "$_abb" "$_abpid" [ -e "$D/.rc" ] && AS_RC=$(cat "$D/.rc") AS_OUT=$(cat "$D/.out" 2>/dev/null) rm -f "$D/.rc" "$D/.out" return 0 } # _abwait -- poll for the sentinel, kill at the bound, then sweep # this run's leftovers. Sets AS_RC=TIMEOUT unless the sentinel appeared. _abwait() { if read -r _abu _ < /proc/uptime; then _abs0=${_abu%.*}; else _abs0=; fi case $_abs0 in ''|*[!0-9]*) _abs0= ;; esac # The tick fallback is five times the bound in ITERATIONS, not seconds: # sleep(1) does not sleep on this image (~0.29 s, # linux-in-the-browser#120), so counting iterations as seconds would fire the # bound at a fifth of it and report a working case as a hang -- the one error # this watchdog must not make. It is reached only when /proc/uptime is # unreadable, which is the transient brintos/linux#73 leaves behind. _abt=0 _abcap=$(($1 * 5)) AS_RC=TIMEOUT while [ ! -e "$D/.rc" ]; do _abover=0 if [ -n "$_abs0" ] && read -r _abu _ < /proc/uptime; then _abn=${_abu%.*} case $_abn in ''|*[!0-9]*) [ "$_abt" -ge "$_abcap" ] && _abover=1 ;; *) [ $((_abn - _abs0)) -ge "$1" ] && _abover=1 ;; esac else [ "$_abt" -ge "$_abcap" ] && _abover=1 fi [ "$_abover" = 1 ] && { kill -9 "$2" 2>/dev/null; break; } sleep 1 _abt=$((_abt + 1)) done asweep } # asweep -- a killed subshell leaves autossh itself parked, and the child it # forked parked under it; the next case would then be measuring two autosshes on # the same ports. Explicit SIGKILL is the one signal that does reach a blocked # process here. The wide match is on this run's unique temp directory, which # appears in every child's argv, so it can only ever reach this suite's own # processes -- the loopback sshd of the live section included, which is why that # section tears down in an order that does not depend on this. asweep() { if command -v pkill >/dev/null 2>&1; then pkill -9 -f "bst-autossh.$$" 2>/dev/null pkill -9 -x autossh 2>/dev/null fi return 0 } # --- an autossh left running, so the START half can be read off it ------------ # astart : launch autossh in the background and leave it running. # astop tears the whole run down. astart() { autossh "$@" > "$D/as.out" 2>&1 & AS_PID=$! } astop() { kill -9 "$AS_PID" 2>/dev/null; asweep; } # apoll : poll a real condition, never a fixed delay # (sleep(1) does not sleep here). Returns 0 as soon as the condition holds. apoll() { _apb=$1; shift _apt=0 while [ "$_apt" -lt $((_apb * 5)) ]; do if eval "$@"; then return 0; fi sleep 1 _apt=$((_apt + 1)) done return 1 } # wedge -- the red for a watch-half case that ran and did not come # back. This is the one failure mode the platform's own gaps predict, and an # unattributed red here would read as autossh returning garbage. wedge() { bst_notok "$1" "brintos/linux#21 + #74 (a blocked process is never signalled; a blocking wait never wakes): autossh's pause(2)/waitpid(2) never saw the child die -- killed at the bound. $2" } # gap -- the red for a live case already traced to a # platform gap, named at the case. An unrecognised failure falls through to a # plain red on purpose: that is what a regression in autossh itself should look # like, and it must not be absorbed by an attribution written for another cause. gap() { case "$2" in *"chroot"*"not implemented"*|*"chroot"*"Function not implemented"*) bst_notok "$1" "brintos/linux#58 (chroot ENOSYS): sshd's privsep preauth child fatals, so no session and no forward -- $(_bst_1line "$2")" ;; # autossh's own log says it exec'd /usr/bin/ssh, and ssh's own -E log is # EMPTY: the child was started and died without writing a line. That is the # 33 MB exec image going down before it reaches its first write, which is # the same failure the sshd re-exec child shows at 40 MB and the reason -r # is passed above. *SSH_CHILD_WROTE_NOTHING*) bst_notok "$1" "brintos/linux#44 + #15 (a large exec image dies before its first write): autossh exec'd /usr/bin/ssh and the 33 MB child produced no output at all in its own -E log, so no forward was ever set up -- $(_bst_1line "$2")" ;; *) bst_notok "$1" "$(_bst_1line "$2")" ;; esac } # --- identity and build configuration ----------------------------------------- check_out autossh/version "autossh 1.4g" sh -c "autossh -V 2>&1" # No arguments is a usage error: text on stdout, status 1. Both halves matter -- # a build that printed usage and exited 0 would break every script testing it. out=$(autossh 2>&1); rc=$? case "$out" in *"usage: autossh"*) if [ "$rc" -eq 1 ]; then bst_ok autossh/usage else bst_notok autossh/usage "usage printed but rc=$rc want=1"; fi ;; *) bst_notok autossh/usage "rc=$rc no usage text: $(_bst_1line "$out")" ;; esac # The usage text is also the documented environment surface, and every knob this # suite drives comes from it. A build whose configure lost one of these would # still run and would silently ignore the case that sets it. miss="" for v in AUTOSSH_PATH AUTOSSH_GATETIME AUTOSSH_MAXSTART AUTOSSH_LOGFILE \ AUTOSSH_LOGLEVEL AUTOSSH_POLL AUTOSSH_FIRST_POLL AUTOSSH_PORT; do case "$out" in *"$v"*) ;; *) miss="$miss $v" ;; esac done check_eq autossh/env-surface "" "$miss" # --with-ssh=/usr/bin/ssh is the one thing this recipe configures, and it is a # string compiled into the binary rather than anything the CLI will report. check_out autossh/ssh-path-compiled-in "/usr/bin/ssh" \ sh -c "strings /usr/bin/autossh 2>/dev/null | grep -x /usr/bin/ssh" # ...and the RDEPENDS that path implies is satisfied on this image. if [ -x /usr/bin/ssh ]; then bst_ok autossh/ssh-rdepends else bst_notok autossh/ssh-rdepends "/usr/bin/ssh absent -- the recipe's RDEPENDS on ssh did not land"; fi # --- ORDERING: the live section runs FIRST, and that is load-bearing ---------- # It is the only part of this suite that execs the image's 33 MB ssh-keygen and # 37 MB sshd, and an exec that size is served out of a fresh process-table slot # and a fresh 64 MB user SAB. Every other case here forks: each poll iteration # is a `sleep` process (sleep(1) returns in ~0.29 s here, # linux-in-the-browser#120), and an exited process's SAB is not reclaimed # promptly -- a full run of the halves below leaves tens of them uncollected. # Reaching the big execs in that state traps the loader # (`wasm32_user_exec_load trapped: unreachable` out of a `hwjs-rt set-over-set` # park violation), which deadlocks __wasm32_alloc_process_task against a dead # lane and wedges the WHOLE GUEST -- taking every sibling suite in the boot down # with it and costing this suite all of its output, since the runner buffers a # suite's lines and emits them only when it ends. # # Measured on the batch-664 image: the live section reached at ~2.2 GiB peak RSS # wedges; run first, at ~1.3 GiB, it and the rest of the suite complete. # # BRINTOS-WORKAROUND: brintos/linux#44 revisit -- the exec-image path's failure # under memory pressure is the platform side of this. When it holds up under a # churned process table this ordering stops mattering and the section can move # back to where it reads best. # --- why the live section's poll bounds are SMALL ---------------------------- # Every iteration of a greeting probe below opens a TCP connection to the # loopback sshd, and sshd forks a child of the image's 37 MB binary to serve it. # The forward can never come up here (brintos/linux#58, at the case), so those # probes never succeed and burn their whole bound -- at the 40 s bounds this # section was written with, that is ~400 forks of a 37 MB binary, the guest # grows ~2.3 GiB/min and the runner quarantines the suite for runaway growth # (measured: 6.2 GiB peak on the batch-664 image). A bound here buys nothing # anyway: a probe that has failed five times in a row is not going to succeed on # the two hundredth. The bring-up probe keeps a slightly larger bound because it # is the one that DOES succeed, and it exits the moment it does. # # BRINTOS-WORKAROUND: brintos/linux#58 revisit -- when sshd's privsep child stops # fatalling, the forward comes up, these probes succeed on the first iteration # and the bound stops mattering. # --- the ticket's named criterion, against a real sshd on loopback ------------ # openssh is the server rather than dropbear: it is the one on this image that # gets far enough to greet, so a red here lands on the gap that actually blocks # the forward instead of on an earlier one. The bring-up is 88-openssh.test's, # privilege-separation workaround included, because a second spelling of it # would drift from the suite that owns it. if [ -x /usr/sbin/sshd ] && [ -x /usr/bin/ssh-keygen ]; then ssh-keygen -q -t ed25519 -N "" -f "$D/id_ed25519" 2>/dev/null ssh-keygen -q -t ed25519 -N "" -f "$D/hostkey" 2>/dev/null chmod 600 "$D/hostkey" "$D/id_ed25519" 2>/dev/null mkdir -p "$D/home/.ssh" cat "$D/id_ed25519.pub" > "$D/home/.ssh/authorized_keys" chmod 700 "$D/home/.ssh"; chmod 600 "$D/home/.ssh/authorized_keys" # BRINTOS-WORKAROUND: brintos/linux#59 remove -- sshd refuses to start unless # its privilege-separation directory is uid 0 and neither group- nor # world-writable, and nothing under the virtiofs root can present uid 0. # tmpfs entries can, so the directory is made there and linked into the # compiled-in path. When the root filesystem presents uid 0, `mkdir -p # /var/run/sshd && chmod 755` is all this needs. PSP=${TMPDIR:-/tmp}/.bst-autossh-psp.$$ rm -rf "$PSP" /var/run/sshd mkdir -p "$PSP"; chmod 755 "$PSP" ln -s "$PSP" /var/run/sshd 2>/dev/null # No UsePAM: this recipe builds openssh without it, so the option is a parse # error that would stop sshd before it binds rather than being merely inert. cat > "$D/sshd_config" < "$D/sshd.log" 2>&1 & SPID=$! apoll 4 "case \"\$($GREET $SPORT 2000 2>&1)\" in *'banner=SSH-'*) true ;; *) false ;; esac" banner=$($GREET $SPORT 2000 2>&1) case "$banner" in *"banner=SSH-"*) # The forward's far end is the sshd's own listening port, so "data crossed # the forwarded port" is the server's version string arriving through it. # That needs no second service on the image and no writable peer -- it is # the greeting just read directly, taken the long way round. SSHOPTS="-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o ExitOnForwardFailure=yes -o LogLevel=ERROR" aclear # AUTOSSH_MAXSTART is what stops this case being a memory bomb, and it is # not tidiness. The forward cannot come up on this image -- sshd's privsep # preauth child fatals on the unwired chroot(2) (brintos/linux#58) -- so # ssh exits at once, every time; with GATETIME=0 removing the give-up # window and no cap, autossh re-execs the image's 33 MB /usr/bin/ssh in a # tight loop. Each exec takes a fresh 64 MB user SAB that an exited # process does not give back promptly, so the guest grows ~2.8 GiB/min and # the runner quarantines the suite for runaway growth -- measured at 7.5 # GiB peak on the batch-664 image before this cap. # # 10 is above what the restart case below needs (one kill, one restart) # and far under the ceiling: 10 starts is ~640 MB of SAB, which the guest # carries. AUTOSSH_GATETIME=0; AUTOSSH_MAXSTART=10 AUTOSSH_LOGFILE="$D/log6"; AUTOSSH_LOGLEVEL=7 export AUTOSSH_GATETIME AUTOSSH_MAXSTART AUTOSSH_LOGFILE AUTOSSH_LOGLEVEL astart -M 0 -N $SSHOPTS -E "$D/ssh.log" -p $SPORT -i "$D/id_ed25519" \ -L $FPORT:127.0.0.1:$SPORT 127.0.0.1 # autossh reached fork+execvp of the REAL ssh with our forward on its # argv. This is the real-peer half that IS reachable, and it is asserted # from autossh's own log rather than from ps, which cannot be read # reliably for a process that may already be gone. if apoll 4 "grep -q 'execing /usr/bin/ssh' '$D/log6' 2>/dev/null"; then bst_ok autossh/live-exec-ssh else bst_notok autossh/live-exec-ssh "autossh never exec'd /usr/bin/ssh: $(_bst_1line "$(cat "$D/log6" 2>/dev/null)")" fi # THE CRITERION, first half: does data cross the forwarded port. apoll 2 "case \"\$($GREET $FPORT 2000 2>&1)\" in *'banner=SSH-'*) true ;; *) false ;; esac" fwd=$($GREET $FPORT 2000 2>&1) case "$fwd" in *"banner=SSH-"*) bst_ok autossh/live-forward ;; *) # Which of the two gaps stopped it is decided by whether the ssh child # wrote ANYTHING: an empty -E log means it never got that far, and a # non-empty one means it ran and the server refused it. Passing the # distinction as a token keeps gap() the only place that names a # defect. sshtail=$(tail -3 "$D/ssh.log" 2>/dev/null) case "$sshtail" in *[!\ ]*) ssherr="ssh:'$(_bst_1line "$sshtail")'" ;; *) ssherr="SSH_CHILD_WROTE_NOTHING" ;; esac gap autossh/live-forward "no data across 127.0.0.1:$FPORT ($(_bst_1line "$fwd")) -- $ssherr sshd:'$(_bst_1line "$(grep -m1 -e chroot -e fatal "$D/sshd.log" 2>/dev/null)")'" ;; esac # THE CRITERION, second half: kill the ssh child and the forward must come # back. Scored on its own -- a different mechanism from the first half # and, on this image, blocked by a different gap. # # The child is killed BY PID, read from autossh's log, and not with a # pkill pattern: every pattern wide enough to match the ssh child also # matches autossh itself (it was invoked with the same -L) and the sshd # (its config path is under the same temp directory), so a pattern kill # would tear down the very things whose reaction is being measured. before=$(grep -c 'starting ssh' "$D/log6" 2>/dev/null); before=${before:-0} kid=$(sed -n 's/.*ssh child pid is \([0-9][0-9]*\).*/\1/p' "$D/log6" 2>/dev/null | tail -1) if [ -n "$kid" ]; then kill -9 "$kid" 2>/dev/null restarted=0 apoll 4 "[ \"\$(grep -c 'starting ssh' '$D/log6' 2>/dev/null)\" -gt $before ]" && restarted=1 after=$(grep -c 'starting ssh' "$D/log6" 2>/dev/null); after=${after:-0} if [ "$restarted" = 1 ]; then apoll 2 "case \"\$($GREET $FPORT 2000 2>&1)\" in *'banner=SSH-'*) true ;; *) false ;; esac" fwd2=$($GREET $FPORT 2000 2>&1) case "$fwd2" in *"banner=SSH-"*) bst_ok autossh/live-restart ;; *) gap autossh/live-restart "ssh was restarted but the forward did not carry data: $(_bst_1line "$fwd2")" ;; esac else wedge autossh/live-restart "the ssh child was killed and autossh never started a replacement (starting-ssh lines: $before then $after)" fi else bst_notok autossh/live-restart "autossh logged no ssh child pid, so there was nothing to kill: $(_bst_1line "$(cat "$D/log6" 2>/dev/null)")" fi kill -9 "$SPID" 2>/dev/null astop ;; *) kill -9 "$SPID" 2>/dev/null why="sshd never greeted on $SPORT: $(_bst_1line "$(tail -4 "$D/sshd.log" 2>/dev/null)")" for c in live-exec-ssh live-forward live-restart; do bst_notok "autossh/$c" "$why" done ;; esac rm -rf "$PSP" /var/run/sshd else # Present-and-absent, not present-and-broken: with no server on the image the # criterion cannot be posed at all, and saying so is not the same as scoring # it. for c in live-exec-ssh live-forward live-restart; do bst_skip "autossh/$c" "no /usr/sbin/sshd on this image to point a forward at" done fi # The live section's sshd, its ssh child and autossh are all gone before the # fork-heavy halves below start. asweep # --- the START half: what the child actually got ------------------------------ # The stub stands in for ssh so the start half can be measured with no server, # no keys and no network: it records the argv autossh handed it, then exits with # the status the case put in $D/rc. The status is read from a FILE and not from # the environment because these cases invoke a shell FUNCTION, and a variable # assignment prefixed to a function call persists in the caller afterwards -- # one leaked exit status would turn the restart case's child into a clean exit # and make its red unreadable. cat > "$D/stub" <> "$D/argv" echo x >> "$D/starts" exit \$(cat "$D/rc" 2>/dev/null || echo 1) EOF chmod +x "$D/stub" echo 1 > "$D/rc" : > "$D/argv"; : > "$D/starts" # -M 0 disables the monitoring loop; AUTOSSH_GATETIME=0 removes the 30-second # "did it make it out of the gate" window, which otherwise makes the FIRST start # a special case. The bound is short because the only thing read here is whether # the child ran at all -- autossh is expected to be parked in pause(2) when the # bound trips, and that is the next cases' finding, not this one's. aclear AUTOSSH_PATH="$D/stub"; AUTOSSH_GATETIME=0; AUTOSSH_MAXSTART=1 AUTOSSH_LOGFILE="$D/log1"; AUTOSSH_LOGLEVEL=7 export AUTOSSH_PATH AUTOSSH_GATETIME AUTOSSH_MAXSTART AUTOSSH_LOGFILE AUTOSSH_LOGLEVEL ab 30 autossh -M 0 -N user@127.0.0.1 if [ -s "$D/starts" ]; then bst_ok autossh/exec-child else bst_notok autossh/exec-child "the stub never ran: autossh did not fork+exec it. log:'$(_bst_1line "$(cat "$D/log1" 2>/dev/null)")'"; fi # The ssh options the user gave must reach ssh unchanged, and -M 0 must NOT add # a monitoring forward. This is the whole of autossh's argv contract with # monitoring off. check_eq autossh/argv-passthrough "-N user@127.0.0.1" "$(head -1 "$D/argv" 2>/dev/null)" # -M 0 is reported as such: proves $AUTOSSH_LOGFILE opened and the monitoring # decision was taken, both before any signal is needed. check_out autossh/logfile-monitor-off "monitoring disabled" \ sh -c "cat '$D/log1' 2>/dev/null" # --- the START half: the monitoring socket ------------------------------------ # With -M autossh binds the ECHO port (port+1) on loopback itself and # synthesises the forward pair that would carry its heartbeat. Both are # reachable with no signal and no peer, and together they are the whole local # footprint of the monitoring machinery. : > "$D/argv" cat > "$D/sleeper" <> "$D/argv" while : ; do sleep 5; done EOF chmod +x "$D/sleeper" aclear AUTOSSH_PATH="$D/sleeper"; AUTOSSH_GATETIME=0 AUTOSSH_LOGFILE="$D/log2"; AUTOSSH_LOGLEVEL=7 export AUTOSSH_PATH AUTOSSH_GATETIME AUTOSSH_LOGFILE AUTOSSH_LOGLEVEL astart -M $MPORT -N user@127.0.0.1 apoll 20 "[ -s '$D/argv' ]" mon=$($GREET $((MPORT + 1)) 2000 2>&1) argv=$(head -1 "$D/argv" 2>/dev/null) astop # The echo socket is autossh's own, not ssh's: the stub above forwards nothing, # so an accepted connection here was accepted by autossh itself. case "$mon" in "RESULT ok"*) bst_ok autossh/monitor-listens ;; *) bst_notok autossh/monitor-listens "nothing accepting on 127.0.0.1:$((MPORT + 1)): $(_bst_1line "$mon")" ;; esac # Both directions of the heartbeat: -L sends it out on the monitor port, -R # brings it back to the echo port autossh has just proved it is listening on. case "$argv" in *"-L $MPORT:127.0.0.1:$MPORT"*) bst_ok autossh/monitor-argv-local ;; *) bst_notok autossh/monitor-argv-local "want:'-L $MPORT:127.0.0.1:$MPORT' got:'$(_bst_1line "$argv")'" ;; esac case "$argv" in *"-R $MPORT:127.0.0.1:$((MPORT + 1))"*) bst_ok autossh/monitor-argv-remote ;; *) bst_notok autossh/monitor-argv-remote "want:'-R $MPORT:127.0.0.1:$((MPORT + 1))' got:'$(_bst_1line "$argv")'" ;; esac # --- the WATCH half: restarting a child that died ----------------------------- # THE CASE THIS PACKAGE EXISTS FOR, in its smallest honest form: a child that # exits non-zero must be started again, up to AUTOSSH_MAXSTART times, after # which autossh exits 0 saying so. No ssh, no server, no network -- just # fork/exec, notice the death, fork/exec again. Three starts is under # N_FAST_TRIES (5), so autossh's backoff never sleeps and the scenario fits the # bound with room to spare. echo 1 > "$D/rc" : > "$D/starts"; : > "$D/argv" aclear AUTOSSH_PATH="$D/stub"; AUTOSSH_GATETIME=0; AUTOSSH_MAXSTART=3 AUTOSSH_LOGFILE="$D/log3"; AUTOSSH_LOGLEVEL=7 export AUTOSSH_PATH AUTOSSH_GATETIME AUTOSSH_MAXSTART AUTOSSH_LOGFILE AUTOSSH_LOGLEVEL ab 30 autossh -M 0 -N user@127.0.0.1 n=$(wc -l < "$D/starts" 2>/dev/null); n=$(printf '%s' "${n:-0}" | tr -d ' ') if [ "$n" = 3 ]; then bst_ok autossh/restart-loop elif [ "$n" = 0 ]; then bst_notok autossh/restart-loop "the stub never ran at all -- not a watch-half result" else wedge autossh/restart-loop "started $n of 3 (each restart needs a delivered SIGCHLD)"; fi # ...and the status that goes with it, scored on its own: "the child was # restarted three times" and "autossh then said so and exited 0" fail # independently here, the first being the artifact above and the second needing # the blocked-signal path one last time. if [ "$AS_RC" = 0 ]; then bst_ok autossh/restart-loop-status elif [ "$AS_RC" = TIMEOUT ]; then wedge autossh/restart-loop-status "expected rc=0 (max start count reached)" else bst_notok autossh/restart-loop-status "rc=$AS_RC want=0 out:'$(_bst_1line "$AS_OUT")'"; fi # The gate: with the DEFAULT AUTOSSH_GATETIME (30 s) -- which is why this case # sets everything EXCEPT it -- a first child that dies immediately means the # connection never established, and autossh must give up with status 1 rather # than loop. This is the branch that stops a misconfigured tunnel spinning # forever, and it too is on the far side of pause(2). echo 1 > "$D/rc" : > "$D/starts" aclear AUTOSSH_PATH="$D/stub"; AUTOSSH_LOGFILE="$D/log4"; AUTOSSH_LOGLEVEL=7 export AUTOSSH_PATH AUTOSSH_LOGFILE AUTOSSH_LOGLEVEL ab 60 autossh -M 0 -N user@127.0.0.1 if [ "$AS_RC" = 1 ] && grep -q 'prematurely' "$D/log4" 2>/dev/null; then bst_ok autossh/gate-premature elif [ "$AS_RC" = TIMEOUT ]; then wedge autossh/gate-premature "expected rc=1 and 'ssh exited prematurely' in the log" else bst_notok autossh/gate-premature "rc=$AS_RC want=1 log:'$(_bst_1line "$(cat "$D/log4" 2>/dev/null)")'" fi # The other side of the same branch: a child that exits ZERO is ssh finishing # normally, and autossh must follow it out rather than restart it. Same wedge, # opposite expectation, which is what makes the pair a test of the decision # rather than of the loop. echo 0 > "$D/rc" : > "$D/starts" aclear AUTOSSH_PATH="$D/stub"; AUTOSSH_GATETIME=0; AUTOSSH_MAXSTART=3 AUTOSSH_LOGFILE="$D/log5"; AUTOSSH_LOGLEVEL=7 export AUTOSSH_PATH AUTOSSH_GATETIME AUTOSSH_MAXSTART AUTOSSH_LOGFILE AUTOSSH_LOGLEVEL ab 30 autossh -M 0 -N user@127.0.0.1 n=$(wc -l < "$D/starts" 2>/dev/null); n=$(printf '%s' "${n:-0}" | tr -d ' ') if [ "$AS_RC" = 0 ] && [ "$n" = 1 ]; then bst_ok autossh/clean-exit-no-restart elif [ "$AS_RC" = TIMEOUT ]; then wedge autossh/clean-exit-no-restart "expected rc=0 after exactly one start, got $n start(s)" else bst_notok autossh/clean-exit-no-restart "rc=$AS_RC want=0, starts=$n want=1" fi asweep rm -rf "$D"