cpu-loader 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cpu_loader-0.1.2/src/cpu_loader.egg-info → cpu_loader-0.1.3}/PKG-INFO +1 -1
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/pyproject.toml +3 -3
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/setup.py +2 -2
- cpu_loader-0.1.3/src/cpu_loader/__init__.py +5 -0
- cpu_loader-0.1.3/src/cpu_loader/__main__.py +6 -0
- {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/cpu_loader.py +1 -1
- cpu_loader-0.1.3/src/cpu_loader/main.py +328 -0
- {cpu_loader-0.1.2 → cpu_loader-0.1.3/src/cpu_loader.egg-info}/PKG-INFO +1 -1
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/src/cpu_loader.egg-info/SOURCES.txt +7 -5
- cpu_loader-0.1.3/src/cpu_loader.egg-info/entry_points.txt +2 -0
- cpu_loader-0.1.3/src/cpu_loader.egg-info/top_level.txt +1 -0
- cpu_loader-0.1.2/src/cpu_loader.egg-info/entry_points.txt +0 -2
- cpu_loader-0.1.2/src/cpu_loader.egg-info/top_level.txt +0 -5
- cpu_loader-0.1.2/src/main.py +0 -777
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/LICENSE +0 -0
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/MANIFEST.in +0 -0
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/README.md +0 -0
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/setup.cfg +0 -0
- {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/cpu_loader_core.c +0 -0
- {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/example.py +0 -0
- {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/mqtt_publisher.py +0 -0
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/src/cpu_loader.egg-info/dependency_links.txt +0 -0
- {cpu_loader-0.1.2 → cpu_loader-0.1.3}/src/cpu_loader.egg-info/requires.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cpu-loader"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "A tool to generate CPU load on a system with runtime configuration through a WebUI and REST API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.8"
|
|
@@ -37,7 +37,7 @@ dependencies = [
|
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
[project.scripts]
|
|
40
|
-
cpu-loader = "
|
|
40
|
+
cpu-loader = "cpu_loader.__main__:run"
|
|
41
41
|
|
|
42
42
|
[project.urls]
|
|
43
43
|
Homepage = "https://github.com/the78mole/cpu-loader"
|
|
@@ -51,4 +51,4 @@ build-backend = "setuptools.build_meta"
|
|
|
51
51
|
|
|
52
52
|
[tool.setuptools]
|
|
53
53
|
package-dir = {"" = "src"}
|
|
54
|
-
|
|
54
|
+
packages = ["cpu_loader"]
|
|
@@ -9,8 +9,8 @@ if platform.system() != 'Windows':
|
|
|
9
9
|
extra_link_args = ['-pthread']
|
|
10
10
|
|
|
11
11
|
module = Extension(
|
|
12
|
-
'cpu_loader_core',
|
|
13
|
-
sources=['src/cpu_loader_core.c'],
|
|
12
|
+
'cpu_loader.cpu_loader_core',
|
|
13
|
+
sources=['src/cpu_loader/cpu_loader_core.c'],
|
|
14
14
|
extra_compile_args=extra_compile_args,
|
|
15
15
|
extra_link_args=extra_link_args,
|
|
16
16
|
)
|
|
@@ -7,7 +7,7 @@ import multiprocessing
|
|
|
7
7
|
from typing import Dict
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
|
-
import cpu_loader_core
|
|
10
|
+
from cpu_loader import cpu_loader_core # type: ignore[attr-defined]
|
|
11
11
|
except ImportError:
|
|
12
12
|
raise ImportError(
|
|
13
13
|
"cpu_loader_core module not found. Please build the C extension with: "
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CPU Loader FastAPI Application
|
|
3
|
+
Provides REST API and WebUI for controlling CPU load.
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from typing import Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
import psutil
|
|
12
|
+
import uvicorn
|
|
13
|
+
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
|
14
|
+
from fastapi.responses import HTMLResponse
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from cpu_loader.cpu_loader import CPULoader
|
|
18
|
+
from cpu_loader.mqtt_publisher import MQTTPublisher
|
|
19
|
+
|
|
20
|
+
# Configure logging
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
23
|
+
)
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Pydantic models for request/response validation
|
|
28
|
+
class ThreadLoadRequest(BaseModel):
|
|
29
|
+
load_percent: float = Field(
|
|
30
|
+
..., ge=0, le=100, description="Load percentage (0-100)"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AllThreadsLoadRequest(BaseModel):
|
|
35
|
+
load_percent: float = Field(
|
|
36
|
+
..., ge=0, le=100, description="Load percentage (0-100)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ThreadCountRequest(BaseModel):
|
|
41
|
+
num_threads: int = Field(
|
|
42
|
+
..., gt=0, description="Number of threads (must be positive)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ThreadsStatusResponse(BaseModel):
|
|
47
|
+
num_threads: int
|
|
48
|
+
loads: Dict[int, float]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CPUMetricsResponse(BaseModel):
|
|
52
|
+
total_cpu_percent: float
|
|
53
|
+
per_cpu_percent: List[float]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Global CPU loader instance, MQTT publisher, and WebSocket connections
|
|
57
|
+
cpu_loader = None
|
|
58
|
+
mqtt_publisher: Optional[MQTTPublisher] = None
|
|
59
|
+
websocket_connections: Set[WebSocket] = set()
|
|
60
|
+
monitoring_task = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def cpu_monitoring_loop():
|
|
64
|
+
"""Background task that monitors CPU usage and broadcasts to all WebSocket clients."""
|
|
65
|
+
# Initialize psutil
|
|
66
|
+
psutil.cpu_percent(interval=None, percpu=True)
|
|
67
|
+
|
|
68
|
+
while True:
|
|
69
|
+
try:
|
|
70
|
+
# Wait for 1 second
|
|
71
|
+
await asyncio.sleep(1.0)
|
|
72
|
+
|
|
73
|
+
# Get CPU metrics (non-blocking after first call)
|
|
74
|
+
per_cpu = psutil.cpu_percent(interval=None, percpu=True)
|
|
75
|
+
total_cpu = sum(per_cpu) / len(per_cpu) if per_cpu else 0.0
|
|
76
|
+
|
|
77
|
+
# Prepare message
|
|
78
|
+
message = {
|
|
79
|
+
"type": "cpu_metrics",
|
|
80
|
+
"total_cpu_percent": round(total_cpu, 1),
|
|
81
|
+
"per_cpu_percent": [round(cpu, 1) for cpu in per_cpu],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Broadcast to all connected clients
|
|
85
|
+
if websocket_connections:
|
|
86
|
+
disconnected = set()
|
|
87
|
+
for websocket in websocket_connections:
|
|
88
|
+
try:
|
|
89
|
+
await websocket.send_json(message)
|
|
90
|
+
except Exception:
|
|
91
|
+
disconnected.add(websocket)
|
|
92
|
+
|
|
93
|
+
# Remove disconnected clients
|
|
94
|
+
websocket_connections.difference_update(disconnected)
|
|
95
|
+
|
|
96
|
+
# Publish to MQTT if enabled
|
|
97
|
+
if mqtt_publisher:
|
|
98
|
+
mqtt_publisher.publish_cpu_metrics(total_cpu, per_cpu)
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error in CPU monitoring loop: {e}")
|
|
102
|
+
await asyncio.sleep(1.0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@asynccontextmanager
|
|
106
|
+
async def lifespan(app: FastAPI):
|
|
107
|
+
"""Lifespan context manager for startup and shutdown events."""
|
|
108
|
+
global cpu_loader, mqtt_publisher, monitoring_task
|
|
109
|
+
# Startup
|
|
110
|
+
cpu_loader = CPULoader()
|
|
111
|
+
|
|
112
|
+
# Initialize MQTT publisher with settings from arguments or environment
|
|
113
|
+
mqtt_args = getattr(app.state, "mqtt_args", {})
|
|
114
|
+
try:
|
|
115
|
+
mqtt_publisher = MQTTPublisher(**mqtt_args)
|
|
116
|
+
except ImportError:
|
|
117
|
+
logger.warning("MQTT publishing disabled: paho-mqtt not installed")
|
|
118
|
+
mqtt_publisher = None
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Failed to initialize MQTT publisher: {e}")
|
|
121
|
+
mqtt_publisher = None
|
|
122
|
+
|
|
123
|
+
monitoring_task = asyncio.create_task(cpu_monitoring_loop())
|
|
124
|
+
yield
|
|
125
|
+
# Shutdown
|
|
126
|
+
if monitoring_task:
|
|
127
|
+
monitoring_task.cancel()
|
|
128
|
+
try:
|
|
129
|
+
await monitoring_task
|
|
130
|
+
except asyncio.CancelledError:
|
|
131
|
+
pass
|
|
132
|
+
if mqtt_publisher:
|
|
133
|
+
mqtt_publisher.disconnect()
|
|
134
|
+
cpu_loader.shutdown()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Initialize FastAPI app
|
|
138
|
+
app = FastAPI(
|
|
139
|
+
title="CPU Loader API",
|
|
140
|
+
description="Control CPU load generation with configurable threads",
|
|
141
|
+
version="1.0.0",
|
|
142
|
+
lifespan=lifespan,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.get("/", response_class=HTMLResponse)
|
|
147
|
+
async def get_webui():
|
|
148
|
+
"""Serve the WebUI."""
|
|
149
|
+
from pathlib import Path
|
|
150
|
+
|
|
151
|
+
template_path = Path(__file__).parent / "templates" / "index.html"
|
|
152
|
+
html_content = template_path.read_text()
|
|
153
|
+
return HTMLResponse(content=html_content)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.get("/api/threads", response_model=ThreadsStatusResponse)
|
|
157
|
+
async def get_threads_status():
|
|
158
|
+
"""Get the current status of all threads."""
|
|
159
|
+
return ThreadsStatusResponse(
|
|
160
|
+
num_threads=cpu_loader.get_num_threads(), loads=cpu_loader.get_all_loads()
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.websocket("/ws/cpu-metrics")
|
|
165
|
+
async def websocket_cpu_metrics(websocket: WebSocket):
|
|
166
|
+
"""WebSocket endpoint for real-time CPU metrics."""
|
|
167
|
+
await websocket.accept()
|
|
168
|
+
websocket_connections.add(websocket)
|
|
169
|
+
try:
|
|
170
|
+
# Keep connection alive
|
|
171
|
+
while True:
|
|
172
|
+
# Wait for any message (ping/pong)
|
|
173
|
+
await websocket.receive_text()
|
|
174
|
+
except WebSocketDisconnect:
|
|
175
|
+
websocket_connections.discard(websocket)
|
|
176
|
+
except Exception:
|
|
177
|
+
websocket_connections.discard(websocket)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.get("/api/cpu-metrics", response_model=CPUMetricsResponse)
|
|
181
|
+
async def get_cpu_metrics():
|
|
182
|
+
"""Get current CPU utilization metrics."""
|
|
183
|
+
per_cpu = psutil.cpu_percent(interval=0.1, percpu=True)
|
|
184
|
+
total_cpu = psutil.cpu_percent(interval=0)
|
|
185
|
+
return CPUMetricsResponse(total_cpu_percent=total_cpu, per_cpu_percent=per_cpu)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@app.post("/api/threads")
|
|
189
|
+
async def set_thread_count(request: ThreadCountRequest):
|
|
190
|
+
"""Set the number of threads."""
|
|
191
|
+
try:
|
|
192
|
+
cpu_loader.set_num_threads(request.num_threads)
|
|
193
|
+
|
|
194
|
+
# Publish updated settings to MQTT
|
|
195
|
+
if mqtt_publisher:
|
|
196
|
+
mqtt_publisher.publish_load_settings(
|
|
197
|
+
cpu_loader.get_num_threads(), cpu_loader.get_all_loads()
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"status": "success",
|
|
202
|
+
"num_threads": request.num_threads,
|
|
203
|
+
"message": f"Thread count set to {request.num_threads}",
|
|
204
|
+
}
|
|
205
|
+
except ValueError as e:
|
|
206
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.put("/api/threads/{thread_id}/load")
|
|
210
|
+
async def set_thread_load(thread_id: int, request: ThreadLoadRequest):
|
|
211
|
+
"""Set the CPU load for a specific thread."""
|
|
212
|
+
try:
|
|
213
|
+
cpu_loader.set_thread_load(thread_id, request.load_percent)
|
|
214
|
+
|
|
215
|
+
# Publish updated settings to MQTT
|
|
216
|
+
if mqtt_publisher:
|
|
217
|
+
mqtt_publisher.publish_load_settings(
|
|
218
|
+
cpu_loader.get_num_threads(), cpu_loader.get_all_loads()
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"status": "success",
|
|
223
|
+
"thread_id": thread_id,
|
|
224
|
+
"load_percent": request.load_percent,
|
|
225
|
+
"message": f"Thread {thread_id} load set to {request.load_percent}%",
|
|
226
|
+
}
|
|
227
|
+
except ValueError as e:
|
|
228
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@app.post("/api/threads/load/all")
|
|
232
|
+
async def set_all_thread_loads(request: AllThreadsLoadRequest):
|
|
233
|
+
"""Set the same CPU load for all threads."""
|
|
234
|
+
try:
|
|
235
|
+
cpu_loader.set_all_loads(request.load_percent)
|
|
236
|
+
|
|
237
|
+
# Publish updated settings to MQTT
|
|
238
|
+
if mqtt_publisher:
|
|
239
|
+
mqtt_publisher.publish_load_settings(
|
|
240
|
+
cpu_loader.get_num_threads(), cpu_loader.get_all_loads()
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"status": "success",
|
|
245
|
+
"load_percent": request.load_percent,
|
|
246
|
+
"num_threads": cpu_loader.get_num_threads(),
|
|
247
|
+
"message": f"All threads set to {request.load_percent}%",
|
|
248
|
+
}
|
|
249
|
+
except ValueError as e:
|
|
250
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def parse_args():
|
|
254
|
+
"""Parse command-line arguments."""
|
|
255
|
+
parser = argparse.ArgumentParser(
|
|
256
|
+
description="CPU Loader - Generate controllable CPU load with REST API and WebUI"
|
|
257
|
+
)
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
"--host",
|
|
260
|
+
default="0.0.0.0",
|
|
261
|
+
help="Host to bind the server to (default: 0.0.0.0)",
|
|
262
|
+
)
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"--port",
|
|
265
|
+
type=int,
|
|
266
|
+
default=8000,
|
|
267
|
+
help="Port to bind the server to (default: 8000)",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# MQTT arguments
|
|
271
|
+
mqtt_group = parser.add_argument_group("MQTT settings")
|
|
272
|
+
mqtt_group.add_argument(
|
|
273
|
+
"--mqtt-broker-host",
|
|
274
|
+
help="MQTT broker hostname (env: MQTT_BROKER_HOST)",
|
|
275
|
+
)
|
|
276
|
+
mqtt_group.add_argument(
|
|
277
|
+
"--mqtt-broker-port",
|
|
278
|
+
type=int,
|
|
279
|
+
help="MQTT broker port (env: MQTT_BROKER_PORT, default: 1883)",
|
|
280
|
+
)
|
|
281
|
+
mqtt_group.add_argument(
|
|
282
|
+
"--mqtt-username",
|
|
283
|
+
help="MQTT username (env: MQTT_USERNAME)",
|
|
284
|
+
)
|
|
285
|
+
mqtt_group.add_argument(
|
|
286
|
+
"--mqtt-password",
|
|
287
|
+
help="MQTT password (env: MQTT_PASSWORD)",
|
|
288
|
+
)
|
|
289
|
+
mqtt_group.add_argument(
|
|
290
|
+
"--mqtt-topic-prefix",
|
|
291
|
+
help="MQTT topic prefix (env: MQTT_TOPIC_PREFIX, default: cpu-loader)",
|
|
292
|
+
)
|
|
293
|
+
mqtt_group.add_argument(
|
|
294
|
+
"--mqtt-client-id",
|
|
295
|
+
help="MQTT client ID (env: MQTT_CLIENT_ID, default: cpu-loader)",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return parser.parse_args()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def run():
|
|
302
|
+
"""Entry point for the CPU Loader application."""
|
|
303
|
+
args = parse_args()
|
|
304
|
+
|
|
305
|
+
# Prepare MQTT arguments (only non-None values)
|
|
306
|
+
mqtt_args = {}
|
|
307
|
+
if args.mqtt_broker_host:
|
|
308
|
+
mqtt_args["broker_host"] = args.mqtt_broker_host
|
|
309
|
+
if args.mqtt_broker_port:
|
|
310
|
+
mqtt_args["broker_port"] = args.mqtt_broker_port
|
|
311
|
+
if args.mqtt_username:
|
|
312
|
+
mqtt_args["username"] = args.mqtt_username
|
|
313
|
+
if args.mqtt_password:
|
|
314
|
+
mqtt_args["password"] = args.mqtt_password
|
|
315
|
+
if args.mqtt_topic_prefix:
|
|
316
|
+
mqtt_args["topic_prefix"] = args.mqtt_topic_prefix
|
|
317
|
+
if args.mqtt_client_id:
|
|
318
|
+
mqtt_args["client_id"] = args.mqtt_client_id
|
|
319
|
+
|
|
320
|
+
# Store MQTT args in app state for lifespan to access
|
|
321
|
+
app.state.mqtt_args = mqtt_args
|
|
322
|
+
|
|
323
|
+
# Run the server
|
|
324
|
+
uvicorn.run(app, host=args.host, port=args.port)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
run()
|
|
@@ -3,11 +3,13 @@ MANIFEST.in
|
|
|
3
3
|
README.md
|
|
4
4
|
pyproject.toml
|
|
5
5
|
setup.py
|
|
6
|
-
src/cpu_loader.py
|
|
7
|
-
src/
|
|
8
|
-
src/
|
|
9
|
-
src/
|
|
10
|
-
src/
|
|
6
|
+
src/cpu_loader/__init__.py
|
|
7
|
+
src/cpu_loader/__main__.py
|
|
8
|
+
src/cpu_loader/cpu_loader.py
|
|
9
|
+
src/cpu_loader/cpu_loader_core.c
|
|
10
|
+
src/cpu_loader/example.py
|
|
11
|
+
src/cpu_loader/main.py
|
|
12
|
+
src/cpu_loader/mqtt_publisher.py
|
|
11
13
|
src/cpu_loader.egg-info/PKG-INFO
|
|
12
14
|
src/cpu_loader.egg-info/SOURCES.txt
|
|
13
15
|
src/cpu_loader.egg-info/dependency_links.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cpu_loader
|
cpu_loader-0.1.2/src/main.py
DELETED
|
@@ -1,777 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
CPU Loader FastAPI Application
|
|
3
|
-
Provides REST API and WebUI for controlling CPU load.
|
|
4
|
-
"""
|
|
5
|
-
import argparse
|
|
6
|
-
import asyncio
|
|
7
|
-
import logging
|
|
8
|
-
from contextlib import asynccontextmanager
|
|
9
|
-
from typing import Dict, List, Optional, Set
|
|
10
|
-
|
|
11
|
-
import psutil
|
|
12
|
-
import uvicorn
|
|
13
|
-
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
|
14
|
-
from fastapi.responses import HTMLResponse
|
|
15
|
-
from pydantic import BaseModel, Field
|
|
16
|
-
|
|
17
|
-
from cpu_loader import CPULoader
|
|
18
|
-
from mqtt_publisher import MQTTPublisher
|
|
19
|
-
|
|
20
|
-
# Configure logging
|
|
21
|
-
logging.basicConfig(
|
|
22
|
-
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
23
|
-
)
|
|
24
|
-
logger = logging.getLogger(__name__)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# Pydantic models for request/response validation
|
|
28
|
-
class ThreadLoadRequest(BaseModel):
|
|
29
|
-
load_percent: float = Field(
|
|
30
|
-
..., ge=0, le=100, description="Load percentage (0-100)"
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class AllThreadsLoadRequest(BaseModel):
|
|
35
|
-
load_percent: float = Field(
|
|
36
|
-
..., ge=0, le=100, description="Load percentage (0-100)"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class ThreadCountRequest(BaseModel):
|
|
41
|
-
num_threads: int = Field(
|
|
42
|
-
..., gt=0, description="Number of threads (must be positive)"
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class ThreadsStatusResponse(BaseModel):
|
|
47
|
-
num_threads: int
|
|
48
|
-
loads: Dict[int, float]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class CPUMetricsResponse(BaseModel):
|
|
52
|
-
total_cpu_percent: float
|
|
53
|
-
per_cpu_percent: List[float]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# Global CPU loader instance, MQTT publisher, and WebSocket connections
|
|
57
|
-
cpu_loader = None
|
|
58
|
-
mqtt_publisher: Optional[MQTTPublisher] = None
|
|
59
|
-
websocket_connections: Set[WebSocket] = set()
|
|
60
|
-
monitoring_task = None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
async def cpu_monitoring_loop():
|
|
64
|
-
"""Background task that monitors CPU usage and broadcasts to all WebSocket clients."""
|
|
65
|
-
# Initialize psutil
|
|
66
|
-
psutil.cpu_percent(interval=None, percpu=True)
|
|
67
|
-
|
|
68
|
-
while True:
|
|
69
|
-
try:
|
|
70
|
-
# Wait for 1 second
|
|
71
|
-
await asyncio.sleep(1.0)
|
|
72
|
-
|
|
73
|
-
# Get CPU metrics (non-blocking after first call)
|
|
74
|
-
per_cpu = psutil.cpu_percent(interval=None, percpu=True)
|
|
75
|
-
total_cpu = sum(per_cpu) / len(per_cpu) if per_cpu else 0.0
|
|
76
|
-
|
|
77
|
-
# Prepare message
|
|
78
|
-
message = {
|
|
79
|
-
"type": "cpu_metrics",
|
|
80
|
-
"total_cpu_percent": round(total_cpu, 1),
|
|
81
|
-
"per_cpu_percent": [round(cpu, 1) for cpu in per_cpu],
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Broadcast to all connected clients
|
|
85
|
-
if websocket_connections:
|
|
86
|
-
disconnected = set()
|
|
87
|
-
for websocket in websocket_connections:
|
|
88
|
-
try:
|
|
89
|
-
await websocket.send_json(message)
|
|
90
|
-
except Exception:
|
|
91
|
-
disconnected.add(websocket)
|
|
92
|
-
|
|
93
|
-
# Remove disconnected clients
|
|
94
|
-
websocket_connections.difference_update(disconnected)
|
|
95
|
-
|
|
96
|
-
# Publish to MQTT if enabled
|
|
97
|
-
if mqtt_publisher:
|
|
98
|
-
mqtt_publisher.publish_cpu_metrics(total_cpu, per_cpu)
|
|
99
|
-
|
|
100
|
-
except Exception as e:
|
|
101
|
-
logger.error(f"Error in CPU monitoring loop: {e}")
|
|
102
|
-
await asyncio.sleep(1.0)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@asynccontextmanager
|
|
106
|
-
async def lifespan(app: FastAPI):
|
|
107
|
-
"""Lifespan context manager for startup and shutdown events."""
|
|
108
|
-
global cpu_loader, mqtt_publisher, monitoring_task
|
|
109
|
-
# Startup
|
|
110
|
-
cpu_loader = CPULoader()
|
|
111
|
-
|
|
112
|
-
# Initialize MQTT publisher with settings from arguments or environment
|
|
113
|
-
mqtt_args = getattr(app.state, "mqtt_args", {})
|
|
114
|
-
try:
|
|
115
|
-
mqtt_publisher = MQTTPublisher(**mqtt_args)
|
|
116
|
-
except ImportError:
|
|
117
|
-
logger.warning("MQTT publishing disabled: paho-mqtt not installed")
|
|
118
|
-
mqtt_publisher = None
|
|
119
|
-
except Exception as e:
|
|
120
|
-
logger.error(f"Failed to initialize MQTT publisher: {e}")
|
|
121
|
-
mqtt_publisher = None
|
|
122
|
-
|
|
123
|
-
monitoring_task = asyncio.create_task(cpu_monitoring_loop())
|
|
124
|
-
yield
|
|
125
|
-
# Shutdown
|
|
126
|
-
if monitoring_task:
|
|
127
|
-
monitoring_task.cancel()
|
|
128
|
-
try:
|
|
129
|
-
await monitoring_task
|
|
130
|
-
except asyncio.CancelledError:
|
|
131
|
-
pass
|
|
132
|
-
if mqtt_publisher:
|
|
133
|
-
mqtt_publisher.disconnect()
|
|
134
|
-
cpu_loader.shutdown()
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# Initialize FastAPI app
|
|
138
|
-
app = FastAPI(
|
|
139
|
-
title="CPU Loader API",
|
|
140
|
-
description="Control CPU load generation with configurable threads",
|
|
141
|
-
version="1.0.0",
|
|
142
|
-
lifespan=lifespan,
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@app.get("/", response_class=HTMLResponse)
|
|
147
|
-
async def get_webui():
|
|
148
|
-
"""Serve the WebUI."""
|
|
149
|
-
html_content = """
|
|
150
|
-
<!DOCTYPE html>
|
|
151
|
-
<html lang="en">
|
|
152
|
-
<head>
|
|
153
|
-
<meta charset="UTF-8">
|
|
154
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
155
|
-
<title>CPU Loader Control</title>
|
|
156
|
-
<style>
|
|
157
|
-
body {
|
|
158
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
159
|
-
max-width: 900px;
|
|
160
|
-
margin: 50px auto;
|
|
161
|
-
padding: 20px;
|
|
162
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
163
|
-
min-height: 100vh;
|
|
164
|
-
}
|
|
165
|
-
.container {
|
|
166
|
-
background: white;
|
|
167
|
-
border-radius: 15px;
|
|
168
|
-
padding: 30px;
|
|
169
|
-
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
170
|
-
}
|
|
171
|
-
h1 {
|
|
172
|
-
color: #333;
|
|
173
|
-
text-align: center;
|
|
174
|
-
margin-bottom: 10px;
|
|
175
|
-
}
|
|
176
|
-
.subtitle {
|
|
177
|
-
text-align: center;
|
|
178
|
-
color: #666;
|
|
179
|
-
margin-bottom: 30px;
|
|
180
|
-
}
|
|
181
|
-
.status {
|
|
182
|
-
background: #f0f0f0;
|
|
183
|
-
padding: 15px;
|
|
184
|
-
border-radius: 8px;
|
|
185
|
-
margin-bottom: 25px;
|
|
186
|
-
text-align: center;
|
|
187
|
-
}
|
|
188
|
-
.status-item {
|
|
189
|
-
display: inline-block;
|
|
190
|
-
margin: 0 15px;
|
|
191
|
-
}
|
|
192
|
-
.cpu-metrics {
|
|
193
|
-
background: #f8f9fa;
|
|
194
|
-
padding: 20px;
|
|
195
|
-
border-radius: 10px;
|
|
196
|
-
margin-bottom: 25px;
|
|
197
|
-
}
|
|
198
|
-
.cpu-metrics h3 {
|
|
199
|
-
margin: 0 0 15px 0;
|
|
200
|
-
color: #333;
|
|
201
|
-
font-size: 16px;
|
|
202
|
-
}
|
|
203
|
-
.cpu-bar-container {
|
|
204
|
-
margin-bottom: 12px;
|
|
205
|
-
}
|
|
206
|
-
.cpu-bar-label {
|
|
207
|
-
display: flex;
|
|
208
|
-
justify-content: space-between;
|
|
209
|
-
margin-bottom: 5px;
|
|
210
|
-
font-size: 13px;
|
|
211
|
-
color: #666;
|
|
212
|
-
}
|
|
213
|
-
.cpu-bar-wrapper {
|
|
214
|
-
background: #e0e0e0;
|
|
215
|
-
border-radius: 8px;
|
|
216
|
-
height: 20px;
|
|
217
|
-
overflow: hidden;
|
|
218
|
-
position: relative;
|
|
219
|
-
}
|
|
220
|
-
.cpu-bar {
|
|
221
|
-
background: linear-gradient(90deg, #4CAF50 0%, #FFC107 50%, #F44336 100%);
|
|
222
|
-
height: 100%;
|
|
223
|
-
transition: width 0.3s ease;
|
|
224
|
-
border-radius: 8px;
|
|
225
|
-
}
|
|
226
|
-
.cpu-bar.total {
|
|
227
|
-
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
228
|
-
}
|
|
229
|
-
.preset-buttons {
|
|
230
|
-
display: flex;
|
|
231
|
-
gap: 10px;
|
|
232
|
-
margin-bottom: 30px;
|
|
233
|
-
flex-wrap: wrap;
|
|
234
|
-
justify-content: center;
|
|
235
|
-
}
|
|
236
|
-
.preset-btn {
|
|
237
|
-
background: #667eea;
|
|
238
|
-
color: white;
|
|
239
|
-
border: none;
|
|
240
|
-
padding: 12px 20px;
|
|
241
|
-
border-radius: 8px;
|
|
242
|
-
cursor: pointer;
|
|
243
|
-
font-size: 14px;
|
|
244
|
-
font-weight: 600;
|
|
245
|
-
transition: all 0.3s;
|
|
246
|
-
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
247
|
-
}
|
|
248
|
-
.preset-btn:hover {
|
|
249
|
-
background: #5568d3;
|
|
250
|
-
transform: translateY(-2px);
|
|
251
|
-
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
|
252
|
-
}
|
|
253
|
-
.preset-btn:active {
|
|
254
|
-
transform: translateY(0);
|
|
255
|
-
}
|
|
256
|
-
.thread-controls {
|
|
257
|
-
display: grid;
|
|
258
|
-
gap: 20px;
|
|
259
|
-
}
|
|
260
|
-
.thread-control {
|
|
261
|
-
background: #f8f9fa;
|
|
262
|
-
padding: 20px;
|
|
263
|
-
border-radius: 10px;
|
|
264
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
265
|
-
}
|
|
266
|
-
.thread-header {
|
|
267
|
-
display: flex;
|
|
268
|
-
justify-content: space-between;
|
|
269
|
-
align-items: center;
|
|
270
|
-
margin-bottom: 12px;
|
|
271
|
-
}
|
|
272
|
-
.thread-label {
|
|
273
|
-
font-weight: 600;
|
|
274
|
-
color: #333;
|
|
275
|
-
font-size: 16px;
|
|
276
|
-
}
|
|
277
|
-
.thread-value {
|
|
278
|
-
background: #667eea;
|
|
279
|
-
color: white;
|
|
280
|
-
padding: 4px 12px;
|
|
281
|
-
border-radius: 15px;
|
|
282
|
-
font-weight: 600;
|
|
283
|
-
min-width: 60px;
|
|
284
|
-
text-align: center;
|
|
285
|
-
}
|
|
286
|
-
.slider-container {
|
|
287
|
-
position: relative;
|
|
288
|
-
}
|
|
289
|
-
.slider {
|
|
290
|
-
width: 100%;
|
|
291
|
-
height: 8px;
|
|
292
|
-
border-radius: 5px;
|
|
293
|
-
background: #ddd;
|
|
294
|
-
outline: none;
|
|
295
|
-
-webkit-appearance: none;
|
|
296
|
-
}
|
|
297
|
-
.slider::-webkit-slider-thumb {
|
|
298
|
-
-webkit-appearance: none;
|
|
299
|
-
appearance: none;
|
|
300
|
-
width: 24px;
|
|
301
|
-
height: 24px;
|
|
302
|
-
border-radius: 50%;
|
|
303
|
-
background: #667eea;
|
|
304
|
-
cursor: pointer;
|
|
305
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
306
|
-
}
|
|
307
|
-
.slider::-moz-range-thumb {
|
|
308
|
-
width: 24px;
|
|
309
|
-
height: 24px;
|
|
310
|
-
border-radius: 50%;
|
|
311
|
-
background: #667eea;
|
|
312
|
-
cursor: pointer;
|
|
313
|
-
border: none;
|
|
314
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
315
|
-
}
|
|
316
|
-
.slider-labels {
|
|
317
|
-
display: flex;
|
|
318
|
-
justify-content: space-between;
|
|
319
|
-
margin-top: 5px;
|
|
320
|
-
font-size: 12px;
|
|
321
|
-
color: #666;
|
|
322
|
-
}
|
|
323
|
-
.error {
|
|
324
|
-
background: #fee;
|
|
325
|
-
color: #c33;
|
|
326
|
-
padding: 10px;
|
|
327
|
-
border-radius: 5px;
|
|
328
|
-
margin-bottom: 20px;
|
|
329
|
-
display: none;
|
|
330
|
-
}
|
|
331
|
-
.success {
|
|
332
|
-
background: #efe;
|
|
333
|
-
color: #3c3;
|
|
334
|
-
padding: 10px;
|
|
335
|
-
border-radius: 5px;
|
|
336
|
-
margin-bottom: 20px;
|
|
337
|
-
display: none;
|
|
338
|
-
}
|
|
339
|
-
</style>
|
|
340
|
-
</head>
|
|
341
|
-
<body>
|
|
342
|
-
<div class="container">
|
|
343
|
-
<h1>🔥 CPU Loader Control</h1>
|
|
344
|
-
<p class="subtitle">Control CPU load per thread in real-time</p>
|
|
345
|
-
|
|
346
|
-
<div class="status">
|
|
347
|
-
<div class="status-item">
|
|
348
|
-
<strong>Active Threads:</strong> <span id="thread-count">-</span>
|
|
349
|
-
</div>
|
|
350
|
-
<div class="status-item">
|
|
351
|
-
<strong>Target Load:</strong> <span id="avg-load">-</span>%
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
|
|
355
|
-
<div class="cpu-metrics">
|
|
356
|
-
<h3>📊 Real-Time CPU Usage</h3>
|
|
357
|
-
<div class="cpu-bar-container">
|
|
358
|
-
<div class="cpu-bar-label">
|
|
359
|
-
<span><strong>Total CPU</strong></span>
|
|
360
|
-
<span id="total-cpu-value">-</span>
|
|
361
|
-
</div>
|
|
362
|
-
<div class="cpu-bar-wrapper">
|
|
363
|
-
<div class="cpu-bar total" id="total-cpu-bar" style="width: 0%"></div>
|
|
364
|
-
</div>
|
|
365
|
-
</div>
|
|
366
|
-
<div id="per-cpu-bars"></div>
|
|
367
|
-
</div>
|
|
368
|
-
|
|
369
|
-
<div class="error" id="error-message"></div>
|
|
370
|
-
<div class="success" id="success-message"></div>
|
|
371
|
-
|
|
372
|
-
<div class="preset-buttons">
|
|
373
|
-
<button class="preset-btn" onclick="setAllLoads(0)">0%</button>
|
|
374
|
-
<button class="preset-btn" onclick="setAllLoads(10)">10%</button>
|
|
375
|
-
<button class="preset-btn" onclick="setAllLoads(25)">25%</button>
|
|
376
|
-
<button class="preset-btn" onclick="setAllLoads(50)">50%</button>
|
|
377
|
-
<button class="preset-btn" onclick="setAllLoads(80)">80%</button>
|
|
378
|
-
<button class="preset-btn" onclick="setAllLoads(90)">90%</button>
|
|
379
|
-
<button class="preset-btn" onclick="setAllLoads(100)">100%</button>
|
|
380
|
-
</div>
|
|
381
|
-
|
|
382
|
-
<div id="thread-controls" class="thread-controls">
|
|
383
|
-
<!-- Thread controls will be dynamically inserted here -->
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
|
|
387
|
-
<script>
|
|
388
|
-
let numThreads = 0;
|
|
389
|
-
let updateTimeout = null;
|
|
390
|
-
let ws = null;
|
|
391
|
-
let numCPUs = 0;
|
|
392
|
-
|
|
393
|
-
function connectWebSocket() {
|
|
394
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
395
|
-
const wsUrl = `${protocol}//${window.location.host}/ws/cpu-metrics`;
|
|
396
|
-
|
|
397
|
-
ws = new WebSocket(wsUrl);
|
|
398
|
-
|
|
399
|
-
ws.onopen = () => {
|
|
400
|
-
console.log('WebSocket connected');
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
ws.onmessage = (event) => {
|
|
404
|
-
const data = JSON.parse(event.data);
|
|
405
|
-
if (data.type === 'cpu_metrics') {
|
|
406
|
-
updateCPUMetrics(data);
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
ws.onerror = (error) => {
|
|
411
|
-
console.error('WebSocket error:', error);
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
ws.onclose = () => {
|
|
415
|
-
console.log('WebSocket disconnected, reconnecting...');
|
|
416
|
-
setTimeout(connectWebSocket, 2000);
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function updateCPUMetrics(data) {
|
|
421
|
-
// Update total CPU
|
|
422
|
-
const totalCpu = data.total_cpu_percent;
|
|
423
|
-
document.getElementById('total-cpu-value').textContent = `${totalCpu}%`;
|
|
424
|
-
document.getElementById('total-cpu-bar').style.width = `${totalCpu}%`;
|
|
425
|
-
|
|
426
|
-
// Update per-CPU bars
|
|
427
|
-
const perCpuContainer = document.getElementById('per-cpu-bars');
|
|
428
|
-
if (data.per_cpu_percent.length !== numCPUs) {
|
|
429
|
-
numCPUs = data.per_cpu_percent.length;
|
|
430
|
-
// Recreate all CPU bars
|
|
431
|
-
perCpuContainer.innerHTML = '';
|
|
432
|
-
data.per_cpu_percent.forEach((cpu, index) => {
|
|
433
|
-
const barHtml = `
|
|
434
|
-
<div class="cpu-bar-container">
|
|
435
|
-
<div class="cpu-bar-label">
|
|
436
|
-
<span>CPU ${index}</span>
|
|
437
|
-
<span id="cpu-${index}-value">-</span>
|
|
438
|
-
</div>
|
|
439
|
-
<div class="cpu-bar-wrapper">
|
|
440
|
-
<div class="cpu-bar" id="cpu-${index}-bar" style="width: 0%"></div>
|
|
441
|
-
</div>
|
|
442
|
-
</div>
|
|
443
|
-
`;
|
|
444
|
-
perCpuContainer.insertAdjacentHTML('beforeend', barHtml);
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Update values
|
|
449
|
-
data.per_cpu_percent.forEach((cpu, index) => {
|
|
450
|
-
document.getElementById(`cpu-${index}-value`).textContent = `${cpu}%`;
|
|
451
|
-
document.getElementById(`cpu-${index}-bar`).style.width = `${cpu}%`;
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
async function fetchStatus() {
|
|
456
|
-
try {
|
|
457
|
-
const response = await fetch('/api/threads');
|
|
458
|
-
const data = await response.json();
|
|
459
|
-
|
|
460
|
-
numThreads = data.num_threads;
|
|
461
|
-
document.getElementById('thread-count').textContent = numThreads;
|
|
462
|
-
|
|
463
|
-
// Calculate average load
|
|
464
|
-
const loads = Object.values(data.loads);
|
|
465
|
-
const avgLoad = loads.length > 0
|
|
466
|
-
? (loads.reduce((a, b) => a + b, 0) / loads.length).toFixed(1)
|
|
467
|
-
: 0;
|
|
468
|
-
document.getElementById('avg-load').textContent = avgLoad;
|
|
469
|
-
|
|
470
|
-
// Update or create thread controls
|
|
471
|
-
updateThreadControls(data.loads);
|
|
472
|
-
} catch (error) {
|
|
473
|
-
showError('Failed to fetch status: ' + error.message);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function updateThreadControls(loads) {
|
|
478
|
-
const container = document.getElementById('thread-controls');
|
|
479
|
-
|
|
480
|
-
// If number of threads changed, recreate all controls
|
|
481
|
-
if (container.children.length !== Object.keys(loads).length) {
|
|
482
|
-
container.innerHTML = '';
|
|
483
|
-
|
|
484
|
-
for (let i = 0; i < Object.keys(loads).length; i++) {
|
|
485
|
-
const control = createThreadControl(i, loads[i]);
|
|
486
|
-
container.appendChild(control);
|
|
487
|
-
}
|
|
488
|
-
} else {
|
|
489
|
-
// Just update values
|
|
490
|
-
Object.entries(loads).forEach(([threadId, load]) => {
|
|
491
|
-
const slider = document.getElementById(`slider-${threadId}`);
|
|
492
|
-
const value = document.getElementById(`value-${threadId}`);
|
|
493
|
-
if (slider && value) {
|
|
494
|
-
slider.value = load;
|
|
495
|
-
value.textContent = load.toFixed(0) + '%';
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function createThreadControl(threadId, initialLoad) {
|
|
502
|
-
const div = document.createElement('div');
|
|
503
|
-
div.className = 'thread-control';
|
|
504
|
-
div.innerHTML = `
|
|
505
|
-
<div class="thread-header">
|
|
506
|
-
<span class="thread-label">Thread ${threadId}</span>
|
|
507
|
-
<span class="thread-value" id="value-${threadId}">
|
|
508
|
-
${initialLoad.toFixed(0)}%
|
|
509
|
-
</span>
|
|
510
|
-
</div>
|
|
511
|
-
<div class="slider-container">
|
|
512
|
-
<input type="range" min="0" max="100" value="${initialLoad}"
|
|
513
|
-
class="slider" id="slider-${threadId}"
|
|
514
|
-
oninput="updateThreadLoad(${threadId}, this.value)">
|
|
515
|
-
<div class="slider-labels">
|
|
516
|
-
<span>0%</span>
|
|
517
|
-
<span>25%</span>
|
|
518
|
-
<span>50%</span>
|
|
519
|
-
<span>75%</span>
|
|
520
|
-
<span>100%</span>
|
|
521
|
-
</div>
|
|
522
|
-
</div>
|
|
523
|
-
`;
|
|
524
|
-
return div;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async function updateThreadLoad(threadId, loadPercent) {
|
|
528
|
-
// Update UI immediately for responsiveness
|
|
529
|
-
document.getElementById(`value-${threadId}`).textContent = loadPercent + '%';
|
|
530
|
-
|
|
531
|
-
// Debounce API calls
|
|
532
|
-
if (updateTimeout) {
|
|
533
|
-
clearTimeout(updateTimeout);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
updateTimeout = setTimeout(async () => {
|
|
537
|
-
try {
|
|
538
|
-
const response = await fetch(`/api/threads/${threadId}/load`, {
|
|
539
|
-
method: 'PUT',
|
|
540
|
-
headers: {
|
|
541
|
-
'Content-Type': 'application/json',
|
|
542
|
-
},
|
|
543
|
-
body: JSON.stringify({ load_percent: parseFloat(loadPercent) })
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
if (!response.ok) {
|
|
547
|
-
throw new Error('Failed to update thread load');
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Refresh status to get accurate average
|
|
551
|
-
await fetchStatus();
|
|
552
|
-
} catch (error) {
|
|
553
|
-
showError('Failed to update thread ' + threadId + ': ' + error.message);
|
|
554
|
-
}
|
|
555
|
-
}, 100);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function setAllLoads(loadPercent) {
|
|
559
|
-
try {
|
|
560
|
-
const response = await fetch('/api/threads/load/all', {
|
|
561
|
-
method: 'POST',
|
|
562
|
-
headers: {
|
|
563
|
-
'Content-Type': 'application/json',
|
|
564
|
-
},
|
|
565
|
-
body: JSON.stringify({ load_percent: loadPercent })
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
if (!response.ok) {
|
|
569
|
-
throw new Error('Failed to set loads');
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
showSuccess(`All threads set to ${loadPercent}%`);
|
|
573
|
-
|
|
574
|
-
// Update UI
|
|
575
|
-
await fetchStatus();
|
|
576
|
-
} catch (error) {
|
|
577
|
-
showError('Failed to set all loads: ' + error.message);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function showError(message) {
|
|
582
|
-
const errorDiv = document.getElementById('error-message');
|
|
583
|
-
errorDiv.textContent = message;
|
|
584
|
-
errorDiv.style.display = 'block';
|
|
585
|
-
setTimeout(() => {
|
|
586
|
-
errorDiv.style.display = 'none';
|
|
587
|
-
}, 5000);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function showSuccess(message) {
|
|
591
|
-
const successDiv = document.getElementById('success-message');
|
|
592
|
-
successDiv.textContent = message;
|
|
593
|
-
successDiv.style.display = 'block';
|
|
594
|
-
setTimeout(() => {
|
|
595
|
-
successDiv.style.display = 'none';
|
|
596
|
-
}, 3000);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Initial load and periodic refresh
|
|
600
|
-
fetchStatus();
|
|
601
|
-
connectWebSocket();
|
|
602
|
-
setInterval(fetchStatus, 5000);
|
|
603
|
-
</script>
|
|
604
|
-
</body>
|
|
605
|
-
</html>
|
|
606
|
-
"""
|
|
607
|
-
return HTMLResponse(content=html_content)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
@app.get("/api/threads", response_model=ThreadsStatusResponse)
|
|
611
|
-
async def get_threads_status():
|
|
612
|
-
"""Get the current status of all threads."""
|
|
613
|
-
return ThreadsStatusResponse(
|
|
614
|
-
num_threads=cpu_loader.get_num_threads(), loads=cpu_loader.get_all_loads()
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
@app.websocket("/ws/cpu-metrics")
|
|
619
|
-
async def websocket_cpu_metrics(websocket: WebSocket):
|
|
620
|
-
"""WebSocket endpoint for real-time CPU metrics."""
|
|
621
|
-
await websocket.accept()
|
|
622
|
-
websocket_connections.add(websocket)
|
|
623
|
-
try:
|
|
624
|
-
# Keep connection alive
|
|
625
|
-
while True:
|
|
626
|
-
# Wait for any message (ping/pong)
|
|
627
|
-
await websocket.receive_text()
|
|
628
|
-
except WebSocketDisconnect:
|
|
629
|
-
websocket_connections.discard(websocket)
|
|
630
|
-
except Exception:
|
|
631
|
-
websocket_connections.discard(websocket)
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
@app.get("/api/cpu-metrics", response_model=CPUMetricsResponse)
|
|
635
|
-
async def get_cpu_metrics():
|
|
636
|
-
"""Get current CPU utilization metrics."""
|
|
637
|
-
per_cpu = psutil.cpu_percent(interval=0.1, percpu=True)
|
|
638
|
-
total_cpu = psutil.cpu_percent(interval=0)
|
|
639
|
-
return CPUMetricsResponse(total_cpu_percent=total_cpu, per_cpu_percent=per_cpu)
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
@app.post("/api/threads")
|
|
643
|
-
async def set_thread_count(request: ThreadCountRequest):
|
|
644
|
-
"""Set the number of threads."""
|
|
645
|
-
try:
|
|
646
|
-
cpu_loader.set_num_threads(request.num_threads)
|
|
647
|
-
|
|
648
|
-
# Publish updated settings to MQTT
|
|
649
|
-
if mqtt_publisher:
|
|
650
|
-
mqtt_publisher.publish_load_settings(
|
|
651
|
-
cpu_loader.get_num_threads(), cpu_loader.get_all_loads()
|
|
652
|
-
)
|
|
653
|
-
|
|
654
|
-
return {
|
|
655
|
-
"status": "success",
|
|
656
|
-
"num_threads": request.num_threads,
|
|
657
|
-
"message": f"Thread count set to {request.num_threads}",
|
|
658
|
-
}
|
|
659
|
-
except ValueError as e:
|
|
660
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
@app.put("/api/threads/{thread_id}/load")
|
|
664
|
-
async def set_thread_load(thread_id: int, request: ThreadLoadRequest):
|
|
665
|
-
"""Set the CPU load for a specific thread."""
|
|
666
|
-
try:
|
|
667
|
-
cpu_loader.set_thread_load(thread_id, request.load_percent)
|
|
668
|
-
|
|
669
|
-
# Publish updated settings to MQTT
|
|
670
|
-
if mqtt_publisher:
|
|
671
|
-
mqtt_publisher.publish_load_settings(
|
|
672
|
-
cpu_loader.get_num_threads(), cpu_loader.get_all_loads()
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
return {
|
|
676
|
-
"status": "success",
|
|
677
|
-
"thread_id": thread_id,
|
|
678
|
-
"load_percent": request.load_percent,
|
|
679
|
-
"message": f"Thread {thread_id} load set to {request.load_percent}%",
|
|
680
|
-
}
|
|
681
|
-
except ValueError as e:
|
|
682
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
@app.post("/api/threads/load/all")
|
|
686
|
-
async def set_all_thread_loads(request: AllThreadsLoadRequest):
|
|
687
|
-
"""Set the same CPU load for all threads."""
|
|
688
|
-
try:
|
|
689
|
-
cpu_loader.set_all_loads(request.load_percent)
|
|
690
|
-
|
|
691
|
-
# Publish updated settings to MQTT
|
|
692
|
-
if mqtt_publisher:
|
|
693
|
-
mqtt_publisher.publish_load_settings(
|
|
694
|
-
cpu_loader.get_num_threads(), cpu_loader.get_all_loads()
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
return {
|
|
698
|
-
"status": "success",
|
|
699
|
-
"load_percent": request.load_percent,
|
|
700
|
-
"num_threads": cpu_loader.get_num_threads(),
|
|
701
|
-
"message": f"All threads set to {request.load_percent}%",
|
|
702
|
-
}
|
|
703
|
-
except ValueError as e:
|
|
704
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
def parse_args():
|
|
708
|
-
"""Parse command-line arguments."""
|
|
709
|
-
parser = argparse.ArgumentParser(
|
|
710
|
-
description="CPU Loader - Generate controllable CPU load with REST API and WebUI"
|
|
711
|
-
)
|
|
712
|
-
parser.add_argument(
|
|
713
|
-
"--host",
|
|
714
|
-
default="0.0.0.0",
|
|
715
|
-
help="Host to bind the server to (default: 0.0.0.0)",
|
|
716
|
-
)
|
|
717
|
-
parser.add_argument(
|
|
718
|
-
"--port",
|
|
719
|
-
type=int,
|
|
720
|
-
default=8000,
|
|
721
|
-
help="Port to bind the server to (default: 8000)",
|
|
722
|
-
)
|
|
723
|
-
|
|
724
|
-
# MQTT arguments
|
|
725
|
-
mqtt_group = parser.add_argument_group("MQTT settings")
|
|
726
|
-
mqtt_group.add_argument(
|
|
727
|
-
"--mqtt-broker-host",
|
|
728
|
-
help="MQTT broker hostname (env: MQTT_BROKER_HOST)",
|
|
729
|
-
)
|
|
730
|
-
mqtt_group.add_argument(
|
|
731
|
-
"--mqtt-broker-port",
|
|
732
|
-
type=int,
|
|
733
|
-
help="MQTT broker port (env: MQTT_BROKER_PORT, default: 1883)",
|
|
734
|
-
)
|
|
735
|
-
mqtt_group.add_argument(
|
|
736
|
-
"--mqtt-username",
|
|
737
|
-
help="MQTT username (env: MQTT_USERNAME)",
|
|
738
|
-
)
|
|
739
|
-
mqtt_group.add_argument(
|
|
740
|
-
"--mqtt-password",
|
|
741
|
-
help="MQTT password (env: MQTT_PASSWORD)",
|
|
742
|
-
)
|
|
743
|
-
mqtt_group.add_argument(
|
|
744
|
-
"--mqtt-topic-prefix",
|
|
745
|
-
help="MQTT topic prefix (env: MQTT_TOPIC_PREFIX, default: cpu-loader)",
|
|
746
|
-
)
|
|
747
|
-
mqtt_group.add_argument(
|
|
748
|
-
"--mqtt-client-id",
|
|
749
|
-
help="MQTT client ID (env: MQTT_CLIENT_ID, default: cpu-loader)",
|
|
750
|
-
)
|
|
751
|
-
|
|
752
|
-
return parser.parse_args()
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if __name__ == "__main__":
|
|
756
|
-
args = parse_args()
|
|
757
|
-
|
|
758
|
-
# Prepare MQTT arguments (only non-None values)
|
|
759
|
-
mqtt_args = {}
|
|
760
|
-
if args.mqtt_broker_host:
|
|
761
|
-
mqtt_args["broker_host"] = args.mqtt_broker_host
|
|
762
|
-
if args.mqtt_broker_port:
|
|
763
|
-
mqtt_args["broker_port"] = args.mqtt_broker_port
|
|
764
|
-
if args.mqtt_username:
|
|
765
|
-
mqtt_args["username"] = args.mqtt_username
|
|
766
|
-
if args.mqtt_password:
|
|
767
|
-
mqtt_args["password"] = args.mqtt_password
|
|
768
|
-
if args.mqtt_topic_prefix:
|
|
769
|
-
mqtt_args["topic_prefix"] = args.mqtt_topic_prefix
|
|
770
|
-
if args.mqtt_client_id:
|
|
771
|
-
mqtt_args["client_id"] = args.mqtt_client_id
|
|
772
|
-
|
|
773
|
-
# Store MQTT args in app state for lifespan to access
|
|
774
|
-
app.state.mqtt_args = mqtt_args
|
|
775
|
-
|
|
776
|
-
# Run the server
|
|
777
|
-
uvicorn.run(app, host=args.host, port=args.port)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|