|
import Flask |
|
from flask import request, render_template_string, jsonify, redirect, url_for |
|
import requests |
|
import threading |
|
import uuid |
|
import time |
|
import copy |
|
|
|
app = Flask(__name__) |
|
|
|
|
|
TARGET_URL = "https://hook.us1.make.com/zal5qn0ggbewmvtsbo2uenfno8tz3n56" |
|
BASE_PAYLOAD = { |
|
"name": "Testeur Auto ", |
|
"email": "[email protected]", |
|
"company": "aragon Inc.", |
|
"message": "Ceci est un test automatisé via Flask.", |
|
"date": "2023-10-27T10:30:00Z", |
|
"source": "http://simulateur-bsbs-flask.com" |
|
} |
|
|
|
|
|
jobs = {} |
|
jobs_lock = threading.Lock() |
|
|
|
|
|
|
|
|
|
HTML_INDEX = """ |
|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Lanceur de Requêtes</title> |
|
<style> |
|
body { font-family: sans-serif; margin: 20px; } |
|
label { display: block; margin-bottom: 5px; } |
|
input[type=number] { width: 100px; padding: 8px; margin-bottom: 15px; } |
|
button { padding: 10px 15px; cursor: pointer; } |
|
.error { color: red; margin-top: 10px; } |
|
</style> |
|
</head> |
|
<body> |
|
<h1>Envoyer des Requêtes POST en Masse</h1> |
|
<form method="POST" action="/start"> |
|
<label for="num_requests">Nombre de requêtes à envoyer :</label> |
|
<input type="number" id="num_requests" name="num_requests" min="1" required> |
|
<button type="submit">Lancer les requêtes</button> |
|
</form> |
|
{% if error %} |
|
<p class="error">{{ error }}</p> |
|
{% endif %} |
|
|
|
<h2>Tâches en cours / terminées :</h2> |
|
<ul> |
|
{% for job_id, job_info in jobs_list.items() %} |
|
<li> |
|
<a href="{{ url_for('job_status', job_id=job_id) }}">Tâche {{ job_id }}</a> |
|
({{ job_info.status }}, {{ job_info.completed_count }}/{{ job_info.total }} complétées, {{ job_info.error_count }} erreurs) |
|
</li> |
|
{% else %} |
|
<li>Aucune tâche récente.</li> |
|
{% endfor %} |
|
</ul> |
|
</body> |
|
</html> |
|
""" |
|
|
|
|
|
HTML_STATUS = """ |
|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Statut Tâche {{ job_id }}</title> |
|
<style> |
|
body { font-family: sans-serif; margin: 20px; } |
|
#progress-bar-container { width: 100%; background-color: #f0f0f0; border-radius: 5px; margin-bottom: 10px; } |
|
#progress-bar { width: 0%; height: 30px; background-color: #4CAF50; text-align: center; line-height: 30px; color: white; border-radius: 5px; transition: width 0.5s ease-in-out; } |
|
.error-log { margin-top: 15px; max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9;} |
|
.error-log p { margin: 5px 0; font-size: 0.9em; } |
|
.status-message { font-weight: bold; margin-bottom: 15px; } |
|
</style> |
|
</head> |
|
<body> |
|
<h1>Statut de la Tâche : {{ job_id }}</h1> |
|
<div id="status-message" class="status-message">Chargement...</div> |
|
<div id="progress-bar-container"> |
|
<div id="progress-bar">0%</div> |
|
</div> |
|
<p>Requêtes complétées : <span id="completed">0</span> / <span id="total">?</span></p> |
|
<p>Erreurs : <span id="errors">0</span></p> |
|
<div id="error-details" class="error-log" style="display: none;"> |
|
<h3>Détails des erreurs :</h3> |
|
<div id="error-list"></div> |
|
</div> |
|
<p><a href="/">Retour à l'accueil</a></p> |
|
|
|
<script> |
|
const jobId = "{{ job_id }}"; |
|
const statusMessageEl = document.getElementById('status-message'); |
|
const progressBarEl = document.getElementById('progress-bar'); |
|
const completedEl = document.getElementById('completed'); |
|
const totalEl = document.getElementById('total'); |
|
const errorsEl = document.getElementById('errors'); |
|
const errorDetailsEl = document.getElementById('error-details'); |
|
const errorListEl = document.getElementById('error-list'); |
|
|
|
let intervalId = null; |
|
|
|
function updateStatus() { |
|
fetch(`/api/status/${jobId}`) |
|
.then(response => { |
|
if (!response.ok) { |
|
throw new Error(`Erreur HTTP: ${response.status}`); |
|
} |
|
return response.json(); |
|
}) |
|
.then(data => { |
|
if (!data) { // Gère le cas où la tâche n'est pas encore prête |
|
statusMessageEl.textContent = "En attente de démarrage..."; |
|
return; |
|
} |
|
|
|
completedEl.textContent = data.completed_count; |
|
totalEl.textContent = data.total; |
|
errorsEl.textContent = data.error_count; |
|
statusMessageEl.textContent = `Statut : ${data.status}`; |
|
|
|
let percentage = 0; |
|
if (data.total > 0) { |
|
percentage = Math.round((data.completed_count / data.total) * 100); |
|
} |
|
progressBarEl.style.width = percentage + '%'; |
|
progressBarEl.textContent = percentage + '%'; |
|
|
|
// Afficher les erreurs |
|
if (data.error_count > 0 && data.errors && data.errors.length > 0) { |
|
errorListEl.innerHTML = ''; // Clear previous errors |
|
data.errors.forEach(err => { |
|
const p = document.createElement('p'); |
|
p.textContent = `Req ${err.index}: ${err.error}`; |
|
errorListEl.appendChild(p); |
|
}); |
|
errorDetailsEl.style.display = 'block'; |
|
} else { |
|
errorDetailsEl.style.display = 'none'; |
|
} |
|
|
|
|
|
if (data.status === 'completed' || data.status === 'failed') { |
|
if (intervalId) { |
|
clearInterval(intervalId); |
|
intervalId = null; // Arrête les mises à jour |
|
console.log("Mises à jour arrêtées car la tâche est terminée."); |
|
if (data.status === 'completed' && data.error_count == 0) { |
|
progressBarEl.style.backgroundColor = '#4CAF50'; // Vert |
|
} else if (data.status === 'completed' && data.error_count > 0) { |
|
progressBarEl.style.backgroundColor = '#ff9800'; // Orange |
|
} else { // failed |
|
progressBarEl.style.backgroundColor = '#f44336'; // Rouge |
|
} |
|
} |
|
} |
|
}) |
|
.catch(error => { |
|
console.error("Erreur lors de la récupération du statut:", error); |
|
statusMessageEl.textContent = "Erreur lors de la récupération du statut."; |
|
if (intervalId) { |
|
clearInterval(intervalId); // Arrête en cas d'erreur persistante |
|
intervalId = null; |
|
} |
|
}); |
|
} |
|
|
|
// Mettre à jour immédiatement puis toutes les 2 secondes |
|
updateStatus(); |
|
intervalId = setInterval(updateStatus, 2000); |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
|
|
|
|
def send_single_request(target_url, payload, job_id, request_index): |
|
"""Fonction pour envoyer UNE requête POST.""" |
|
|
|
current_payload = copy.deepcopy(payload) |
|
current_payload['message'] += f" (Requête {request_index + 1})" |
|
current_payload['request_uuid'] = str(uuid.uuid4()) |
|
|
|
try: |
|
response = requests.post(target_url, json=current_payload, timeout=30) |
|
response.raise_for_status() |
|
return True, None |
|
except requests.exceptions.RequestException as e: |
|
print(f"Erreur requête {request_index + 1} pour job {job_id}: {e}") |
|
return False, f"Req {request_index + 1}: {str(e)}" |
|
|
|
def background_task(job_id, num_requests, target_url, base_payload): |
|
"""Fonction exécutée dans un thread séparé pour envoyer les requêtes.""" |
|
print(f"Tâche {job_id}: Démarrage de {num_requests} requêtes vers {target_url}") |
|
completed_count = 0 |
|
error_count = 0 |
|
error_messages = [] |
|
|
|
|
|
with jobs_lock: |
|
if job_id not in jobs: |
|
jobs[job_id] = { |
|
'status': 'running', |
|
'total': num_requests, |
|
'completed_count': 0, |
|
'error_count': 0, |
|
'errors': [] |
|
} |
|
|
|
for i in range(num_requests): |
|
success, error_msg = send_single_request(target_url, base_payload, job_id, i) |
|
completed_count += 1 |
|
if not success: |
|
error_count += 1 |
|
error_messages.append({'index': i + 1, 'error': error_msg}) |
|
|
|
|
|
with jobs_lock: |
|
jobs[job_id]['completed_count'] = completed_count |
|
jobs[job_id]['error_count'] = error_count |
|
|
|
jobs[job_id]['errors'] = error_messages[-50:] |
|
|
|
|
|
|
|
|
|
|
|
final_status = 'failed' if error_count == num_requests else ('completed' if error_count == 0 else 'completed_with_errors') |
|
with jobs_lock: |
|
jobs[job_id]['status'] = final_status |
|
|
|
print(f"Tâche {job_id}: Terminé. {completed_count - error_count} succès, {error_count} erreurs.") |
|
|
|
|
|
|
|
@app.route('/', methods=['GET']) |
|
def index(): |
|
"""Affiche la page d'accueil avec le formulaire.""" |
|
|
|
with jobs_lock: |
|
|
|
|
|
sorted_jobs = dict(sorted(jobs.items(), reverse=True)) |
|
return render_template_string(HTML_INDEX, jobs_list=sorted_jobs) |
|
|
|
@app.route('/start', methods=['POST']) |
|
def start_requests(): |
|
"""Reçoit le nombre de requêtes et lance la tâche en arrière-plan.""" |
|
try: |
|
num_requests = int(request.form.get('num_requests')) |
|
if num_requests <= 0: |
|
raise ValueError("Le nombre de requêtes doit être positif.") |
|
except (TypeError, ValueError) as e: |
|
with jobs_lock: |
|
sorted_jobs = dict(sorted(jobs.items(), reverse=True)) |
|
return render_template_string(HTML_INDEX, error=f"Nombre invalide: {e}", jobs_list=sorted_jobs), 400 |
|
|
|
job_id = str(uuid.uuid4())[:8] |
|
|
|
|
|
with jobs_lock: |
|
jobs[job_id] = { |
|
'status': 'starting', |
|
'total': num_requests, |
|
'completed_count': 0, |
|
'error_count': 0, |
|
'errors': [] |
|
} |
|
|
|
|
|
thread = threading.Thread(target=background_task, args=(job_id, num_requests, TARGET_URL, BASE_PAYLOAD)) |
|
thread.daemon = True |
|
thread.start() |
|
|
|
print(f"Nouvelle tâche démarrée avec ID: {job_id}") |
|
|
|
return redirect(url_for('job_status', job_id=job_id)) |
|
|
|
@app.route('/status/<job_id>', methods=['GET']) |
|
def job_status(job_id): |
|
"""Affiche la page HTML de suivi pour une tâche spécifique.""" |
|
with jobs_lock: |
|
if job_id not in jobs: |
|
return "Tâche non trouvée", 404 |
|
|
|
return render_template_string(HTML_STATUS, job_id=job_id) |
|
|
|
@app.route('/api/status/<job_id>', methods=['GET']) |
|
def api_job_status(job_id): |
|
"""Fournit l'état actuel d'une tâche au format JSON (pour le JavaScript).""" |
|
with jobs_lock: |
|
job_info = jobs.get(job_id) |
|
|
|
if job_info: |
|
|
|
return jsonify(copy.deepcopy(job_info)) |
|
else: |
|
|
|
return jsonify({"error": "Tâche non trouvée"}), 404 |
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
app.run(host='0.0.0.0', port=5000, debug=True) |