more-compute 0.1.0__py3-none-any.whl
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.
- kernel_run.py +283 -0
- more_compute-0.1.0.dist-info/METADATA +163 -0
- more_compute-0.1.0.dist-info/RECORD +26 -0
- more_compute-0.1.0.dist-info/WHEEL +5 -0
- more_compute-0.1.0.dist-info/entry_points.txt +2 -0
- more_compute-0.1.0.dist-info/licenses/LICENSE +21 -0
- more_compute-0.1.0.dist-info/top_level.txt +2 -0
- morecompute/__init__.py +6 -0
- morecompute/cli.py +31 -0
- morecompute/execution/__init__.py +5 -0
- morecompute/execution/__main__.py +10 -0
- morecompute/execution/executor.py +381 -0
- morecompute/execution/worker.py +244 -0
- morecompute/notebook.py +81 -0
- morecompute/process_worker.py +209 -0
- morecompute/server.py +641 -0
- morecompute/services/pod_manager.py +503 -0
- morecompute/services/prime_intellect.py +316 -0
- morecompute/static/styles.css +1056 -0
- morecompute/utils/__init__.py +17 -0
- morecompute/utils/cache_util.py +23 -0
- morecompute/utils/error_utils.py +322 -0
- morecompute/utils/notebook_util.py +44 -0
- morecompute/utils/python_environment_util.py +197 -0
- morecompute/utils/special_commands.py +458 -0
- morecompute/utils/system_environment_util.py +134 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import io
|
|
3
|
+
import sys
|
|
4
|
+
import asyncio
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
import shlex
|
|
8
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
9
|
+
from typing import Dict, Any, Optional, Tuple, Union
|
|
10
|
+
from fastapi import WebSocket
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# this file is not tested that all functions work, need to write a test file / manually check
|
|
14
|
+
# to-do
|
|
15
|
+
|
|
16
|
+
class AsyncSpecialCommandHandler:
|
|
17
|
+
"""Handles all special commands asynchronously with streaming support: shell (!), line magics (%), and cell magics (%%)"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, globals_dict: dict):
|
|
20
|
+
self.globals_dict = globals_dict
|
|
21
|
+
self.captured_outputs = {} # Store captured outputs from %%capture
|
|
22
|
+
|
|
23
|
+
def is_special_command(self, source_code: Union[str, list, tuple]) -> bool:
|
|
24
|
+
"""Check if the source code is a special command"""
|
|
25
|
+
text = self._coerce_source_to_text(source_code)
|
|
26
|
+
stripped = text.strip()
|
|
27
|
+
return (stripped.startswith('!') or
|
|
28
|
+
stripped.startswith('%%') or
|
|
29
|
+
stripped.startswith('%'))
|
|
30
|
+
|
|
31
|
+
async def execute_special_command(self, source_code: Union[str, list, tuple], result: Dict[str, Any],
|
|
32
|
+
start_time: float, execution_count: int,
|
|
33
|
+
websocket: Optional[WebSocket] = None,
|
|
34
|
+
cell_index: Optional[int] = None) -> Dict[str, Any]:
|
|
35
|
+
"""Execute a special command and return the result"""
|
|
36
|
+
text = self._coerce_source_to_text(source_code)
|
|
37
|
+
stripped = text.strip()
|
|
38
|
+
|
|
39
|
+
if stripped.startswith('!'):
|
|
40
|
+
return await self._execute_shell_command(stripped[1:], result, start_time, websocket, cell_index)
|
|
41
|
+
elif stripped.startswith('%%'):
|
|
42
|
+
return await self._execute_cell_magic(text, result, start_time, execution_count, websocket)
|
|
43
|
+
elif stripped.startswith('%'):
|
|
44
|
+
return await self._execute_line_magic(stripped[1:], result, start_time, websocket)
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError("Not a special command")
|
|
47
|
+
|
|
48
|
+
def _coerce_source_to_text(self, source_code: Union[str, list, tuple]) -> str:
|
|
49
|
+
"""Normalize incoming source to a single text string"""
|
|
50
|
+
try:
|
|
51
|
+
if isinstance(source_code, str):
|
|
52
|
+
return source_code
|
|
53
|
+
if isinstance(source_code, (list, tuple)):
|
|
54
|
+
return "".join(source_code)
|
|
55
|
+
return str(source_code)
|
|
56
|
+
except Exception:
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
async def _execute_shell_command(self, command: str, result: Dict[str, Any],
|
|
60
|
+
start_time: float, websocket: Optional[WebSocket] = None,
|
|
61
|
+
cell_index: Optional[int] = None) -> Dict[str, Any]:
|
|
62
|
+
"""Execute a shell command with real-time streaming output"""
|
|
63
|
+
try:
|
|
64
|
+
# Prepare environment and command for streaming
|
|
65
|
+
env = self._prepare_streaming_environment(command)
|
|
66
|
+
cmd_parts = self._prepare_command_parts(command)
|
|
67
|
+
|
|
68
|
+
# Send execution start notification
|
|
69
|
+
if websocket:
|
|
70
|
+
await websocket.send_json({
|
|
71
|
+
"type": "execution_start",
|
|
72
|
+
"data": {
|
|
73
|
+
"command": f"!{command}",
|
|
74
|
+
**({"cell_index": cell_index} if cell_index is not None else {})
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
# Create subprocess with streaming
|
|
79
|
+
process = await asyncio.create_subprocess_exec(
|
|
80
|
+
*cmd_parts,
|
|
81
|
+
stdout=asyncio.subprocess.PIPE,
|
|
82
|
+
stderr=asyncio.subprocess.PIPE,
|
|
83
|
+
env=env,
|
|
84
|
+
cwd=os.getcwd()
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Stream output concurrently
|
|
88
|
+
stdout_task = asyncio.create_task(
|
|
89
|
+
self._stream_output(process.stdout, "stdout", result, websocket, cell_index)
|
|
90
|
+
)
|
|
91
|
+
stderr_task = asyncio.create_task(
|
|
92
|
+
self._stream_output(process.stderr, "stderr", result, websocket, cell_index)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Wait for both streams to complete
|
|
96
|
+
await asyncio.gather(stdout_task, stderr_task)
|
|
97
|
+
|
|
98
|
+
# Wait for process completion
|
|
99
|
+
return_code = await process.wait()
|
|
100
|
+
|
|
101
|
+
# Send completion notification
|
|
102
|
+
if websocket:
|
|
103
|
+
await websocket.send_json({
|
|
104
|
+
"type": "execution_complete",
|
|
105
|
+
"data": {
|
|
106
|
+
"return_code": return_code,
|
|
107
|
+
"status": "error" if return_code != 0 else "ok",
|
|
108
|
+
**({"cell_index": cell_index} if cell_index is not None else {})
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
# If pip install/uninstall occurred, notify clients to refresh packages
|
|
113
|
+
try:
|
|
114
|
+
if websocket and (command.startswith('pip install') or command.startswith('pip uninstall') or 'pip install' in command or 'pip uninstall' in command):
|
|
115
|
+
await websocket.send_json({
|
|
116
|
+
"type": "packages_updated",
|
|
117
|
+
"data": {"action": "pip"}
|
|
118
|
+
})
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# Check if command failed
|
|
123
|
+
if return_code != 0:
|
|
124
|
+
result["status"] = "error"
|
|
125
|
+
result["error"] = {
|
|
126
|
+
"ename": "ShellCommandError",
|
|
127
|
+
"evalue": f"Command failed with return code {return_code}",
|
|
128
|
+
"traceback": [f"Shell command failed: {command}"]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
result["status"] = "error"
|
|
133
|
+
result["error"] = {
|
|
134
|
+
"ename": type(e).__name__,
|
|
135
|
+
"evalue": str(e),
|
|
136
|
+
"traceback": [f"Shell command error: {str(e)}"]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if websocket:
|
|
140
|
+
await websocket.send_json({
|
|
141
|
+
"type": "execution_error",
|
|
142
|
+
"data": {
|
|
143
|
+
"error": result["error"]
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
# Calculate execution time
|
|
148
|
+
result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
async def interrupt(self):
|
|
152
|
+
# Placeholder for future process-based interruption logic
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
def _prepare_streaming_environment(self, shell_cmd: str) -> dict:
|
|
156
|
+
"""Prepare environment variables for unbuffered output"""
|
|
157
|
+
env = os.environ.copy()
|
|
158
|
+
|
|
159
|
+
# Always set unbuffered Python
|
|
160
|
+
env['PYTHONUNBUFFERED'] = '1'
|
|
161
|
+
env['PYTHONDONTWRITEBYTECODE'] = '1'
|
|
162
|
+
|
|
163
|
+
# Additional settings for specific commands
|
|
164
|
+
if 'pip install' in shell_cmd:
|
|
165
|
+
env['PIP_DISABLE_PIP_VERSION_CHECK'] = '1'
|
|
166
|
+
env['PIP_NO_CACHE_DIR'] = '1'
|
|
167
|
+
|
|
168
|
+
return env
|
|
169
|
+
|
|
170
|
+
def _prepare_command_parts(self, shell_cmd: str) -> list:
|
|
171
|
+
"""Convert shell command to subprocess-compatible format"""
|
|
172
|
+
|
|
173
|
+
if shell_cmd.startswith('pip '):
|
|
174
|
+
# Route pip through Python module for better control
|
|
175
|
+
parts = ['python', '-m'] + shlex.split(shell_cmd)
|
|
176
|
+
# Add progress bar control for pip
|
|
177
|
+
if 'install' in shell_cmd and '--progress-bar' not in shell_cmd:
|
|
178
|
+
parts.extend(['--progress-bar', 'off'])
|
|
179
|
+
return parts
|
|
180
|
+
|
|
181
|
+
elif shell_cmd.startswith('python '):
|
|
182
|
+
# Add unbuffered flag to python commands
|
|
183
|
+
parts = shlex.split(shell_cmd)
|
|
184
|
+
parts.insert(1, '-u') # Add -u after 'python'
|
|
185
|
+
return parts
|
|
186
|
+
|
|
187
|
+
else:
|
|
188
|
+
# For other shell commands, use shell execution
|
|
189
|
+
return ['/bin/zsh', '-c', shell_cmd] # macOS with zsh
|
|
190
|
+
|
|
191
|
+
async def _stream_output(self, stream, stream_type: str, result: Dict[str, Any],
|
|
192
|
+
websocket: Optional[WebSocket] = None,
|
|
193
|
+
cell_index: Optional[int] = None):
|
|
194
|
+
"""Read from a stream and send to websocket, while capturing the output."""
|
|
195
|
+
|
|
196
|
+
output_text = ""
|
|
197
|
+
while True:
|
|
198
|
+
try:
|
|
199
|
+
line = await stream.readline()
|
|
200
|
+
if not line:
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
decoded_line = line.decode('utf-8')
|
|
204
|
+
output_text += decoded_line
|
|
205
|
+
|
|
206
|
+
if websocket:
|
|
207
|
+
await websocket.send_json({
|
|
208
|
+
"type": "stream_output",
|
|
209
|
+
"data": {
|
|
210
|
+
"stream": stream_type,
|
|
211
|
+
"text": decoded_line,
|
|
212
|
+
**({"cell_index": cell_index} if cell_index is not None else {})
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
except asyncio.CancelledError:
|
|
216
|
+
break
|
|
217
|
+
except Exception as e:
|
|
218
|
+
# Handle potential errors during streaming
|
|
219
|
+
error_message = f"Error reading stream: {e}\n"
|
|
220
|
+
output_text += error_message
|
|
221
|
+
if websocket:
|
|
222
|
+
await websocket.send_json({
|
|
223
|
+
"type": "stream_output",
|
|
224
|
+
"data": {
|
|
225
|
+
"stream": "stderr",
|
|
226
|
+
"text": error_message,
|
|
227
|
+
**({"cell_index": cell_index} if cell_index is not None else {})
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
# Add the captured text to the final result object
|
|
233
|
+
if output_text:
|
|
234
|
+
# Look for an existing stream output of the same type to append to
|
|
235
|
+
existing_output = next((o for o in result["outputs"] if o.get("name") == stream_type), None)
|
|
236
|
+
if existing_output:
|
|
237
|
+
existing_output["text"] += output_text
|
|
238
|
+
else:
|
|
239
|
+
result["outputs"].append({
|
|
240
|
+
"output_type": "stream",
|
|
241
|
+
"name": stream_type,
|
|
242
|
+
"text": output_text
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
async def _execute_cell_magic(self, source_code: str, result: Dict[str, Any],
|
|
246
|
+
start_time: float, execution_count: int,
|
|
247
|
+
websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
248
|
+
"""Execute a cell magic command"""
|
|
249
|
+
lines = source_code.strip().split('\n')
|
|
250
|
+
magic_line = lines[0] # e.g., "%%capture", "%%time"
|
|
251
|
+
cell_content = '\n'.join(lines[1:]) if len(lines) > 1 else ""
|
|
252
|
+
|
|
253
|
+
# Parse magic command and arguments
|
|
254
|
+
magic_parts = shlex.split(magic_line)
|
|
255
|
+
magic_name = magic_parts[0][2:] # Remove %%
|
|
256
|
+
magic_args = magic_parts[1:] if len(magic_parts) > 1 else []
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
if magic_name == "capture":
|
|
260
|
+
return await self._handle_capture_magic(magic_args, cell_content, result, start_time, execution_count, websocket)
|
|
261
|
+
elif magic_name == "time":
|
|
262
|
+
return await self._handle_time_magic(cell_content, result, start_time, execution_count, websocket)
|
|
263
|
+
elif magic_name == "writefile":
|
|
264
|
+
return await self._handle_writefile_magic(magic_args, cell_content, result, start_time, websocket)
|
|
265
|
+
else:
|
|
266
|
+
result["status"] = "error"
|
|
267
|
+
result["error"] = {
|
|
268
|
+
"ename": "UnknownMagicError",
|
|
269
|
+
"evalue": f"Unknown cell magic: %%{magic_name}",
|
|
270
|
+
"traceback": [f"Cell magic %%{magic_name} is not implemented"]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if websocket:
|
|
274
|
+
await websocket.send_json({
|
|
275
|
+
"type": "execution_error",
|
|
276
|
+
"data": {
|
|
277
|
+
"error": result["error"]
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
except Exception as e:
|
|
281
|
+
result["status"] = "error"
|
|
282
|
+
result["error"] = {
|
|
283
|
+
"ename": type(e).__name__,
|
|
284
|
+
"evalue": str(e),
|
|
285
|
+
"traceback": [f"Cell magic error: {str(e)}"]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if websocket:
|
|
289
|
+
await websocket.send_json({
|
|
290
|
+
"type": "execution_error",
|
|
291
|
+
"data": {
|
|
292
|
+
"error": result["error"]
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
async def _execute_line_magic(self, magic_line: str, result: Dict[str, Any],
|
|
300
|
+
start_time: float, websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
301
|
+
"""Execute a line magic command"""
|
|
302
|
+
# Parse magic command and arguments
|
|
303
|
+
parts = shlex.split(magic_line)
|
|
304
|
+
magic_name = parts[0]
|
|
305
|
+
magic_args = parts[1:] if len(parts) > 1 else []
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
if magic_name == "pwd":
|
|
309
|
+
return await self._handle_pwd_magic(result, start_time, websocket)
|
|
310
|
+
elif magic_name == "cd":
|
|
311
|
+
return await self._handle_cd_magic(magic_args, result, start_time, websocket)
|
|
312
|
+
elif magic_name == "ls":
|
|
313
|
+
return await self._handle_ls_magic(magic_args, result, start_time, websocket)
|
|
314
|
+
elif magic_name == "env":
|
|
315
|
+
return await self._handle_env_magic(magic_args, result, start_time, websocket)
|
|
316
|
+
elif magic_name == "who":
|
|
317
|
+
return await self._handle_who_magic(result, start_time, websocket)
|
|
318
|
+
elif magic_name == "whos":
|
|
319
|
+
return await self._handle_whos_magic(result, start_time, websocket)
|
|
320
|
+
else:
|
|
321
|
+
result["status"] = "error"
|
|
322
|
+
result["error"] = {
|
|
323
|
+
"ename": "UnknownMagicError",
|
|
324
|
+
"evalue": f"Unknown line magic: %{magic_name}",
|
|
325
|
+
"traceback": [f"Line magic %{magic_name} is not implemented"]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if websocket:
|
|
329
|
+
await websocket.send_json({
|
|
330
|
+
"type": "execution_error",
|
|
331
|
+
"data": {
|
|
332
|
+
"error": result["error"]
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
except Exception as e:
|
|
336
|
+
result["status"] = "error"
|
|
337
|
+
result["error"] = {
|
|
338
|
+
"ename": type(e).__name__,
|
|
339
|
+
"evalue": str(e),
|
|
340
|
+
"traceback": [f"Line magic error: {str(e)}"]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if websocket:
|
|
344
|
+
await websocket.send_json({
|
|
345
|
+
"type": "execution_error",
|
|
346
|
+
"data": {
|
|
347
|
+
"error": result["error"]
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
|
|
352
|
+
return result
|
|
353
|
+
|
|
354
|
+
# Cell Magic Implementations
|
|
355
|
+
|
|
356
|
+
async def _handle_capture_magic(self, args: list, cell_content: str, result: Dict[str, Any],
|
|
357
|
+
start_time: float, execution_count: int,
|
|
358
|
+
websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
359
|
+
"""Handle %%capture magic - capture stdout/stderr without displaying"""
|
|
360
|
+
output_var = args[0] if args else None
|
|
361
|
+
no_stdout = "--no-stdout" in args
|
|
362
|
+
no_stderr = "--no-stderr" in args
|
|
363
|
+
|
|
364
|
+
# Capture outputs
|
|
365
|
+
stdout_capture = None if no_stdout else io.StringIO()
|
|
366
|
+
stderr_capture = None if no_stderr else io.StringIO()
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
# Execute the cell content with output capture
|
|
370
|
+
if stdout_capture or stderr_capture:
|
|
371
|
+
with redirect_stdout(stdout_capture or sys.stdout), \
|
|
372
|
+
redirect_stderr(stderr_capture or sys.stderr):
|
|
373
|
+
compiled_code = compile(cell_content, '<cell>', 'exec')
|
|
374
|
+
exec(compiled_code, self.globals_dict)
|
|
375
|
+
else:
|
|
376
|
+
compiled_code = compile(cell_content, '<cell>', 'exec')
|
|
377
|
+
exec(compiled_code, self.globals_dict)
|
|
378
|
+
|
|
379
|
+
# Store captured output in a variable if specified
|
|
380
|
+
if output_var:
|
|
381
|
+
captured_data = {
|
|
382
|
+
'stdout': stdout_capture.getvalue() if stdout_capture else '',
|
|
383
|
+
'stderr': stderr_capture.getvalue() if stderr_capture else ''
|
|
384
|
+
}
|
|
385
|
+
self.globals_dict[output_var] = captured_data
|
|
386
|
+
self.captured_outputs[output_var] = captured_data
|
|
387
|
+
|
|
388
|
+
# Don't add outputs to result (they're captured, not displayed)
|
|
389
|
+
if websocket:
|
|
390
|
+
await websocket.send_json({
|
|
391
|
+
"type": "execution_complete",
|
|
392
|
+
"data": {
|
|
393
|
+
"status": "ok",
|
|
394
|
+
"message": "Output captured" + (f" in variable '{output_var}'" if output_var else "")
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
result["status"] = "error"
|
|
400
|
+
result["error"] = {
|
|
401
|
+
"ename": type(e).__name__,
|
|
402
|
+
"evalue": str(e),
|
|
403
|
+
"traceback": [f"Capture magic error: {str(e)}"]
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if websocket:
|
|
407
|
+
await websocket.send_json({
|
|
408
|
+
"type": "execution_error",
|
|
409
|
+
"data": {
|
|
410
|
+
"error": result["error"]
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
return result
|
|
415
|
+
|
|
416
|
+
# Add other magic method implementations here...
|
|
417
|
+
# (Time magic, writefile magic, line magics like pwd, cd, ls, etc.)
|
|
418
|
+
# I'll implement a few key ones to keep this focused:
|
|
419
|
+
|
|
420
|
+
async def _handle_pwd_magic(self, result: Dict[str, Any], start_time: float,
|
|
421
|
+
websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
422
|
+
"""Handle %pwd magic - print working directory"""
|
|
423
|
+
try:
|
|
424
|
+
pwd = os.getcwd()
|
|
425
|
+
output_data = {
|
|
426
|
+
"output_type": "execute_result",
|
|
427
|
+
"execution_count": None,
|
|
428
|
+
"data": {
|
|
429
|
+
"text/plain": f"'{pwd}'"
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
result["outputs"].append(output_data)
|
|
433
|
+
|
|
434
|
+
if websocket:
|
|
435
|
+
await websocket.send_json({
|
|
436
|
+
"type": "execute_result",
|
|
437
|
+
"data": {
|
|
438
|
+
"data": output_data["data"]
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
result["status"] = "error"
|
|
444
|
+
result["error"] = {
|
|
445
|
+
"ename": type(e).__name__,
|
|
446
|
+
"evalue": str(e),
|
|
447
|
+
"traceback": [f"PWD magic error: {str(e)}"]
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if websocket:
|
|
451
|
+
await websocket.send_json({
|
|
452
|
+
"type": "execution_error",
|
|
453
|
+
"data": {
|
|
454
|
+
"error": result["error"]
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
return result
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import psutil # type: ignore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeviceMetrics:
|
|
11
|
+
"""Collects system metrics with graceful fallbacks.
|
|
12
|
+
|
|
13
|
+
- Uses psutil for CPU, memory, disk, network and process metrics
|
|
14
|
+
- Uses nvidia-smi when available for basic GPU metrics
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def get_all_devices(self) -> Dict[str, Any]:
|
|
18
|
+
return {
|
|
19
|
+
"timestamp": time.time(),
|
|
20
|
+
"cpu": self.get_cpu_metrics(),
|
|
21
|
+
"memory": self.get_memory_metrics(),
|
|
22
|
+
"gpu": self.get_gpu_metrics(),
|
|
23
|
+
"storage": self.get_storage_metrics(),
|
|
24
|
+
"network": self.get_network_metrics(),
|
|
25
|
+
"process": self.get_process_metrics(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def get_cpu_metrics(self) -> Dict[str, Any]:
|
|
29
|
+
try:
|
|
30
|
+
cpu_percent = psutil.cpu_percent(interval=None)
|
|
31
|
+
freq = psutil.cpu_freq()
|
|
32
|
+
return {
|
|
33
|
+
"percent": cpu_percent,
|
|
34
|
+
"frequency_mhz": freq.current if freq else None,
|
|
35
|
+
"cores": psutil.cpu_count(logical=True),
|
|
36
|
+
}
|
|
37
|
+
except Exception:
|
|
38
|
+
return {"percent": None, "frequency_mhz": None, "cores": None}
|
|
39
|
+
|
|
40
|
+
def get_memory_metrics(self) -> Dict[str, Any]:
|
|
41
|
+
try:
|
|
42
|
+
vm = psutil.virtual_memory()
|
|
43
|
+
sm = psutil.swap_memory()
|
|
44
|
+
return {
|
|
45
|
+
"total": vm.total,
|
|
46
|
+
"available": vm.available,
|
|
47
|
+
"used": vm.used,
|
|
48
|
+
"percent": vm.percent,
|
|
49
|
+
"swap_total": sm.total,
|
|
50
|
+
"swap_used": sm.used,
|
|
51
|
+
"swap_percent": sm.percent,
|
|
52
|
+
}
|
|
53
|
+
except Exception:
|
|
54
|
+
return {"total": None, "available": None, "used": None, "percent": None}
|
|
55
|
+
|
|
56
|
+
def get_gpu_metrics(self) -> Optional[List[Dict[str, Any]]]:
|
|
57
|
+
"""Return list of GPU dicts or None when no GPU available.
|
|
58
|
+
|
|
59
|
+
Uses nvidia-smi if present. If not found or fails, returns None.
|
|
60
|
+
"""
|
|
61
|
+
if shutil.which("nvidia-smi") is None:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
# Query a few basic metrics; avoid JSON for broader compat
|
|
65
|
+
query = "power.draw,clocks.sm,temperature.gpu,utilization.gpu,memory.used,memory.total"
|
|
66
|
+
cmd = [
|
|
67
|
+
"nvidia-smi",
|
|
68
|
+
f"--query-gpu={query}",
|
|
69
|
+
"--format=csv,noheader,nounits",
|
|
70
|
+
]
|
|
71
|
+
out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT, timeout=3)
|
|
72
|
+
gpus: List[Dict[str, Any]] = []
|
|
73
|
+
for line in out.strip().splitlines():
|
|
74
|
+
parts = [p.strip() for p in line.split(",")]
|
|
75
|
+
if len(parts) >= 6:
|
|
76
|
+
gpus.append(
|
|
77
|
+
{
|
|
78
|
+
"power_w": _to_float(parts[0]),
|
|
79
|
+
"clock_sm_mhz": _to_float(parts[1]),
|
|
80
|
+
"temperature_c": _to_float(parts[2]),
|
|
81
|
+
"util_percent": _to_float(parts[3]),
|
|
82
|
+
"mem_used_mb": _to_float(parts[4]),
|
|
83
|
+
"mem_total_mb": _to_float(parts[5]),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return gpus or None
|
|
87
|
+
except Exception:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def get_storage_metrics(self) -> Dict[str, Any]:
|
|
91
|
+
try:
|
|
92
|
+
usage = psutil.disk_usage("/")
|
|
93
|
+
io = psutil.disk_io_counters()
|
|
94
|
+
return {
|
|
95
|
+
"total": usage.total,
|
|
96
|
+
"used": usage.used,
|
|
97
|
+
"percent": usage.percent,
|
|
98
|
+
"read_bytes": getattr(io, "read_bytes", None),
|
|
99
|
+
"write_bytes": getattr(io, "write_bytes", None),
|
|
100
|
+
}
|
|
101
|
+
except Exception:
|
|
102
|
+
return {"total": None, "used": None, "percent": None}
|
|
103
|
+
|
|
104
|
+
def get_network_metrics(self) -> Dict[str, Any]:
|
|
105
|
+
try:
|
|
106
|
+
net = psutil.net_io_counters()
|
|
107
|
+
return {
|
|
108
|
+
"bytes_sent": net.bytes_sent,
|
|
109
|
+
"bytes_recv": net.bytes_recv,
|
|
110
|
+
"packets_sent": net.packets_sent,
|
|
111
|
+
"packets_recv": net.packets_recv,
|
|
112
|
+
}
|
|
113
|
+
except Exception:
|
|
114
|
+
return {"bytes_sent": None, "bytes_recv": None}
|
|
115
|
+
|
|
116
|
+
def get_process_metrics(self) -> Dict[str, Any]:
|
|
117
|
+
try:
|
|
118
|
+
p = psutil.Process(os.getpid())
|
|
119
|
+
mem = p.memory_info()
|
|
120
|
+
return {
|
|
121
|
+
"rss": mem.rss,
|
|
122
|
+
"vms": getattr(mem, "vms", None),
|
|
123
|
+
"threads": p.num_threads(),
|
|
124
|
+
"cpu_percent": p.cpu_percent(interval=None),
|
|
125
|
+
}
|
|
126
|
+
except Exception:
|
|
127
|
+
return {"rss": None, "threads": None}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _to_float(value: str) -> Optional[float]:
|
|
131
|
+
try:
|
|
132
|
+
return float(value)
|
|
133
|
+
except Exception:
|
|
134
|
+
return None
|