update issues of freeze

This commit is contained in:
2026-01-04 14:54:35 +01:00
parent bb80a65346
commit 2c43e719e3
7 changed files with 161 additions and 6 deletions

View File

@@ -6,7 +6,15 @@ TestArena is an automated build and test execution system for ESP32 projects. It
1. **Deploy**: Run `sudo ./deploy.sh` on your Ubuntu server. 1. **Deploy**: Run `sudo ./deploy.sh` on your Ubuntu server.
2. **Access**: Open `http://<server-ip>:8080/` in your browser. 2. **Access**: Open `http://<server-ip>:8080/` in your browser.
3. **Monitor**: Use the dashboard to track test queues and view real-time logs. 3. **Monitor**: Use the dashboard to track test queues, view individual tasks, and check service health.
4. **Restart**: If services need a manual restart, use `sudo ./restart_services.sh`.
## 🛠️ Key Features
- **Service Robustness**: Systemd services are configured to auto-restart on failure and after reboot.
- **Monitoring Dashboard**: Real-time status of App and Worker services, plus detailed task tracking for each queue.
- **Task Timeouts**: Running tasks have a 1-hour timeout to prevent queue blocking.
- **Remote Management**: A dedicated restart script for easy remote execution via SSH.
## 📚 Documentation ## 📚 Documentation

24
restart_services.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# TestArena Service Restart Script
# This script restarts all components of the TestArena system.
# Usage: sudo ./restart_services.sh
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root (use sudo ./restart_services.sh)"
exit 1
fi
echo "🔄 Restarting TestArena Services..."
echo "🌐 Restarting Nginx..."
systemctl restart nginx
echo "📱 Restarting TestArena App..."
systemctl restart testarena-app
echo "⚙️ Restarting TestArena Worker..."
systemctl restart testarena-worker
echo "✅ All services restarted!"
systemctl status testarena-app testarena-worker nginx --no-pager

View File

@@ -12,6 +12,8 @@ Environment="XDG_RUNTIME_DIR=/tmp"
Environment="DATABASE_URL=sqlite:////home/asf/testarena/testarena.db" Environment="DATABASE_URL=sqlite:////home/asf/testarena/testarena.db"
ExecStart=/home/asf/testarena_backend/venv/bin/uvicorn testarena_app.main:app --host 0.0.0.0 --port 8000 ExecStart=/home/asf/testarena_backend/venv/bin/uvicorn testarena_app.main:app --host 0.0.0.0 --port 8000
Restart=always Restart=always
RestartSec=10
StartLimitIntervalSec=0
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -12,6 +12,8 @@ Environment="XDG_RUNTIME_DIR=/tmp"
Environment="DATABASE_URL=sqlite:////home/asf/testarena/testarena.db" Environment="DATABASE_URL=sqlite:////home/asf/testarena/testarena.db"
ExecStart=/home/asf/testarena_backend/venv/bin/python3 -m testarena_app.worker ExecStart=/home/asf/testarena_backend/venv/bin/python3 -m testarena_app.worker
Restart=always Restart=always
RestartSec=10
StartLimitIntervalSec=0
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -158,6 +158,26 @@ async def delete_queue(id: str, db: Session = Depends(database.get_db)):
raise HTTPException(status_code=404, detail="ID not found") raise HTTPException(status_code=404, detail="ID not found")
@app.get("/api/system/status")
async def system_status():
"""Check the status of system services"""
services = ["testarena-app", "testarena-worker", "nginx"]
status = {}
for service in services:
try:
# Use systemctl is-active for a quick check
res = os.system(f"systemctl is-active --quiet {service}")
status[service] = "online" if res == 0 else "offline"
except:
status[service] = "unknown"
return status
@app.get("/api/queue/{id}/tasks")
async def get_queue_tasks(id: str, db: Session = Depends(database.get_db)):
"""Get all tasks for a specific queue"""
tasks = db.query(models.Task).filter(models.Task.queue_id == id).all()
return tasks
@app.get("/") @app.get("/")
async def root(): async def root():
return FileResponse(os.path.join(static_dir, "index.html")) return FileResponse(os.path.join(static_dir, "index.html"))

View File

@@ -209,6 +209,12 @@
color: #f87171; color: #f87171;
} }
.status-timed-out {
background: rgba(245, 158, 11, 0.1);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.3);
}
.btn-abort { .btn-abort {
background: rgba(239, 68, 68, 0.1); background: rgba(239, 68, 68, 0.1);
color: #f87171; color: #f87171;
@@ -291,14 +297,24 @@
<div class="dot"></div> <div class="dot"></div>
<span>Connecting...</span> <span>Connecting...</span>
</div> </div>
<div id="service-status" style="display: flex; gap: 1rem;">
<div class="status-badge" title="App Service">
<div id="app-dot" class="dot"></div>
<span>App</span>
</div>
<div class="status-badge" title="Worker Service">
<div id="worker-dot" class="dot"></div>
<span>Worker</span>
</div>
</div>
</header> </header>
<div class="grid"> <div class="grid">
<div class="card"> <div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2> <h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round"> stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" /> <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="3" y1="9" x2="21" y2="9" /> <line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" /> <line x1="9" y1="21" x2="9" y2="9" />
@@ -323,6 +339,37 @@
<!-- Dynamic content --> <!-- Dynamic content -->
</tbody> </tbody>
</table> </table>
<div id="tasks-section"
style="margin-top: 3rem; display: none; border-top: 1px solid var(--glass-border); padding-top: 2rem;">
<div
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
Tasks for <span id="selected-queue-id"></span>
</h2>
<button class="btn-abort"
style="background: var(--glass); color: var(--text); border-color: var(--glass-border);"
onclick="hideTasks()">Close</button>
</div>
<table id="tasks-table">
<thead>
<tr>
<th>Task ID</th>
<th>Scenario</th>
<th>Status</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<!-- Dynamic content -->
</tbody>
</table>
</div>
</div> </div>
<div class="card"> <div class="card">
@@ -356,6 +403,8 @@
const badge = document.getElementById('connection-status'); const badge = document.getElementById('connection-status');
badge.querySelector('.dot').classList.add('online'); badge.querySelector('.dot').classList.add('online');
badge.querySelector('span').textContent = 'System Online'; badge.querySelector('span').textContent = 'System Online';
fetchServiceStatus();
} catch (e) { } catch (e) {
const badge = document.getElementById('connection-status'); const badge = document.getElementById('connection-status');
badge.querySelector('.dot').classList.remove('online'); badge.querySelector('.dot').classList.remove('online');
@@ -363,6 +412,22 @@
} }
} }
async function fetchServiceStatus() {
try {
const response = await fetch('/api/system/status');
const status = await response.json();
const appDot = document.getElementById('app-dot');
const workerDot = document.getElementById('worker-dot');
if (status['testarena-app'] === 'online') appDot.classList.add('online');
else appDot.classList.remove('online');
if (status['testarena-worker'] === 'online') workerDot.classList.add('online');
else workerDot.classList.remove('online');
} catch (e) { }
}
function renderTable() { function renderTable() {
const searchTerm = document.getElementById('search-input').value.toLowerCase(); const searchTerm = document.getElementById('search-input').value.toLowerCase();
const tbody = document.querySelector('#queue-table tbody'); const tbody = document.querySelector('#queue-table tbody');
@@ -375,8 +440,9 @@
tr.innerHTML = ` tr.innerHTML = `
<td style="font-weight: 600;">${q.id}</td> <td style="font-weight: 600;">${q.id}</td>
<td><span style="opacity: 0.8;">${q.environment}</span></td> <td><span style="opacity: 0.8;">${q.environment}</span></td>
<td><span class="status-pill status-${q.status.toLowerCase()}">${q.status}</span></td> <td><span class="status-pill status-${q.status.toLowerCase().replace(' ', '-')}">${q.status}</span></td>
<td style="display: flex; gap: 0.5rem;"> <td style="display: flex; gap: 0.5rem;">
<button class="btn-abort" style="background: rgba(99, 102, 241, 0.1); color: #818cf8; border-color: rgba(99, 102, 241, 0.2);" onclick="viewTasks('${q.id}')">Tasks</button>
<button class="btn-abort" onclick="abortQueue('${q.id}')">Abort</button> <button class="btn-abort" onclick="abortQueue('${q.id}')">Abort</button>
<button class="btn-abort" style="background: rgba(239, 68, 68, 0.2); border-color: var(--danger);" onclick="deleteQueue('${q.id}')">Delete</button> <button class="btn-abort" style="background: rgba(239, 68, 68, 0.2); border-color: var(--danger);" onclick="deleteQueue('${q.id}')">Delete</button>
</td> </td>
@@ -426,6 +492,37 @@
} }
} }
async function viewTasks(queueId) {
document.getElementById('tasks-section').style.display = 'block';
document.getElementById('selected-queue-id').textContent = queueId;
document.getElementById('tasks-section').scrollIntoView({ behavior: 'smooth' });
try {
const response = await fetch(`/api/queue/${queueId}/tasks`);
const tasks = await response.json();
const tbody = document.querySelector('#tasks-table tbody');
tbody.innerHTML = '';
tasks.forEach(t => {
const tr = document.createElement('tr');
const resultStr = t.result ? JSON.stringify(t.result).substring(0, 50) + '...' : '-';
tr.innerHTML = `
<td>${t.id}</td>
<td title="${t.scenario_path}">${t.scenario_path.split('/').pop()}</td>
<td><span class="status-pill status-${t.status.toLowerCase().replace(' ', '-')}">${t.status}</span></td>
<td><small>${resultStr}</small></td>
`;
tbody.appendChild(tr);
});
} catch (e) {
addLog(`Failed to fetch tasks for ${queueId}`, 'danger');
}
}
function hideTasks() {
document.getElementById('tasks-section').style.display = 'none';
}
function addLog(msg, type = 'info') { function addLog(msg, type = 'info') {
const logs = document.getElementById('logs'); const logs = document.getElementById('logs');
const entry = document.createElement('div'); const entry = document.createElement('div');

View File

@@ -170,10 +170,12 @@ def run_worker():
task_dir = os.path.join(queue_dir, task.id) task_dir = os.path.join(queue_dir, task.id)
os.makedirs(task_dir, exist_ok=True) os.makedirs(task_dir, exist_ok=True)
ret = run_command_with_logging(cmd, queue_log, cwd=repo_dir) ret = run_command_with_logging(cmd, queue_log, cwd=repo_dir, timeout=3600)
if ret == 0: if ret == 0:
task.status = "Finished" task.status = "Finished"
elif ret == 124:
task.status = "Timed Out"
else: else:
task.status = "Error" task.status = "Error"