Добавил live streaming runtime команд

Этот коммит содержится в:
Виктор
2026-05-12 01:05:18 +09:00
родитель 89df074030
Коммит 1fdbd5227c
+67 -83
Просмотреть файл
@@ -1,7 +1,10 @@
import json
import os import os
import subprocess
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
from backend.android_tools import find_apks from backend.android_tools import find_apks
@@ -39,43 +42,13 @@ class RuntimeCommandRequest(BaseModel):
def _runtime_commands() -> dict[str, dict]: def _runtime_commands() -> dict[str, dict]:
return { return {
'git_pull': { 'git_pull': {'label': 'Git Pull', 'command': 'git pull', 'timeout': 600},
'label': 'Git Pull', 'flutter_clean': {'label': 'Flutter Clean', 'command': _flutter('clean'), 'timeout': 900},
'command': 'git pull', 'flutter_pub_get': {'label': 'Flutter Pub Get', 'command': _flutter('pub get'), 'timeout': 900},
'timeout': 600, 'flutter_build_apk': {'label': 'Flutter Build APK', 'command': _flutter('build apk --release'), 'timeout': 3600},
}, 'flutter_run_profile': {'label': 'Flutter Run Profile', 'command': _flutter('run --profile'), 'timeout': 1800, 'device': True},
'flutter_clean': { 'adb_reconnect': {'label': 'ADB Reconnect', 'command': f'{_runtime_env_prefix()}adb reconnect', 'timeout': 120},
'label': 'Flutter Clean', 'adb_logcat': {'label': 'ADB Logcat Snapshot', 'command': f'{_runtime_env_prefix()}adb logcat -d -t 300', 'timeout': 120, 'device': True},
'command': _flutter('clean'),
'timeout': 900,
},
'flutter_pub_get': {
'label': 'Flutter Pub Get',
'command': _flutter('pub get'),
'timeout': 900,
},
'flutter_build_apk': {
'label': 'Flutter Build APK',
'command': _flutter('build apk --release'),
'timeout': 3600,
},
'flutter_run_profile': {
'label': 'Flutter Run Profile',
'command': _flutter('run --profile'),
'timeout': 1800,
'device': True,
},
'adb_reconnect': {
'label': 'ADB Reconnect',
'command': f'{_runtime_env_prefix()}adb reconnect',
'timeout': 120,
},
'adb_logcat': {
'label': 'ADB Logcat Snapshot',
'command': f'{_runtime_env_prefix()}adb logcat -d -t 300',
'timeout': 120,
'device': True,
},
} }
@@ -89,22 +62,49 @@ def _workspace_cwd(workspace: str) -> str:
def _with_device(command: str, device: str | None, enabled: bool) -> str: def _with_device(command: str, device: str | None, enabled: bool) -> str:
if not enabled or not device: if not enabled or not device:
return command return command
if 'adb ' in command: if 'adb ' in command:
return command.replace('adb ', f'adb -s {device} ', 1) return command.replace('adb ', f'adb -s {device} ', 1)
if 'flutter ' in command and ' -d ' not in command: if 'flutter ' in command and ' -d ' not in command:
return f'{command} -d {device}' return f'{command} -d {device}'
return command return command
def _event(payload: dict) -> str:
return json.dumps(payload, ensure_ascii=False) + '\n'
def _stream_command(command: str, cwd: str, timeout: int):
process = subprocess.Popen(
command,
cwd=cwd,
shell=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
)
yield _event({'type': 'start', 'message': command})
try:
assert process.stdout is not None
for line in iter(process.stdout.readline, ''):
if not line:
break
yield _event({'type': 'line', 'message': line.rstrip()})
returncode = process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
process.kill()
returncode = -1
yield _event({'type': 'error', 'message': 'Command timeout'})
yield _event({'type': 'done', 'returncode': returncode})
@router.get('/logs') @router.get('/logs')
async def runtime_logs(): async def runtime_logs():
return { return {'success': True, 'logs': get_logs()}
'success': True,
'logs': get_logs(),
}
@router.get('/commands') @router.get('/commands')
@@ -112,11 +112,7 @@ async def runtime_commands():
return { return {
'success': True, 'success': True,
'commands': [ 'commands': [
{ {'id': command_id, 'label': config['label'], 'needs_device': bool(config.get('device'))}
'id': command_id,
'label': config['label'],
'needs_device': bool(config.get('device')),
}
for command_id, config in _runtime_commands().items() for command_id, config in _runtime_commands().items()
], ],
} }
@@ -133,39 +129,41 @@ async def runtime_event(payload: dict):
async def runtime_command(payload: RuntimeCommandRequest): async def runtime_command(payload: RuntimeCommandRequest):
commands = _runtime_commands() commands = _runtime_commands()
config = commands.get(payload.command) config = commands.get(payload.command)
if not config: if not config:
raise HTTPException(status_code=400, detail='Runtime command is not allowed') raise HTTPException(status_code=400, detail='Runtime command is not allowed')
cwd = _workspace_cwd(payload.workspace) cwd = _workspace_cwd(payload.workspace)
command = _with_device( command = _with_device(config['command'], payload.device, bool(config.get('device')))
config['command'],
payload.device,
bool(config.get('device')),
)
add_log(f"Runtime command started: {config['label']}") add_log(f"Runtime command started: {config['label']}")
result = run_command(command, cwd, timeout=int(config.get('timeout', 900))) result = run_command(command, cwd, timeout=int(config.get('timeout', 900)))
add_log(f"Runtime command finished: {config['label']} (exit {result['returncode']})")
add_log( return {'success': result['returncode'] == 0, 'command': payload.command, 'label': config['label'], 'result': result}
f"Runtime command finished: {config['label']} "
f"(exit {result['returncode']})"
@router.post('/command-stream')
async def runtime_command_stream(payload: RuntimeCommandRequest):
commands = _runtime_commands()
config = commands.get(payload.command)
if not config:
raise HTTPException(status_code=400, detail='Runtime command is not allowed')
cwd = _workspace_cwd(payload.workspace)
command = _with_device(config['command'], payload.device, bool(config.get('device')))
add_log(f"Runtime stream started: {config['label']}")
return StreamingResponse(
_stream_command(command, cwd, int(config.get('timeout', 900))),
media_type='application/x-ndjson',
) )
return {
'success': result['returncode'] == 0,
'command': payload.command,
'label': config['label'],
'result': result,
}
@router.post('/install-latest-apk') @router.post('/install-latest-apk')
async def runtime_install_latest_apk(payload: RuntimeCommandRequest): async def runtime_install_latest_apk(payload: RuntimeCommandRequest):
cwd = _workspace_cwd(payload.workspace) cwd = _workspace_cwd(payload.workspace)
apks = find_apks(cwd) apks = find_apks(cwd)
if not apks: if not apks:
raise HTTPException(status_code=404, detail='APK not found. Build project first.') raise HTTPException(status_code=404, detail='APK not found. Build project first.')
@@ -174,17 +172,10 @@ async def runtime_install_latest_apk(payload: RuntimeCommandRequest):
adb = f'{_runtime_env_prefix()}adb -s {payload.device}' adb = f'{_runtime_env_prefix()}adb -s {payload.device}'
command = f'{adb} install -r "{apks[0]}"' command = f'{adb} install -r "{apks[0]}"'
add_log(f'Runtime APK install started: {apks[0]}') add_log(f'Runtime APK install started: {apks[0]}')
result = run_command(command, cwd, timeout=900) result = run_command(command, cwd, timeout=900)
add_log(f"Runtime APK install finished (exit {result['returncode']})") add_log(f"Runtime APK install finished (exit {result['returncode']})")
return {'success': result['returncode'] == 0, 'apk': apks[0], 'device': payload.device, 'result': result}
return {
'success': result['returncode'] == 0,
'apk': apks[0],
'device': payload.device,
'result': result,
}
@router.post('/restart-app') @router.post('/restart-app')
@@ -198,14 +189,7 @@ async def runtime_restart_app(payload: RuntimeCommandRequest):
adb = f'{_runtime_env_prefix()}adb -s {payload.device}' adb = f'{_runtime_env_prefix()}adb -s {payload.device}'
command = f'{adb} shell monkey -p {payload.package_name} 1' command = f'{adb} shell monkey -p {payload.package_name} 1'
add_log(f'Runtime app restart requested: {payload.package_name}') add_log(f'Runtime app restart requested: {payload.package_name}')
result = run_command(command, cwd, timeout=120) result = run_command(command, cwd, timeout=120)
add_log(f"Runtime app restart finished (exit {result['returncode']})") add_log(f"Runtime app restart finished (exit {result['returncode']})")
return {'success': result['returncode'] == 0, 'package_name': payload.package_name, 'device': payload.device, 'result': result}
return {
'success': result['returncode'] == 0,
'package_name': payload.package_name,
'device': payload.device,
'result': result,
}