mirror of
https://github.com/RetroDECK/RetroDECK.git
synced 2025-04-10 19:15:12 +00:00
Initial API implementation
This commit is contained in:
parent
739248b24c
commit
9c4263e207
164
developer_toolbox/api_test_client.sh
Normal file
164
developer_toolbox/api_test_client.sh
Normal 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
165
functions/api.sh
Normal 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\"}"
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'"
|
||||
|
|
Loading…
Reference in a new issue