Skip to content

Commit 8f91058

Browse files
committed
Initial version of RetroDECK API component functions
1 parent adc13e8 commit 8f91058

1 file changed

Lines changed: 351 additions & 0 deletions

File tree

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
#!/bin/bash
2+
3+
# Socket location (Flatpak): $XDG_RUNTIME_DIR/app/$FLATPAK_ID/retrodeck-api.sock
4+
# Socket location (host): /run/user/<UID>/app/net.retrodeck.retrodeck/retrodeck-api.sock
5+
6+
if [[ -n "$FLATPAK_ID" ]]; then
7+
export api_socket_path="${XDG_RUNTIME_DIR}/app/${FLATPAK_ID}/retrodeck-api.sock"
8+
export api_pid_file="${XDG_RUNTIME_DIR}/app/${FLATPAK_ID}/retrodeck-api.pid"
9+
else
10+
# Fallback for use outside Flatpak
11+
export api_socket_path="/tmp/retrodeck-api.sock"
12+
export api_pid_file="/tmp/retrodeck-api.pid"
13+
fi
14+
15+
export api_timeout=30
16+
export api_version_current="1"
17+
18+
local component_path="$(get_component_path "retrodeck-api")"
19+
export api_connection_handler_path="${component_path}/libexec/api_connection_handler.sh"
20+
21+
retrodeck_api() {
22+
case "$1" in
23+
start) api_start_server ;;
24+
stop) api_stop_server ;;
25+
status) api_status_server ;;
26+
*)
27+
echo "Usage: retrodeck_api {start|stop|status}"
28+
return 1
29+
;;
30+
esac
31+
}
32+
33+
api_start_server() {
34+
if [[ -f "$api_pid_file" ]] && kill -0 "$(cat "$api_pid_file")" 2>/dev/null; then
35+
log d "API server is already running (PID: $(cat "$api_pid_file"))"
36+
return 1
37+
fi
38+
39+
local socket_dir
40+
socket_dir=$(dirname "$api_socket_path")
41+
mkdir -p "$socket_dir"
42+
43+
# Clean up stale socket from previous unclean shutdown
44+
rm -f "$api_socket_path"
45+
46+
# Verify connection handler script exists
47+
if [[ ! -f "$api_connection_handler_path" ]]; then
48+
log e "API connection handler not found at: $api_connection_handler_path"
49+
return 1
50+
fi
51+
52+
# Check for duplicate endpoints across manifests
53+
api_check_duplicate_endpoints
54+
55+
api_run_server &
56+
local server_pid=$!
57+
echo "$server_pid" > "$api_pid_file"
58+
log d "API server started (PID: $server_pid) on socket: $api_socket_path"
59+
}
60+
61+
api_stop_server() {
62+
if [[ -f "$api_pid_file" ]]; then
63+
local pid
64+
pid=$(cat "$api_pid_file")
65+
if kill "$pid" 2>/dev/null; then
66+
log d "Stopping API server (PID: $pid)..."
67+
# Kill any child handler processes in the same process group
68+
kill -- -"$pid" 2>/dev/null
69+
rm -f "$api_pid_file" "$api_socket_path"
70+
return 0
71+
else
72+
log d "API server not running; cleaning up residual files"
73+
rm -f "$api_pid_file" "$api_socket_path"
74+
return 1
75+
fi
76+
else
77+
log d "No running API server found"
78+
return 1
79+
fi
80+
}
81+
82+
api_status_server() {
83+
if [[ -f "$api_pid_file" ]] && kill -0 "$(cat "$api_pid_file")" 2>/dev/null; then
84+
log d "API server is running (PID: $(cat "$api_pid_file"))"
85+
return 0
86+
else
87+
log d "API server is not running"
88+
return 1
89+
fi
90+
}
91+
92+
api_run_server() {
93+
log d "API server running (PID: $$), socket: $api_socket_path"
94+
95+
trap 'log d "API server shutting down..."; rm -f "$api_pid_file" "$api_socket_path"; exit 0' EXIT INT TERM
96+
97+
socat UNIX-LISTEN:"${api_socket_path}",fork,reuseaddr,mode=660 \
98+
EXEC:"stdbuf -oL bash ${api_connection_handler_path}",nofork 2>/dev/null
99+
100+
# If socat exits unexpectedly, clean up
101+
log d "socat exited unexpectedly, cleaning up"
102+
rm -f "$api_pid_file" "$api_socket_path"
103+
}
104+
105+
api_handle_request() {
106+
# USAGE: api_handle_request "$json_request_line"
107+
108+
local json_input="$1"
109+
110+
# Validate JSON format
111+
if ! jq empty <<< "$json_input" 2>/dev/null; then
112+
api_build_response "error" "unknown" "" "Invalid JSON format"
113+
return
114+
fi
115+
116+
# Extract protocol-level fields
117+
local action request_id api_version request request_data
118+
action=$(jq -r '.action // empty' <<< "$json_input")
119+
request_id=$(jq -r '.request_id // empty' <<< "$json_input")
120+
api_version=$(jq -r '.version // empty' <<< "$json_input")
121+
request=$(jq -r '.request // empty' <<< "$json_input")
122+
request_data=$(jq -r '.data // empty' <<< "$json_input")
123+
124+
# Validate required protocol fields
125+
if [[ -z "$request_id" ]]; then
126+
api_build_response "error" "unknown" "" "Missing required field: request_id"
127+
return
128+
fi
129+
130+
if [[ -z "$api_version" ]]; then
131+
api_build_response "error" "$request_id" "" "Missing required field: version"
132+
return
133+
fi
134+
135+
if [[ -z "$action" ]]; then
136+
api_build_response "error" "$request_id" "" "Missing required field: action"
137+
return
138+
fi
139+
140+
# Handle built-in server actions
141+
case "$action" in
142+
"check_status")
143+
api_build_response "success" "$request_id" '"ok"' ""
144+
return
145+
;;
146+
"list_endpoints")
147+
local endpoints
148+
endpoints=$(api_builtin_list_endpoints)
149+
api_build_response "success" "$request_id" "$endpoints" ""
150+
return
151+
;;
152+
esac
153+
154+
if [[ -z "$request" ]]; then
155+
api_build_response "error" "$request_id" "" "Missing required field: request"
156+
return
157+
fi
158+
159+
api_handle_request "$action" "$request" "$request_data" "$request_id"
160+
}
161+
162+
api_handle_request() {
163+
# USAGE: api_handle_request "$action" "$request" "$request_data" "$request_id"
164+
165+
local action="$1"
166+
local request="$2"
167+
local request_data="$3"
168+
local request_id="$4"
169+
170+
local endpoint_key="${action}::${request}"
171+
172+
# Look up endpoint definition across all component manifests
173+
local endpoint_def
174+
endpoint_def=$(jq -r --arg key "$endpoint_key" '
175+
[.[] | .manifest.api_endpoints // {} | .[$key] // empty]
176+
| if length > 0 then first else empty end
177+
' "$component_manifest_cache_file" 2>/dev/null)
178+
179+
if [[ -z "$endpoint_def" ]]; then
180+
api_build_response "error" "$request_id" "" "Unknown endpoint: $endpoint_key"
181+
return
182+
fi
183+
184+
# Extract endpoint metadata
185+
local target_function
186+
target_function=$(jq -r '.function' <<< "$endpoint_def")
187+
188+
# Verify target function exists
189+
if ! declare -f "$target_function" > /dev/null 2>&1; then
190+
api_build_response "error" "$request_id" "" "Endpoint function not found: $target_function"
191+
return
192+
fi
193+
194+
# Parse request data as JSON object
195+
local data_json
196+
if [[ -n "$request_data" && "$request_data" != "null" ]]; then
197+
if ! jq empty <<< "$request_data" 2>/dev/null; then
198+
api_build_response "error" "$request_id" "" "Invalid JSON in data field"
199+
return
200+
fi
201+
data_json="$request_data"
202+
else
203+
data_json="{}"
204+
fi
205+
206+
# Validate required fields are present in data
207+
local missing_fields
208+
missing_fields=$(jq -r --argjson data "$data_json" '
209+
[.required_fields // [] | .[] | select(. as $f | $data | has($f) | not)]
210+
| if length > 0 then join(", ") else empty end
211+
' <<< "$endpoint_def")
212+
213+
if [[ -n "$missing_fields" ]]; then
214+
api_build_response "error" "$request_id" "" "Missing required data fields: $missing_fields"
215+
return
216+
fi
217+
218+
local -a func_args=()
219+
220+
# Extract required field values in declared order
221+
# Complex types (arrays, objects) are passed as JSON strings for the backend function to parse
222+
while IFS= read -r field_name; do
223+
[[ -z "$field_name" ]] && continue
224+
local field_value
225+
field_value=$(jq -r --arg f "$field_name" \
226+
'.[$f] | if type == "array" or type == "object" then tojson else . end' \
227+
<<< "$data_json")
228+
func_args+=("$field_value")
229+
done < <(jq -r '.required_fields // [] | .[]' <<< "$endpoint_def")
230+
231+
# Extract optional field values in declared order (empty string if absent)
232+
while IFS= read -r field_name; do
233+
[[ -z "$field_name" ]] && continue
234+
local field_value
235+
field_value=$(jq -r --arg f "$field_name" \
236+
'if has($f) then .[$f] | if type == "array" or type == "object" then tojson else . end else empty end' \
237+
<<< "$data_json")
238+
func_args+=("$field_value")
239+
done < <(jq -r '.optional_fields // [] | .[]' <<< "$endpoint_def")
240+
241+
# Set request_id for progress reporting
242+
_api_request_id="$request_id"
243+
244+
# Set up fd for streaming progress
245+
exec 3>&1
246+
247+
local result
248+
if result=$("$target_function" "${func_args[@]}"); then
249+
if jq empty <<< "$result" 2>/dev/null; then
250+
api_build_response "success" "$request_id" "$result" ""
251+
else
252+
local json_result
253+
json_result=$(jq -c -n --arg r "$result" '$r')
254+
api_build_response "success" "$request_id" "$json_result" ""
255+
fi
256+
else
257+
local error_msg="$result"
258+
if [[ -z "$error_msg" ]]; then
259+
error_msg="Endpoint function returned an error"
260+
fi
261+
api_build_response "error" "$request_id" "" "$error_msg"
262+
fi
263+
264+
exec 3>&-
265+
}
266+
267+
api_send_progress() {
268+
# USAGE: api_send_progress "$current" "$total" "$message"
269+
270+
local current="$1"
271+
local total="$2"
272+
local message="$3"
273+
274+
jq -c -n \
275+
--arg status "progress" \
276+
--arg request_id "$_api_request_id" \
277+
--argjson current "$current" \
278+
--argjson total "$total" \
279+
--arg message "$message" \
280+
'{status: $status, request_id: $request_id, progress: {current: $current, total: $total, message: $message}}' >&3
281+
}
282+
283+
api_build_response() {
284+
# USAGE: api_build_response "$status" "$request_id" "[$result_json]" "[$error_message]"
285+
286+
local status="$1"
287+
local request_id="$2"
288+
local result_json="$3"
289+
local error_message="$4"
290+
291+
if [[ "$status" == "success" ]]; then
292+
jq -c -n \
293+
--arg status "$status" \
294+
--arg request_id "$request_id" \
295+
--argjson result "${result_json:-null}" \
296+
'{status: $status, request_id: $request_id, result: $result}'
297+
else
298+
jq -c -n \
299+
--arg status "$status" \
300+
--arg request_id "$request_id" \
301+
--arg message "$error_message" \
302+
'{status: $status, request_id: $request_id, message: $message}'
303+
fi
304+
}
305+
306+
api_builtin_list_endpoints() {
307+
# USAGE: api_builtin_list_endpoints
308+
309+
jq -c '{
310+
"built_in": {
311+
"check_status": {
312+
"description": "Check if the API server is running",
313+
"required_fields": [],
314+
"optional_fields": []
315+
},
316+
"list_endpoints": {
317+
"description": "List all available API endpoints",
318+
"required_fields": [],
319+
"optional_fields": []
320+
}
321+
},
322+
"endpoints": (
323+
[.[] | .manifest.api_endpoints // {} | to_entries[]]
324+
| group_by(.key)
325+
| map({
326+
key: first.key,
327+
value: (first.value + (if length > 1 then {"note": "duplicate endpoint across components, framework definition takes priority"} else {} end))
328+
})
329+
| from_entries
330+
)
331+
}' "$component_manifest_cache_file" 2>/dev/null || echo '{"built_in":{},"endpoints":{}}'
332+
}
333+
334+
api_check_duplicate_endpoints() {
335+
# USAGE: api_check_duplicate_endpoints
336+
337+
local duplicates
338+
duplicates=$(jq -r '
339+
[.[] | .manifest.component_name as $comp | (.manifest.api_endpoints // {} | keys[]) as $ep | {component: $comp, endpoint: $ep}]
340+
| group_by(.endpoint)
341+
| map(select(length > 1))
342+
| .[]
343+
| "Duplicate API endpoint \"" + (first.endpoint) + "\" found in components: " + ([.[].component] | join(", ")) + ". Framework definition takes priority."
344+
' "$component_manifest_cache_file" 2>/dev/null)
345+
346+
if [[ -n "$duplicates" ]]; then
347+
while IFS= read -r warning; do
348+
log w "$warning"
349+
done <<< "$duplicates"
350+
fi
351+
}

0 commit comments

Comments
 (0)