Initial API implementation

This commit is contained in:
icenine451 2025-04-04 12:08:35 -04:00
parent 739248b24c
commit 9c4263e207
4 changed files with 342 additions and 1 deletions

View file

@ -0,0 +1,164 @@
#!/bin/bash
# Set up paths for named pipes (same as in the server)
rd_api_dir="$HOME/.var/app/net.retrodeck.retrodeck/config/retrodeck/api"
REQUEST_PIPE="$rd_api_dir/retrodeck_api_pipe"
# Function to send a request and get a response
send_request() {
local request="$1"
local timeout="${2:-5}"
# Check if pipes exist
if [[ ! -p "$REQUEST_PIPE" ]]; then
echo "Error: Request pipe does not exist at $REQUEST_PIPE" >&2
echo "Make sure the API server is running." >&2
return 1
fi
# Create a unique request ID
local request_id="client_$(date +%s)_$$"
local response_pipe="$rd_api_dir/response_${request_id}"
# Create response pipe
mkfifo "$response_pipe"
chmod 600 "$response_pipe"
# Add request_id to the JSON if it doesn't have one already
# First, validate JSON and then add the request_id
if ! echo "$request" | jq -e . >/dev/null 2>&1; then
echo "Error: Invalid JSON request: $request" >&2
return 1
fi
if ! echo "$request" | jq -e '.request_id' >/dev/null 2>&1; then
# We need to properly quote the request_id for jq
request=$(echo "$request" | jq --arg rid "$request_id" '. + {request_id: $rid}')
else
request_id=$(echo "$request" | jq -r '.request_id')
fi
# Start reading the response pipe in the background with a timeout
# Use 'cat' instead of 'read' to capture multiline responses
local response
# Write to pipe first, then read from response pipe
echo "$request" > "$REQUEST_PIPE"
response=$(timeout "$timeout" cat "$response_pipe")
# Clean up response pipe
rm -f "$response_pipe"
# Check if we got a response
if [[ -z "$response" ]]; then
echo "Error: No response received within $timeout seconds" >&2
return 1
fi
# Return the response
echo "$response"
}
# Function to display help
show_help() {
echo "Bash API Client"
echo "Usage: $0 [options] [JSON request]"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " -a, --action ACTION API action to perform"
echo " -d, --data DATA Data for the request"
echo " -t, --timeout TIMEOUT Manually-defined timeout for the request (in seconds)"
echo ""
echo "Examples:"
echo " $0 '{\"action\":\"process_data\",\"data\":\"test\"}'"
echo " $0 --action process_data --data \"test data\""
}
# If no arguments, show help
if [[ $# -eq 0 ]]; then
show_help
exit 0
fi
# Parse arguments
ACTION=""
TIMEOUT=5
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-a|--action)
ACTION="$2"
shift 2
;;
-d|--data)
DATA="$2"
shift 2
;;
-t|--timeout)
TIMEOUT="$2"
shift 2
;;
*)
# Assume it's a JSON string if it starts with {
if [[ "$1" == {* ]]; then
JSON_REQUEST="$1"
else
echo "Unknown option: $1"
show_help
exit 1
fi
shift
;;
esac
done
# If we have a JSON request, use it directly
if [[ -n "$JSON_REQUEST" ]]; then
# Validate JSON
if ! echo "$JSON_REQUEST" | jq . >/dev/null 2>&1; then
echo "Error: Invalid JSON request" >&2
exit 1
fi
# Send the request
echo "sending request: $JSON_REQUEST"
response=$(send_request "$JSON_REQUEST" "$TIMEOUT")
exit_code=$?
# Pretty-print the response
if [[ $exit_code -eq 0 ]]; then
echo "$response" | jq .
fi
exit $exit_code
fi
# Otherwise, build a JSON request from the arguments
if [[ -n "$ACTION" ]]; then
# Create JSON object with proper quoting
JSON_REQUEST=$(jq -n --arg action "$ACTION" '{action: $action}')
# Add data if provided
if [[ -n "$DATA" ]]; then
JSON_REQUEST=$(echo "$JSON_REQUEST" | jq --arg data "$DATA" '. + {data: $data}')
fi
# Send the request
response=$(send_request "$JSON_REQUEST" "$TIMEOUT")
exit_code=$?
# Pretty-print the response
if [[ $exit_code -eq 0 ]]; then
echo "$response" | jq .
fi
exit $exit_code
else
echo "Error: No action specified and no JSON request provided" >&2
show_help
exit 1
fi

165
functions/api.sh Normal file
View file

@ -0,0 +1,165 @@
#!/bin/bash
# This is the main processing point for the RetroDECK API
# It will accept JSON objects as requests in a single FIFO request pipe ($REQUEST_PIPE)
# and return each processed response through a unique named pipe, which MUST be created by the requesting client.
# Each JSON object needs, at minimum a "action" element with a valid value and a "request_id" with a unique value.
# Each processed response will be returned on a FIFO named pipe at the location "$XDG_CONFIG_HOME/retrodeck/api/response_$request_id" so that actions can be processed asynchronously
# If the response pipe does not exist when the data is done processing the response will not be sent at all, so the client must ensure that the response pipe exists when the JSON object is sent to the server!
# The response ID can be any unique value, an example ID generation statement in Bash is request_id="retrodeck_request_$(date +%s)_$$"
# The server can be started, stopped or have its running status checked by calling the script like this: retrodeck_api start
# retrodeck_api stop
# retrodeck_api status
retrodeck_api() {
# Handle command-line arguments
case "$1" in
start) start_server ;;
stop) stop_server ;;
status) status_server ;;
*)
echo "Usage: $0 {start|stop|status}"
exit 1
;;
esac
}
start_server() {
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
log d "Server is already running (PID: $(cat "$PID_FILE"))"
return 1
fi
if [[ ! -p "$REQUEST_PIPE" ]]; then # Create the request pipe if it doesn't exist.
mkfifo "$REQUEST_PIPE"
chmod 600 "$REQUEST_PIPE"
fi
run_server & # Run server in background
local SERVER_PID=$!
echo "$SERVER_PID" > "$PID_FILE"
log d "Server started (PID: $SERVER_PID)"
}
stop_server() {
if [[ -f "$PID_FILE" ]]; then
local PID
PID=$(cat "$PID_FILE")
if kill "$PID" 2>/dev/null; then
log d "Stopping server (PID: $PID)..."
rm -f "$PID_FILE" "$REQUEST_PIPE"
return 0
else
log d "Server not running; cleaning up residual files"
rm -f "$PID_FILE" "$REQUEST_PIPE"
return 1
fi
else
log d "No running server found."
return 1
fi
}
status_server() {
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
log d "Server is running (PID: $(cat "$PID_FILE"))."
else
log d "Server is not running."
fi
}
run_server() {
log d "Server is running with PID $$ (Process Group $$)..."
log d "Request pipe: $REQUEST_PIPE"
cleanup() {
# Cleanup function to ensure named pipe is removed on exit
log d "Cleaning up server resources..."
rm -f "$REQUEST_PIPE"
exit 0
}
trap cleanup EXIT INT TERM
local buffer="" # Buffer to accumulate lines from the request pipe, needed for multi-line JSON requests
while true; do
if IFS= read -r line; then # Read one line from the request pipe
buffer+="$line"$'\n' # Append the line (plus a newline) to the buffer
if echo "$buffer" | jq empty 2>/dev/null; then # Check if the accumulated buffer is valid JSON
log d "Received complete request:"
log d "$buffer"
process_request "$buffer" & # Process the complete JSON request asynchronously
buffer="" # Clear the buffer for the next request.
fi
fi
done < "$REQUEST_PIPE"
}
process_request() {
# This is the main API function loop. From the passed JSON object $1, it will check for values in the "action", "request_id" (which are always required fields) as well as any function-specific data.
# The "request_id" is also used in the construction of the response named_pipe the requesting client needs to create. The response named pipe will always be /tmp/response_$request_id and will be cleaned up by the server once the response has been sent.
# USAGE: process_request "$JSON_OBJECT"
local json_input="$1"
local action
local request_id
# Validate JSON format
if ! echo "$json_input" | jq empty 2>/dev/null; then
echo "Error: Invalid JSON format" >&2
return 1
fi
# Extract the action and parameters from the JSON input
action=$(echo "$json_input" | jq -r '.action // empty')
request_id=$(echo "$json_input" | jq -r '.request_id // empty')
if [[ -z "$action" || -z "$request_id" ]]; then
echo "Invalid request: missing action or request_id" >&2
return 1
fi
local response_pipe="$rd_api_dir/response_${request_id}"
if [[ ! -p "$response_pipe" ]]; then
echo "Error: Response pipe $response_pipe does not exist" >&2
return 1
fi
if [[ -z "$action" ]]; then
echo "{\"status\":\"error\",\"message\":\"Missing required field: action\",\"request_id\":\"$request_id\"}" > "$response_pipe"
return 1
fi
# Process request asynchronously
{
case "$action" in
"check_status")
echo "{\"status\":\"success\",\"request_id\":\"$request_id\"}" > "$response_pipe"
;;
"wait")
local data
data=$(echo "$json_input" | jq -r '.data // empty')
if [[ -z "$data" ]]; then
echo "{\"status\":\"error\",\"message\":\"Missing required field: data\",\"request_id\":\"$request_id\"}" > "$response_pipe"
return 1
fi
local result=$(wait_example_function "$data")
echo "{\"status\":\"success\",\"result\":$result,\"request_id\":\"$request_id\"}" > "$response_pipe"
;;
*)
echo "{\"status\":\"error\",\"message\":\"Unknown action: $action\",\"request_id\":\"$request_id\"}" > "$response_pipe"
;;
esac
# Remove response pipe after writing response
rm -f "$response_pipe"
} &
}
wait_example_function() {
# This is a dummy function used for API testing only. All it does is sleep for the amount of seconds provided in the "data" field of the received JSON object
local input="$1"
sleep "$1" # Dummy implementation for demonstration
echo "{\"waited\":\"waited $input seconds\"}"
}

View file

@ -92,8 +92,15 @@ features="$config/retrodeck/reference_lists/features.json"
es_systems="/app/share/es-de/resources/systems/linux/es_systems.xml" # ES-DE supported system list
es_find_rules="/app/share/es-de/resources/systems/linux/es_find_rules.xml" # ES-DE emulator find rules
# API-related file locations
rd_api_dir="$XDG_CONFIG_HOME/retrodeck/api"
REQUEST_PIPE="$rd_api_dir/retrodeck_api_pipe"
PID_FILE="$rd_api_dir/retrodeck_api_server.pid"
# File lock file for multi-threaded write operations to the same file
RD_FILE_LOCK="/tmp/retrodeck_file_lock"
RD_FILE_LOCK="$rd_api_dir/retrodeck_file_lock"
# Godot data transfer temp files

View file

@ -279,6 +279,11 @@ while [[ $# -gt 0 ]]; do
open_component "${@:2}"
exit 0
;;
--api)
retrodeck_api start
wait
exit $?
;;
-*)
# Catch-all for unrecognized options starting with a dash
log e "Error: Unknown option '$1'"