File size: 13,768 Bytes
47ce5e8 a415b40 b237e11 a415b40 687de48 a415b40 b237e11 a415b40 b237e11 a415b40 687de48 a415b40 687de48 b237e11 a415b40 b237e11 a415b40 b237e11 a415b40 687de48 a415b40 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
import flask
from flask import request, render_template_string, jsonify, redirect, url_for
import requests
import threading
import uuid # Pour générer des identifiants uniques pour chaque tâche
import time
import copy # Pour copier le payload pour chaque requête
app = Flask(__name__)
# --- Configuration ---
TARGET_URL = "https://hook.us1.make.com/zal5qn0ggbewmvtsbo2uenfno8tz3n56"
BASE_PAYLOAD = {
"name": "Testeur Auto ",
"email": "[email protected]", # Ajout de '+auto' pour distinguer
"company": "aragon Inc.",
"message": "Ceci est un test automatisé via Flask.",
"date": "2023-10-27T10:30:00Z", # Tu pourrais rendre cette date dynamique si besoin
"source": "http://simulateur-bsbs-flask.com"
}
# Structure pour stocker l'état des tâches (jobs) en mémoire
# Format: { 'job_id': {'status': 'running'/'completed'/'failed', 'total': N, 'completed_count': M, 'error_count': E, 'errors': [...] } }
jobs = {}
jobs_lock = threading.Lock() # Pour éviter les problèmes d'accès concurrents au dict jobs
# --- Templates HTML ---
# Page d'accueil pour démarrer les requêtes
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>
"""
# Page pour suivre la progression d'une tâche spécifique
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>
"""
# --- Fonctions Logiques ---
def send_single_request(target_url, payload, job_id, request_index):
"""Fonction pour envoyer UNE requête POST."""
# Crée une copie pour éviter de modifier l'original et pour ajouter un identifiant
current_payload = copy.deepcopy(payload)
current_payload['message'] += f" (Requête {request_index + 1})" # Ajoute un numéro à chaque message
current_payload['request_uuid'] = str(uuid.uuid4()) # Ajoute un id unique par requête
try:
response = requests.post(target_url, json=current_payload, timeout=30) # Timeout de 30s
response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx, 5xx)
return True, None # Succès
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)}" # Échec avec message d'erreur
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 = []
# Initialiser le statut (on le fait déjà dans /start mais re-vérifier est ok)
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})
# Mettre à jour la progression dans le dictionnaire partagé (avec verrou)
with jobs_lock:
jobs[job_id]['completed_count'] = completed_count
jobs[job_id]['error_count'] = error_count
# Gardons seulement les X dernières erreurs pour éviter de saturer la mémoire
jobs[job_id]['errors'] = error_messages[-50:] # Garde les 50 dernières erreurs
# Petite pause optionnelle pour ne pas submerger la cible (ex: 0.1 seconde)
# time.sleep(0.1)
# Marquer la tâche comme terminée
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.")
# --- Routes Flask ---
@app.route('/', methods=['GET'])
def index():
"""Affiche la page d'accueil avec le formulaire."""
# On passe une copie triée des jobs récents au template
with jobs_lock:
# Trie par exemple par clé (qui approxime l'ordre de création ici)
# ou ajoute un timestamp à la création du job pour trier par date.
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] # ID de tâche court et unique
# Initialiser l'état de la tâche avant de démarrer le thread
with jobs_lock:
jobs[job_id] = {
'status': 'starting', # ou 'queued'
'total': num_requests,
'completed_count': 0,
'error_count': 0,
'errors': []
}
# Créer et démarrer le thread
thread = threading.Thread(target=background_task, args=(job_id, num_requests, TARGET_URL, BASE_PAYLOAD))
thread.daemon = True # Permet au programme principal de quitter même si des threads tournent
thread.start()
print(f"Nouvelle tâche démarrée avec ID: {job_id}")
# Rediriger vers la page de statut de cette tâche
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
# La page HTML utilisera l'API /api/status pour les mises à jour dynamiques
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:
# Renvoyer une copie pour éviter les modifs concurrentes pendant la sérialisation JSON
return jsonify(copy.deepcopy(job_info))
else:
# Renvoyer une réponse JSON même pour une erreur 404
return jsonify({"error": "Tâche non trouvée"}), 404
# --- Démarrage de l'application ---
if __name__ == '__main__':
# Utilise host='0.0.0.0' pour rendre accessible depuis d'autres machines sur le réseau
# Attention: debug=True ne doit PAS être utilisé en production
app.run(host='0.0.0.0', port=5000, debug=True) |