diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4bfb4a1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,48 @@
+# EditorConfig for LyreBirdAudio
+# https://editorconfig.org
+
+root = true
+
+# Default settings for all files
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+# Shell scripts
+[*.sh]
+indent_style = space
+indent_size = 4
+
+# BATS test files
+[*.bats]
+indent_style = space
+indent_size = 4
+
+# Markdown files
+[*.md]
+trim_trailing_whitespace = false
+indent_size = 2
+
+# YAML files
+[*.{yml,yaml}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# Makefile (requires tabs)
+[Makefile]
+indent_style = tab
+
+# Configuration files
+[*.conf]
+indent_size = 4
+
+# Git config files
+[.git*]
+indent_size = 4
diff --git a/.github/workflows/bash-ci.yml b/.github/workflows/bash-ci.yml
index a19ba50..7e27cb7 100644
--- a/.github/workflows/bash-ci.yml
+++ b/.github/workflows/bash-ci.yml
@@ -85,10 +85,10 @@ jobs:
while IFS= read -r script; do
if bash -n "$script" 2>&1; then
- echo "✓ $script"
+ echo "[PASS] $script"
success=$((success + 1))
else
- echo "✗ $script"
+ echo "[FAIL] $script"
errors=$((errors + 1))
fi
done < <(find . -name '*.sh' -type f | sort)
@@ -181,9 +181,9 @@ jobs:
notes=$(shellcheck --format=json "$script" 2>/dev/null | jq '[.[] | select(.level == "info" or .level == "style")] | length' || echo 0)
if [[ "$errors" == "0" && "$warnings" == "0" ]]; then
- status="✓"
+ status="PASS"
else
- status="✗"
+ status="FAIL"
fi
echo "| ${status} ${script} | ${errors} | ${warnings} | ${notes} |" >> "$GITHUB_STEP_SUMMARY"
@@ -226,7 +226,7 @@ jobs:
# -d shows diff, -i 4 = 4 space indent, -bn = binary ops at start of line
# -ci = indent case statements
if diff=$(shfmt -d -i 4 -bn -ci "$script" 2>&1); then
- echo "✓"
+ echo "[PASS]"
else
echo "needs formatting"
echo "$diff"
@@ -339,11 +339,11 @@ jobs:
if [[ -n "$dangerous_eval" ]]; then
echo "$dangerous_eval"
- echo " ⚠ Found potentially dangerous eval - review for command injection"
+ echo " [WARN] Found potentially dangerous eval - review for command injection"
warnings=$((warnings + 1))
warning_details+=("Dangerous eval patterns found")
else
- echo " ✓ No dangerous eval patterns (safe patterns like 'eval set --' are OK)"
+ echo " [OK] No dangerous eval patterns (safe patterns like 'eval set --' are OK)"
fi
echo ""
@@ -356,11 +356,11 @@ jobs:
if [[ -n "$insecure_dl" ]]; then
echo "$insecure_dl"
- echo " ⚠ Found downloads with disabled certificate verification"
+ echo " [WARN] Found downloads with disabled certificate verification"
warnings=$((warnings + 1))
warning_details+=("Insecure download flags")
else
- echo " ✓ No insecure download flags found"
+ echo " [OK] No insecure download flags found"
fi
echo ""
@@ -375,11 +375,11 @@ jobs:
if [[ -n "$world_writable" ]]; then
echo "$world_writable"
- echo " ⚠ Found world-writable permissions"
+ echo " [WARN] Found world-writable permissions"
warnings=$((warnings + 1))
warning_details+=("World-writable permissions")
else
- echo " ✓ No world-writable permissions found"
+ echo " [OK] No world-writable permissions found"
fi
echo ""
@@ -396,11 +396,11 @@ jobs:
if [[ -n "$unquoted_cmd" ]]; then
echo "$unquoted_cmd"
- echo " ⚠ Found unquoted command substitution in file operations"
+ echo " [WARN] Found unquoted command substitution in file operations"
warnings=$((warnings + 1))
warning_details+=("Unquoted command substitution")
else
- echo " ✓ No unquoted command substitution risks found"
+ echo " [OK] No unquoted command substitution risks found"
fi
echo ""
@@ -419,11 +419,11 @@ jobs:
if [[ -n "$hardcoded" ]]; then
echo "$hardcoded"
- echo " ⚠ Found potential hardcoded secrets"
+ echo " [WARN] Found potential hardcoded secrets"
warnings=$((warnings + 1))
warning_details+=("Hardcoded secrets")
else
- echo " ✓ No hardcoded secrets found"
+ echo " [OK] No hardcoded secrets found"
fi
echo ""
@@ -441,11 +441,11 @@ jobs:
if [[ -n "$insecure_tmp" ]]; then
echo "$insecure_tmp"
- echo " ⚠ Found predictable temp file names (consider using mktemp)"
+ echo " [WARN] Found predictable temp file names (consider using mktemp)"
warnings=$((warnings + 1))
warning_details+=("Predictable temp files")
else
- echo " ✓ No insecure temp file patterns found"
+ echo " [OK] No insecure temp file patterns found"
fi
echo ""
@@ -472,12 +472,12 @@ jobs:
echo ""
echo "| Check | Status |"
echo "|-------|--------|"
- echo "| Dangerous eval patterns | ✓ |"
- echo "| Insecure downloads (-k/--insecure) | ✓ |"
- echo "| World-writable permissions (777/666) | ✓ |"
- echo "| Unquoted command substitutions | ✓ |"
- echo "| Hardcoded secrets | ✓ |"
- echo "| Predictable temp files | ✓ |"
+ echo "| Dangerous eval patterns | OK |"
+ echo "| Insecure downloads (-k/--insecure) | OK |"
+ echo "| World-writable permissions (777/666) | OK |"
+ echo "| Unquoted command substitutions | OK |"
+ echo "| Hardcoded secrets | OK |"
+ echo "| Predictable temp files | OK |"
echo ""
echo "_Note: This scan uses pattern matching and may have false positives/negatives._"
echo "_ShellCheck provides more comprehensive variable quoting analysis._"
@@ -496,7 +496,7 @@ jobs:
run: |
{
# Header
- echo "# 🔍 Bash CI Pipeline Results"
+ echo "# Bash CI Pipeline Results"
echo ""
echo "---"
echo ""
@@ -507,9 +507,9 @@ jobs:
# Determine overall result
if [[ "${{ needs.bash-syntax.result }}" == "success" && "${{ needs.shellcheck.result }}" == "success" ]]; then
- echo "### ✅ All Required Checks Passed"
+ echo "### All Required Checks Passed"
else
- echo "### ❌ Required Checks Failed"
+ echo "### Required Checks Failed"
fi
echo ""
@@ -518,21 +518,21 @@ jobs:
echo ""
echo "| Check | Type | Status | Description |"
echo "|-------|------|--------|-------------|"
- echo "| **Bash Syntax** | 🔴 Required | ${{ needs.bash-syntax.result == 'success' && '✅ Passed' || '❌ Failed' }} | Validates script syntax with \`bash -n\` |"
- echo "| **ShellCheck** | 🔴 Required | ${{ needs.shellcheck.result == 'success' && '✅ Passed' || '❌ Failed' }} | Static analysis for bugs and issues |"
- echo "| **shfmt** | 🟡 Advisory | ${{ needs.shfmt.result == 'success' && '✅ Passed' || '⚠️ Warnings' }} | Code formatting consistency |"
- echo "| **bashate** | 🟡 Advisory | ${{ needs.bashate.result == 'success' && '✅ Passed' || '⚠️ Warnings' }} | OpenStack style guidelines |"
- echo "| **Security** | 🟡 Advisory | ${{ needs.security-scan.result == 'success' && '✅ Passed' || '⚠️ Warnings' }} | Security anti-pattern detection |"
+ echo "| **Bash Syntax** | Required | ${{ needs.bash-syntax.result == 'success' && 'Passed' || 'Failed' }} | Validates script syntax with \`bash -n\` |"
+ echo "| **ShellCheck** | Required | ${{ needs.shellcheck.result == 'success' && 'Passed' || 'Failed' }} | Static analysis for bugs and issues |"
+ echo "| **shfmt** | Advisory | ${{ needs.shfmt.result == 'success' && 'Passed' || 'Warnings' }} | Code formatting consistency |"
+ echo "| **bashate** | Advisory | ${{ needs.bashate.result == 'success' && 'Passed' || 'Warnings' }} | OpenStack style guidelines |"
+ echo "| **Security** | Advisory | ${{ needs.security-scan.result == 'success' && 'Passed' || 'Warnings' }} | Security anti-pattern detection |"
echo ""
# Legend
echo ""
- echo "📋 Check Type Legend
"
+ echo "Check Type Legend
"
echo ""
echo "| Type | Meaning |"
echo "|------|---------|"
- echo "| 🔴 Required | Must pass for PR to be merged |"
- echo "| 🟡 Advisory | Informational only, does not block merge |"
+ echo "| Required | Must pass for PR to be merged |"
+ echo "| Advisory | Informational only, does not block merge |"
echo ""
echo " "
echo ""
@@ -588,7 +588,7 @@ jobs:
echo ""
# Quick Fix Guide
- echo "## 🛠️ Quick Fix Guide"
+ echo "## Quick Fix Guide"
echo ""
echo ""
echo "How to fix common issues
"
@@ -635,17 +635,17 @@ jobs:
failed=0
if [[ "${{ needs.bash-syntax.result }}" != "success" ]]; then
- echo "::error::❌ Bash syntax validation failed"
+ echo "::error::Bash syntax validation failed"
failed=1
else
- echo "✅ Bash syntax: passed"
+ echo "Bash syntax: passed"
fi
if [[ "${{ needs.shellcheck.result }}" != "success" ]]; then
- echo "::error::❌ ShellCheck found errors or warnings"
+ echo "::error::ShellCheck found errors or warnings"
failed=1
else
- echo "✅ ShellCheck: passed"
+ echo "ShellCheck: passed"
fi
echo ""
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 75fe718..46f8972 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -64,7 +64,7 @@ brew install shellcheck bats-core
```bash
# Check all scripts pass syntax validation
for script in *.sh; do
- bash -n "$script" && echo "✓ $script"
+ bash -n "$script" && echo "[OK] $script"
done
# Run ShellCheck on all scripts
@@ -196,11 +196,57 @@ if ! command -v ffmpeg &>/dev/null; then
fi
```
+#### Logging
+
+LyreBirdAudio provides standardized logging through `lyrebird-common.sh`. Both patterns are acceptable:
+
+```bash
+# Wrapper functions (preferred for new code)
+log_info "Starting service..."
+log_warn "Configuration missing, using defaults"
+log_error "Failed to connect"
+log_debug "Variable value: ${value}"
+
+# Direct log calls (also acceptable)
+log INFO "Starting service..."
+log WARN "Configuration missing"
+```
+
+The logging functions write to both stderr and a log file when available.
+
+#### Variable Naming Prefixes
+
+Configuration variables follow these prefix conventions:
+
+- **`LYREBIRD_*`**: LyreBirdAudio-specific settings (alerts, storage, orchestrator)
+- **`MEDIAMTX_*`**: MediaMTX-related settings (API, ports, paths)
+
+```bash
+# LyreBirdAudio settings
+LYREBIRD_ALERT_ENABLED=true
+LYREBIRD_RECORDING_DIR=/var/lib/recordings
+
+# MediaMTX settings
+MEDIAMTX_API_PORT=9997
+MEDIAMTX_CONFIG_DIR=/etc/mediamtx
+```
+
#### Comments
- Comment complex logic, not obvious code
- Use TODO for future work
- Explain regex patterns and complex commands
+- Use section headers to organize code:
+
+```bash
+# ============================================================================
+# Section Name
+# ============================================================================
+
+# Inline comment for complex logic
+```
+
+Example:
```bash
# Match USB device path format: /sys/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.3
diff --git a/README.md b/README.md
index 3393f7c..b697fb5 100644
--- a/README.md
+++ b/README.md
@@ -387,7 +387,7 @@ sudo ./lyrebird-diagnostics.sh quick
# rtsp://your-ip:8554/device-name
```
-**Reboot Recommended:** After initial and after each individual USB device mapping, reboot for udev rules to take full effect. You will manually have to start the Orcestrator again after each reboot: cd LyreBirdAudio && sudo ./lyrebird-orchestrator.sh
+**Reboot Recommended:** After initial and after each individual USB device mapping, reboot for udev rules to take full effect. You will manually have to start the Orchestrator again after each reboot: cd LyreBirdAudio && sudo ./lyrebird-orchestrator.sh
### Post-Installation
@@ -1426,7 +1426,7 @@ This modular design prevents duplicate business logic and ensures maintainabilit
|--------|---------|---------|
| lyrebird-orchestrator.sh | 2.1.2 | Unified management interface |
| lyrebird-updater.sh | 1.5.1 | Version management with rollback |
-| mediamtx-stream-manager.sh | 1.4.2 | Stream lifecycle management |
+| mediamtx-stream-manager.sh | 1.4.3 | Stream lifecycle management |
| usb-audio-mapper.sh | 1.2.1 | USB device persistence via udev |
| lyrebird-mic-check.sh | 1.0.0 | Hardware capability detection |
| lyrebird-diagnostics.sh | 1.0.2 | System diagnostics |
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..76abbd8
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,91 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | --------- |
+| 2.x.x | Yes |
+| 1.x.x | No |
+
+## Reporting a Vulnerability
+
+We take security seriously. If you discover a security vulnerability in LyreBirdAudio, please report it responsibly.
+
+### How to Report
+
+1. **Do NOT** open a public GitHub issue for security vulnerabilities
+2. Email security concerns to the maintainer (see GitHub profile)
+3. Include as much detail as possible:
+ - Description of the vulnerability
+ - Steps to reproduce
+ - Potential impact
+ - Suggested fix (if any)
+
+### What to Expect
+
+- **Acknowledgment**: Within 48 hours of your report
+- **Initial Assessment**: Within 7 days
+- **Resolution Timeline**: Depends on severity
+ - Critical: 24-48 hours
+ - High: 7 days
+ - Medium: 30 days
+ - Low: Next release
+
+### Security Best Practices
+
+When deploying LyreBirdAudio, follow these security recommendations:
+
+#### Network Security
+
+- Run MediaMTX behind a reverse proxy with TLS termination
+- Restrict API access to localhost or trusted networks
+- Use firewall rules to limit RTSP port exposure
+- Consider VPN for remote stream access
+
+#### File System Security
+
+- Run scripts with minimal required privileges
+- Avoid running as root when possible
+- Use appropriate file permissions (640 for configs, 750 for scripts)
+- Store recordings in a dedicated partition
+
+#### Configuration Security
+
+- Never commit webhook URLs or API keys to version control
+- Use environment variables for sensitive configuration
+- Rotate credentials regularly
+- Monitor logs for unauthorized access attempts
+
+#### Webhook Security
+
+- Use HTTPS endpoints for webhook delivery
+- Verify webhook signatures when possible
+- Implement rate limiting
+- Monitor for failed delivery attempts
+
+## Security Features
+
+LyreBirdAudio includes several security-conscious features:
+
+1. **SHA256 Verification**: All MediaMTX downloads are verified against checksums
+2. **Secure Temp Files**: Uses `mktemp` for temporary file creation
+3. **Input Sanitization**: RTSP paths and user inputs are validated
+4. **No Hardcoded Credentials**: Configuration is environment-driven
+5. **Atomic Operations**: File operations use atomic patterns where possible
+6. **Signal Handling**: Graceful shutdown and cleanup on termination
+7. **Path Validation**: Dangerous operations validate path safety
+
+## Disclosure Policy
+
+We follow a coordinated disclosure process:
+
+1. Reporter contacts maintainers privately
+2. Issue is confirmed and assessed
+3. Fix is developed and tested
+4. Security advisory is prepared
+5. Patch is released
+6. Advisory is published after users have time to update
+
+## Acknowledgments
+
+We appreciate security researchers who help keep LyreBirdAudio secure. Responsible disclosures will be acknowledged in release notes (with permission).
diff --git a/docs/ADR.md b/docs/ADR.md
new file mode 100644
index 0000000..488deda
--- /dev/null
+++ b/docs/ADR.md
@@ -0,0 +1,240 @@
+# Architecture Decision Records (ADR)
+
+This document records significant architectural decisions made during the development of LyreBirdAudio.
+
+## ADR-001: Bash as Primary Implementation Language
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+We needed to choose an implementation language for LyreBirdAudio's automation and management scripts.
+
+### Decision
+Use Bash (4.0+) as the primary implementation language for all scripts.
+
+### Rationale
+- **Universal Availability**: Bash is pre-installed on virtually all Linux distributions
+- **No Dependencies**: No additional runtime or package installation required
+- **System Integration**: Native access to system commands, process management, and file operations
+- **Sysadmin Familiarity**: Target users are comfortable reading and modifying Bash scripts
+- **Transparency**: Easy to audit and understand what the scripts do
+
+### Consequences
+- Limited to Linux/Unix systems (acceptable for target use case)
+- More verbose than Python for complex logic
+- Requires careful handling of edge cases (quoting, spaces in paths)
+- Must enforce `set -euo pipefail` for safety
+
+---
+
+## ADR-002: MediaMTX as RTSP Server
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+We needed an RTSP server that could handle audio streams from USB microphones.
+
+### Decision
+Use MediaMTX (formerly rtsp-simple-server) as the RTSP streaming server.
+
+### Rationale
+- **Single Binary**: No complex installation or dependencies
+- **Low Resource Usage**: Suitable for Raspberry Pi and similar devices
+- **REST API**: Enables programmatic stream management
+- **Active Development**: Regular updates and security patches
+- **Multi-Protocol**: Supports RTSP, RTMP, HLS, WebRTC
+
+### Consequences
+- Dependency on external project's release schedule
+- Must handle API version changes
+- Need to implement download verification (SHA256)
+
+---
+
+## ADR-003: FFmpeg for Audio Transcoding
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+Need to capture audio from ALSA devices and stream to MediaMTX.
+
+### Decision
+Use FFmpeg as the audio capture and transcoding pipeline.
+
+### Rationale
+- **ALSA Support**: Native Linux audio device support
+- **Codec Flexibility**: Supports all common audio codecs
+- **Stability**: Battle-tested in production environments
+- **Configurability**: Extensive options for quality tuning
+
+### Consequences
+- FFmpeg must be installed (usually available in package managers)
+- FFmpeg process management complexity
+- Need to handle FFmpeg crashes and restarts
+
+---
+
+## ADR-004: Shared Library Pattern (lyrebird-common.sh)
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+Multiple scripts needed common functionality (logging, colors, error handling).
+
+### Decision
+Create a shared library (`lyrebird-common.sh`) that scripts can source.
+
+### Rationale
+- **DRY Principle**: Avoid duplicating utility functions
+- **Consistency**: Uniform logging format and error handling
+- **Backward Compatibility**: Scripts can define functions before sourcing (override pattern)
+- **Optional**: Scripts work without the library (fallback definitions)
+
+### Consequences
+- Must maintain backward compatibility
+- Order of sourcing matters
+- Need to guard against multiple inclusion
+
+---
+
+## ADR-005: Atomic File Operations
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+Scripts modify configuration files and state files that could be read by other processes.
+
+### Decision
+Use atomic file operations: write to `.tmp` file, then `mv` to final location.
+
+### Rationale
+- **No Partial Reads**: Readers never see half-written files
+- **Crash Safety**: Original file preserved if write fails
+- **POSIX Guarantee**: `mv` on same filesystem is atomic
+
+### Consequences
+- Slightly more complex code
+- Need to clean up stale `.tmp` files
+- Requires write access to target directory
+
+---
+
+## ADR-006: USB Device Persistence via udev
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+USB audio devices have non-deterministic device names (`/dev/snd/...`) that change on reboot.
+
+### Decision
+Use udev rules to create persistent symlinks based on USB topology (bus/port path).
+
+### Rationale
+- **Kernel Integration**: udev is the standard Linux device manager
+- **Persistence**: Symlinks survive reboots and device reconnection
+- **No Daemon**: Rules are processed by udev automatically
+
+### Consequences
+- Requires root to install udev rules
+- May need reboot for rules to take effect
+- USB hub changes require remapping
+
+---
+
+## ADR-007: Webhook-Based Alerting
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+Need to notify users of stream failures and system issues.
+
+### Decision
+Implement webhook-based alerting with support for Discord, Slack, Pushover, and generic endpoints.
+
+### Rationale
+- **Flexibility**: Users choose their preferred notification platform
+- **No Infrastructure**: No email server or SMS gateway required
+- **Extensibility**: Easy to add new webhook formats
+- **Modern**: Integrates with existing monitoring stacks
+
+### Consequences
+- Requires network connectivity for alerts
+- Need to implement rate limiting
+- Must handle webhook delivery failures
+
+---
+
+## ADR-008: Prometheus Metrics Format
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+Need to expose operational metrics for monitoring dashboards.
+
+### Decision
+Export metrics in Prometheus/OpenMetrics text format.
+
+### Rationale
+- **Industry Standard**: Prometheus is widely adopted
+- **Simple Format**: Plain text, easy to debug
+- **Ecosystem**: Works with Grafana, AlertManager, etc.
+- **Pull Model**: No push infrastructure required
+
+### Consequences
+- Must maintain metric naming conventions
+- Need HTTP server or textfile collector integration
+- Metric cardinality must be controlled
+
+---
+
+## ADR-009: Lockfile-Based Concurrency Control
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+Multiple script invocations could interfere with each other.
+
+### Decision
+Use lockfiles with timeout and stale lock detection.
+
+### Rationale
+- **Simple**: Well-understood mechanism
+- **Debuggable**: Can inspect lock state
+- **Atomic**: Uses `flock` for atomic acquisition
+
+### Consequences
+- Lockfiles can become stale on crashes
+- Need to implement timeout and cleanup
+- Path to lockfile must be consistent
+
+---
+
+## ADR-010: Signal Handler Pattern
+
+**Date**: April 2025
+**Status**: Accepted
+
+### Context
+Scripts must clean up resources (processes, temp files) on termination.
+
+### Decision
+Use `trap` to register cleanup functions for EXIT, INT, and TERM signals.
+
+### Rationale
+- **Reliable Cleanup**: Runs regardless of exit cause
+- **Process Management**: Can kill child processes
+- **State Reset**: Can remove PID files and locks
+
+### Consequences
+- Cleanup must be idempotent (may run multiple times)
+- Signal handlers should be simple (avoid complex logic)
+- Exit code must be preserved
diff --git a/install_mediamtx.sh b/install_mediamtx.sh
index b92a92c..9251c54 100644
--- a/install_mediamtx.sh
+++ b/install_mediamtx.sh
@@ -1308,7 +1308,7 @@ update_mediamtx() {
if new_version=$("${INSTALL_DIR}/mediamtx" --version 2>&1 | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | head -1); then
if [[ "${new_version}" != "${current_version}" ]]; then
upgrade_success=true
- log_info "Built-in upgrade completed successfully (${current_version} → ${new_version})"
+ log_info "Built-in upgrade completed successfully (${current_version} -> ${new_version})"
else
log_warn "Built-in upgrade reported success but version unchanged"
fi
@@ -1540,7 +1540,7 @@ show_status() {
# Installation
if [[ -f "${INSTALL_DIR}/mediamtx" ]]; then
- echo -e "Installation: ${GREEN}✓ Installed${NC}"
+ echo -e "Installation: ${GREEN}[OK] Installed${NC}"
echo "Location: ${INSTALL_DIR}/mediamtx"
local version="unknown"
@@ -1549,16 +1549,16 @@ show_status() {
fi
echo "Version: ${version}"
else
- echo -e "Installation: ${RED}✗ Not installed${NC}"
+ echo -e "Installation: ${RED}[--] Not installed${NC}"
fi
# Configuration
echo ""
if [[ -f "${CONFIG_DIR}/${CONFIG_NAME}" ]]; then
- echo -e "Configuration: ${GREEN}✓ Present${NC}"
+ echo -e "Configuration: ${GREEN}[OK] Present${NC}"
echo "Config file: ${CONFIG_DIR}/${CONFIG_NAME}"
else
- echo -e "Configuration: ${YELLOW}⚠ Missing${NC}"
+ echo -e "Configuration: ${YELLOW}[!] Missing${NC}"
fi
# Service and process status
@@ -1567,23 +1567,23 @@ show_status() {
management_mode=$(detect_management_mode)
if command -v systemctl &>/dev/null && [[ -f "${SERVICE_DIR}/${SERVICE_NAME}" ]]; then
- echo -e "Service: ${GREEN}✓ Configured${NC}"
+ echo -e "Service: ${GREEN}[OK] Configured${NC}"
else
- echo -e "Service: ${YELLOW}⚠ Not configured${NC}"
+ echo -e "Service: ${YELLOW}[!] Not configured${NC}"
fi
case "${management_mode}" in
systemd)
- echo -e "Status: ${GREEN}● Running (systemd)${NC}"
+ echo -e "Status: ${GREEN}[OK] Running (systemd)${NC}"
;;
stream-manager)
- echo -e "Status: ${YELLOW}⚠ Running (stream manager)${NC}"
+ echo -e "Status: ${YELLOW}[!] Running (stream manager)${NC}"
;;
manual)
- echo -e "Status: ${YELLOW}⚠ Running (manual)${NC}"
+ echo -e "Status: ${YELLOW}[!] Running (manual)${NC}"
;;
none)
- echo -e "Status: ${RED}○ Not running${NC}"
+ echo -e "Status: ${RED}[--] Not running${NC}"
;;
esac
@@ -1613,17 +1613,17 @@ show_status() {
if [[ -n "${port_user}" ]]; then
if [[ "${port_user}" == "mediamtx" ]]; then
- echo -e " ${label} (${port}): ${GREEN}✓ In use by MediaMTX${NC}"
+ echo -e " ${label} (${port}): ${GREEN}[OK] In use by MediaMTX${NC}"
else
- echo -e " ${label} (${port}): ${GREEN}✓ In use by ${port_user}${NC}"
+ echo -e " ${label} (${port}): ${GREEN}[OK] In use by ${port_user}${NC}"
fi
elif [[ -n "${port_status}" ]]; then
- echo -e " ${label} (${port}): ${GREEN}✓ In use${NC}"
+ echo -e " ${label} (${port}): ${GREEN}[OK] In use${NC}"
else
if [[ "${management_mode}" == "stream-manager" ]] || [[ "${management_mode}" == "systemd" ]]; then
- echo -e " ${label} (${port}): ${YELLOW}⚠ Status unknown${NC}"
+ echo -e " ${label} (${port}): ${YELLOW}[?] Status unknown${NC}"
else
- echo -e " ${label} (${port}): ${YELLOW}○ Not in use${NC}"
+ echo -e " ${label} (${port}): ${YELLOW}[--] Not in use${NC}"
fi
fi
done
@@ -1678,7 +1678,7 @@ verify_installation() {
echo ""
if [[ ${errors} -eq 0 ]] && [[ ${warnings} -eq 0 ]]; then
- log_info "✓ Installation verified successfully"
+ log_info "Installation verified successfully"
return 0
elif [[ ${errors} -eq 0 ]]; then
log_warn "Installation verified with ${warnings} warning(s)"
diff --git a/lyrebird-alerts.sh b/lyrebird-alerts.sh
index ea37ece..d6aab8a 100755
--- a/lyrebird-alerts.sh
+++ b/lyrebird-alerts.sh
@@ -109,6 +109,13 @@ LYREBIRD_ALERT_RETRIES="${LYREBIRD_ALERT_RETRIES:-3}"
LYREBIRD_HOSTNAME="${LYREBIRD_HOSTNAME:-$(hostname -s 2>/dev/null || echo 'unknown')}"
LYREBIRD_LOCATION="${LYREBIRD_LOCATION:-}"
+# Validate timeout bounds (5-120 seconds)
+if [[ "$LYREBIRD_ALERT_TIMEOUT" -lt 5 ]]; then
+ LYREBIRD_ALERT_TIMEOUT=5
+elif [[ "$LYREBIRD_ALERT_TIMEOUT" -gt 120 ]]; then
+ LYREBIRD_ALERT_TIMEOUT=120
+fi
+
# Additional webhook URLs (space-separated)
LYREBIRD_WEBHOOK_URLS="${LYREBIRD_WEBHOOK_URLS:-}"
@@ -124,7 +131,8 @@ LYREBIRD_NTFY_SERVER="${LYREBIRD_NTFY_SERVER:-https://ntfy.sh}"
# Alert Levels
#=============================================================================
-# shellcheck disable=SC2034 # These constants are exported for use by other scripts
+# Alert level constants - SC2034: These are exported for use by sourcing scripts
+# shellcheck disable=SC2034
readonly ALERT_LEVEL_INFO="info"
# shellcheck disable=SC2034
readonly ALERT_LEVEL_WARNING="warning"
@@ -141,14 +149,20 @@ declare -A ALERT_COLORS=(
[critical]=15548997 # Red
)
-# Emoji for each level
-declare -A ALERT_EMOJI=(
- [info]="ℹ️"
- [warning]="⚠️"
- [error]="❌"
- [critical]="🚨"
+# Text prefixes for each alert level
+declare -A ALERT_PREFIX=(
+ [info]="[INFO]"
+ [warning]="[WARN]"
+ [error]="[ERROR]"
+ [critical]="[CRITICAL]"
)
+# Get alert prefix for level
+get_alert_prefix() {
+ local level="$1"
+ echo "${ALERT_PREFIX[$level]:-[INFO]}"
+}
+
#=============================================================================
# Alert Types (for deduplication keys)
#=============================================================================
@@ -173,6 +187,37 @@ readonly ALERT_TYPE_CUSTOM="custom"
# Helper Functions
#=============================================================================
+# URL-encode a string (handles all special characters)
+url_encode() {
+ local string="$1"
+ # Use jq if available, otherwise fall back to printf-based encoding
+ if command -v jq &>/dev/null; then
+ printf '%s' "$string" | jq -sRr @uri
+ else
+ # Fallback: encode using printf with hex conversion
+ local length="${#string}"
+ local i char
+ for ((i = 0; i < length; i++)); do
+ char="${string:i:1}"
+ case "$char" in
+ [a-zA-Z0-9.~_-]) printf '%s' "$char" ;;
+ *) printf '%%%02X' "'$char" ;;
+ esac
+ done
+ fi
+}
+
+# Mask sensitive URLs for safe debug output (shows first 30 chars + ***)
+mask_url() {
+ local url="$1"
+ local visible_len=30
+ if [[ ${#url} -gt $visible_len ]]; then
+ echo "${url:0:$visible_len}***"
+ else
+ echo "$url"
+ fi
+}
+
# Load configuration file if it exists
load_config() {
if [[ -f "$ALERT_CONFIG_FILE" ]]; then
@@ -306,7 +351,8 @@ format_discord() {
local message="$3"
local alert_type="$4"
local color="${ALERT_COLORS[$level]:-3447003}"
- local emoji="${ALERT_EMOJI[$level]:-ℹ️}"
+ local emoji
+ emoji=$(get_alert_prefix "$level")
local timestamp
timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
@@ -341,7 +387,8 @@ format_slack() {
local title="$2"
local message="$3"
local alert_type="$4"
- local emoji="${ALERT_EMOJI[$level]:-ℹ️}"
+ local emoji
+ emoji=$(get_alert_prefix "$level")
# Escape special characters for JSON
message="${message//\\/\\\\}"
@@ -416,12 +463,13 @@ format_pushover() {
*) priority="0" ;;
esac
- # URL-encode message (simple version)
- message="${message// /%20}"
- title="${title// /%20}"
+ # Properly URL-encode message and title (handles &, =, and all special chars)
+ local encoded_message encoded_title
+ encoded_message=$(url_encode "$message")
+ encoded_title=$(url_encode "$title")
# Return form-encoded data
- echo "token=${LYREBIRD_PUSHOVER_TOKEN}&user=${LYREBIRD_PUSHOVER_USER}&title=${title}&message=${message}&priority=${priority}"
+ echo "token=${LYREBIRD_PUSHOVER_TOKEN}&user=${LYREBIRD_PUSHOVER_USER}&title=${encoded_title}&message=${encoded_message}&priority=${priority}"
}
#=============================================================================
@@ -439,7 +487,7 @@ send_webhook() {
while ((attempt < retries)); do
((attempt++))
- log_debug "Sending webhook (attempt ${attempt}/${retries}): ${webhook_url}"
+ log_debug "Sending webhook (attempt ${attempt}/${retries}): $(mask_url "$webhook_url")"
local http_code
local curl_args=(-s -w '%{http_code}' -o /dev/null --connect-timeout "$timeout" --max-time "$((timeout * 2))")
@@ -488,9 +536,9 @@ send_webhook() {
return 0
fi
- # Retry with backoff
+ # Retry with backoff and jitter (prevents thundering herd)
if ((attempt < retries)); then
- local delay=$((attempt * 2))
+ local delay=$(( (attempt * 2) + (RANDOM % 3) ))
log_debug "Webhook failed, retrying in ${delay}s..."
sleep "$delay"
fi
@@ -591,6 +639,8 @@ send_alert() {
if $any_success; then
# Update rate limit only on success
update_rate_limit "$alert_hash"
+ # Clean up old state files periodically
+ cleanup_state
return 0
else
return 3
@@ -980,8 +1030,7 @@ cmd_send() {
cmd_test() {
echo "Sending test alert..."
- # Temporarily enable if disabled
- local was_enabled="${LYREBIRD_ALERT_ENABLED}"
+ # Force enable for test (no need to save/restore - script exits after test)
LYREBIRD_ALERT_ENABLED="true"
if send_alert "info" "Test Alert from LyreBirdAudio" \
@@ -993,8 +1042,6 @@ cmd_test() {
echo "Failed to send test alert. Check your configuration."
return 1
fi
-
- LYREBIRD_ALERT_ENABLED="$was_enabled"
}
#=============================================================================
diff --git a/lyrebird-common.sh b/lyrebird-common.sh
index 454cc2b..4d64ef2 100755
--- a/lyrebird-common.sh
+++ b/lyrebird-common.sh
@@ -680,9 +680,9 @@ if ! declare -f lyrebird_with_spinner &>/dev/null; then
"$@" || exit_code=$?
if ((exit_code == 0)); then
- lyrebird_spinner_stop "✓ ${message} - Done"
+ lyrebird_spinner_stop "[OK] ${message} - Done"
else
- lyrebird_spinner_stop "✗ ${message} - Failed"
+ lyrebird_spinner_stop "[FAIL] ${message} - Failed"
fi
return "$exit_code"
diff --git a/lyrebird-metrics.sh b/lyrebird-metrics.sh
index 73b891b..a0f9c84 100755
--- a/lyrebird-metrics.sh
+++ b/lyrebird-metrics.sh
@@ -31,7 +31,7 @@ readonly SCRIPT_NAME
# Configuration
readonly MEDIAMTX_API_HOST="${MEDIAMTX_HOST:-localhost}"
readonly MEDIAMTX_API_PORT="${MEDIAMTX_API_PORT:-9997}"
-# shellcheck disable=SC2034 # Used by external scripts or for future features
+# shellcheck disable=SC2034 # Exported for use by install_mediamtx.sh and external scripts
readonly MEDIAMTX_RTSP_PORT="${MEDIAMTX_PORT:-8554}"
readonly HEARTBEAT_FILE="${HEARTBEAT_FILE:-/run/mediamtx-audio.heartbeat}"
readonly PID_FILE="${PID_FILE:-/run/mediamtx-audio.pid}"
@@ -133,6 +133,8 @@ collect_mediamtx_metrics() {
if [[ -n "$start_time_ticks" ]] && [[ -f "/proc/uptime" ]]; then
local clk_tck
clk_tck=$(getconf CLK_TCK 2>/dev/null || echo 100)
+ # Guard against division by zero
+ [[ "$clk_tck" -eq 0 ]] && clk_tck=100
local uptime_sec
uptime_sec=$(awk '{print int($1)}' /proc/uptime)
local proc_age=$((uptime_sec - (start_time_ticks / clk_tck)))
@@ -265,6 +267,25 @@ collect_system_metrics() {
fi
}
+# Helper function for API calls with retry for transient network failures
+# Usage: api_call_with_retry [timeout] [retries]
+api_call_with_retry() {
+ local url="$1"
+ local timeout="${2:-5}"
+ local retries="${3:-2}"
+ local attempt=0
+ local result=""
+
+ while ((attempt < retries)); do
+ ((attempt++))
+ result=$(curl -s --connect-timeout "$timeout" "$url" 2>/dev/null) && break
+ # Brief pause before retry
+ ((attempt < retries)) && sleep 1
+ done
+
+ echo "$result"
+}
+
# Collect MediaMTX API metrics (if available)
# Enhanced in v1.1.0 with full MediaMTX v1.15.5 API coverage
collect_api_metrics() {
@@ -274,10 +295,10 @@ collect_api_metrics() {
local api_url="http://${MEDIAMTX_API_HOST}:${MEDIAMTX_API_PORT}"
- # Check API availability and get instance info
+ # Check API availability and get instance info (with retry for transient failures)
local api_up=0
local info_json
- info_json=$(curl -s --connect-timeout 2 "${api_url}/v3/info" 2>/dev/null)
+ info_json=$(api_call_with_retry "${api_url}/v3/info" 2 2)
if [[ -n "$info_json" ]] && echo "$info_json" | grep -q '"version"'; then
api_up=1
fi
@@ -484,6 +505,19 @@ generate_all_metrics() {
# Simple HTTP server using netcat (for basic metrics serving)
serve_metrics() {
local port="${1:-9100}"
+ local nc_pid=""
+
+ # Cleanup function to kill any lingering nc processes
+ cleanup_server() {
+ log "Shutting down metrics server..."
+ [[ -n "$nc_pid" ]] && kill "$nc_pid" 2>/dev/null || true
+ # Kill any orphaned nc processes on our port
+ pkill -f "nc.*-l.*$port" 2>/dev/null || true
+ exit 0
+ }
+
+ # Set up signal handlers for graceful shutdown
+ trap cleanup_server EXIT INT TERM
if ! has_command nc && ! has_command netcat; then
log "ERROR: nc (netcat) required for --serve mode"
diff --git a/lyrebird-orchestrator.sh b/lyrebird-orchestrator.sh
index 696725e..998d871 100644
--- a/lyrebird-orchestrator.sh
+++ b/lyrebird-orchestrator.sh
@@ -252,11 +252,11 @@ log() {
# Output functions with consistent formatting
success() {
- echo -e "${GREEN}✓${NC} $*"
+ echo -e "${GREEN}[OK]${NC} $*"
}
error() {
- echo -e "${RED}✗${NC} $*"
+ echo -e "${RED}[ERROR]${NC} $*"
}
warning() {
@@ -264,7 +264,7 @@ warning() {
}
info() {
- echo -e "${CYAN}→${NC} $*"
+ echo -e "${CYAN}[INFO]${NC} $*"
}
pause() {
diff --git a/lyrebird-storage.sh b/lyrebird-storage.sh
index 6745bad..e71454d 100755
--- a/lyrebird-storage.sh
+++ b/lyrebird-storage.sh
@@ -38,21 +38,48 @@ readonly SCRIPT_NAME
readonly RECORDING_DIR="${LYREBIRD_RECORDING_DIR:-/var/lib/mediamtx-ffmpeg/recordings}"
readonly LOG_DIR="${LYREBIRD_LOG_DIR:-/var/log/lyrebird}"
readonly MEDIAMTX_LOG="${MEDIAMTX_LOG:-/var/log/mediamtx.out}"
+readonly MEDIAMTX_LOG_DIR="${MEDIAMTX_LOG%/*}" # Directory containing MediaMTX log
readonly TEMP_DIR="${LYREBIRD_TEMP_DIR:-/tmp}"
readonly BUFFER_DIR="${LYREBIRD_BUFFER_DIR:-/dev/shm/lyrebird-buffer}"
-# Retention policies (in days)
-readonly RECORDING_RETENTION_DAYS="${RECORDING_RETENTION_DAYS:-30}"
-readonly LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-7}"
-readonly TEMP_RETENTION_HOURS="${TEMP_RETENTION_HOURS:-24}"
-
-# Disk thresholds (percentage)
-readonly DISK_WARNING_PERCENT="${DISK_WARNING_PERCENT:-80}"
-readonly DISK_CRITICAL_PERCENT="${DISK_CRITICAL_PERCENT:-90}"
-readonly DISK_EMERGENCY_PERCENT="${DISK_EMERGENCY_PERCENT:-95}"
-
-# Minimum free space (MB)
-readonly MIN_FREE_SPACE_MB="${MIN_FREE_SPACE_MB:-500}"
+# Retention policies (in days) - must be positive integers
+_rec_ret=${RECORDING_RETENTION_DAYS:-30}
+_log_ret=${LOG_RETENTION_DAYS:-7}
+_tmp_ret=${TEMP_RETENTION_HOURS:-24}
+
+# Ensure retention values are positive (minimum 1)
+(( _rec_ret < 1 )) && _rec_ret=1
+(( _log_ret < 1 )) && _log_ret=1
+(( _tmp_ret < 1 )) && _tmp_ret=1
+
+readonly RECORDING_RETENTION_DAYS="$_rec_ret"
+readonly LOG_RETENTION_DAYS="$_log_ret"
+readonly TEMP_RETENTION_HOURS="$_tmp_ret"
+unset _rec_ret _log_ret _tmp_ret
+
+# Disk thresholds (percentage) - validate bounds 0-100
+_disk_warning=${DISK_WARNING_PERCENT:-80}
+_disk_critical=${DISK_CRITICAL_PERCENT:-90}
+_disk_emergency=${DISK_EMERGENCY_PERCENT:-95}
+
+# Clamp percentage values to valid range
+(( _disk_warning < 0 )) && _disk_warning=0
+(( _disk_warning > 100 )) && _disk_warning=100
+(( _disk_critical < 0 )) && _disk_critical=0
+(( _disk_critical > 100 )) && _disk_critical=100
+(( _disk_emergency < 0 )) && _disk_emergency=0
+(( _disk_emergency > 100 )) && _disk_emergency=100
+
+readonly DISK_WARNING_PERCENT="$_disk_warning"
+readonly DISK_CRITICAL_PERCENT="$_disk_critical"
+readonly DISK_EMERGENCY_PERCENT="$_disk_emergency"
+unset _disk_warning _disk_critical _disk_emergency
+
+# Minimum free space (MB) - must be positive
+_min_free=${MIN_FREE_SPACE_MB:-500}
+(( _min_free < 0 )) && _min_free=0
+readonly MIN_FREE_SPACE_MB="$_min_free"
+unset _min_free
# Log file size limits (bytes)
readonly MAX_LOG_SIZE="${MAX_LOG_SIZE:-104857600}" # 100MB
@@ -60,6 +87,36 @@ readonly MAX_LOG_SIZE="${MAX_LOG_SIZE:-104857600}" # 100MB
# Dry run mode (set to true to see what would be deleted)
DRY_RUN="${DRY_RUN:-false}"
+# ============================================================================
+# Safety Validation
+# ============================================================================
+
+# Validate that a path is under allowed parent directories for deletion
+# This prevents accidental deletion of system files if misconfigured
+validate_safe_path() {
+ local path="$1"
+ local allowed_parents=("/dev/shm" "/tmp" "/var/tmp" "/run")
+
+ # Resolve to absolute path
+ local resolved_path
+ resolved_path=$(realpath -m "$path" 2>/dev/null) || resolved_path="$path"
+
+ for parent in "${allowed_parents[@]}"; do
+ if [[ "$resolved_path" == "$parent"* ]]; then
+ return 0
+ fi
+ done
+
+ return 1
+}
+
+# Validate BUFFER_DIR at startup
+if [[ -n "${BUFFER_DIR:-}" ]] && ! validate_safe_path "$BUFFER_DIR"; then
+ echo "ERROR: BUFFER_DIR '$BUFFER_DIR' is not under allowed directories (/dev/shm, /tmp, /var/tmp, /run)" >&2
+ echo "This is a safety check to prevent accidental deletion of system files." >&2
+ exit 1
+fi
+
# ============================================================================
# Helper Functions
# ============================================================================
@@ -75,6 +132,22 @@ log_warn() { log WARN "$@"; }
log_error() { log ERROR "$@"; }
log_debug() { [[ "${DEBUG:-false}" == "true" ]] && log DEBUG "$@" || true; }
+# ============================================================================
+# Signal Handling
+# ============================================================================
+
+# Cleanup function for graceful shutdown
+_storage_cleanup() {
+ local exit_code=$?
+ log_debug "Storage script cleanup triggered (exit code: $exit_code)"
+ # Remove any temporary files we may have created
+ rm -f -- /tmp/lyrebird-storage-*.tmp 2>/dev/null || true
+ exit "$exit_code"
+}
+
+# Set up signal handlers for graceful shutdown
+trap _storage_cleanup EXIT INT TERM
+
# Format bytes to human readable
format_bytes() {
local bytes="$1"
@@ -127,7 +200,7 @@ safe_delete() {
log_info "[DRY RUN] Would delete ${desc}: $path"
else
log_debug "Deleting ${desc}: $path"
- rm -rf "$path"
+ rm -rf -- "$path"
fi
}
@@ -181,14 +254,14 @@ cleanup_logs() {
fi
# Clean rotated MediaMTX logs
- if [[ -d "$(dirname "$MEDIAMTX_LOG")" ]]; then
+ if [[ -d "$MEDIAMTX_LOG_DIR" ]]; then
while IFS= read -r -d '' file; do
local size
size=$(stat -c%s "$file" 2>/dev/null || echo 0)
safe_delete "$file" "mediamtx log"
((++count))
((freed_bytes += size))
- done < <(find "$(dirname "$MEDIAMTX_LOG")" -type f -name "mediamtx*.out.*" -mtime "+${LOG_RETENTION_DAYS}" -print0 2>/dev/null)
+ done < <(find "$MEDIAMTX_LOG_DIR" -type f -name "mediamtx*.out.*" -mtime "+${LOG_RETENTION_DAYS}" -print0 2>/dev/null)
fi
log_info "Cleaned $count log file(s), freed $(format_bytes $freed_bytes)"
@@ -228,6 +301,10 @@ cleanup_temp() {
truncate_large_logs() {
log_info "Checking for oversized log files"
+ # Clean up any stale .tmp files from interrupted previous runs
+ find "$MEDIAMTX_LOG_DIR" -name "*.tmp" -mmin +5 -delete 2>/dev/null || true
+ [[ -d "$LOG_DIR" ]] && find "$LOG_DIR" -name "*.tmp" -mmin +5 -delete 2>/dev/null || true
+
# Check MediaMTX log
if [[ -f "$MEDIAMTX_LOG" ]]; then
local size
@@ -282,13 +359,15 @@ emergency_cleanup() {
# Delete all rotated logs
find "$LOG_DIR" -name "*.gz" -delete 2>/dev/null || true
- find "$(dirname "$MEDIAMTX_LOG")" -name "*.gz" -delete 2>/dev/null || true
+ find "$MEDIAMTX_LOG_DIR" -name "*.gz" -delete 2>/dev/null || true
# Clear temp directory
find "$TEMP_DIR" -maxdepth 1 -name "lyrebird-*" -type f -delete 2>/dev/null || true
- # Clear buffer directory
- [[ -d "$BUFFER_DIR" ]] && rm -rf "${BUFFER_DIR:?}"/* 2>/dev/null || true
+ # Clear buffer directory (with safety validation)
+ if [[ -d "$BUFFER_DIR" ]] && validate_safe_path "$BUFFER_DIR"; then
+ rm -rf "${BUFFER_DIR:?}"/* 2>/dev/null || true
+ fi
}
# ============================================================================
diff --git a/tests/test_integration.bats b/tests/test_integration.bats
new file mode 100644
index 0000000..6622d1a
--- /dev/null
+++ b/tests/test_integration.bats
@@ -0,0 +1,214 @@
+#!/usr/bin/env bats
+# test_integration.bats - Integration tests for LyreBirdAudio
+# Part of LyreBirdAudio - RTSP Audio Streaming Suite
+#
+# These tests verify end-to-end functionality with mock devices and services.
+# They require a more complete test environment than unit tests.
+#
+# Prerequisites:
+# - Mock audio device available (or virtual audio device)
+# - MediaMTX not running (tests manage their own instance)
+# - Write access to /tmp
+#
+# Run with: bats tests/test_integration.bats
+
+# ============================================================================
+# Test Setup and Teardown
+# ============================================================================
+
+setup() {
+ TEST_DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" && pwd )"
+ PROJECT_ROOT="$( cd "$TEST_DIR/.." && pwd )"
+
+ # Source common library if available
+ if [[ -f "$PROJECT_ROOT/lyrebird-common.sh" ]]; then
+ source "$PROJECT_ROOT/lyrebird-common.sh"
+ fi
+
+ # Create isolated test environment
+ export TEST_TMP=$(mktemp -d)
+ export TEST_CONFIG_DIR="$TEST_TMP/config"
+ export TEST_STATE_DIR="$TEST_TMP/state"
+ export TEST_LOG_DIR="$TEST_TMP/logs"
+
+ mkdir -p "$TEST_CONFIG_DIR" "$TEST_STATE_DIR" "$TEST_LOG_DIR"
+
+ # Mock device configuration
+ export MOCK_DEVICE_NAME="test-audio-device"
+ export MOCK_DEVICE_PATH="/dev/null" # Safe placeholder
+}
+
+teardown() {
+ # Clean up test environment
+ rm -rf "$TEST_TMP" 2>/dev/null || true
+
+ # Ensure no orphaned processes from tests
+ pkill -f "test-mediamtx" 2>/dev/null || true
+}
+
+# ============================================================================
+# Stream Lifecycle Tests
+# ============================================================================
+
+@test "INTEGRATION: stream manager starts with valid config" {
+ skip "Integration test - requires mock audio device"
+
+ # Create minimal test configuration
+ cat > "$TEST_CONFIG_DIR/audio-devices.conf" << 'EOF'
+# Test device configuration
+DEVICE_test_device="/dev/null"
+EOF
+
+ # Start stream manager in test mode
+ run timeout 5 "$PROJECT_ROOT/mediamtx-stream-manager.sh" --config-dir "$TEST_CONFIG_DIR" status
+
+ # Verify it runs without crashing
+ [[ "$status" -eq 0 ]] || [[ "$status" -eq 1 ]] # 0=running, 1=not running (both valid)
+}
+
+@test "INTEGRATION: stream manager handles missing device gracefully" {
+ skip "Integration test - requires mock audio device"
+
+ # Create config with non-existent device
+ cat > "$TEST_CONFIG_DIR/audio-devices.conf" << 'EOF'
+DEVICE_nonexistent="/dev/nonexistent-device-12345"
+EOF
+
+ run "$PROJECT_ROOT/mediamtx-stream-manager.sh" --config-dir "$TEST_CONFIG_DIR" start
+
+ # Should fail gracefully, not crash
+ [[ "$status" -ne 0 ]]
+ [[ "$output" =~ "not found" ]] || [[ "$output" =~ "error" ]]
+}
+
+# ============================================================================
+# USB Hot-Plug Simulation Tests
+# ============================================================================
+
+@test "INTEGRATION: USB mapper detects simulated device addition" {
+ skip "Integration test - requires udev simulation"
+
+ # This would require:
+ # 1. Creating a mock udev event
+ # 2. Triggering the USB mapper
+ # 3. Verifying the device was detected
+
+ # Placeholder for future implementation
+ [[ true ]]
+}
+
+@test "INTEGRATION: USB mapper handles device removal" {
+ skip "Integration test - requires udev simulation"
+
+ # Placeholder for device removal test
+ [[ true ]]
+}
+
+# ============================================================================
+# API Interaction Tests
+# ============================================================================
+
+@test "INTEGRATION: metrics collector connects to MediaMTX API" {
+ skip "Integration test - requires running MediaMTX instance"
+
+ # Would need to:
+ # 1. Start a mock MediaMTX API server
+ # 2. Run metrics collection
+ # 3. Verify metrics were collected
+
+ run "$PROJECT_ROOT/lyrebird-metrics.sh" --once
+
+ # Should produce some output even if MediaMTX is not running
+ [[ -n "$output" ]]
+}
+
+@test "INTEGRATION: alerts send to mock webhook endpoint" {
+ skip "Integration test - requires mock webhook server"
+
+ # Would need to:
+ # 1. Start a mock HTTP server
+ # 2. Configure alerts to point to it
+ # 3. Trigger an alert
+ # 4. Verify the mock received the request
+
+ [[ true ]]
+}
+
+# ============================================================================
+# Error Recovery Tests
+# ============================================================================
+
+@test "INTEGRATION: orchestrator recovers from stream failure" {
+ skip "Integration test - requires running services"
+
+ # This would test:
+ # 1. Starting a stream
+ # 2. Simulating stream failure
+ # 3. Verifying automatic recovery
+
+ [[ true ]]
+}
+
+@test "INTEGRATION: storage manager handles disk full condition" {
+ skip "Integration test - requires disk simulation"
+
+ # Would need to:
+ # 1. Create a small test filesystem
+ # 2. Fill it up
+ # 3. Verify cleanup runs correctly
+
+ [[ true ]]
+}
+
+# ============================================================================
+# End-to-End Workflow Tests
+# ============================================================================
+
+@test "INTEGRATION: full installation workflow" {
+ skip "Integration test - requires root and network"
+
+ # Would test the complete installation process:
+ # 1. Pre-flight checks
+ # 2. MediaMTX download/install
+ # 3. Configuration setup
+ # 4. Service creation
+
+ [[ true ]]
+}
+
+@test "INTEGRATION: update workflow preserves configuration" {
+ skip "Integration test - requires installed instance"
+
+ # Would test:
+ # 1. Creating custom configuration
+ # 2. Running update
+ # 3. Verifying config preserved
+
+ [[ true ]]
+}
+
+# ============================================================================
+# Performance Tests
+# ============================================================================
+
+@test "INTEGRATION: stream startup time under threshold" {
+ skip "Integration test - requires performance testing setup"
+
+ # Would measure:
+ # - Time to start first stream
+ # - Time to become ready
+ # - Memory usage
+
+ [[ true ]]
+}
+
+@test "INTEGRATION: concurrent stream handling" {
+ skip "Integration test - requires multiple mock devices"
+
+ # Would test:
+ # - Starting multiple streams simultaneously
+ # - Resource allocation
+ # - No race conditions
+
+ [[ true ]]
+}
diff --git a/usb-audio-mapper.sh b/usb-audio-mapper.sh
index 659c388..8c352ff 100644
--- a/usb-audio-mapper.sh
+++ b/usb-audio-mapper.sh
@@ -17,8 +17,8 @@
# - Enhanced error handling and validation
# - Added comprehensive exception handling
-# Set bash pipefail for better error handling
-set -o pipefail
+# Set strict mode for better error handling
+set -euo pipefail
# Source shared library if available (backward compatible)
# Provides: colors, logging, command_exists, compute_hash, exit codes