#!/usr/bin/env bash # Author: cirakg & claude ai # Summary: Shared helpers for the container scripts (SDK and key lookup, headless display and simulator). # # Source this file; don't run it. # # Paths, as seen inside the container: # /work the project folder (bind mount) # /home/dev the project's .cache/home folder (bind mount); the Garmin # tools keep the SDK and device files under ~/.Garmin # /work/.cache/keys the developer key (never baked into an image) CIQ_HOME="${HOME}/.Garmin/ConnectIQ" KEY_DIR="/work/.cache/keys" DEV_KEY="${KEY_DIR}/developer_key.der" BUILD_DIR="/work/build" # Virtual display for the headless simulator, and the TCP port (1234, hex # 04D2 as in /proc/net/tcp) on which the simulator accepts monkeydo. SIM_DISPLAY=":99" SIM_PORT_HEX="04D2" SIM_START_TIMEOUT_S=60 # Print an error and stop. Used for every user-facing failure, so the message # says what to do next instead of leaving a bare stack trace. die() { echo "error: $*" >&2 exit 1 } # Progress line on stderr, so stdout stays clean for machine-readable output. log() { echo "==> $*" >&2 } # Find the SDK selected by `make sdk` and put its bin/ folder on PATH. # Sets CIQ_SDK to the SDK root folder. ciq_require_sdk() { local cfg="${CIQ_HOME}/current-sdk.cfg" [[ -f "${cfg}" ]] || die "Connect IQ SDK not installed. Run 'make sdk' first (you accept Garmin's licence and log in there)." CIQ_SDK="$(tr -d '\r\n' < "${cfg}")" CIQ_SDK="${CIQ_SDK%/}" [[ -x "${CIQ_SDK}/bin/monkeyc" ]] || die "SDK folder '${CIQ_SDK}' has no bin/monkeyc. Run 'make sdk' again." export CIQ_SDK export PATH="${CIQ_SDK}/bin:${PATH}" } # The developer key signs every build, including test builds. ciq_require_key() { [[ -f "${DEV_KEY}" ]] || die "No developer key at .cache/keys/developer_key.der. Run 'make key' first." } # Fail early, with a pointer to `make sdk`, when a device's files are missing. ciq_require_device() { local device="$1" [[ -f "${CIQ_HOME}/Devices/${device}/compiler.json" ]] \ || die "No device files for '${device}'. Is it in manifest.xml? Then run 'make sdk' to download them." } # Start a virtual X display (Xvfb) with the openbox window manager. The window # manager matters: screenshots and scripted input find the simulator window # through it. ciq_start_display() { export DISPLAY="${SIM_DISPLAY}" Xvfb "${DISPLAY}" -screen 0 1600x1200x24 -nolisten tcp >/tmp/xvfb.log 2>&1 & local _ for _ in $(seq 1 50); do xdpyinfo >/dev/null 2>&1 && break sleep 0.1 done xdpyinfo >/dev/null 2>&1 || die "Xvfb did not start (see /tmp/xvfb.log)." openbox >/tmp/openbox.log 2>&1 & } # True when something listens on the simulator's monkeydo port. ciq_simulator_listening() { awk -v port=":${SIM_PORT_HEX}" \ 'NR > 1 && $4 == "0A" && substr($2, length($2) - 4) == port { found = 1 } END { exit !found }' \ /proc/net/tcp } # Start the Connect IQ simulator (needs ciq_require_sdk and ciq_start_display) # and wait until it accepts monkeydo connections. ciq_start_simulator() { # The simulator's single-instance lock (~/Sim-) stores its process ID # and survives in .cache/home after the container is gone. A later container # often reuses that process ID for something else, and the simulator then # believes it is already running and never opens its port. Each container # runs exactly one simulator, so the stale lock is safe to remove. rm -f "${HOME}"/Sim-* simulator >/tmp/simulator.log 2>&1 & local pid=$! _ for _ in $(seq 1 $((SIM_START_TIMEOUT_S * 5))); do ciq_simulator_listening && return 0 kill -0 "${pid}" 2>/dev/null || break sleep 0.2 done # The container (and /tmp) disappears after a failure, so show the log now. echo "--- last lines of the simulator log ---" >&2 tail -n 25 /tmp/simulator.log >&2 || true die "The Connect IQ simulator did not start within ${SIM_START_TIMEOUT_S} s." } # The simulator's main window ID, for screenshots and scripted input. ciq_simulator_window() { xdotool search --name '^CIQ Simulator' | head -n 1 } # Longest wait for an app's first frame after launching it. FIRST_FRAME_TIMEOUT_S=60 # Launch the debug build of the app on DEVICE and wait for its first frame, # which the app reports with a "DIAG first_frame" console line. The console # goes to LOG, followed by a "HARNESS first_frame wall_ms=N" line: the # wall-clock time from launch to first frame, including the JVM start of # monkeydo and the program load the app can't time itself. The app keeps # running (for screenshots) until ciq_stop_app or the next launch. # Returns 1 if no first frame appeared in time. ciq_launch_app() { local device="$1" app_log="$2" start _ start="$(date +%s%3N)" monkeydo "${BUILD_DIR}/debug/${device}.prg" "${device}" >"${app_log}" 2>&1 & CIQ_MONKEYDO_PID=$! for _ in $(seq 1 $((FIRST_FRAME_TIMEOUT_S * 20))); do if grep -q "DIAG first_frame" "${app_log}"; then echo "HARNESS first_frame wall_ms=$(($(date +%s%3N) - start))" >>"${app_log}" return 0 fi # monkeydo only exits on its own when the app could not run. kill -0 "${CIQ_MONKEYDO_PID}" 2>/dev/null || break sleep 0.05 done return 1 } # Stop the monkeydo session started by ciq_launch_app. ciq_stop_app() { kill "${CIQ_MONKEYDO_PID:-0}" 2>/dev/null || true wait "${CIQ_MONKEYDO_PID:-0}" 2>/dev/null || true }