Добавил live streaming runtime команд
Этот коммит содержится в:
@@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user