diff --git a/.github/workflows/desktop-linux.yml b/.github/workflows/desktop-linux.yml index fad3c27923..0562f755c9 100644 --- a/.github/workflows/desktop-linux.yml +++ b/.github/workflows/desktop-linux.yml @@ -211,8 +211,8 @@ jobs: - name: Install tools run: | sudo apt-get update - sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 - dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 + sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core + dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core - name: Download core library uses: actions/download-artifact@v7 @@ -275,7 +275,9 @@ jobs: - name: Run smoke test if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.link.outcome == 'success' }} - run: scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/smoke-test" + run: | + export NAMES_SERVER=$(cat "$RUNNER_TEMP/backend/names-server") + scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/smoke-test" - name: Stop test backend if: ${{ !cancelled() }} diff --git a/scripts/ci/desktop-linux/backend.sh b/scripts/ci/desktop-linux/backend.sh index 2c1225de83..75f6da24ff 100755 --- a/scripts/ci/desktop-linux/backend.sh +++ b/scripts/ci/desktop-linux/backend.sh @@ -4,13 +4,17 @@ # and one channel registered and approved by a CLI peer, and a search request that must find both. # Both have profile images as large as the apps make them, so their search results don't fit in one service # response, and the search must page through them. +# For SimpleX names, an SMP server on 127.0.0.1 resolves .testing names through a local resolver, and a CLI peer +# with an address on it claims the name seeded.testing. # Usage: # scripts/ci/desktop-linux/backend.sh start writes the directory's short address to /directory-link -# scripts/ci/desktop-linux/backend.sh stop stops the relay and the directory, prints their logs -# The relay and the directory keep running between start and stop. +# and the names server's address to /names-server +# scripts/ci/desktop-linux/backend.sh stop stops the servers, prints their logs +# The relay, the directory, the SMP server and the resolver keep running between start and stop. set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) directory_name="SimpleX Directory" admin=ci_admin search_text="ci seed" @@ -18,8 +22,8 @@ search_text="ci seed" max_image_size=12500 # the largest service response body, compressed if it has to be (maxServiceBodyLength) max_service_body=10737 -group_colour="rgb(200,40,40)" -channel_colour="rgb(40,170,60)" +name_label=seeded +resolver_port=8090 # Runs one chat command in a CLI client that stays online for after it; the output is in $work/out. chat() { @@ -63,18 +67,21 @@ db_query() { } # Prints a profile image data URI made as the apps make one: JPEG at quality 85, scaled down by the square root -# of its excess, at most halved per step, until it is at most max_image_size characters. The source is noise -# over the colour, which JPEG can't compress much, so the image ends up near the limit. +# of its excess, at most halved per step, until it is at most max_image_size characters. The picture fills the +# whole square, a gradient between two shades of one colour with a large white letter; noise keeps JPEG from +# compressing it much, so the image ends up near the limit. avatar() { - local colour=$1 source=$work/avatar-source.png file=$work/avatar.jpg size=512 uri - convert -size 512x512 xc:gray50 -seed 1 +noise Random -modulate 100,0 -fill "$colour" -colorize 70% "$source" + local top=$1 bottom=$2 letter=$3 source=$work/avatar-source.png file=$work/avatar.jpg size=512 uri font + font=$(fc-match -f '%{file}' 'DejaVu Sans:bold') + convert -size 512x512 gradient:"$top-$bottom" -seed 1 -attenuate 0.5 +noise Gaussian \ + -font "$font" -pointsize 320 -fill white -gravity center -annotate +0+0 "$letter" "$source" while :; do convert "$source" -resize "${size}x${size}" -quality 85 "$file" uri="data:image/jpg;base64,$(base64 -w 0 "$file")" [ "${#uri}" -gt "$max_image_size" ] || break size=$(awk -v s="$size" -v n="${#uri}" -v m="$max_image_size" 'BEGIN { r = sqrt(n / m); if (r > 2) r = 2; print int(s / r) }') done - echo "avatar over $colour: ${size}x${size} px, ${#uri} characters, mean colour $(convert "$file" -resize '1x1!' -format '%[pixel:p{0,0}]' info:)" >&2 + echo "avatar \"$letter\" over $top to $bottom: ${size}x${size} px, ${#uri} characters, mean colour $(convert "$file" -resize '1x1!' -format '%[pixel:p{0,0}]' info:)" >&2 echo "$uri" } @@ -169,9 +176,9 @@ group_json() { seed_group() { local user_id user_id=$(db_query "$admin" "SELECT user_id FROM users WHERE local_display_name = '$admin'") - group_image=$(avatar "$group_colour") - admin 1 "/_group $user_id $(group_json ci_group "CI seed group" "$group_image" "")" - expect_out "group #ci_group \(CI seed group\) is created" + group_image=$(avatar "rgb(235,80,70)" "rgb(150,25,30)" G) + admin 1 "/_group $user_id $(group_json ci_group "CI seeded group" "$group_image" "")" + expect_out "group #ci_group \(CI seeded group\) is created" admin 5 "/a ci_group '$directory_name' admin" expect_out "invitation to join the group #ci_group sent to" approve ci_group "Group approved!" @@ -183,9 +190,9 @@ seed_channel() { local user_id relay_id user_id=$(db_query "$admin" "SELECT user_id FROM users WHERE local_display_name = '$admin'") relay_id=$(db_query "$admin" "SELECT chat_relay_id FROM chat_relays WHERE display_name = 'ci_relay' AND deleted = 0") - channel_image=$(avatar "$channel_colour") - admin 10 "/_public group $user_id $relay_id $(group_json ci_channel "CI seed channel" "$channel_image" ",\"support\":{\"enable\":\"off\"}")" - expect_out "group #ci_channel \(CI seed channel\) is created" + channel_image=$(avatar "rgb(80,200,90)" "rgb(25,120,45)" C) + admin 10 "/_public group $user_id $relay_id $(group_json ci_channel "CI seeded channel" "$channel_image" ",\"support\":{\"enable\":\"off\"}")" + expect_out "group #ci_channel \(CI seeded channel\) is created" local group_id group_id=$(db_query "$admin" "SELECT group_id FROM groups WHERE local_display_name = 'ci_channel'") admin_until "relay id [0-9]+: active" 180 "/_get relays #$group_id" @@ -194,6 +201,71 @@ seed_channel() { approve ci_channel "Channel approved!" } +# An SMP server on 127.0.0.1 whose [NAMES] section asks the local resolver, which answers from names.json. +start_names_server() { + local cfg=$work/smp/etc logs=$work/smp/var ini deadline + echo '{}' > "$work/names.json" + nohup python3 "$here/names-resolver.py" "$resolver_port" "$work/names.json" < /dev/null > "$work/resolver.log" 2>&1 & + echo $! > "$work/resolver.pid" + SMP_SERVER_CFG_PATH=$cfg SMP_SERVER_LOG_PATH=$logs "$bin/smp-server" init -y --ip 127.0.0.1 --no-password --disable-web + ini=$cfg/smp-server.ini + # only 5223: 443 needs root + sed -i -e 's/^port = 5223,443$/port = 5223/' \ + -e '/^\[NAMES\]/,/^\[/{s/^enable: off$/enable: on/;s|^# resolver_endpoint: http://127.0.0.1:8000$|resolver_endpoint: http://127.0.0.1:'"$resolver_port"'|}' "$ini" + if ! grep -qx 'port = 5223' "$ini" || + ! sed -n '/^\[NAMES\]/,/^\[/p' "$ini" | grep -qx 'enable: on' || + ! sed -n '/^\[NAMES\]/,/^\[/p' "$ini" | grep -qx "resolver_endpoint: http://127.0.0.1:$resolver_port"; then + echo "error: $ini has a different port or [NAMES] than expected:" + grep -E '^port|^\[NAMES\]|enable|resolver' "$ini" + return 1 + fi + SMP_SERVER_CFG_PATH=$cfg SMP_SERVER_LOG_PATH=$logs nohup "$bin/smp-server" start < /dev/null > "$work/smp-server.log" 2>&1 & + echo $! > "$work/smp-server.pid" + deadline=$((SECONDS + 60)) + until (exec 3<> /dev/tcp/127.0.0.1/5223) 2> /dev/null; do + if ! kill -0 "$(cat "$work/smp-server.pid")" 2> /dev/null || [ "$SECONDS" -ge "$deadline" ]; then + echo "error: the SMP server is not listening on 5223" + cat "$work/smp-server.log" + return 1 + fi + sleep 1 + done + names_server="smp://$(cat "$cfg/fingerprint")@127.0.0.1" + echo "$names_server" > "$work/names-server" + echo "names server: $names_server" +} + +# A CLI client started with -s uses only that server and, once its user exists, gives it every role, names included. +names_client() { + local db=$1 + shift + chat "$db" "$@" --user-display-name "$db" -s "$names_server" +} + +# ci_contact's address is on the names server; the resolver maps seeded.testing to it, and ci_contact claims the name. +seed_name() { + local image link + image=$(avatar "rgb(70,130,240)" "rgb(30,50,170)" N) + names_client ci_contact 1 "/_profile 1 {\"displayName\":\"ci_contact\",\"fullName\":\"\",\"shortDescr\":\"CI seeded contact\",\"image\":\"$image\"}" + names_client ci_contact 3 "/ad" + link=$(awk '/Your new chat address is created!/ {getline; getline; print; exit}' "$work/out") + case "$link" in + https://*/a#*) ;; + *) + echo "error: no short address for ci_contact in the output" + return 1 + ;; + esac + jq -n --arg n "$name_label" --arg l "$link" '{($n): {simplexContact: [$l]}}' > "$work/names.json" + names_client ci_contact 5 "/_set domain 1 $name_label.testing" + # what the app does on the Search key, from a client that knows nothing but the names server; it finds the + # address only if ci_contact's claim is in the address data + names_client name_probe 1 "/user" + names_client name_probe 5 "/_connect plan 1 @$name_label.testing" + expect_out "contact address: ok to connect" + echo "@$name_label.testing resolves to ci_contact's address" +} + page_summary() { local resp=$1 compressed="" if command -v zstd > /dev/null; then @@ -266,17 +338,19 @@ start() { seed_group seed_channel probe_search + start_names_server + seed_name } stop() { local p - for p in directory relay; do + for p in directory relay smp-server resolver; do if [ -f "$work/$p.pid" ]; then kill -INT "$(cat "$work/$p.pid")" 2> /dev/null || true fi done sleep 3 - for p in directory relay; do + for p in directory relay smp-server resolver; do echo "::group::$p log" cat "$work/$p.log" 2> /dev/null || true echo "::endgroup::" diff --git a/scripts/ci/desktop-linux/build-backend.sh b/scripts/ci/desktop-linux/build-backend.sh index fcc8c08850..9261c56ae1 100755 --- a/scripts/ci/desktop-linux/build-backend.sh +++ b/scripts/ci/desktop-linux/build-backend.sh @@ -1,8 +1,10 @@ #!/bin/bash -# Builds the CLI and the directory service that the app job runs as its test backend. +# Builds the CLI, the directory service and the SMP server that the app job runs as its test backend. # Usage: scripts/ci/desktop-linux/build-backend.sh # They need the server-capable configuration (no client_library), as build.yml's Linux job builds the CLI. +# The SMP server, which resolves the test's SimpleX names, comes from a clone of the simplexmq commit that +# cabal.project pins: cabal builds only the libraries of a source-repository-package. set -euo pipefail @@ -23,10 +25,19 @@ EOF cabal build exe:simplex-chat exe:simplex-directory-service +simplexmq_tag=$(sed -n '/simplex-chat\/simplexmq.git/{n;s/^ *tag: *//p}' cabal.project) +smq=$(mktemp -d) +git -C "$smq" init -q +git -C "$smq" fetch -q --depth 1 https://github.com/simplex-chat/simplexmq.git "$simplexmq_tag" +git -C "$smq" checkout -q FETCH_HEAD +cp cabal.project.local "$smq" +(cd "$smq" && cabal build exe:smp-server) + pkg=$(mktemp -d) for exe in simplex-chat simplex-directory-service; do cp "$(cabal list-bin "exe:$exe")" "$pkg" done +cp "$(cd "$smq" && cabal list-bin exe:smp-server)" "$pkg" ldd "$pkg"/* tar -C "$pkg" -czf "$out" . diff --git a/scripts/ci/desktop-linux/names-resolver.py b/scripts/ci/desktop-linux/names-resolver.py new file mode 100644 index 0000000000..34ff8cfd8b --- /dev/null +++ b/scripts/ci/desktop-linux/names-resolver.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +# A SimpleX name resolver for the test SMP server's [NAMES] section, as simplexmq's test resolver answers: +# GET /v2/resolve/ with a NameResponse for every name, registered or not, and GET /health. +# Names are read from the registry file on each request: {"