# BST-TIMEOUT: 300
#
# MEASURED, NOT GUESSED: this suite reported
# `==BST-SUITE dbus elapsed=30s bound=300s cases=53==' on a wasm32-hwjs guest
# (local-verify, 2026-09-10). 300 is deliberately ten times that rather than the
# corpus's usual three, because what this file spends is dominated by PROCESS
# SPAWNS -- a daemon, a probe, a monitor and about forty dbus-send invocations --
# and a spawn here is a fresh module instantiation whose cost is the most
# variable thing on this substrate. 30 s is also only half the 60 s default, so
# without the marker a guest merely twice as slow as this one would trip it.
#
# dbus 1.14.10 (poky meta/recipes-core/dbus, autotools) on wasm32-hwjs --
# BRINTOS-2601. libdbus-1, dbus-daemon, and the dbus-send / dbus-monitor /
# dbus-launch tools.
#
# THE SUITE CLOSES ON ITS OWN FIXTURES AND REACHES NO PEER OUTSIDE ITSELF. It
# writes a session.conf of its own, starts a dbus-daemon on a unix socket of its
# own under $TMPDIR, and stops it again. Nothing here touches the system bus,
# /run/dbus, any device or any terminal, and the config file names no
# -- so the bus can activate nothing and every
# peer on it is a process this file started.
#
# THE ARGUMENT-TYPE CASES ARE A REAL ROUND TRIP THROUGH TWO IMPLEMENTATIONS,
# not a self-check. Nothing already on the image answers a method call: the bus
# driver's own interface takes only `s' and `u', and dbus-send cannot serve. So
# data/dbus-probe (src/dbus_probe.c, cross-linked against the very
# /usr/lib/libdbus-1.a this port installs) supplies the service, and dbus-send
# stays the caller. Each type is therefore marshalled by dbus-send,
# demarshalled and re-marshalled by libdbus in the probe, then demarshalled and
# printed by dbus-send -- and the case compares that printed form BYTE-EXACTLY.
#
# EVERY EXPECTED STRING BELOW WAS MEASURED, NOT PREDICTED. They come from an
# x86-64 control built from this same src/dbus_probe.c against the host's
# libdbus (2026-09-10), driven by these same dbus-send command lines. The
# control's dbus was 1.16.2 and the target's is 1.14.10, so the formats were
# then re-read out of 1.14.10's OWN tools/dbus-print-message.c rather than
# assumed to have been stable: every scalar (`int16 %d', `uint64 %PRIu64',
# `double %g', `boolean true/false', `object path "', `signature "',
# `variant ') and every container (`array [' / `]', `dict entry(' / `)',
# `struct {' / `}') is identical between the two. What the target can still
# change is the VALUES, which is the thing under test.
#
# ONE FORM DELIBERATELY NOT EXERCISED, because it is a trap for the next person
# to add a case here: an array of BYTE does not print as `array [ byte .. ]'.
# dbus-print-message.c:93,155,166 gives it three special forms of its own
# (`array of bytes [', `array of bytes ".."', and a `+ \0' variant for a
# NUL-terminated one), chosen by whether the content is printable. dbus-send
# can send `array:byte:1,2', so the case looks available; its expected string
# is not the one the pattern above would lead you to write.
#
# A
# substring test would pass against a reply carrying the right type and the
# wrong value, or the arguments in the wrong order; comparing the whole
# volatile-free tail does not. Only dbus-send's first line is dropped -- it
# carries the timestamp, the sender's unique name and two serial numbers, and
# nothing else in the output moves between runs.
#
# TWO OF THE BUS'S OWN TYPES ARE ORIGINATED BY THE PROBE RATHER THAN ECHOED,
# because dbus-send has no syntax for sending them: STRUCT and SIGNATURE. They
# are covered by MakeStruct and MakeSignature, which take no argument and put
# one in the reply, so the marshalling is still asserted in the direction that
# can be. The one type NOT covered at all is UNIX_FD (`h'): the build has
# HAVE_UNIX_FD_PASSING, but dbus-send cannot send a file descriptor and the
# probe has none to pass, so nothing here would be measuring dbus.
#
# THREE NEGATIVE CONTROLS, because everything above would pass vacuously
# against a bus that says yes to whatever it is handed: a call to a name nobody
# owns must come back ServiceUnknown, a call to a method the probe does not
# implement must come back UnknownMethod, and a call after the daemon has been
# stopped must fail rather than succeed.
#
# NOTHING HERE SLEEPS FOR A DURATION and nothing uses timeout(1). `sleep N'
# does not sleep on this image (linux-in-the-browser#120), and timeout(1)'s
# alarm(2) was unwired before the brintos/linux#77 fix, so every wait below is
# an ITERATION COUNT whose sleep is only a yield, and the one command that could
# hang is run under bst_run.
#
# `kill -0' IS NOT USED TO ASK WHETHER THE DAEMON IS ALIVE -- it succeeds on a
# zombie, so a daemon that died at startup would read as running. Read
# /proc//status, the same way 83-dlt-daemon does.
#
# THE SOCKET PATH IS LENGTH-CHECKED FIRST. sun_path is 108 bytes and
# dbus-daemon's failure for an over-long one is "Failed to start message bus:
# Socket name too long" with no socket and no other clue. Measured while
# building the x86-64 control, where $TMPDIR was long enough to trip it. The
# case exists so a future image with a longer $TMPDIR reds with the cause named
# rather than as a daemon that would not start.
. "$BST_LIB"
require dbus-daemon dbus/present
D=${TMPDIR:-/tmp}/bst-dbus.$$
rm -rf "$D"; mkdir -p "$D" || { bst_notok dbus/workdir "cannot create $D"; exit 0; }
SOCK=$D/bus
running() {
case "$(sed -n 's/^State:[ \t]*//p' "/proc/$1/status" 2>/dev/null)" in
"") return 1 ;; # gone entirely
Z*) return 1 ;; # exited, awaiting reap -- dead for every purpose here
*) return 0 ;;
esac
}
# Nothing this suite starts may outlive it. dbus-daemon --nofork, dbus-monitor
# and the probe all run until told otherwise, and if the runner kills this file
# at its own bound they would survive into the NEXT suite on the same guest,
# holding memory the guest never returns (README, Sharding). The trap is the
# only thing that runs on that path -- the tidy-up at the foot of the file does
# not.
cleanup() {
[ -n "$MPID" ] && kill "$MPID" 2>/dev/null
[ -n "$QPID" ] && kill "$QPID" 2>/dev/null
[ -n "$DPID" ] && kill "$DPID" 2>/dev/null
rm -rf "$D"
}
DPID=""; QPID=""; MPID=""
trap cleanup EXIT HUP INT TERM
# wait_for -- poll a file this suite owns
# until one of its lines matches, with ONLY BUILTINS in the loop body.
#
# That constraint is the whole reason this is a function rather than an inline
# grep loop. bst.sh is explicit about it: a relative sleep placed directly after
# the shell reaps a forked child returns instantly instead of sleeping
# (linux-in-the-browser#120), so a loop that greps and then sleeps does not wait
# at all -- it spins as fast as the guest schedules it and grows the host
# process without bound (brintos/hardwarejs#20). read, case, [ and sleep are
# builtins; grep is not.
wait_for() {
wf_n=0
while [ "$wf_n" -lt "$3" ]; do
wf_hit=0
while IFS= read -r wf_l; do
case "$wf_l" in $2) wf_hit=1 ;; esac
# The marker is read on the redirect's OWN line or the one immediately above
# it, so it belongs here rather than above the `while' -- which is where it
# was, and the build-time gate rejected the suite for it.
# BST-REDIRECT-OK: $1 is always a file under $D, which is under $TMPDIR.
done < "$1"
[ "$wf_hit" -eq 1 ] && return 0
wf_n=$((wf_n + 1))
sleep 1
done
return 1
}
# sv [arg...] -- a method call on the probe's object.
sv() {
dbus-send --session --print-reply --dest=io.brintos.SelfTest \
/io/brintos/SelfTest io.brintos.SelfTest."$@" 2>&1
}
# db_case [arg...] -- compare the reply's
# volatile-free tail, flattened to one line, against what the control produced.
db_case() {
db_id=$1; db_want=$2; shift 2
db_raw=$(sv "$@")
# Drop the first line ONLY when it is the volatile method-return header. An
# error reply is a SINGLE line and is the whole of what dbus-send says, so
# dropping it unconditionally turns every diagnosable failure into an empty
# got: -- the one result a reader cannot act on. The likeliest target failure,
# a probe that never answers, arrives as exactly that single line
# (org.freedesktop.DBus.Error.NoReply).
case "$db_raw" in
"method return"*) db_got=$(printf '%s\n' "$db_raw" | sed -n '2,$p') ;;
*) db_got=$db_raw ;;
esac
db_got=$(printf '%s\n' "$db_got" | tr -s ' \t\n' ' ' | sed 's/^ *//; s/ *$//')
check_eq "dbus/$db_id" "$db_want" "$db_got"
}
# ------------------------------------------------------------- the tool set
for t in dbus-daemon dbus-send dbus-monitor dbus-launch dbus-uuidgen \
dbus-run-session dbus-cleanup-sockets dbus-update-activation-environment; do
if command -v "$t" >/dev/null 2>&1; then bst_ok "dbus/have-$t"
else bst_notok "dbus/have-$t" "not on PATH"; fi
done
check_out dbus/version "1.14.10" dbus-daemon --version
check_out dbus/launch-version "1.14.10" dbus-launch --version
# The three deliverables that are files rather than behaviour: the static
# archive is the whole of the library on this arm (the -lib package is empty
# because --disable-shared leaves no lib*.so.*), plus the public header and the
# pkg-config file a consumer needs to find it.
check dbus/staticdev-archive test -f /usr/lib/libdbus-1.a
check dbus/dev-header test -f /usr/include/dbus-1.0/dbus/dbus.h
check dbus/dev-pkgconfig test -f /usr/lib/pkgconfig/dbus-1.pc
# dbus-common's two shipped configurations.
for f in /etc/dbus-1/session.conf /etc/dbus-1/system.conf; do
if [ -f "$f" ]; then bst_ok "dbus/have-$(basename "$f")"
else bst_notok "dbus/have-$(basename "$f")" "absent"; fi
done
# dbus-uuidgen is a whole tool whose entire output is one 32-character hex id.
uuid=$(dbus-uuidgen 2>&1)
case "$uuid" in
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
bst_ok dbus/uuidgen ;;
*) bst_notok dbus/uuidgen "not a 32-char hex id: $(_bst_1line "$uuid")" ;;
esac
# ----------------------------------------------------- the private session bus
if [ "${#SOCK}" -gt 100 ]; then
bst_notok dbus/sock-path-fits "\$TMPDIR makes $SOCK ${#SOCK} bytes; sun_path holds 108"
else
bst_ok dbus/sock-path-fits
fi
CFG=$D/session.conf
{ echo ''
echo ''
echo ' session'
echo " unix:path=$SOCK"
echo ' EXTERNAL'
echo ' '
echo ' '
echo ' '
echo ' '
echo ' '
echo ' '
echo ''
} > "$CFG"
DBUS_SESSION_BUS_ADDRESS="unix:path=$SOCK"; export DBUS_SESSION_BUS_ADDRESS
: > "$D/daemon.log"
dbus-daemon --config-file="$CFG" --nofork > "$D/daemon.log" 2>&1 &
DPID=$!
# Readiness is the socket appearing, not a number of seconds. Bounded by
# iterations, and it gives up as soon as the daemon is no longer alive so a
# daemon that died at startup costs one iteration rather than sixty.
# Builtins only in the body, per wait_for above -- [ -S ] is a builtin, running
# forks. Liveness is therefore checked once, after the wait, which costs a dead
# daemon the full bound and buys every iteration a real second.
i=0
while [ "$i" -lt 60 ]; do
[ -S "$SOCK" ] && break
i=$((i + 1)); sleep 1
done
if running "$DPID"; then daemon_ran=1; bst_ok dbus/daemon-alive
else daemon_ran=0; bst_notok dbus/daemon-alive "$(_bst_1line "$(cat "$D/daemon.log" 2>/dev/null)")"; fi
if [ -S "$SOCK" ]; then bst_ok dbus/socket-created
else bst_notok dbus/socket-created "no socket at $SOCK: $(_bst_1line "$(cat "$D/daemon.log" 2>/dev/null)")"; fi
if running "$DPID"; then
# --------------------------------------------- the bus driver answers for itself
# GetId is the id of the machine the bus runs on, so its VALUE is not fixed
# here -- its shape is. A 32-character hex string is the whole of what the
# spec promises, and it is what separates "the driver answered" from "the
# driver answered with an empty string".
gid=$(dbus-send --session --print-reply --dest=org.freedesktop.DBus / \
org.freedesktop.DBus.GetId 2>&1 \
| sed -n 's/.*string "\([0-9a-f]*\)".*/\1/p')
check_eq dbus/getid-length 32 "${#gid}"
check_out dbus/listnames "org.freedesktop.DBus" \
dbus-send --session --print-reply --dest=org.freedesktop.DBus / \
org.freedesktop.DBus.ListNames
# ... and the caller's OWN unique name, which is the half that proves the
# driver is tracking connections rather than reciting a constant.
check_out dbus/listnames-caller '":1.' \
dbus-send --session --print-reply --dest=org.freedesktop.DBus / \
org.freedesktop.DBus.ListNames
check dbus/peer-ping \
dbus-send --session --print-reply --dest=org.freedesktop.DBus / \
org.freedesktop.DBus.Peer.Ping
check_out dbus/introspect "&1)
case "$eout" in
*"org.freedesktop.DBus.Error.ServiceUnknown"*) bst_ok dbus/error-unknown-service ;;
*) bst_notok dbus/error-unknown-service "$(_bst_1line "$eout")" ;;
esac
# ------------------------------------------------------------- the probe
probe=$D/dbus-probe
if [ -f "$BST_DATA/dbus-probe" ] \
&& cp "$BST_DATA/dbus-probe" "$probe" 2>/dev/null \
&& chmod +x "$probe" 2>/dev/null; then
bst_ok dbus/probe-stage
: > "$D/probe.log"
"$probe" > "$D/probe.log" 2>&1 &
QPID=$!
wait_for "$D/probe.log" 'P-ready' 60
if grep -q '^P-connect ok$' "$D/probe.log" 2>/dev/null; then bst_ok dbus/probe-connect
else bst_notok dbus/probe-connect "$(_bst_1line "$(cat "$D/probe.log" 2>/dev/null)")"; fi
if grep -q '^P-name ok$' "$D/probe.log" 2>/dev/null; then bst_ok dbus/probe-owns-name
else bst_notok dbus/probe-owns-name "$(_bst_1line "$(cat "$D/probe.log" 2>/dev/null)")"; fi
if grep -q '^P-ready$' "$D/probe.log" 2>/dev/null; then
bst_ok dbus/probe-ready
# The bus now knows a well-known name, which is a different question from
# ListNames reciting its own.
check_out dbus/name-has-owner "boolean true" \
dbus-send --session --print-reply --dest=org.freedesktop.DBus / \
org.freedesktop.DBus.NameHasOwner string:io.brintos.SelfTest
# ------------------------------- one case per argument type the bus marshals
db_case echo-byte 'byte 42' EchoBasic byte:42
db_case echo-boolean 'boolean true' EchoBasic boolean:true
db_case echo-int16 'int16 -3' EchoBasic int16:-3
db_case echo-uint16 'uint16 7' EchoBasic uint16:7
db_case echo-int32 'int32 -2601' EchoBasic int32:-2601
db_case echo-uint32 'uint32 2601' EchoBasic uint32:2601
# The two 64-bit cases carry values that do NOT survive a double, so a
# build that widened them through one would come back wrong rather than
# merely differently formatted.
db_case echo-int64 'int64 -9007199254740993' EchoBasic int64:-9007199254740993
db_case echo-uint64 'uint64 18446744073709551615' EchoBasic uint64:18446744073709551615
db_case echo-double 'double 2.5' EchoBasic double:2.5
db_case echo-string 'string "brintOS-dbus"' EchoBasic string:brintOS-dbus
db_case echo-objpath 'object path "/io/brintos/Thing"' EchoBasic objpath:/io/brintos/Thing
db_case echo-array 'array [ string "alpha" string "beta" ]' \
EchoArray array:string:alpha,beta
db_case echo-dict 'array [ dict entry( string "k" string "v" ) ]' \
EchoDict dict:string:string:k,v
db_case echo-variant 'variant int32 7' EchoVariant variant:int32:7
db_case make-struct 'struct { int32 2601 string "brintos" double 2.5 }' MakeStruct
db_case make-signature 'signature "a{sv}"' MakeSignature
# NEGATIVE CONTROL 2 -- a method the probe does not implement. libdbus must
# turn that into the standard error rather than a reply.
mout=$(sv NoSuchMethod)
case "$mout" in
*"org.freedesktop.DBus.Error.UnknownMethod"*) bst_ok dbus/error-unknown-method ;;
*) bst_notok dbus/error-unknown-method "$(_bst_1line "$mout")" ;;
esac
# ------------------------------------------------- a signal, seen by dbus-monitor
: > "$D/mon.log"
dbus-monitor --session "type='signal',interface='io.brintos.SelfTest'" \
> "$D/mon.log" 2>&1 &
MPID=$!
# dbus-monitor is ready once the bus has told it its own name; asserting on
# that rather than on a duration is what keeps the Emit below from racing it.
wait_for "$D/mon.log" '*NameAcquired*' 40
sv Emit string:BRINTOS-2601-PING > /dev/null 2>&1
wait_for "$D/mon.log" '*BRINTOS-2601-PING*' 40
if grep -q "member=Pinged" "$D/mon.log" 2>/dev/null \
&& grep -q "BRINTOS-2601-PING" "$D/mon.log" 2>/dev/null; then
bst_ok dbus/signal-seen
else
bst_notok dbus/signal-seen "$(_bst_1line "$(cat "$D/mon.log" 2>/dev/null)")"
fi
kill "$MPID" 2>/dev/null
else
bst_notok dbus/probe-ready "$(_bst_1line "$(cat "$D/probe.log" 2>/dev/null)")"
for c in name-has-owner echo-byte echo-boolean echo-int16 echo-uint16 \
echo-int32 echo-uint32 echo-int64 echo-uint64 echo-double \
echo-string echo-objpath echo-array echo-dict echo-variant \
make-struct make-signature error-unknown-method signal-seen; do
bst_notok "dbus/$c" "the probe never became ready -- see dbus/probe-ready"
done
fi
# The probe leaves on its own Quit, which is also the last method call this
# suite makes -- so a clean exit here is one more round trip, not just a
# tidy-up.
sv Quit > /dev/null 2>&1
wait_for "$D/probe.log" 'P-exit *' 40
if grep -q '^P-exit ok$' "$D/probe.log" 2>/dev/null; then bst_ok dbus/probe-quit
else bst_notok dbus/probe-quit "$(_bst_1line "$(tail -2 "$D/probe.log" 2>/dev/null)")"; fi
kill "$QPID" 2>/dev/null
else
bst_notok dbus/probe-stage "cannot stage $BST_DATA/dbus-probe"
for c in probe-connect probe-owns-name probe-ready name-has-owner echo-byte \
echo-boolean echo-int16 echo-uint16 echo-int32 echo-uint32 \
echo-int64 echo-uint64 echo-double echo-string echo-objpath \
echo-array echo-dict echo-variant make-struct make-signature \
error-unknown-method signal-seen probe-quit; do
bst_notok "dbus/$c" "no probe to talk to -- see dbus/probe-stage"
done
fi
else
for c in getid-length listnames listnames-caller peer-ping introspect \
error-unknown-service probe-stage probe-connect probe-owns-name \
probe-ready name-has-owner echo-byte echo-boolean echo-int16 \
echo-uint16 echo-int32 echo-uint32 echo-int64 echo-uint64 \
echo-double echo-string echo-objpath echo-array echo-dict \
echo-variant make-struct make-signature error-unknown-method \
signal-seen probe-quit; do
bst_notok "dbus/$c" "no daemon to talk to -- see dbus/daemon-alive"
done
fi
# dbus-run-session starts a bus of its OWN and tears it down again when the
# command exits -- a second, independent path through daemon startup, and the
# only case here that does not use the bus this suite started.
#
# IT THEREFORE SITS OUTSIDE THE BLOCK ABOVE. It was inside it, and that was
# wrong in the one situation where it is most informative: a private daemon
# that will not start would have skipped this case as "no daemon to talk to",
# which is a cause that does not apply to it. Bounded because it is the one
# command in the file that could fail by not returning.
bst_run 60 dbus-run-session -- /bin/true
if [ "$rc" -eq 0 ]; then bst_ok dbus/run-session
elif [ "$rc" -eq 124 ]; then bst_notok dbus/run-session "did not return within 60 s"
else bst_notok dbus/run-session "rc=$rc $(_bst_1line "$out")"; fi
# --------------------------------------------------------------- the clean stop
#
# GATED ON THE DAEMON HAVING ACTUALLY RUN, and that gate is the whole point of
# this block rather than a formality. All three cases below pass by DEFAULT when
# there is no daemon: a process that never started is not running, its socket
# does not exist, and a call to a bus that was never listening fails. Ungated,
# this file reported "the daemon shut down cleanly on SIGTERM" and "the bus is
# unreachable after stop" about a daemon that had never started -- measured, and
# the same false-green class BRINTOS-1057 already paid for once on the sibling
# dlt-daemon port.
if [ "$daemon_ran" -eq 1 ]; then
kill -TERM "$DPID" 2>/dev/null
i=0
while [ "$i" -lt 40 ]; do
[ -e "$SOCK" ] || break
i=$((i + 1)); sleep 1
done
if running "$DPID"; then bst_notok dbus/daemon-stopped "still running after SIGTERM"
else bst_ok dbus/daemon-stopped; fi
# dbus-daemon unlinks its listening socket on the way out; a leftover is a
# shutdown that did not run its cleanup.
if [ -e "$SOCK" ]; then bst_notok dbus/socket-removed "$SOCK survived shutdown"
else bst_ok dbus/socket-removed; fi
# NEGATIVE CONTROL 3 -- with the bus gone the same call that worked above must
# now fail. Without this, every case above would also pass against a dbus-send
# that reports success without connecting to anything.
check_rc dbus/unreachable-after-stop 1 \
dbus-send --session --print-reply --dest=org.freedesktop.DBus / \
org.freedesktop.DBus.GetId
else
for c in daemon-stopped socket-removed unreachable-after-stop; do
bst_notok "dbus/$c" "no daemon was ever running -- see dbus/daemon-alive"
done
fi
# $D is removed by the EXIT trap, which also covers the paths that never reach
# here.