aboutsummaryrefslogtreecommitdiff
path: root/docker/scripts/ciq-env.sh
blob: 6216321b7a5aabd8f50167c3703faf92e8193c0d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/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-<user>) 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.
  # A simulator this container already started is reused: a second one
  # would open a second window, and input could go to the wrong one.
  ciq_simulator_listening && return 0
  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 app on DEVICE (the debug build, or MODE's build: uitest) 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.
# Apps with a glance start in it; the glance is opened with START or a tap,
# so the wall time includes that. Returns 1 if no first frame appeared in time.
ciq_launch_app() {
  local device="$1" app_log="$2" mode="${3:-debug}" start _
  start="$(date +%s%3N)"
  # Both monkeydo and the HARNESS line below append, so neither overwrites
  # the other's output.
  : >"${app_log}"
  monkeydo "${BUILD_DIR}/${mode}/${device}.prg" "${device}" >>"${app_log}" 2>&1 &
  CIQ_MONKEYDO_PID=$!
  local presses=0 glance_seen_at=0 tick=0
  for _ in $(seq 1 $((FIRST_FRAME_TIMEOUT_S * 20))); do
    tick=$((tick + 1))
    # The simulator starts apps that have a glance in the glance. Open the
    # app from it, as a user would: 2 s after the glance appears (earlier
    # presses get lost), again every 8 s if the app hasn't opened.
    if ((glance_seen_at == 0)) && grep -q "DIAG shown glance" "${app_log}"; then
      glance_seen_at=${tick}
    fi
    if ((glance_seen_at > 0 && presses < 3 && tick - glance_seen_at == 40 + presses * 160)); then
      presses=$((presses + 1))
      python3 /work/tools/ui_flow.py --open-from-glance --device-dir "${CIQ_HOME}/Devices/${device}" \
        --window "$(ciq_simulator_window)" --out "${BUILD_DIR}/ui-input" >&2 || return 1
    fi
    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
}