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.
Files changed (23) hide show
  1. {cpu_loader-0.1.2/src/cpu_loader.egg-info → cpu_loader-0.1.3}/PKG-INFO +1 -1
  2. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/pyproject.toml +3 -3
  3. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/setup.py +2 -2
  4. cpu_loader-0.1.3/src/cpu_loader/__init__.py +5 -0
  5. cpu_loader-0.1.3/src/cpu_loader/__main__.py +6 -0
  6. {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/cpu_loader.py +1 -1
  7. cpu_loader-0.1.3/src/cpu_loader/main.py +328 -0
  8. {cpu_loader-0.1.2 → cpu_loader-0.1.3/src/cpu_loader.egg-info}/PKG-INFO +1 -1
  9. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/src/cpu_loader.egg-info/SOURCES.txt +7 -5
  10. cpu_loader-0.1.3/src/cpu_loader.egg-info/entry_points.txt +2 -0
  11. cpu_loader-0.1.3/src/cpu_loader.egg-info/top_level.txt +1 -0
  12. cpu_loader-0.1.2/src/cpu_loader.egg-info/entry_points.txt +0 -2
  13. cpu_loader-0.1.2/src/cpu_loader.egg-info/top_level.txt +0 -5
  14. cpu_loader-0.1.2/src/main.py +0 -777
  15. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/LICENSE +0 -0
  16. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/MANIFEST.in +0 -0
  17. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/README.md +0 -0
  18. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/setup.cfg +0 -0
  19. {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/cpu_loader_core.c +0 -0
  20. {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/example.py +0 -0
  21. {cpu_loader-0.1.2/src → cpu_loader-0.1.3/src/cpu_loader}/mqtt_publisher.py +0 -0
  22. {cpu_loader-0.1.2 → cpu_loader-0.1.3}/src/cpu_loader.egg-info/dependency_links.txt +0 -0
  23. {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
  Metadata-Version: 2.4
2
2
  Name: cpu-loader
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A tool to generate CPU load on a system with runtime configuration through a WebUI and REST API
5
5
  Author-email: the78mole <the78mole@users.noreply.github.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cpu-loader"
3
- version = "0.1.2"
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 = "main:run"
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
- py-modules = ["cpu_loader", "main", "example", "mqtt_publisher"]
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
  )
@@ -0,0 +1,5 @@
1
+ """CPU Loader - Generate configurable CPU load with WebUI and REST API."""
2
+
3
+ from .cpu_loader import CPULoader
4
+
5
+ __all__ = ["CPULoader"]
@@ -0,0 +1,6 @@
1
+ """Main entry point for CPU Loader application."""
2
+
3
+ from cpu_loader.main import run
4
+
5
+ if __name__ == "__main__":
6
+ run()
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cpu-loader
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A tool to generate CPU load on a system with runtime configuration through a WebUI and REST API
5
5
  Author-email: the78mole <the78mole@users.noreply.github.com>
6
6
  License: MIT
@@ -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/cpu_loader_core.c
8
- src/example.py
9
- src/main.py
10
- src/mqtt_publisher.py
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,2 @@
1
+ [console_scripts]
2
+ cpu-loader = cpu_loader.__main__:run
@@ -0,0 +1 @@
1
+ cpu_loader
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- cpu-loader = main:run
@@ -1,5 +0,0 @@
1
- cpu_loader
2
- cpu_loader_core
3
- example
4
- main
5
- mqtt_publisher
@@ -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>&#x1F525; 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