. "$BST_LIB" # BST-TIMEOUT: 900 # rdist 6.1.5 -- the remote file distribution client (rdist) and its server # (rdistd), from meta-networking recipes-connectivity. # # THE DISTRIBUTION IS REAL, NOT PROBED. rdist(1) documents that when the target # host is the string `localhost' and the remote user equals the local user, it # runs the server as # # /bin/sh -c rdistd -S # # over a socketpair instead of invoking a transport program. Both halves of the # pair therefore run on this machine and the protocol is spoken end to end, with # no network, no second host and no remote login -- which is exactly what this # image can offer. `-P ' is exercised too, on the other documented # route: rdist execs the transport as ` -l ', and the # man page's own advice for a transport that does not take that syntax is to # wrap it in a shell script, which is what $W/transport is. # # WHAT IS PROBED IS ONLY WHAT NEEDS A SECOND HOST. The default transport # /usr/ucb/rsh is not on this image; ssh(1) is, but there is no peer to reach # and no sshd. Those cases are skipped from a probe that establishes the absence # rather than from an assumption -- and the probe is asserted in its own right. # # NOTHING HERE USES timeout(1). On this image GNU timeout never expires: # coreutils arms its bound with alarm(2), unwired on wasm32 # (brintos/linux#77), and there is no busybox applet to fall back on. Counting # `sleep 1' iterations is not a bound either -- the guest's event loop stalls # for many seconds at a time while a multi-MB wasm image is staged, and a # 25-iteration bound was measured taking over 800 s. Every bound below is # therefore wall-clock, read from date(1), with a kill at the end of it. require rdist rdist/present W=${TMPDIR:-/tmp}/bst-rdist.$$ rm -rf "$W"; mkdir -p "$W/src/sub" "$W/empty" || { bst_notok rdist/workdir "cannot create $W"; exit 0; } bst_ok rdist/workdir say() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "want '$2' in: $(_bst_1line "$3")" ;; esac } nosay() { case "$3" in *"$2"*) bst_notok "$1" "unexpected '$2' in: $(_bst_1line "$3")" ;; *) bst_ok "$1" ;; esac } # rd -- run rdist under a wall-clock bound. Sets RC (the exit # status, or the string TIMEOUT) and leaves the combined output in $W/out. RC="" rd() { _lim=$1; shift : > "$W/out" _t0=$(date +%s) rdist "$@" > "$W/out" 2>&1 & _p=$! while kill -0 "$_p" 2>/dev/null; do if [ $(( $(date +%s) - _t0 )) -ge "$_lim" ]; then kill -TERM "$_p" 2>/dev/null; sleep 1; kill -KILL "$_p" 2>/dev/null wait "$_p" 2>/dev/null; RC=TIMEOUT; return 0 fi sleep 1 done wait "$_p"; RC=$? return 0 } OUT() { cat "$W/out" 2>/dev/null; } # --- presence and identity --------------------------------------------------- check rdist/on-path command -v rdist check rdist/rdistd-on-path command -v rdistd check rdist/man-rdist test -s /usr/share/man/man1/rdist.1 check rdist/man-rdistd test -s /usr/share/man/man8/rdistd.8 rd 60 -V check_eq rdist/version-rc 0 "$RC" say rdist/version "6.1.5" "$(OUT)" say rdist/version-proto "Protocol Version 6" "$(OUT)" # --- usage text and argument validation -------------------------------------- rd 60 -Z check_eq rdist/usage-rc 1 "$RC" say rdist/usage-invalid-option "invalid option" "$(OUT)" say rdist/usage-banner "Usage:" "$(OUT)" say rdist/usage-names-P "-P " "$(OUT)" say rdist/usage-names-p "-p " "$(OUT)" say rdist/usage-c-form "-c source" "$(OUT)" say rdist/usage-V-form "-V" "$(OUT)" # -c takes at least a source and a destination; one argument is a usage error. rd 60 -c "$W/src" check_eq rdist/c-too-few-rc 1 "$RC" say rdist/c-too-few-usage "Usage:" "$(OUT)" rd 60 -a notanumber -f /dev/null say rdist/a-not-a-number "is not a number" "$(OUT)" rd 60 -M notanumber -f /dev/null say rdist/M-not-a-number "is not a number" "$(OUT)" # rdistd is a server and refuses to be run as anything else. rdistd > "$W/rdistd.out" 2>&1; RDISTD_RC=$? check_eq rdist/rdistd-requires-S-rc 1 "$RDISTD_RC" # --- distfile handling and the exit codes ------------------------------------ rd 60 -f "$W/no-such-distfile" check_eq rdist/missing-distfile-rc 1 "$RC" say rdist/missing-distfile-msg "Cannot access file" "$(OUT)" # With no -f and no distfile in the working directory, rdist gives up by name. # The cd is inside a substitution: a failed top-level cd is fatal to the suite # shell on this image (brintos/linux#73), and a fork contains it to one case. NODF=$( cd "$W/empty" && rdist 2>&1; echo "rc=$?" ) say rdist/no-distfile-found "No distfile found" "$NODF" say rdist/no-distfile-rc "rc=1" "$NODF" printf 'this is not a distfile at all ;;; ->\n' > "$W/bad.df" rd 60 -f "$W/bad.df" check_eq rdist/unparsable-rc 1 "$RC" say rdist/unparsable-msg "Error in distfile" "$(OUT)" say rdist/unparsable-line "line 1" "$(OUT)" # --- payloads ---------------------------------------------------------------- # 44800 bytes of fixed text (800 lines of 56): enough to cross rdist's BUFSIZ transfer loop many # times, so a short copy is visible to cmp rather than hidden in one block. awk 'BEGIN{for(n=1;n<=800;n++) printf "%04d rdist payload line, the quick brown fox 0123456789\n", n}' > "$W/src/big.txt" # Every byte value 0..255. LC_ALL=C is load-bearing: under a UTF-8 locale awk's # %c emits the UTF-8 encoding of 128..255 and the payload silently stops # covering the high half. LC_ALL=C awk 'BEGIN{for(i=0;i<256;i++) printf "%c", i}' > "$W/src/bin.dat" printf 'nested\n' > "$W/src/sub/deep.txt" printf 'skip me\n' > "$W/src/skipped.txt" check_eq rdist/payload-text 44800 "$(wc -c < "$W/src/big.txt" | tr -d ' ')" check_eq rdist/payload-binary 256 "$(wc -c < "$W/src/bin.dat" | tr -d ' ')" chmod 640 "$W/src/big.txt" chmod 604 "$W/src/bin.dat" touch -t 200102030405.06 "$W/src/big.txt" # --- distfile parsing, asserted with -n (no transport is involved) ----------- cat > "$W/parse.df" < \${HOSTS} install $W/pd ; except ( $W/src/skipped.txt ) ; special ( $W/src/big.txt ) "echo SPECIAL-DID-RUN" ; beta: ( $W/src/bin.dat ) -> \${HOSTS} install $W/pd2 ; EOF rd 90 -n -f "$W/parse.df" check_eq rdist/parse-rc 0 "$RC" say rdist/parse-variable-host "updating host localhost" "$(OUT)" say rdist/parse-variable-files "big.txt" "$(OUT)" say rdist/parse-install-dest "$W/pd" "$(OUT)" say rdist/parse-second-label "bin.dat" "$(OUT)" # -n PLANS the distribution, it does not perform it: `except' is applied in # sendit() per file and `special' runs on the target, so neither shows here and # the excepted file is still listed. Both are asserted for real below, on a # distribution that actually happens. Pinned as its own case so that a future -n # that DOES filter is a deliberate change rather than a silent one. say rdist/parse-dryrun-plans-only "skipped.txt" "$(OUT)" # A label argument selects one entry; the other must not appear. rd 90 -n -f "$W/parse.df" beta check_eq rdist/parse-label-rc 0 "$RC" say rdist/parse-label-selects "bin.dat" "$(OUT)" nosay rdist/parse-label-excludes "big.txt" "$(OUT)" # A distfile read from stdin. The pipeline forks before the redirect. PARSED=$( cat "$W/parse.df" | rdist -n -f - 2>&1 ) say rdist/distfile-from-stdin "updating host localhost" "$PARSED" # Set subtraction in a name list. cat > "$W/arith.df" < ( localhost ) install $W/pd3 ; EOF rd 90 -n -f "$W/arith.df" say rdist/parse-list-subtract "big.txt" "$(OUT)" nosay rdist/parse-list-subtract-drop "skipped.txt" "$(OUT)" # --- -P transport selection -------------------------------------------------- # -P runs its argument through searchpath() and refuses a path with no existing # component, before any connection is attempted. rd 60 -P /no/such/transport/anywhere -f "$W/parse.df" check_eq rdist/P-rejects-rc 1 "$RC" say rdist/P-rejects-nonexistent "No component of path" "$(OUT)" say rdist/P-rejects-usage "Usage:" "$(OUT)" # --- the transport probe ----------------------------------------------------- # The default transport is /usr/ucb/rsh. Establish its absence as a fact before # any case rests on it. check rdist/probe-no-ucb-rsh test ! -e /usr/ucb/rsh if [ -e /usr/ucb/rsh ]; then HAVE_DEFAULT_TRANSPORT=yes; else HAVE_DEFAULT_TRANSPORT=no; fi # --- the distribution, for real ---------------------------------------------- cat > "$W/transport" < -l . Drop the three # rsh-shaped arguments and run the command, which is \`rdistd -S'. echo "\$1 \$2 \$3" > $W/transport.witness shift 3 exec /bin/sh -c "\$1" EOF chmod 755 "$W/transport" # dist [extra rdist args...] -- one distribution of # src/big.txt and src/bin.dat into . dist() { _b=$1; _dest=$2; _host=$3; shift 3 rm -rf "$_dest"; mkdir -p "$_dest" cat > "$W/d.df" < \${H} install $_dest ; EOF rd "$_b" "$@" -f "$W/d.df" } # 1. The documented localhost fast path: /bin/sh -c rdistd -S over a socketpair. dist 180 "$W/dst1" localhost -F check_eq rdist/xfer-rc 0 "$RC" say rdist/xfer-updating "updating host localhost" "$(OUT)" nosay rdist/xfer-no-error "ERROR" "$(OUT)" check rdist/xfer-text-lands test -f "$W/dst1/big.txt" check rdist/xfer-binary-lands test -f "$W/dst1/bin.dat" check rdist/xfer-text-identical cmp "$W/src/big.txt" "$W/dst1/big.txt" check rdist/xfer-binary-identical cmp "$W/src/bin.dat" "$W/dst1/bin.dat" # rdist(1)'s first sentence is that it preserves owner, group, mode and mtime. check_eq rdist/xfer-mode-preserved "$(ls -l "$W/src/big.txt" | cut -c1-10)" "$(ls -l "$W/dst1/big.txt" 2>/dev/null | cut -c1-10)" check_eq rdist/xfer-mode-preserved-2 "$(ls -l "$W/src/bin.dat" | cut -c1-10)" "$(ls -l "$W/dst1/bin.dat" 2>/dev/null | cut -c1-10)" check_eq rdist/xfer-mtime-preserved "$(date -r "$W/src/big.txt" '+%Y-%m-%d %H:%M' 2>/dev/null)" "$(date -r "$W/dst1/big.txt" '+%Y-%m-%d %H:%M' 2>/dev/null)" check_eq rdist/xfer-owner-preserved "$(ls -l "$W/src/big.txt" | awk '{print $3":"$4}')" "$(ls -l "$W/dst1/big.txt" 2>/dev/null | awk '{print $3":"$4}')" # 2. THE SECOND PASS IS THE POINT, NOT A REPEAT. Only a target that already # exists makes the server answer a query with QC_YES -- the reply carrying # size, mtime, mode, owner and group. That is the one wire format on the server # side that carries a time_t, and it is unreachable on a first distribution. rd 180 -F -f "$W/d.df" check_eq rdist/xfer-second-pass-rc 0 "$RC" nosay rdist/xfer-second-pass-parses "not delimited" "$(OUT)" nosay rdist/xfer-second-pass-noerr "ERROR" "$(OUT)" check rdist/xfer-second-pass-intact cmp "$W/src/big.txt" "$W/dst1/big.txt" # 3. A changed source must actually be re-sent on the second pass. printf 'CHANGED\n' >> "$W/src/big.txt" rd 180 -F -f "$W/d.df" check_eq rdist/xfer-update-rc 0 "$RC" check rdist/xfer-update-resent cmp "$W/src/big.txt" "$W/dst1/big.txt" # 4. The same distribution through a -P transport wrapper. The peer is # 127.0.0.1, not "localhost", and that is deliberate: rshrcmd() resolves the # host with gethostbyname(3) BEFORE it forks -- so an unresolvable name never # reaches the transport at all -- and it has a documented fast path that runs # `/bin/sh -c rdistd -S' directly, skipping the transport program, when the host # is the literal string "localhost". A dotted quad resolves without a resolver # and is not that string, so this is the one spelling that genuinely execs the # -P program. The witness file is what proves it did. rm -f "$W/transport.witness" dist 180 "$W/dst2" 127.0.0.1 -F -P "$W/transport" check_eq rdist/xfer-P-rc 0 "$RC" say rdist/xfer-P-updating "updating host 127.0.0.1" "$(OUT)" check rdist/xfer-P-transport-ran test -s "$W/transport.witness" say rdist/xfer-P-transport-argv "127.0.0.1 -l" "$(cat "$W/transport.witness" 2>/dev/null)" check rdist/xfer-P-text-identical cmp "$W/src/big.txt" "$W/dst2/big.txt" check rdist/xfer-P-binary-identical cmp "$W/src/bin.dat" "$W/dst2/bin.dat" # 5. A directory, recursively, with an except and a special. rm -rf "$W/dst3"; mkdir -p "$W/dst3" cat > "$W/dir.df" < ( localhost ) install $W/dst3 ; except ( $W/src/skipped.txt ) ; special ( $W/src/bin.dat ) "echo SPECIAL-ON-TARGET" ; EOF rd 300 -F -f "$W/dir.df" check_eq rdist/xfer-dir-rc 0 "$RC" # A source directory maps ONTO the destination: src/'s contents land directly # in dst3, not under a dst3/src/ subtree. check rdist/xfer-dir-nested cmp "$W/src/sub/deep.txt" "$W/dst3/sub/deep.txt" check rdist/xfer-dir-toplevel cmp "$W/src/big.txt" "$W/dst3/big.txt" check rdist/xfer-dir-except test ! -e "$W/dst3/skipped.txt" say rdist/xfer-dir-special "SPECIAL-ON-TARGET" "$(OUT)" # 6. -c command-line mode, which builds the same distribution with no distfile. rm -rf "$W/dst4"; mkdir -p "$W/dst4" rd 180 -F -c "$W/src/bin.dat" "localhost:$W/dst4/." check_eq rdist/cmdargs-rc 0 "$RC" check rdist/cmdargs-lands cmp "$W/src/bin.dat" "$W/dst4/bin.dat" # and without the trailing dot the same command installs the file AS that name, # which is what rdist(1) NOTES documents and is easy to trip over. rm -rf "$W/dst4b" rd 180 -F -c "$W/src/bin.dat" "localhost:$W/dst4b" check_eq rdist/cmdargs-nodot-rc 0 "$RC" check rdist/cmdargs-nodot-is-file test -f "$W/dst4b" check rdist/cmdargs-nodot-identical cmp "$W/src/bin.dat" "$W/dst4b" # 7. The default (forking) driver is a different code path from -F: the work # happens in a child and the parent collects it through waitup(). THE TRANSFER # ITSELF COMPLETES -- that is asserted, not skipped. What does not happen is the # parent exiting afterwards, and the cause is not in rdist. # # waitup() blocks in select(2) on the child's pipe and relies, by its own # comment, on SIGCHLD interrupting that select when the last child leaves: # reap() is the SIGCHLD handler and the only thing that sets needscan for a # normally-exited child. Measured under -D: select was called 10 times and # returned 9, childscan() ran 0 times and "died unexpectedly" 0 times -- i.e. # reap() never ran, so the parent waits on a wakeup that is never posted. That # is brintos/linux#21, signal delivery not working at all, with the wait-side # half of the same thing at brintos/linux#74. # The bound here is deliberately short. This case is EXPECTED to reach it -- # that is the finding -- so whatever it is set to is spent in full on every # run. The transfer it is waiting on completes in a few seconds; 90 s is # ample to observe that and cheap enough not to dominate the suite. At 300 s # it was 300 of the suite's ~375 s, which put the whole file within 2.4x of # its own wall-clock bound -- and one run of three duly hit it, losing the # last three cases to a suite-timeout rather than to anything about rdist. dist 90 "$W/dst5" localhost check rdist/xfer-forking-lands cmp "$W/src/big.txt" "$W/dst5/big.txt" if [ "$RC" = TIMEOUT ]; then bst_skip rdist/xfer-forking-exits "the transfer completed and was compared byte-for-byte, but the forking parent never exits: waitup() waits for SIGCHLD to break its select(2) and no signal is delivered on this kernel -- brintos/linux#21 (and #74), both open. -F is unaffected and is what every other case here uses." else check_eq rdist/xfer-forking-exits 0 "$RC" fi pkill -TERM rdist 2>/dev/null; pkill -TERM rdistd 2>/dev/null; sleep 1 # 8. What rdist does when the source is not there. The man page is explicit that # source files must reside on the local host; a named source that does not is an # error, and it must not leave a partial file behind. rm -rf "$W/dst6"; mkdir -p "$W/dst6" cat > "$W/missing.df" < ( localhost ) install $W/dst6 ; EOF rd 180 -F -f "$W/missing.df" check_eq rdist/missing-source-rc 1 "$RC" check rdist/missing-source-no-file test ! -e "$W/dst6/does-not-exist" # --- what genuinely needs a second host -------------------------------------- if [ "$HAVE_DEFAULT_TRANSPORT" = yes ]; then bst_notok rdist/remote-login "the probe found /usr/ucb/rsh present; this case assumed it absent" else bst_skip rdist/remote-login "no remote login available: /usr/ucb/rsh absent, and ssh(1) has no peer or sshd on this single-host image -- the distribution itself is asserted above over rdistd -S, which needs neither" fi rm -rf "$W"