v4.0.0: 라이브 서비스 /srv/homeclaw로 이전 + Android 실기기 USB 연동 + 관리자 IP 차단 뷰어
- homeclaw 라이브 서비스를 /home/kim/homeclaw에서 /srv/homeclaw(480G 전용 파티션)로 컷오버, 하드코딩된 절대경로 전부 치환 (config, mcp-servers, routes, scripts, web-ui) - Android 에뮬레이터 컨테이너 리소스를 32스레드 Xeon 기준으로 상향(3g/3cpu → 6g/10cpu) - 코드 에디터에 USB 실기기 패널 추가: 기기 감지, APK 설치, Logcat, scrcpy 화면 미러링 (companion.py에 독립 프로세스로 scrcpy 스폰하는 로직 포함) - 관리자 컨텍스트 뷰어에 차단된 IP 목록 탭 추가 - ollama primary 모델을 gemma4:31b-cloud로, 음성 TTS를 edge_tts로 전환 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -393,6 +393,54 @@ def _pick_folder():
|
||||
return path or None
|
||||
|
||||
|
||||
def _pick_file(title: str, filetypes):
|
||||
"""네이티브 OS 파일 선택 창을 열고 절대경로를 반환한다 (동기, executor에서 실행)."""
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes('-topmost', True)
|
||||
try:
|
||||
path = filedialog.askopenfilename(
|
||||
title=title or '파일 선택',
|
||||
filetypes=filetypes or [('모든 파일', '*.*')],
|
||||
)
|
||||
finally:
|
||||
root.destroy()
|
||||
return path or None
|
||||
|
||||
|
||||
def _spawn_scrcpy(serial):
|
||||
"""scrcpy를 companion.py와 완전히 독립된(disconnect/재시작에도 안 죽는) 프로세스로 띄운다.
|
||||
|
||||
exec/shell 세션의 모든 자식 프로세스는 WS 연결 종료 시 handle()의
|
||||
finally 블록에서 kill되므로, scrcpy처럼 브라우저 탭과 무관하게 계속
|
||||
떠 있어야 하는 네이티브 GUI 창은 shells/runner에 등록하지 않고
|
||||
OS 세션에서 완전히 분리해서 스폰해야 한다.
|
||||
"""
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
_refresh_env()
|
||||
scrcpy_path = shutil.which('scrcpy')
|
||||
if not scrcpy_path:
|
||||
return {'ok': False, 'error': 'scrcpy를 찾을 수 없습니다. https://github.com/Genymobile/scrcpy 에서 설치 후 PATH에 추가해주세요.'}
|
||||
|
||||
cmd = [scrcpy_path] + (['-s', serial] if serial else [])
|
||||
log_path = tempfile.mktemp(suffix='.scrcpy.log')
|
||||
try:
|
||||
log_file = open(log_path, 'w', encoding='utf-8', errors='replace')
|
||||
popen_kwargs = dict(stdout=log_file, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
|
||||
if IS_WINDOWS:
|
||||
popen_kwargs['creationflags'] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
else:
|
||||
popen_kwargs['start_new_session'] = True
|
||||
proc = subprocess.Popen(cmd, **popen_kwargs)
|
||||
except Exception as e:
|
||||
return {'ok': False, 'error': f'scrcpy 실행 실패: {e}'}
|
||||
return {'ok': True, 'pid': proc.pid, '_proc': proc, '_log_path': log_path}
|
||||
|
||||
|
||||
def _safe_join(root_path: str, rel_path: str) -> str:
|
||||
root_abs = os.path.abspath(root_path)
|
||||
full = os.path.abspath(os.path.join(root_abs, rel_path))
|
||||
@@ -579,6 +627,42 @@ async def handle(ws):
|
||||
'error': f'폴더 선택 창을 열 수 없습니다: {e}',
|
||||
}))
|
||||
|
||||
elif t == 'browse_file':
|
||||
req_id = msg.get('reqId')
|
||||
title = msg.get('title') or '파일 선택'
|
||||
filetypes = msg.get('filetypes')
|
||||
filetypes = [tuple(ft) for ft in filetypes] if filetypes else None
|
||||
try:
|
||||
path = await asyncio.get_event_loop().run_in_executor(None, _pick_file, title, filetypes)
|
||||
await ws.send(json.dumps({'type': 'browse_file_result', 'reqId': req_id, 'path': path}))
|
||||
except Exception as e:
|
||||
await ws.send(json.dumps({
|
||||
'type': 'browse_file_result', 'reqId': req_id, 'path': None,
|
||||
'error': f'파일 선택 창을 열 수 없습니다: {e}',
|
||||
}))
|
||||
|
||||
elif t == 'scrcpy_start':
|
||||
req_id = msg.get('reqId')
|
||||
serial = msg.get('serial')
|
||||
try:
|
||||
result = await asyncio.get_event_loop().run_in_executor(None, _spawn_scrcpy, serial)
|
||||
if result.get('ok'):
|
||||
proc = result.pop('_proc')
|
||||
log_path = result.pop('_log_path')
|
||||
await asyncio.sleep(1.2)
|
||||
if proc.poll() is not None:
|
||||
# 스폰 직후 바로 종료됨 — 로그 tail을 에러로 반환
|
||||
tail = ''
|
||||
try:
|
||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
tail = f.read()[-2000:]
|
||||
except Exception:
|
||||
pass
|
||||
result = {'ok': False, 'error': f'scrcpy가 바로 종료되었습니다: {tail or "(로그 없음)"}'}
|
||||
await ws.send(json.dumps({'type': 'scrcpy_result', 'reqId': req_id, **result}))
|
||||
except Exception as e:
|
||||
await ws.send(json.dumps({'type': 'scrcpy_result', 'reqId': req_id, 'ok': False, 'error': str(e)}))
|
||||
|
||||
elif t == 'fs_list':
|
||||
req_id = msg.get('reqId')
|
||||
root_path = msg.get('cwd') or ''
|
||||
|
||||
Reference in New Issue
Block a user