QuecManager non-beta

Its about time I did this!
This commit is contained in:
Cameron Thompson
2025-04-02 23:09:08 -04:00
parent c4a340bd36
commit c42907e346
482 changed files with 47267 additions and 109914 deletions

View File

@@ -0,0 +1,672 @@
#!/bin/sh
# AT Queue Manager for OpenWRT with Preemption Support and Token System
# Located in /www/cgi-bin/services/at_queue_manager
# Constants
QUEUE_DIR="/tmp/at_queue"
QUEUE_FILE="$QUEUE_DIR/queue"
ACTIVE_FILE="$QUEUE_DIR/active"
RESULTS_DIR="$QUEUE_DIR/results"
LOCK_DIR="$QUEUE_DIR/lock"
TOKEN_FILE="$QUEUE_DIR/token"
MAX_TIMEOUT=240
CLEANUP_INTERVAL=300 # 5 minutes in seconds
RESULTS_MAX_AGE=3600 # 1 hour in seconds
POLL_INTERVAL=0.01
PREEMPTION_THRESHOLD=2 # 3 seconds threshold for preemption
TOKEN_TIMEOUT=30 # seconds before token expires
# Utility function for JSON escaping
escape_json() {
printf '%s' "$1" | awk '
BEGIN { RS="\n"; ORS="\\n" }
{
gsub(/\\/, "\\\\")
gsub(/"/, "\\\"")
gsub(/\r/, "")
gsub(/\t/, "\\t")
gsub(/\f/, "\\f")
gsub(/\b/, "\\b")
print
}
' | sed 's/\\n$//'
}
# Exclusive lock functions
acquire_lock() {
local timeout=10
local attempt=0
while [ $attempt -lt $timeout ]; do
if mkdir "$LOCK_DIR" 2>/dev/null; then
logger -t at_queue -p daemon.debug "Lock acquired"
return 0
fi
sleep 0.1
attempt=$((attempt + 1))
done
logger -t at_queue -p daemon.error "Failed to acquire lock after $timeout attempts"
return 1
}
release_lock() {
if [ -d "$LOCK_DIR" ]; then
rmdir "$LOCK_DIR" 2>/dev/null
logger -t at_queue -p daemon.debug "Lock released"
return 0
fi
logger -t at_queue -p daemon.error "Lock directory doesn't exist"
return 1
}
# Ensure required directories exist
init_queue_system() {
mkdir -p "$QUEUE_DIR" "$RESULTS_DIR"
touch "$QUEUE_FILE"
chmod 755 "$QUEUE_DIR"
chmod 644 "$QUEUE_FILE"
chmod 755 "$RESULTS_DIR"
logger -t at_queue -p daemon.info "Queue system initialized"
}
# Cleanup old results and tracking files
cleanup_old_results() {
local current_time=$(date +%s)
# Clean up old execution tracking files
find "$QUEUE_DIR" -name "pid.*" -type f -mmin +60 -delete 2>/dev/null
find "$QUEUE_DIR" -name "*.exit" -type f -mmin +60 -delete 2>/dev/null
find "$QUEUE_DIR" -name "start_time.*" -type f -mmin +60 -delete 2>/dev/null
logger -t at_queue -p daemon.debug "Cleaned up old tracking files"
# Use find with -delete and basic timestamp check for OpenWRT
find "$RESULTS_DIR" -name "*.json" -type f -mmin +60 -delete 2>/dev/null || {
# Fallback method if find fails
for file in "$RESULTS_DIR"/*.json; do
[ -f "$file" ] || continue
local file_time=$(stat -c %Y "$file")
if [ $((current_time - file_time)) -gt $RESULTS_MAX_AGE ]; then
rm -f "$file"
fi
done
}
# Check for expired token
if [ -f "$TOKEN_FILE" ]; then
local token_time=$(cat "$TOKEN_FILE" | jsonfilter -e '@.timestamp')
if [ $((current_time - token_time)) -gt $TOKEN_TIMEOUT ]; then
local token_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id')
logger -t at_queue -p daemon.warn "Removing expired token from $token_holder"
rm -f "$TOKEN_FILE"
fi
fi
logger -t at_queue -p daemon.info "Cleanup: Removed files older than 1 hour"
}
# Generate unique command ID
generate_command_id() {
echo "$(date +%s)_$(head -c 8 /dev/urandom | hexdump -v -e '1/1 "%02x"')"
}
# Start tracking command execution time
start_execution_tracking() {
local cmd_id="$1"
local pid="$2"
local start_time=$(date +%s)
echo "$start_time" > "$QUEUE_DIR/start_time.$cmd_id"
echo "$pid" > "$QUEUE_DIR/pid.$cmd_id"
chmod 644 "$QUEUE_DIR/start_time.$cmd_id"
chmod 644 "$QUEUE_DIR/pid.$cmd_id"
logger -t at_queue -p daemon.debug "Started tracking command $cmd_id (PID: $pid)"
}
# Check if running command should be preempted
should_preempt() {
local current_cmd_id="$1"
local new_priority="$2"
if [ ! -f "$QUEUE_DIR/start_time.$current_cmd_id" ]; then
logger -t at_queue -p daemon.debug "No start time found for $current_cmd_id"
return 1
fi
local start_time=$(cat "$QUEUE_DIR/start_time.$current_cmd_id")
local current_time=$(date +%s)
local execution_time=$((current_time - start_time))
# Get current command's priority
local current_priority
if [ -f "$ACTIVE_FILE" ]; then
current_priority=$(cat "$ACTIVE_FILE" | jsonfilter -e '@.priority')
else
logger -t at_queue -p daemon.debug "No active command found"
return 1
fi
if [ $execution_time -gt $PREEMPTION_THRESHOLD ] && [ $new_priority -lt $current_priority ]; then
logger -t at_queue -p daemon.info "Command $current_cmd_id (priority $current_priority) running for ${execution_time}s is eligible for preemption by priority $new_priority"
return 0
fi
logger -t at_queue -p daemon.debug "Command $current_cmd_id not eligible for preemption (time: ${execution_time}s, current priority: $current_priority, new priority: $new_priority)"
return 1
}
# Handle command preemption
preempt_command() {
local cmd_id="$1"
local pid_file="$QUEUE_DIR/pid.$cmd_id"
if [ -f "$pid_file" ]; then
local pid=$(cat "$pid_file")
logger -t at_queue -p daemon.info "Preempting command $cmd_id (PID: $pid)"
# Send SIGTERM first
kill -TERM $pid 2>/dev/null
# Brief wait for graceful termination
sleep 0.1
# Force kill if still running
if kill -0 $pid 2>/dev/null; then
kill -KILL $pid 2>/dev/null
logger -t at_queue -p daemon.warn "Forced termination of command $cmd_id"
fi
# Record preemption result
write_preemption_result "$cmd_id"
# Cleanup command files
rm -f "$pid_file" "$QUEUE_DIR/start_time.$cmd_id" "$QUEUE_DIR/$cmd_id.exit"
[ -f "$ACTIVE_FILE" ] && rm -f "$ACTIVE_FILE"
logger -t at_queue -p daemon.info "Command $cmd_id preemption complete"
return 0
fi
logger -t at_queue -p daemon.warn "No PID file found for command $cmd_id"
return 1
}
# Record result for preempted command
write_preemption_result() {
local cmd_id="$1"
local end_time=$(date +%s%3N)
local start_time
if [ -f "$QUEUE_DIR/start_time.$cmd_id" ]; then
start_time=$(cat "$QUEUE_DIR/start_time.$cmd_id")000
else
start_time=$end_time
fi
local duration=$((end_time - start_time))
local command_text=$(cat "$ACTIVE_FILE" | jsonfilter -e '@.command')
local response=$(cat << EOF
{
"command": {
"id": "$cmd_id",
"text": "$(escape_json "$command_text")",
"timestamp": "$(date -Iseconds)"
},
"response": {
"status": "preempted",
"raw_output": "Command preempted by higher priority task",
"completion_time": "$end_time",
"duration_ms": $duration
}
}
EOF
)
printf "%s" "$response" > "$RESULTS_DIR/$cmd_id.json"
chmod 644 "$RESULTS_DIR/$cmd_id.json"
logger -t at_queue -p daemon.info "Recorded preemption result for command $cmd_id (duration: ${duration}ms)"
}
# Request a token for direct sms_tool execution
request_token() {
local requestor_id="$1"
local priority="${2:-10}"
local timeout="${3:-10}"
# Acquire lock first
if ! acquire_lock; then
logger -t at_queue -p daemon.error "Failed to acquire lock for token request"
echo "{\"error\":\"Could not acquire lock\",\"status\":\"denied\"}"
return 1
fi
# Check if token file exists (someone else has the token)
if [ -f "$TOKEN_FILE" ]; then
local current_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id')
local current_priority=$(cat "$TOKEN_FILE" | jsonfilter -e '@.priority')
local timestamp=$(cat "$TOKEN_FILE" | jsonfilter -e '@.timestamp')
local current_time=$(date +%s)
# Check for expired token (> TOKEN_TIMEOUT seconds old)
if [ $((current_time - timestamp)) -gt $TOKEN_TIMEOUT ]; then
logger -t at_queue -p daemon.warn "Found expired token from $current_holder, releasing"
rm -f "$TOKEN_FILE"
# Check for priority preemption
elif [ $priority -lt $current_priority ]; then
logger -t at_queue -p daemon.info "Preempting token from $current_holder (priority: $current_priority) for $requestor_id (priority: $priority)"
rm -f "$TOKEN_FILE"
else
# Token in use and cannot be preempted
release_lock
echo "{\"status\":\"denied\",\"holder\":\"$current_holder\",\"priority\":$current_priority}"
return 1
fi
fi
# Also check if there's an active command from the queue
if [ -f "$ACTIVE_FILE" ]; then
local active_id=$(cat "$ACTIVE_FILE" | jsonfilter -e '@.id')
local active_priority=$(cat "$ACTIVE_FILE" | jsonfilter -e '@.priority')
# Only preempt if priority is higher
if [ $priority -ge $active_priority ]; then
release_lock
echo "{\"status\":\"denied\",\"holder\":\"$active_id\",\"priority\":$active_priority}"
return 1
fi
logger -t at_queue -p daemon.info "Direct execution with higher priority than active queue command"
fi
# Grant token
local token_data="{\"id\":\"$requestor_id\",\"priority\":$priority,\"timestamp\":$(date +%s)}"
echo "$token_data" > "$TOKEN_FILE"
chmod 644 "$TOKEN_FILE"
release_lock
echo "{\"status\":\"granted\",\"id\":\"$requestor_id\",\"timeout\":$timeout}"
return 0
}
# Release a previously acquired token
release_token() {
local requestor_id="$1"
if ! acquire_lock; then
logger -t at_queue -p daemon.error "Failed to acquire lock for token release"
return 1
fi
if [ -f "$TOKEN_FILE" ]; then
local current_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id')
if [ "$current_holder" = "$requestor_id" ]; then
rm -f "$TOKEN_FILE"
logger -t at_queue -p daemon.debug "Token released by $requestor_id"
release_lock
echo "{\"status\":\"released\"}"
return 0
else
logger -t at_queue -p daemon.warn "Token release attempted by $requestor_id but held by $current_holder"
fi
else
logger -t at_queue -p daemon.warn "Token release attempted but no token exists"
fi
release_lock
echo "{\"status\":\"not_found\"}"
return 1
}
# Add command to queue with preemption support
enqueue_command() {
local cmd="$1"
local priority="${2:-10}"
local cmd_id=$(generate_command_id)
local timestamp=$(date -Iseconds)
# Ensure queue directory exists
[ ! -d "$QUEUE_DIR" ] && init_queue_system
logger -t at_queue -p daemon.info "Enqueuing command: $cmd (priority: $priority, id: $cmd_id)"
# Acquire lock for queue modification
if ! acquire_lock; then
logger -t at_queue -p daemon.error "Failed to acquire lock for enqueuing command"
echo "{\"error\":\"Queue lock acquisition failed\",\"command\":\"$cmd\"}"
return 1
fi
# Check for active command that can be preempted
if [ -f "$ACTIVE_FILE" ]; then
local active_cmd_id=$(cat "$ACTIVE_FILE" | jsonfilter -e '@.id')
if should_preempt "$active_cmd_id" "$priority"; then
preempt_command "$active_cmd_id"
fi
fi
# Create command entry
local entry="{\"id\":\"$cmd_id\",\"command\":\"$(escape_json "$cmd")\",\"priority\":$priority,\"timestamp\":\"$timestamp\"}"
if [ "$priority" = "1" ]; then
# High priority - prepend to queue
local temp_file=$(mktemp)
echo "$entry" > "$temp_file"
cat "$QUEUE_FILE" >> "$temp_file"
mv "$temp_file" "$QUEUE_FILE"
chmod 644 "$QUEUE_FILE"
logger -t at_queue -p daemon.info "Added high priority command to front of queue"
else
# Normal priority - append to queue
echo "$entry" >> "$QUEUE_FILE"
logger -t at_queue -p daemon.info "Added normal priority command to end of queue"
fi
# Release lock
release_lock
echo "{\"command_id\":\"$cmd_id\",\"status\":\"queued\"}"
}
# Get next command from queue
dequeue_command() {
if [ ! -s "$QUEUE_FILE" ]; then
return 1
fi
# Acquire lock
if ! acquire_lock; then
logger -t at_queue -p daemon.error "Failed to acquire lock for dequeuing command"
return 1
fi
local cmd_entry=$(head -n 1 "$QUEUE_FILE")
local temp_file=$(mktemp)
tail -n +2 "$QUEUE_FILE" > "$temp_file"
mv "$temp_file" "$QUEUE_FILE"
chmod 644 "$QUEUE_FILE"
echo "$cmd_entry" > "$ACTIVE_FILE"
chmod 644 "$ACTIVE_FILE"
# Release lock
release_lock
logger -t at_queue -p daemon.debug "Dequeued command: $(echo "$cmd_entry" | jsonfilter -e '@.command')"
echo "$cmd_entry"
}
# Clean and format AT command output
clean_output() {
local output="$1"
# First format AT command responses for readability
output=$(echo "$output" | sed -E '
# Add newline after AT commands
s/(AT\+[A-Z0-9]+[^ ]*) +/\1\n/g
# Add newline before +RESPONSE lines
s/ +(\+[A-Z0-9]+:)/\n\1/g
# Add newline before OK/ERROR
s/ +(OK|ERROR)$/\n\1/g
')
# Then escape the formatted output for JSON
output=$(escape_json "$output")
echo "$output"
}
# Execute AT command with optimized timeout handling
execute_with_timeout() {
local command="$1"
local timeout="$2"
local cmd_id="$3"
local output_file=$(mktemp)
# Start command in background with immediate output
(sms_tool -D at "$command" > "$output_file" 2>&1; echo $? > "$QUEUE_DIR/$cmd_id.exit") &
local pid=$!
# Start execution tracking
start_execution_tracking "$cmd_id" "$pid"
logger -t at_queue -p daemon.debug "Started command execution: $command (PID: $pid)"
# Wait for completion with shorter polling interval
local start_time=$(date +%s)
local elapsed=0
while [ $elapsed -lt "$timeout" ]; do
if [ -f "$QUEUE_DIR/$cmd_id.exit" ]; then
local exit_code=$(cat "$QUEUE_DIR/$cmd_id.exit")
local output=$(cat "$output_file")
# Cleanup
rm -f "$QUEUE_DIR/pid.$cmd_id" "$QUEUE_DIR/$cmd_id.exit" "$output_file" "$QUEUE_DIR/start_time.$cmd_id"
logger -t at_queue -p daemon.debug "Command completed with exit code $exit_code"
echo "$output"
return $exit_code
fi
elapsed=$(($(date +%s) - start_time))
sleep $POLL_INTERVAL
done
# Handle timeout
if [ -f "$QUEUE_DIR/pid.$cmd_id" ]; then
local pid=$(cat "$QUEUE_DIR/pid.$cmd_id")
kill $pid 2>/dev/null
sleep 0.1
# Force kill if still running
if kill -0 $pid 2>/dev/null; then
kill -KILL $pid 2>/dev/null
fi
local partial_output=$(cat "$output_file" 2>/dev/null || echo "")
# Cleanup
rm -f "$QUEUE_DIR/pid.$cmd_id" "$QUEUE_DIR/$cmd_id.exit" "$output_file" "$QUEUE_DIR/start_time.$cmd_id"
logger -t at_queue -p daemon.warn "Command timed out after $timeout seconds"
echo "${partial_output:-Command timed out after $timeout seconds}"
fi
return 124
}
# Execute AT command and handle response
execute_command() {
local cmd_entry="$1"
local cmd_id=$(echo "$cmd_entry" | jsonfilter -e '@.id')
local cmd_text=$(echo "$cmd_entry" | jsonfilter -e '@.command')
local priority=$(echo "$cmd_entry" | jsonfilter -e '@.priority')
local start_time=$(date +%s%3N)
logger -t at_queue -p daemon.info "Executing command $cmd_id: $cmd_text (priority: $priority)"
# Execute command with timeout
local result=$(execute_with_timeout "$cmd_text" $MAX_TIMEOUT "$cmd_id")
local exit_code=$?
local end_time=$(date +%s%3N)
local duration=$((end_time - start_time))
# Determine status and log level
local status="error"
local log_level="error"
if [ $exit_code -eq 124 ]; then
status="timeout"
logger -t at_queue -p daemon.error "Command $cmd_id timed out after ${duration}ms"
elif echo "$result" | grep -q "OK"; then
status="success"
log_level="info"
logger -t at_queue -p daemon.info "Command $cmd_id completed successfully in ${duration}ms"
elif echo "$result" | grep -q "CME ERROR"; then
status="cme_error"
logger -t at_queue -p daemon.error "Command $cmd_id failed with CME ERROR in ${duration}ms"
else
logger -t at_queue -p daemon.error "Command $cmd_id failed with general error in ${duration}ms"
fi
# Clean and escape the output
local clean_result=$(clean_output "$result")
# Create JSON response
local response=$(cat << EOF
{
"command": {
"id": "$cmd_id",
"text": "$(escape_json "$cmd_text")",
"timestamp": "$(date -Iseconds)"
},
"response": {
"status": "$status",
"raw_output": "$clean_result",
"completion_time": "$end_time",
"duration_ms": $duration
}
}
EOF
)
# Acquire lock for writing result
if ! acquire_lock; then
logger -t at_queue -p daemon.error "Failed to acquire lock for writing result"
else
# Save response
printf "%s" "$response" > "$RESULTS_DIR/$cmd_id.json"
chmod 644 "$RESULTS_DIR/$cmd_id.json"
# Clean up active file
rm -f "$ACTIVE_FILE"
# Release lock
release_lock
fi
echo "$response"
}
# Main queue processing function
process_queue() {
init_queue_system
local last_cleanup=$(date +%s)
local last_log=$(date +%s) # Add a timestamp for less frequent logging
# Make sure the lock directory doesn't exist at startup
[ -d "$LOCK_DIR" ] && rmdir "$LOCK_DIR" 2>/dev/null
logger -t at_queue -p daemon.info "Started queue processing daemon"
while true; do
# Quick cleanup check
local current_time=$(date +%s)
if [ $((current_time - last_cleanup)) -ge $CLEANUP_INTERVAL ]; then
cleanup_old_results
last_cleanup=$current_time
fi
# Skip processing if token is granted to someone
if [ -f "$TOKEN_FILE" ]; then
local token_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id')
local token_time=$(cat "$TOKEN_FILE" | jsonfilter -e '@.timestamp')
local current_time=$(date +%s)
# Check for expired token
if [ $((current_time - token_time)) -gt $TOKEN_TIMEOUT ]; then
logger -t at_queue -p daemon.warn "Removing expired token from $token_holder"
rm -f "$TOKEN_FILE"
else
# Log pause status only every 5 seconds to reduce log spam
if [ $((current_time - last_log)) -ge 5 ]; then
logger -t at_queue -p daemon.debug "Queue processing paused, token held by $token_holder"
last_log=$current_time
fi
sleep $POLL_INTERVAL
continue
fi
fi
# Process queue if not empty and no active command
if [ -s "$QUEUE_FILE" ] && [ ! -f "$ACTIVE_FILE" ]; then
local cmd_entry=$(dequeue_command)
if [ -n "$cmd_entry" ]; then
execute_command "$cmd_entry"
fi
fi
sleep $POLL_INTERVAL
done
}
# CGI command handling
if [ "${SCRIPT_NAME}" != "" ]; then
# Output headers
if [ "$HTTP_HEADERS" != "0" ]; then
echo "Content-Type: application/json"
echo ""
fi
# Parse query string for CGI mode
eval $(echo "$QUERY_STRING" | sed 's/&/;/g')
case "$action" in
"enqueue")
if [ -n "$command" ]; then
logger -t at_queue -p daemon.info "CGI: Received enqueue request for command: $command"
enqueue_command "$command" "$priority"
else
logger -t at_queue -p daemon.error "CGI: Empty command received"
echo "{\"error\":\"No command specified\"}"
fi
;;
"status")
if [ -f "$ACTIVE_FILE" ]; then
logger -t at_queue -p daemon.debug "CGI: Status request - queue active"
cat "$ACTIVE_FILE"
else
logger -t at_queue -p daemon.debug "CGI: Status request - queue idle"
echo "{\"status\":\"idle\"}"
fi
;;
"request_token")
if [ -n "$id" ]; then
logger -t at_queue -p daemon.info "Token request from $id (priority: ${priority:-10})"
request_token "$id" "${priority:-10}" "${timeout:-10}"
else
logger -t at_queue -p daemon.error "Token request missing ID"
echo "{\"error\":\"No requestor ID specified\",\"status\":\"denied\"}"
fi
;;
"release_token")
if [ -n "$id" ]; then
logger -t at_queue -p daemon.info "Token release from $id"
release_token "$id"
else
logger -t at_queue -p daemon.error "Token release missing ID"
echo "{\"error\":\"No requestor ID specified\",\"status\":\"denied\"}"
fi
;;
*)
logger -t at_queue -p daemon.error "CGI: Invalid action received: $action"
echo "{\"error\":\"Invalid action\"}"
;;
esac
exit 0
fi
# CLI command handling
if [ "$1" = "enqueue" ] && [ -n "$2" ]; then
enqueue_command "$2" "${3:-10}"
exit $?
fi
# If not run as CGI, start queue processing
if [ "${SCRIPT_NAME}" = "" ] && [ -z "$1" ]; then
process_queue
fi

View File

@@ -0,0 +1,198 @@
#!/bin/sh
# Configuration
LOGDIR="/www/signal_graphs"
MAX_ENTRIES=10
INTERVAL=60
QUEUE_DIR="/tmp/at_queue"
TOKEN_FILE="$QUEUE_DIR/token"
LOCK_FILE="/tmp/signal_metrics.lock"
METRICS_PID_FILE="/tmp/signal_metrics.pid"
MAX_TOKEN_WAIT=5 # seconds to wait for token acquisition
# Ensure required directories exist
mkdir -p "$LOGDIR" "$QUEUE_DIR"
# Check if another instance is running
check_running() {
if [ -f "$METRICS_PID_FILE" ]; then
pid=$(cat "$METRICS_PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
rm -f "$METRICS_PID_FILE" 2>/dev/null
fi
return 1
}
# Acquire token directly (minimized version)
acquire_token() {
local metrics_id="METRICS_$(date +%s)_$$"
local priority=20 # Lowest priority for metrics
local max_attempts=20
local attempt=0
while [ $attempt -lt $max_attempts ]; do
# Check if token exists
if [ -f "$TOKEN_FILE" ]; then
# Check current token
local current_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id' 2>/dev/null)
local current_priority=$(cat "$TOKEN_FILE" | jsonfilter -e '@.priority' 2>/dev/null)
local timestamp=$(cat "$TOKEN_FILE" | jsonfilter -e '@.timestamp' 2>/dev/null)
local current_time=$(date +%s)
# Check for expired token
if [ $((current_time - timestamp)) -gt 30 ] || [ -z "$current_holder" ]; then
rm -f "$TOKEN_FILE" 2>/dev/null
elif [ $priority -lt $current_priority ]; then
rm -f "$TOKEN_FILE" 2>/dev/null
else
# Wait and try again
sleep 0.5
attempt=$((attempt + 1))
continue
fi
fi
# Try to create token
echo "{\"id\":\"$metrics_id\",\"priority\":$priority,\"timestamp\":$(date +%s)}" > "$TOKEN_FILE" 2>/dev/null
chmod 644 "$TOKEN_FILE" 2>/dev/null
# Verify we got it
local holder=$(cat "$TOKEN_FILE" 2>/dev/null | jsonfilter -e '@.id' 2>/dev/null)
if [ "$holder" = "$metrics_id" ]; then
echo "$metrics_id"
return 0
fi
sleep 0.5
attempt=$((attempt + 1))
done
return 1
}
# Release token directly
release_token() {
local metrics_id="$1"
if [ -f "$TOKEN_FILE" ]; then
local current_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id' 2>/dev/null)
if [ "$current_holder" = "$metrics_id" ]; then
rm -f "$TOKEN_FILE" 2>/dev/null
fi
fi
}
# Execute AT command directly
execute_at_command() {
local CMD="$1"
sms_tool at "$CMD" -t 3 2>/dev/null
}
# Process all metrics commands with a single token
process_all_metrics() {
# Try to get token
local metrics_id=$(acquire_token)
if [ -z "$metrics_id" ]; then
logger -t at_queue -p daemon.warn "Could not acquire token for metrics - will try again later"
return 1
fi
logger -t at_queue -p daemon.info "Processing all metrics with token $metrics_id"
# Execute all metrics commands with the single token
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
# RSRP
local rsrp_output=$(execute_at_command "AT+QRSRP")
if [ -n "$rsrp_output" ] && echo "$rsrp_output" | grep -q "QRSRP"; then
local logfile="$LOGDIR/rsrp.json"
[ ! -s "$logfile" ] && echo "[]" > "$logfile"
local temp_file="${logfile}.tmp.$$"
jq --arg dt "$timestamp" \
--arg out "$rsrp_output" \
'. + [{"datetime": $dt, "output": $out}] | .[-'"$MAX_ENTRIES"':]' \
"$logfile" > "$temp_file" 2>/dev/null && mv "$temp_file" "$logfile"
chmod 644 "$logfile"
fi
sleep 0.5
# RSRQ
local rsrq_output=$(execute_at_command "AT+QRSRQ")
if [ -n "$rsrq_output" ] && echo "$rsrq_output" | grep -q "QRSRQ"; then
local logfile="$LOGDIR/rsrq.json"
[ ! -s "$logfile" ] && echo "[]" > "$logfile"
local temp_file="${logfile}.tmp.$$"
jq --arg dt "$timestamp" \
--arg out "$rsrq_output" \
'. + [{"datetime": $dt, "output": $out}] | .[-'"$MAX_ENTRIES"':]' \
"$logfile" > "$temp_file" 2>/dev/null && mv "$temp_file" "$logfile"
chmod 644 "$logfile"
fi
sleep 0.5
# SINR
local sinr_output=$(execute_at_command "AT+QSINR")
if [ -n "$sinr_output" ] && echo "$sinr_output" | grep -q "QSINR"; then
local logfile="$LOGDIR/sinr.json"
[ ! -s "$logfile" ] && echo "[]" > "$logfile"
local temp_file="${logfile}.tmp.$$"
jq --arg dt "$timestamp" \
--arg out "$sinr_output" \
'. + [{"datetime": $dt, "output": $out}] | .[-'"$MAX_ENTRIES"':]' \
"$logfile" > "$temp_file" 2>/dev/null && mv "$temp_file" "$logfile"
chmod 644 "$logfile"
fi
sleep 0.5
# Data usage
local usage_output=$(execute_at_command "AT+QGDCNT?;+QGDNRCNT?")
if [ -n "$usage_output" ] && echo "$usage_output" | grep -q "QGDCNT\|QGDNRCNT"; then
local logfile="$LOGDIR/data_usage.json"
[ ! -s "$logfile" ] && echo "[]" > "$logfile"
local temp_file="${logfile}.tmp.$$"
jq --arg dt "$timestamp" \
--arg out "$usage_output" \
'. + [{"datetime": $dt, "output": $out}] | .[-'"$MAX_ENTRIES"':]' \
"$logfile" > "$temp_file" 2>/dev/null && mv "$temp_file" "$logfile"
chmod 644 "$logfile"
fi
# Release token
release_token "$metrics_id"
logger -t at_queue -p daemon.info "Metrics processing completed"
return 0
}
# Main continuous logging function with proper locking
start_continuous_logging() {
# Check if already running
if check_running; then
logger -t at_queue -p daemon.error "Signal metrics logging already running"
exit 1
fi
# Store PID
echo "$$" > "$METRICS_PID_FILE"
chmod 644 "$METRICS_PID_FILE"
sleep 20 # Initial delay to allow system startup
logger -t at_queue -p daemon.info "Starting continuous signal metrics logging (PID: $$)"
trap 'logger -t at_queue -p daemon.info "Stopping signal metrics logging"; rm -f "$METRICS_PID_FILE"; exit 0' INT TERM
while true; do
process_all_metrics
sleep "$INTERVAL"
done
}
# Start the continuous logging
start_continuous_logging

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,571 @@
#!/bin/sh
# QuecWatch Daemon
# Monitors cellular connectivity and performs recovery actions
# Load UCI configuration functions
. /lib/functions.sh
# Configuration
QUEUE_DIR="/tmp/at_queue"
TOKEN_FILE="$QUEUE_DIR/token"
LOG_DIR="/tmp/log/quecwatch"
LOG_FILE="$LOG_DIR/quecwatch.log"
PID_FILE="/var/run/quecwatch.pid"
STATUS_FILE="/tmp/quecwatch_status.json"
RETRY_COUNT_FILE="/tmp/quecwatch_retry_count"
UCI_CONFIG="quecmanager"
MAX_TOKEN_WAIT=10 # Maximum seconds to wait for token acquisition
TOKEN_PRIORITY=15 # Medium priority (between profiles and metrics)
# Ensure directories exist
mkdir -p "$LOG_DIR" "$QUEUE_DIR"
# Store PID
echo "$$" > "$PID_FILE"
chmod 644 "$PID_FILE"
# Function to log messages
log_message() {
local level="${2:-info}"
local message="$1"
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
# Log to file
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
# Log to system log
logger -t quecwatch -p "daemon.$level" "$message"
}
# Function to update status
update_status() {
local status="$1"
local message="$2"
local retry="${3:-$CURRENT_RETRIES}"
local max="${4:-$MAX_RETRIES}"
# Create JSON status
cat > "$STATUS_FILE" <<EOF
{
"status": "$status",
"message": "$message",
"retry": $retry,
"maxRetries": $max,
"timestamp": $(date +%s)
}
EOF
chmod 644 "$STATUS_FILE"
log_message "Status updated: $status - $message" "debug"
}
# Function to acquire token for AT commands
acquire_token() {
local requestor_id="QUECWATCH_$(date +%s)_$$"
local priority="$TOKEN_PRIORITY"
local max_attempts=$MAX_TOKEN_WAIT
local attempt=0
log_message "Attempting to acquire token with priority $priority" "debug"
while [ $attempt -lt $max_attempts ]; do
# Check if token file exists
if [ -f "$TOKEN_FILE" ]; then
local current_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id' 2>/dev/null)
local current_priority=$(cat "$TOKEN_FILE" | jsonfilter -e '@.priority' 2>/dev/null)
local timestamp=$(cat "$TOKEN_FILE" | jsonfilter -e '@.timestamp' 2>/dev/null)
local current_time=$(date +%s)
# Check for expired token (> 30 seconds old)
if [ $((current_time - timestamp)) -gt 30 ] || [ -z "$current_holder" ]; then
# Remove expired token
log_message "Found expired token from $current_holder, removing" "debug"
rm -f "$TOKEN_FILE" 2>/dev/null
elif [ $priority -lt $current_priority ]; then
# Preempt lower priority token
log_message "Preempting token from $current_holder (priority: $current_priority)" "debug"
rm -f "$TOKEN_FILE" 2>/dev/null
else
# Check if the token is held by a QuecProfile or cell scan
if echo "$current_holder" | grep -q "CELL_SCAN"; then
log_message "Token held by cell scan (priority: $current_priority), waiting..." "debug"
elif echo "$current_holder" | grep -q "QUECPROFILES"; then
log_message "Token held by profile application (priority: $current_priority), waiting..." "debug"
else
log_message "Token held by $current_holder with priority $current_priority, retrying..." "debug"
fi
sleep 0.5
attempt=$((attempt + 1))
continue
fi
fi
# Try to create token file
echo "{\"id\":\"$requestor_id\",\"priority\":$priority,\"timestamp\":$(date +%s)}" > "$TOKEN_FILE" 2>/dev/null
chmod 644 "$TOKEN_FILE" 2>/dev/null
# Verify we got the token
local holder=$(cat "$TOKEN_FILE" 2>/dev/null | jsonfilter -e '@.id' 2>/dev/null)
if [ "$holder" = "$requestor_id" ]; then
log_message "Successfully acquired token with ID $requestor_id" "debug"
echo "$requestor_id"
return 0
fi
sleep 0.5
attempt=$((attempt + 1))
done
log_message "Failed to acquire token after $max_attempts attempts" "error"
return 1
}
# Function to release token
release_token() {
local requestor_id="$1"
if [ -f "$TOKEN_FILE" ]; then
local current_holder=$(cat "$TOKEN_FILE" | jsonfilter -e '@.id' 2>/dev/null)
if [ "$current_holder" = "$requestor_id" ]; then
rm -f "$TOKEN_FILE" 2>/dev/null
log_message "Released token $requestor_id" "debug"
return 0
fi
log_message "Token held by $current_holder, not by us ($requestor_id)" "warn"
else
log_message "Token file doesn't exist, nothing to release" "debug"
fi
return 1
}
# Function to execute AT command with token
execute_at_command() {
local cmd="$1"
local timeout="${2:-5}"
local token_id="$3"
if [ -z "$token_id" ]; then
log_message "No valid token provided for command: $cmd" "error"
return 1
fi
log_message "Executing AT command: $cmd (timeout: ${timeout}s)" "debug"
# Execute the command with proper timeout
local output
local status=1
output=$(sms_tool at "$cmd" -t "$timeout" 2>&1)
status=$?
if [ $status -ne 0 ]; then
log_message "AT command failed: $cmd (exit code: $status)" "error"
return 1
fi
echo "$output"
return 0
}
# Function to check internet connectivity
check_internet() {
local ping_target
local ping_count=3
# Get ping target from UCI
config_load "$UCI_CONFIG"
config_get ping_target quecwatch ping_target
if [ -z "$ping_target" ]; then
log_message "No ping target configured" "error"
return 1
fi
log_message "Checking internet connectivity to $ping_target" "debug"
if ping -c $ping_count "$ping_target" > /dev/null 2>&1; then
log_message "Internet connectivity check successful" "debug"
return 0
else
log_message "Internet connectivity check failed" "warn"
return 1
fi
}
# Function to get current SIM slot
get_current_sim() {
local token_id=$(acquire_token)
if [ -z "$token_id" ]; then
log_message "Failed to acquire token for SIM slot check" "error"
return 1
fi
log_message "Checking current SIM slot" "debug"
local result=$(execute_at_command "AT+QUIMSLOT?" 5 "$token_id")
local status=$?
# Release token
release_token "$token_id"
if [ $status -eq 0 ] && [ -n "$result" ]; then
# Extract SIM slot number from response
local current_sim=$(echo "$result" | grep -o '+QUIMSLOT: [0-9]' | cut -d' ' -f2)
if [ -n "$current_sim" ]; then
log_message "Current SIM slot: $current_sim" "debug"
echo "$current_sim"
return 0
fi
fi
log_message "Failed to get current SIM slot" "error"
return 1
}
# Function to switch SIM card
switch_sim_card() {
local current_sim
local target_sim
local token_id
log_message "Starting SIM card switch operation" "info"
# Get current SIM slot
current_sim=$(get_current_sim)
if [ $? -ne 0 ]; then
log_message "Failed to get current SIM slot, cannot switch" "error"
return 1
fi
# Determine target SIM
if [ "$current_sim" = "1" ]; then
target_sim=2
else
target_sim=1
fi
log_message "Attempting to switch from SIM $current_sim to SIM $target_sim" "info"
# Get token for AT commands
token_id=$(acquire_token)
if [ -z "$token_id" ]; then
log_message "Failed to acquire token for SIM switch" "error"
return 1
fi
# Detach from network
log_message "Detaching from network" "debug"
execute_at_command "AT+COPS=2" 10 "$token_id"
sleep 2
# Switch SIM slot
log_message "Switching to SIM slot $target_sim" "debug"
local switch_result=$(execute_at_command "AT+QUIMSLOT=$target_sim" 10 "$token_id")
local switch_status=$?
# If switch failed, return error
if [ $switch_status -ne 0 ]; then
log_message "Failed to switch to SIM $target_sim" "error"
release_token "$token_id"
return 1
fi
sleep 5
# Reattach to network
log_message "Reattaching to network" "debug"
execute_at_command "AT+COPS=0" 10 "$token_id"
# Release token
release_token "$token_id"
# Verify switch
sleep 10
local new_sim=$(get_current_sim)
if [ "$new_sim" = "$target_sim" ]; then
log_message "Successfully switched to SIM $target_sim" "info"
return 0
else
log_message "Failed to verify SIM switch, current SIM is $new_sim" "error"
return 1
fi
}
# Function to perform connection recovery
perform_connection_recovery() {
local token_id
log_message "Starting connection recovery" "info"
# Get token for AT commands
token_id=$(acquire_token)
if [ -z "$token_id" ]; then
log_message "Failed to acquire token for connection recovery" "error"
return 1
fi
# First check if CFUN is 1, if not set it to 1
local cfun_status=$(execute_at_command "AT+CFUN?" 5 "$token_id")
if [ $? -ne 0 ]; then
log_message "Failed to get CFUN status" "error"
release_token "$token_id"
return 1
fi
if echo "$cfun_status" | grep -q '+CFUN: 1'; then
log_message "CFUN is already 1, no action needed" "debug"
else
log_message "Setting CFUN to 1"
execute_at_command "AT+CFUN=1" 10 "$token_id"
sleep 2
# Recheck CFUN status
cfun_status=$(execute_at_command "AT+CFUN?" 5 "$token_id")
if [ $? -ne 0 ] || ! echo "$cfun_status" | grep -q '+CFUN: 1'; then
log_message "Failed to set CFUN to 1" "error"
release_token "$token_id"
return 1
fi
log_message "CFUN set to 1 successfully" "debug"
sleep 2
fi
# Detach from network
log_message "Detaching from network" "debug"
execute_at_command "AT+COPS=2" 10 "$token_id"
sleep 2
# Reattach to network
log_message "Reattaching to network" "debug"
execute_at_command "AT+COPS=0" 15 "$token_id"
# Release token
release_token "$token_id"
# Verify recovery
sleep 10
if check_internet; then
log_message "Connection recovery successful" "info"
return 0
else
log_message "Connection recovery failed" "error"
return 1
fi
}
# Load configuration
load_config() {
# Initialize variables
PING_TARGET=""
PING_INTERVAL=60
PING_FAILURES=3
MAX_RETRIES=5
CURRENT_RETRIES=0
CONNECTION_REFRESH=0
REFRESH_COUNT=3
AUTO_SIM_FAILOVER=0
SIM_FAILOVER_SCHEDULE=0
# Load from UCI
config_load "$UCI_CONFIG"
# Get settings with defaults
config_get PING_TARGET quecwatch ping_target
config_get PING_INTERVAL quecwatch ping_interval 60
config_get PING_FAILURES quecwatch ping_failures 3
config_get MAX_RETRIES quecwatch max_retries 5
config_get CURRENT_RETRIES quecwatch current_retries 0
config_get_bool CONNECTION_REFRESH quecwatch connection_refresh 0
config_get REFRESH_COUNT quecwatch refresh_count 3
config_get_bool AUTO_SIM_FAILOVER quecwatch auto_sim_failover 0
config_get SIM_FAILOVER_SCHEDULE quecwatch sim_failover_schedule 0
# Validate required settings
if [ -z "$PING_TARGET" ]; then
log_message "No ping target configured, using default (8.8.8.8)" "warn"
PING_TARGET="8.8.8.8"
uci set "$UCI_CONFIG.quecwatch.ping_target=$PING_TARGET"
uci commit "$UCI_CONFIG"
fi
# Load persisted retry count if available
if [ -f "$RETRY_COUNT_FILE" ]; then
CURRENT_RETRIES=$(cat "$RETRY_COUNT_FILE")
fi
log_message "Configuration loaded: ping_target=$PING_TARGET, interval=$PING_INTERVAL, failures=$PING_FAILURES, max_retries=$MAX_RETRIES, current_retries=$CURRENT_RETRIES" "info"
}
# Save retry count to both UCI and file
save_retry_count() {
local count=$1
# Update UCI
uci set "$UCI_CONFIG.quecwatch.current_retries=$count"
uci commit "$UCI_CONFIG"
# Update file for crash recovery
echo "$count" > "$RETRY_COUNT_FILE"
chmod 644 "$RETRY_COUNT_FILE"
log_message "Updated retry count to $count" "debug"
}
# Main monitoring function
main() {
log_message "QuecWatch daemon starting (PID: $$)" "info"
# Load configuration
load_config
# Initial status update
update_status "active" "Monitoring started"
# Track consecutive failures
local failure_count=0
# For scheduled SIM failover
local sim_failover_interval=0
local initial_sim=""
# If auto SIM failover is enabled, store initial SIM slot
if [ "$AUTO_SIM_FAILOVER" -eq 1 ]; then
initial_sim=$(get_current_sim)
if [ -n "$initial_sim" ]; then
log_message "Auto SIM failover enabled, initial SIM slot: $initial_sim" "info"
fi
fi
# Main monitoring loop
while true; do
log_message "Starting monitoring cycle" "debug"
# Check internet connectivity
if ! check_internet; then
failure_count=$((failure_count + 1))
log_message "Connectivity check failed ($failure_count/$PING_FAILURES)" "warn"
# Update status
update_status "warning" "Connection check failed: $failure_count/$PING_FAILURES failures"
# Check if failure threshold is reached
if [ $failure_count -ge $PING_FAILURES ]; then
# Reset failure counter
failure_count=0
# Increment retry counter
CURRENT_RETRIES=$((CURRENT_RETRIES + 1))
save_retry_count $CURRENT_RETRIES
log_message "Failure threshold reached. Current retry: $CURRENT_RETRIES/$MAX_RETRIES" "warn"
update_status "error" "Connection lost, attempt $CURRENT_RETRIES/$MAX_RETRIES to recover"
# Check if max retries reached
if [ $CURRENT_RETRIES -ge $MAX_RETRIES ]; then
log_message "Maximum retries reached" "error"
# Try SIM failover if enabled
if [ "$AUTO_SIM_FAILOVER" -eq 1 ]; then
log_message "Attempting SIM failover" "info"
update_status "failover" "Maximum retries reached, attempting SIM failover"
if switch_sim_card && check_internet; then
log_message "SIM failover successful, connection restored" "info"
update_status "recovered" "Connection restored via SIM failover"
# Reset retry counter
CURRENT_RETRIES=0
save_retry_count $CURRENT_RETRIES
else
log_message "SIM failover failed, system will reboot" "error"
update_status "rebooting" "SIM failover failed, system will reboot"
# Wait briefly and reboot
sleep 5
reboot
fi
else
log_message "Auto SIM failover disabled, system will reboot" "error"
update_status "rebooting" "Maximum retries reached, system will reboot"
# Wait briefly and reboot
sleep 5
reboot
fi
else
# Try connection recovery
log_message "Attempting connection recovery" "info"
update_status "recovering" "Attempting to restore connection"
if perform_connection_recovery; then
log_message "Connection recovery successful" "info"
update_status "recovered" "Connection restored"
# Reset retry counter
CURRENT_RETRIES=0
save_retry_count $CURRENT_RETRIES
fi
fi
fi
else
# Connection is good
if [ $failure_count -gt 0 ] || [ $CURRENT_RETRIES -gt 0 ]; then
log_message "Connection restored" "info"
update_status "stable" "Connection restored"
# Reset counters
failure_count=0
CURRENT_RETRIES=0
save_retry_count $CURRENT_RETRIES
fi
# Scheduled SIM failover check
if [ "$AUTO_SIM_FAILOVER" -eq 1 ] && [ "$SIM_FAILOVER_SCHEDULE" -gt 0 ] && [ -n "$initial_sim" ]; then
# Get current SIM to check if we're on the backup
local current_sim=$(get_current_sim)
# If we're on backup SIM, check if it's time to try primary again
if [ -n "$current_sim" ] && [ "$current_sim" != "$initial_sim" ]; then
sim_failover_interval=$((sim_failover_interval + 1))
# Check if we've reached the scheduled time
if [ $((sim_failover_interval * PING_INTERVAL)) -ge $((SIM_FAILOVER_SCHEDULE * 60)) ]; then
log_message "Scheduled check: attempting to switch back to primary SIM $initial_sim" "info"
update_status "switchback" "Attempting to switch back to primary SIM"
# Try switching back
if switch_sim_card && check_internet; then
log_message "Successfully switched back to primary SIM" "info"
update_status "stable" "Successfully switched back to primary SIM"
else
log_message "Failed to switch back to primary SIM, staying on backup" "warn"
update_status "stable" "Staying on backup SIM - primary SIM check failed"
# Switch back to backup SIM
current_sim=$(get_current_sim)
if [ -n "$current_sim" ] && [ "$current_sim" = "$initial_sim" ]; then
switch_sim_card
fi
fi
# Reset failover interval
sim_failover_interval=0
fi
fi
fi
fi
# Sleep for the configured interval
sleep $PING_INTERVAL
done
}
# Set up trap for clean shutdown
trap 'log_message "Received signal, exiting" "info"; update_status "stopped" "Daemon stopped"; rm -f "$PID_FILE"; exit 0' INT TERM
# Start the main function
main