aboutsummaryrefslogtreecommitdiff
path: root/docker/scripts/ciq-env.sh
blob: a52db7813d4fb3f8ec86e4f41b8db3db0ad427fd (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
#!/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.
  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
}