From f77449fc832920d12fbbf07af8fb97cbcd087141 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:25:16 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20ba?= =?UTF-8?q?tch=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=20=D1=80=D1=83=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20Flutter=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/flutter_manual_api.py | 79 +++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/backend/flutter_manual_api.py b/backend/flutter_manual_api.py index e031192..df2f8bd 100644 --- a/backend/flutter_manual_api.py +++ b/backend/flutter_manual_api.py @@ -47,6 +47,14 @@ class ManualFlutterRequest(BaseModel): timeout: int | None = 3600 +class ManualFlutterBatchRequest(BaseModel): + workspace: str + commands: str + device: str | None = None + timeout_per_command: int | None = 3600 + stop_on_error: bool = True + + def _runtime_env() -> dict: runtime_home = os.getenv('DEVCONSOLE_RUNTIME_HOME') or os.getenv('HOME') or '/home/devconsole' flutter_home = os.getenv('FLUTTER_HOME') or f'{runtime_home}/flutter' @@ -86,12 +94,25 @@ def _split_args(raw_args: str) -> list[str]: return parts +def _split_batch(raw_commands: str) -> list[str]: + commands = [] + for line in (raw_commands or '').splitlines(): + cleaned = line.strip() + if not cleaned or cleaned.startswith('#'): + continue + commands.append(cleaned) + if not commands: + raise HTTPException(status_code=400, detail='Flutter command batch is empty') + if len(commands) > 30: + raise HTTPException(status_code=400, detail='Too many commands in batch') + return commands + + def _event(payload: dict) -> str: return json.dumps(payload, ensure_ascii=False) + '\n' -def _stream_manual_flutter(workspace: str, args: str, device: str | None, timeout: int): - cwd = _workspace_cwd(workspace) +def _run_flutter_process(cwd: str, args: str, device: str | None, timeout: int, batch_index: int | None = None): parts = _split_args(args) if device and '-d' not in parts and '--device-id' not in parts: parts.extend(['-d', device]) @@ -110,19 +131,20 @@ def _stream_manual_flutter(workspace: str, args: str, device: str | None, timeou preexec_fn=os.setsid, ) ACTIVE_MANUAL_FLUTTER[task_id] = process - add_log('Manual Flutter command started: ' + ' '.join(command)) - yield _event({'type': 'start', 'task_id': task_id, 'message': ' '.join(command)}) + label = ' '.join(command) + add_log('Manual Flutter command started: ' + label) + yield _event({'type': 'start', 'task_id': task_id, 'batch_index': batch_index, 'message': label}) started_at = time.time() try: assert process.stdout is not None while True: if time.time() - started_at > timeout: - yield _event({'type': 'error', 'task_id': task_id, 'message': 'Manual Flutter command timeout'}) + yield _event({'type': 'error', 'task_id': task_id, 'batch_index': batch_index, 'message': 'Manual Flutter command timeout'}) break line = process.stdout.readline() if line: - yield _event({'type': 'line', 'task_id': task_id, 'message': line.rstrip()}) + yield _event({'type': 'line', 'task_id': task_id, 'batch_index': batch_index, 'message': line.rstrip()}) continue if process.poll() is not None: break @@ -134,8 +156,41 @@ def _stream_manual_flutter(workspace: str, args: str, device: str | None, timeou finally: ACTIVE_MANUAL_FLUTTER.pop(task_id, None) - add_log('Manual Flutter command finished: ' + ' '.join(command) + f' exit {returncode}') - yield _event({'type': 'done', 'task_id': task_id, 'returncode': returncode}) + add_log('Manual Flutter command finished: ' + label + f' exit {returncode}') + yield _event({'type': 'done', 'task_id': task_id, 'batch_index': batch_index, 'returncode': returncode, 'message': label}) + return returncode + + +def _stream_manual_flutter(workspace: str, args: str, device: str | None, timeout: int): + cwd = _workspace_cwd(workspace) + yield from _run_flutter_process(cwd, args, device, timeout) + + +def _stream_manual_flutter_batch(payload: ManualFlutterBatchRequest): + cwd = _workspace_cwd(payload.workspace) + commands = _split_batch(payload.commands) + timeout = max(30, min(int(payload.timeout_per_command or 3600), 7200)) + yield _event({'type': 'batch_start', 'message': f'Flutter batch started: {len(commands)} commands', 'total': len(commands)}) + add_log(f'Manual Flutter batch started: {len(commands)} commands') + + for index, command in enumerate(commands, start=1): + yield _event({'type': 'line', 'batch_index': index, 'message': f'▶ [{index}/{len(commands)}] {command}'}) + returncode = None + runner = _run_flutter_process(cwd, command, payload.device, timeout, index) + try: + while True: + item = next(runner) + yield item + except StopIteration as stop: + returncode = stop.value + if returncode != 0 and payload.stop_on_error: + yield _event({'type': 'error', 'batch_index': index, 'message': f'Batch stopped on error: {command}'}) + yield _event({'type': 'batch_done', 'success': False, 'failed_index': index}) + add_log(f'Manual Flutter batch stopped on command {index}') + return + + yield _event({'type': 'batch_done', 'success': True, 'message': 'Flutter batch completed'}) + add_log('Manual Flutter batch completed') @router.post('/flutter-manual-stream') @@ -145,3 +200,11 @@ async def flutter_manual_stream(payload: ManualFlutterRequest): _stream_manual_flutter(payload.workspace, payload.args, payload.device, timeout), media_type='application/x-ndjson', ) + + +@router.post('/flutter-batch-stream') +async def flutter_batch_stream(payload: ManualFlutterBatchRequest): + return StreamingResponse( + _stream_manual_flutter_batch(payload), + media_type='application/x-ndjson', + )