From 9dc13fe034682063d1fe97d6b8f909708b235ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Tue, 12 May 2026 02:18:19 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20ru?= =?UTF-8?q?ntime=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20APK=20artifacts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/runtime_api.py | 63 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/backend/runtime_api.py b/backend/runtime_api.py index e84bf96..d3d8500 100644 --- a/backend/runtime_api.py +++ b/backend/runtime_api.py @@ -10,7 +10,7 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel -from backend.android_tools import find_apks +from backend.android_tools import find_apks, list_devices from backend.ota_publish import publish_project_ota from backend.runtime_logs import get_logs, add_log from backend.shell_runner import run_command @@ -35,9 +35,10 @@ def _flutter(command: str) -> str: class RuntimeCommandRequest(BaseModel): workspace: str - command: str + command: str | None = None device: str | None = None package_name: str | None = None + apk_path: str | None = None class RuntimeStopRequest(BaseModel): @@ -64,6 +65,22 @@ def _workspace_cwd(workspace: str) -> str: return path.as_posix() +def _safe_apk_path(workspace: str, apk_path: str | None) -> Path: + if not apk_path: + raise HTTPException(status_code=400, detail='apk_path is required') + root = Path(workspace).resolve() + target = Path(apk_path).resolve() + if not root.exists(): + raise HTTPException(status_code=400, detail='Workspace path does not exist') + if not target.exists() or not target.is_file(): + raise HTTPException(status_code=404, detail='APK file not found') + if root not in target.parents and target != root: + raise HTTPException(status_code=403, detail='APK is outside workspace') + if target.suffix.lower() != '.apk': + raise HTTPException(status_code=403, detail='Only APK files are allowed') + return target + + def _with_device(command: str, device: str | None, enabled: bool) -> str: if not enabled or not device: return command @@ -221,6 +238,48 @@ async def runtime_install_latest_apk(payload: RuntimeCommandRequest): return {'success': result['returncode'] == 0, 'apk': apks[0], 'device': payload.device, 'result': result} +@router.post('/install-apk') +async def runtime_install_apk(payload: RuntimeCommandRequest): + cwd = _workspace_cwd(payload.workspace) + apk = _safe_apk_path(cwd, payload.apk_path) + adb = f'{_runtime_env_prefix()}adb' + if payload.device: + adb = f'{_runtime_env_prefix()}adb -s {payload.device}' + command = f'{adb} install -r "{apk.as_posix()}"' + add_log(f'Runtime selected APK install started: {apk.name}') + result = run_command(command, cwd, timeout=900) + add_log(f"Runtime selected APK install finished: {apk.name} (exit {result['returncode']})") + return {'success': result['returncode'] == 0, 'apk': apk.as_posix(), 'device': payload.device, 'result': result} + + +@router.post('/install-apk-all-devices') +async def runtime_install_apk_all_devices(payload: RuntimeCommandRequest): + cwd = _workspace_cwd(payload.workspace) + apk = _safe_apk_path(cwd, payload.apk_path) + devices = list_devices().get('devices') or [] + installed = [] + adb_base = f'{_runtime_env_prefix()}adb' + for device in devices: + serial = device.get('serial') + if not serial: + continue + command = f'{adb_base} -s {serial} install -r "{apk.as_posix()}"' + result = run_command(command, cwd, timeout=900) + installed.append({'serial': serial, 'success': result['returncode'] == 0, 'result': result}) + add_log(f'Runtime APK install all devices finished: {apk.name}') + return {'success': all(item['success'] for item in installed) if installed else False, 'apk': apk.as_posix(), 'devices': installed} + + +@router.post('/delete-apk') +async def runtime_delete_apk(payload: RuntimeCommandRequest): + cwd = _workspace_cwd(payload.workspace) + apk = _safe_apk_path(cwd, payload.apk_path) + apk_name = apk.name + apk.unlink() + add_log(f'APK artifact deleted: {apk_name}') + return {'success': True, 'deleted': apk_name} + + @router.post('/restart-app') async def runtime_restart_app(payload: RuntimeCommandRequest): if not payload.package_name: