Update media-server: Add execution timing and improve script/callback execution UI
Backend improvements:
- Add execution_time tracking for script execution
- Add execution_time tracking for callback execution
- Add /api/callbacks/execute/{callback_name} endpoint for debugging callbacks
Frontend improvements:
- Fix duration display showing N/A for fast scripts (0 is falsy in JS)
- Increase duration precision to 3 decimal places (0.001s)
- Always show output section with "(no output)" message when empty
- Improve output formatting with italic gray text for empty output
Documentation:
- Add localization section to README
- Document available languages (English, Russian)
- Add guide for contributing new translations
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
"""Callback management API endpoints."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
@@ -34,6 +37,18 @@ class CallbackCreateRequest(BaseModel):
|
||||
shell: bool = Field(default=True, description="Run command in shell")
|
||||
|
||||
|
||||
class CallbackExecuteResponse(BaseModel):
|
||||
"""Response model for callback execution."""
|
||||
|
||||
success: bool
|
||||
callback: str
|
||||
exit_code: int | None = None
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
error: str | None = None
|
||||
execution_time: float | None = None
|
||||
|
||||
|
||||
def _validate_callback_name(name: str) -> None:
|
||||
"""Validate callback name.
|
||||
|
||||
@@ -84,6 +99,116 @@ async def list_callbacks(_: str = Depends(verify_token)) -> list[CallbackInfo]:
|
||||
]
|
||||
|
||||
|
||||
@router.post("/execute/{callback_name}")
|
||||
async def execute_callback(
|
||||
callback_name: str,
|
||||
_: str = Depends(verify_token),
|
||||
) -> CallbackExecuteResponse:
|
||||
"""Execute a callback for debugging purposes.
|
||||
|
||||
Args:
|
||||
callback_name: Name of the callback to execute
|
||||
|
||||
Returns:
|
||||
Execution result including stdout, stderr, and exit code
|
||||
"""
|
||||
# Validate callback name
|
||||
_validate_callback_name(callback_name)
|
||||
|
||||
# Check if callback exists
|
||||
if callback_name not in settings.callbacks:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Callback '{callback_name}' not found. Use /api/callbacks/list to see configured callbacks.",
|
||||
)
|
||||
|
||||
callback_config = settings.callbacks[callback_name]
|
||||
|
||||
logger.info(f"Executing callback for debugging: {callback_name}")
|
||||
|
||||
try:
|
||||
# Execute in thread pool to not block
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: _run_callback(
|
||||
command=callback_config.command,
|
||||
timeout=callback_config.timeout,
|
||||
shell=callback_config.shell,
|
||||
working_dir=callback_config.working_dir,
|
||||
),
|
||||
)
|
||||
|
||||
return CallbackExecuteResponse(
|
||||
success=result["exit_code"] == 0,
|
||||
callback=callback_name,
|
||||
exit_code=result["exit_code"],
|
||||
stdout=result["stdout"],
|
||||
stderr=result["stderr"],
|
||||
execution_time=result.get("execution_time"),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Callback execution error: {e}")
|
||||
return CallbackExecuteResponse(
|
||||
success=False,
|
||||
callback=callback_name,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
|
||||
def _run_callback(
|
||||
command: str,
|
||||
timeout: int,
|
||||
shell: bool,
|
||||
working_dir: str | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run a callback synchronously.
|
||||
|
||||
Args:
|
||||
command: Command to execute
|
||||
timeout: Timeout in seconds
|
||||
shell: Whether to run in shell
|
||||
working_dir: Working directory
|
||||
|
||||
Returns:
|
||||
Dict with exit_code, stdout, stderr, execution_time
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=shell,
|
||||
cwd=working_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
execution_time = time.time() - start_time
|
||||
return {
|
||||
"exit_code": result.returncode,
|
||||
"stdout": result.stdout[:10000], # Limit output size
|
||||
"stderr": result.stderr[:10000],
|
||||
"execution_time": execution_time,
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
execution_time = time.time() - start_time
|
||||
return {
|
||||
"exit_code": -1,
|
||||
"stdout": "",
|
||||
"stderr": f"Callback timed out after {timeout} seconds",
|
||||
"execution_time": execution_time,
|
||||
}
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
return {
|
||||
"exit_code": -1,
|
||||
"stdout": "",
|
||||
"stderr": str(e),
|
||||
"execution_time": execution_time,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/create/{callback_name}")
|
||||
async def create_callback(
|
||||
callback_name: str,
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
@@ -33,6 +34,7 @@ class ScriptExecuteResponse(BaseModel):
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
error: str | None = None
|
||||
execution_time: float | None = None
|
||||
|
||||
|
||||
class ScriptInfo(BaseModel):
|
||||
@@ -117,6 +119,7 @@ async def execute_script(
|
||||
exit_code=result["exit_code"],
|
||||
stdout=result["stdout"],
|
||||
stderr=result["stderr"],
|
||||
execution_time=result.get("execution_time"),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -143,8 +146,9 @@ def _run_script(
|
||||
working_dir: Working directory
|
||||
|
||||
Returns:
|
||||
Dict with exit_code, stdout, stderr
|
||||
Dict with exit_code, stdout, stderr, execution_time
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
@@ -154,22 +158,28 @@ def _run_script(
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
execution_time = time.time() - start_time
|
||||
return {
|
||||
"exit_code": result.returncode,
|
||||
"stdout": result.stdout[:10000], # Limit output size
|
||||
"stderr": result.stderr[:10000],
|
||||
"execution_time": execution_time,
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
execution_time = time.time() - start_time
|
||||
return {
|
||||
"exit_code": -1,
|
||||
"stdout": "",
|
||||
"stderr": f"Script timed out after {timeout} seconds",
|
||||
"execution_time": execution_time,
|
||||
}
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
return {
|
||||
"exit_code": -1,
|
||||
"stdout": "",
|
||||
"stderr": str(e),
|
||||
"execution_time": execution_time,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user