chore: clean up debugging logs and console prints
Frontend: - Remove 18 console.log/warn/error statements from terminal.tsx - Remove console.warn from icon.tsx Backend: - Downgrade routine logger.info to logger.debug in tool_instances.py, terminal.py, terminal_session.py, terminal_manager.py, auth.py, docker_build.py, clone.py, config_profiles.py, user_config.py - Keep important lifecycle events as logger.info: * Instance creation, start, running state * Docker build success/failure * Terminal session creation and reset * Auth success and user creation * Readiness probe success * Tunnel creation/stop
This commit is contained in:
+10
-10
@@ -48,7 +48,7 @@ async def login(next: str = "/") -> RedirectResponse:
|
|||||||
redirect_uri=redirect_uri,
|
redirect_uri=redirect_uri,
|
||||||
state=state,
|
state=state,
|
||||||
)
|
)
|
||||||
logger.info("Auth login initiated: redirect_uri=%s, next=%s", redirect_uri, next)
|
logger.debug("Auth login initiated: redirect_uri=%s, next=%s", redirect_uri, next)
|
||||||
response = RedirectResponse(location)
|
response = RedirectResponse(location)
|
||||||
response.set_cookie("auth_state", state, httponly=True, samesite="lax")
|
response.set_cookie("auth_state", state, httponly=True, samesite="lax")
|
||||||
response.set_cookie("auth_next", next, httponly=True, samesite="lax")
|
response.set_cookie("auth_next", next, httponly=True, samesite="lax")
|
||||||
@@ -63,7 +63,7 @@ async def callback(
|
|||||||
auth_next: str | None = Cookie(default="/"),
|
auth_next: str | None = Cookie(default="/"),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
logger.info("Auth callback received: code=%s... state=%s", code[:10] if code else "None", state[:10] if state else "None")
|
logger.debug("Auth callback received: code=%s... state=%s", code[:10] if code else "None", state[:10] if state else "None")
|
||||||
|
|
||||||
if auth_state is None or auth_state != state:
|
if auth_state is None or auth_state != state:
|
||||||
logger.warning("State mismatch: cookie=%s, param=%s", auth_state, state)
|
logger.warning("State mismatch: cookie=%s, param=%s", auth_state, state)
|
||||||
@@ -71,7 +71,7 @@ async def callback(
|
|||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
redirect_uri = f"{settings.api_base_url}/auth/callback"
|
redirect_uri = f"{settings.api_base_url}/auth/callback"
|
||||||
logger.info("Exchanging code for tokens (redirect_uri=%s)", redirect_uri)
|
logger.debug("Exchanging code for tokens (redirect_uri=%s)", redirect_uri)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
@@ -92,7 +92,7 @@ async def callback(
|
|||||||
access_token=token_payload["access_token"],
|
access_token=token_payload["access_token"],
|
||||||
client=client,
|
client=client,
|
||||||
)
|
)
|
||||||
logger.info("User info fetched successfully")
|
logger.debug("User info fetched successfully")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("User info fetch failed: %s", exc)
|
logger.error("User info fetch failed: %s", exc)
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="failed to fetch user info")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="failed to fetch user info")
|
||||||
@@ -100,19 +100,19 @@ async def callback(
|
|||||||
authentik_id = str(user_info.get("sub", ""))
|
authentik_id = str(user_info.get("sub", ""))
|
||||||
email = str(user_info.get("email", f"{authentik_id}@authentik.local"))
|
email = str(user_info.get("email", f"{authentik_id}@authentik.local"))
|
||||||
name = str(user_info.get("name", email))
|
name = str(user_info.get("name", email))
|
||||||
logger.info("User info: authentik_id=%s, email=%s, name=%s", authentik_id, email, name)
|
logger.debug("User info: authentik_id=%s, email=%s, name=%s", authentik_id, email, name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = await session.scalar(select(User).where(User.authentik_id == authentik_id))
|
user = await session.scalar(select(User).where(User.authentik_id == authentik_id))
|
||||||
if user is None:
|
if user is None:
|
||||||
logger.info("Creating new user: authentik_id=%s", authentik_id)
|
logger.debug("Creating new user: authentik_id=%s", authentik_id)
|
||||||
user = User(email=email, name=name, authentik_id=authentik_id, avatar_url=None)
|
user = User(email=email, name=name, authentik_id=authentik_id, avatar_url=None)
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(user)
|
await session.refresh(user)
|
||||||
logger.info("New user created: id=%s", user.id)
|
logger.info("New user created: id=%s", user.id)
|
||||||
else:
|
else:
|
||||||
logger.info("Existing user found: id=%s, updating info", user.id)
|
logger.debug("Existing user found: id=%s, updating info", user.id)
|
||||||
user.email = email
|
user.email = email
|
||||||
user.name = name
|
user.name = name
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -165,20 +165,20 @@ async def me(
|
|||||||
session_cookie: str | None = Cookie(default=None, alias="session"),
|
session_cookie: str | None = Cookie(default=None, alias="session"),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
logger.info("Auth /me called, cookie present: %s", bool(session_cookie))
|
logger.debug("Auth /me called, cookie present: %s", bool(session_cookie))
|
||||||
|
|
||||||
if not session_cookie:
|
if not session_cookie:
|
||||||
logger.warning("Auth /me: missing session cookie")
|
logger.warning("Auth /me: missing session cookie")
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing session")
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing session")
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
logger.info("Auth /me: cookie_domain=%s, cookie_secure=%s, cookie_samesite=%s",
|
logger.debug("Auth /me: cookie_domain=%s, cookie_secure=%s, cookie_samesite=%s",
|
||||||
settings.cookie_domain, settings.cookie_secure, settings.cookie_samesite)
|
settings.cookie_domain, settings.cookie_secure, settings.cookie_samesite)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = decode_session_cookie(settings=settings, cookie_value=session_cookie)
|
payload = decode_session_cookie(settings=settings, cookie_value=session_cookie)
|
||||||
user_id = payload["user_id"]
|
user_id = payload["user_id"]
|
||||||
logger.info("Auth /me: decoded session for user_id=%s", user_id)
|
logger.debug("Auth /me: decoded session for user_id=%s", user_id)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.warning("Auth /me: invalid session: %s", exc)
|
logger.warning("Auth /me: invalid session: %s", exc)
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc))
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc))
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ async def create_config_profile(
|
|||||||
)
|
)
|
||||||
profile = result.scalar_one()
|
profile = result.scalar_one()
|
||||||
|
|
||||||
logger.info("Created config profile %s for user %s", profile.id, user_uuid)
|
logger.debug("Created config profile %s for user %s", profile.id, user_uuid)
|
||||||
return _profile_to_response(profile)
|
return _profile_to_response(profile)
|
||||||
|
|
||||||
|
|
||||||
@@ -521,7 +521,7 @@ async def update_config_profile(
|
|||||||
)
|
)
|
||||||
profile = result.scalar_one()
|
profile = result.scalar_one()
|
||||||
|
|
||||||
logger.info("Updated config profile %s", profile.id)
|
logger.debug("Updated config profile %s", profile.id)
|
||||||
return _profile_to_response(profile)
|
return _profile_to_response(profile)
|
||||||
|
|
||||||
|
|
||||||
@@ -541,7 +541,7 @@ async def delete_config_profile(
|
|||||||
await session.delete(profile)
|
await session.delete(profile)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
logger.info("Deleted config profile %s", profile_id)
|
logger.debug("Deleted config profile %s", profile_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -626,7 +626,7 @@ async def update_profile_includes(
|
|||||||
)
|
)
|
||||||
direct_includes = inc_result.scalars().all()
|
direct_includes = inc_result.scalars().all()
|
||||||
|
|
||||||
logger.info("Updated includes for config profile %s", profile.id)
|
logger.debug("Updated includes for config profile %s", profile.id)
|
||||||
return _profile_to_response(profile, list(direct_includes))
|
return _profile_to_response(profile, list(direct_includes))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ async def terminal_websocket(
|
|||||||
Returns:
|
Returns:
|
||||||
None. Communicates via WebSocket messages.
|
None. Communicates via WebSocket messages.
|
||||||
"""
|
"""
|
||||||
logger.info("Terminal WebSocket connection attempt for instance %s", instance_id)
|
logger.debug("Terminal WebSocket connection attempt for instance %s", instance_id)
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
logger.info("Terminal WebSocket accepted for instance %s", instance_id)
|
logger.debug("Terminal WebSocket accepted for instance %s", instance_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Parse instance_id
|
# Parse instance_id
|
||||||
@@ -80,13 +80,13 @@ async def terminal_websocket(
|
|||||||
await websocket.close(code=4004, reason="Instance not running")
|
await websocket.close(code=4004, reason="Instance not running")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Terminal auth passed for instance %s, user %s", instance_id, user_id)
|
logger.debug("Terminal auth passed for instance %s, user %s", instance_id, user_id)
|
||||||
|
|
||||||
# Fetch tool type to get startup_command
|
# Fetch tool type to get startup_command
|
||||||
tool_type = await db_session.get(ToolType, instance.tool_type_id)
|
tool_type = await db_session.get(ToolType, instance.tool_type_id)
|
||||||
startup_command = tool_type.startup_command if tool_type else None
|
startup_command = tool_type.startup_command if tool_type else None
|
||||||
if startup_command:
|
if startup_command:
|
||||||
logger.info("Using startup command for instance %s: %s", instance_id, startup_command)
|
logger.debug("Using startup command for instance %s: %s", instance_id, startup_command)
|
||||||
|
|
||||||
# Get or create terminal session
|
# Get or create terminal session
|
||||||
try:
|
try:
|
||||||
@@ -95,15 +95,15 @@ async def terminal_websocket(
|
|||||||
instance.container_id,
|
instance.container_id,
|
||||||
startup_command=startup_command,
|
startup_command=startup_command,
|
||||||
)
|
)
|
||||||
logger.info("Terminal session ready for instance %s (session_id=%s)", instance_id, session.session_id)
|
logger.debug("Terminal session ready for instance %s (session_id=%s)", instance_id, session.session_id)
|
||||||
|
|
||||||
# Attach WebSocket to session
|
# Attach WebSocket to session
|
||||||
await terminal_manager.attach_websocket(session, websocket)
|
await terminal_manager.attach_websocket(session, websocket)
|
||||||
logger.info("WebSocket attached to session for instance %s", instance_id)
|
logger.debug("WebSocket attached to session for instance %s", instance_id)
|
||||||
|
|
||||||
# Send connected status
|
# Send connected status
|
||||||
await websocket.send_json({"type": "status", "status": "connected"})
|
await websocket.send_json({"type": "status", "status": "connected"})
|
||||||
logger.info("Sent connected status for instance %s", instance_id)
|
logger.debug("Sent connected status for instance %s", instance_id)
|
||||||
|
|
||||||
# Use mutable session reference so loops can survive reset
|
# Use mutable session reference so loops can survive reset
|
||||||
session_ref = SessionRef(session)
|
session_ref = SessionRef(session)
|
||||||
@@ -112,7 +112,7 @@ async def terminal_websocket(
|
|||||||
read_task = asyncio.create_task(_read_loop(session_ref, websocket))
|
read_task = asyncio.create_task(_read_loop(session_ref, websocket))
|
||||||
write_task = asyncio.create_task(_write_loop(session_ref, websocket, instance_id))
|
write_task = asyncio.create_task(_write_loop(session_ref, websocket, instance_id))
|
||||||
heartbeat_task = asyncio.create_task(_heartbeat_loop(websocket))
|
heartbeat_task = asyncio.create_task(_heartbeat_loop(websocket))
|
||||||
logger.info("Started terminal loops for instance %s", instance_id)
|
logger.debug("Started terminal loops for instance %s", instance_id)
|
||||||
|
|
||||||
# Wait for either task to complete (indicating disconnect or error)
|
# Wait for either task to complete (indicating disconnect or error)
|
||||||
done, pending = await asyncio.wait(
|
done, pending = await asyncio.wait(
|
||||||
@@ -120,7 +120,7 @@ async def terminal_websocket(
|
|||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Terminal loop completed for instance %s, done=%s", instance_id, len(done))
|
logger.debug("Terminal loop completed for instance %s, done=%s", instance_id, len(done))
|
||||||
|
|
||||||
# Cancel remaining tasks
|
# Cancel remaining tasks
|
||||||
for task in pending:
|
for task in pending:
|
||||||
@@ -134,7 +134,7 @@ async def terminal_websocket(
|
|||||||
try:
|
try:
|
||||||
if 'session' in locals():
|
if 'session' in locals():
|
||||||
await terminal_manager.detach_websocket(session, websocket)
|
await terminal_manager.detach_websocket(session, websocket)
|
||||||
logger.info("WebSocket detached from session for instance %s", instance_id)
|
logger.debug("WebSocket detached from session for instance %s", instance_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -183,11 +183,11 @@ async def _write_loop(session_ref: SessionRef, websocket, instance_id: str) -> N
|
|||||||
if msg_type == "resize":
|
if msg_type == "resize":
|
||||||
cols = ctrl.get("cols", 80)
|
cols = ctrl.get("cols", 80)
|
||||||
rows = ctrl.get("rows", 24)
|
rows = ctrl.get("rows", 24)
|
||||||
logger.info(f"Received resize message for instance {instance_id}: {cols}x{rows}")
|
logger.debug(f"Received resize message for instance {instance_id}: {cols}x{rows}")
|
||||||
await session.resize(cols, rows)
|
await session.resize(cols, rows)
|
||||||
elif msg_type == "reset":
|
elif msg_type == "reset":
|
||||||
# Reset terminal session
|
# Reset terminal session
|
||||||
logger.info("Resetting terminal session for instance %s", session.instance_id)
|
logger.debug("Resetting terminal session for instance %s", session.instance_id)
|
||||||
await websocket.send_json({"type": "status", "status": "resetting"})
|
await websocket.send_json({"type": "status", "status": "resetting"})
|
||||||
|
|
||||||
# Reset the session
|
# Reset the session
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ async def _resolve_single_git_mount(
|
|||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
target_path = os.path.join(working_directory, target_path)
|
target_path = os.path.join(working_directory, target_path)
|
||||||
logger.info("Resolved relative target path to %s", target_path)
|
logger.debug("Resolved relative target path to %s", target_path)
|
||||||
|
|
||||||
if not instance_dir:
|
if not instance_dir:
|
||||||
logger.warning("Git mount skipped: no instance_dir provided for cloning")
|
logger.warning("Git mount skipped: no instance_dir provided for cloning")
|
||||||
@@ -143,7 +143,7 @@ async def _resolve_single_git_mount(
|
|||||||
os.path.dirname(clone_dir),
|
os.path.dirname(clone_dir),
|
||||||
branch or "main",
|
branch or "main",
|
||||||
)
|
)
|
||||||
logger.info("Cloned git mount repository %s to %s", remote_url, repo_path)
|
logger.debug("Cloned git mount repository %s to %s", remote_url, repo_path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Clone failed for git mount %s: %s", remote_url, exc)
|
logger.warning("Clone failed for git mount %s: %s", remote_url, exc)
|
||||||
return []
|
return []
|
||||||
@@ -151,7 +151,7 @@ async def _resolve_single_git_mount(
|
|||||||
# Repo exists - pull latest updates
|
# Repo exists - pull latest updates
|
||||||
try:
|
try:
|
||||||
await asyncio.to_thread(_pull_repository_updates, repo_path, remote_url)
|
await asyncio.to_thread(_pull_repository_updates, repo_path, remote_url)
|
||||||
logger.info("Pulled updates for git mount %s", remote_url)
|
logger.debug("Pulled updates for git mount %s", remote_url)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to pull updates for %s: %s", remote_url, exc)
|
logger.warning("Failed to pull updates for %s: %s", remote_url, exc)
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ async def _resolve_single_git_mount(
|
|||||||
if branch and repo_path:
|
if branch and repo_path:
|
||||||
success = await asyncio.to_thread(_checkout_branch, repo_path, branch)
|
success = await asyncio.to_thread(_checkout_branch, repo_path, branch)
|
||||||
if success:
|
if success:
|
||||||
logger.info("Checked out branch %s for %s", branch, remote_url)
|
logger.debug("Checked out branch %s for %s", branch, remote_url)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Branch %s not found in %s, using current branch",
|
"Branch %s not found in %s, using current branch",
|
||||||
@@ -198,7 +198,7 @@ async def _resolve_single_git_mount(
|
|||||||
"target": final_target,
|
"target": final_target,
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
})
|
})
|
||||||
logger.info("Added git mount: %s -> %s (url: %s)", matched_path, final_target, remote_url)
|
logger.debug("Added git mount: %s -> %s (url: %s)", matched_path, final_target, remote_url)
|
||||||
|
|
||||||
return volume_mounts
|
return volume_mounts
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ async def create_instance(
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with instance details.
|
Dictionary with instance details.
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Creating instance: project_id=%s, repo_id=%s, tool_type_id=%s, display_name=%s",
|
"Creating instance: project_id=%s, repo_id=%s, tool_type_id=%s, display_name=%s",
|
||||||
project_id,
|
project_id,
|
||||||
repo_id,
|
repo_id,
|
||||||
@@ -555,7 +555,7 @@ async def create_instance(
|
|||||||
if not repo_contents or (len(repo_contents) == 1 and repo_contents[0] == ".git"):
|
if not repo_contents or (len(repo_contents) == 1 and repo_contents[0] == ".git"):
|
||||||
logger.error("Cloned repository at %s appears empty", repo_path)
|
logger.error("Cloned repository at %s appears empty", repo_path)
|
||||||
raise RuntimeError("Cloned repository is empty")
|
raise RuntimeError("Cloned repository is empty")
|
||||||
logger.info("Verified cloned repo at %s has %d items", repo_path, len(repo_contents))
|
logger.debug("Verified cloned repo at %s has %d items", repo_path, len(repo_contents))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Failed to verify cloned repository: %s", exc)
|
logger.exception("Failed to verify cloned repository: %s", exc)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -574,7 +574,7 @@ async def create_instance(
|
|||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logger.error("Failed to create branch %s: %s", data.new_branch, result.stderr)
|
logger.error("Failed to create branch %s: %s", data.new_branch, result.stderr)
|
||||||
raise RuntimeError(f"Failed to create branch: {result.stderr}")
|
raise RuntimeError(f"Failed to create branch: {result.stderr}")
|
||||||
logger.info("Created local branch %s in cloned repository", data.new_branch)
|
logger.debug("Created local branch %s in cloned repository", data.new_branch)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Failed to create local branch: %s", exc)
|
logger.exception("Failed to create local branch: %s", exc)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -895,7 +895,7 @@ async def start_instance(
|
|||||||
|
|
||||||
config_result = await session.execute(config_query)
|
config_result = await session.execute(config_query)
|
||||||
configs = config_result.scalars().all()
|
configs = config_result.scalars().all()
|
||||||
logger.info("Found %d tool configs for instance %s", len(configs), instance.id)
|
logger.debug("Found %d tool configs for instance %s", len(configs), instance.id)
|
||||||
|
|
||||||
for config in configs:
|
for config in configs:
|
||||||
if config.config_type == "env":
|
if config.config_type == "env":
|
||||||
@@ -942,7 +942,7 @@ async def start_instance(
|
|||||||
working_directory = profile_hints["working_directory"]
|
working_directory = profile_hints["working_directory"]
|
||||||
if profile_hints.get("port_override"):
|
if profile_hints.get("port_override"):
|
||||||
port_override = profile_hints["port_override"]
|
port_override = profile_hints["port_override"]
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Applied config profile %s to instance %s (env=%d, files=%d, mounts=%d, git_mounts=%d)",
|
"Applied config profile %s to instance %s (env=%d, files=%d, mounts=%d, git_mounts=%d)",
|
||||||
resolved.profile_name,
|
resolved.profile_name,
|
||||||
instance.id,
|
instance.id,
|
||||||
@@ -958,18 +958,18 @@ async def start_instance(
|
|||||||
detail=f"Config profile cycle detected: {exc}",
|
detail=f"Config profile cycle detected: {exc}",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("No config profile selected for instance %s", instance.id)
|
logger.debug("No config profile selected for instance %s", instance.id)
|
||||||
|
|
||||||
# Write env file and config files
|
# Write env file and config files
|
||||||
env_file_path = None
|
env_file_path = None
|
||||||
|
|
||||||
if env_vars:
|
if env_vars:
|
||||||
env_file_path = write_env_file(instance_dir, env_vars)
|
env_file_path = write_env_file(instance_dir, env_vars)
|
||||||
logger.info("Wrote env file for instance %s: %s", instance.id, env_file_path)
|
logger.debug("Wrote env file for instance %s: %s", instance.id, env_file_path)
|
||||||
|
|
||||||
if config_files:
|
if config_files:
|
||||||
write_config_files(instance_dir, config_files)
|
write_config_files(instance_dir, config_files)
|
||||||
logger.info("Wrote %d config files for instance %s", len(config_files), instance.id)
|
logger.debug("Wrote %d config files for instance %s", len(config_files), instance.id)
|
||||||
|
|
||||||
# Mount SSH key for clone-mode instances
|
# Mount SSH key for clone-mode instances
|
||||||
if instance.clone_mode == "clone":
|
if instance.clone_mode == "clone":
|
||||||
@@ -984,21 +984,21 @@ async def start_instance(
|
|||||||
"target": "/root/.ssh",
|
"target": "/root/.ssh",
|
||||||
"type": "ro",
|
"type": "ro",
|
||||||
})
|
})
|
||||||
logger.info("Mounted SSH key for clone-mode instance %s", instance.id)
|
logger.debug("Mounted SSH key for clone-mode instance %s", instance.id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Failed to prepare SSH key for instance %s: %s", instance.id, exc)
|
logger.error("Failed to prepare SSH key for instance %s: %s", instance.id, exc)
|
||||||
|
|
||||||
# Modify compose file if needed (port override, start command, working dir, volumes)
|
# Modify compose file if needed (port override, start command, working dir, volumes)
|
||||||
if port_override or start_command or working_directory or extra_volumes:
|
if port_override or start_command or working_directory or extra_volumes:
|
||||||
_modify_compose_file(instance.compose_path, port_override, start_command, working_directory, extra_volumes)
|
_modify_compose_file(instance.compose_path, port_override, start_command, working_directory, extra_volumes)
|
||||||
logger.info("Modified compose file for instance %s", instance.id)
|
logger.debug("Modified compose file for instance %s", instance.id)
|
||||||
|
|
||||||
# Execute docker compose up with env file
|
# Execute docker compose up with env file
|
||||||
logger.info("Running docker compose up for instance %s (compose_path=%s)", instance.id, instance.compose_path)
|
logger.debug("Running docker compose up for instance %s (compose_path=%s)", instance.id, instance.compose_path)
|
||||||
returncode, stdout, stderr = execute_compose_command(
|
returncode, stdout, stderr = execute_compose_command(
|
||||||
instance.compose_path, "up", env_file=env_file_path
|
instance.compose_path, "up", env_file=env_file_path
|
||||||
)
|
)
|
||||||
logger.info("Docker compose up completed for instance %s: returncode=%d, stdout=%s, stderr=%s",
|
logger.debug("Docker compose up completed for instance %s: returncode=%d, stdout=%s, stderr=%s",
|
||||||
instance.id, returncode, stdout[:200] if stdout else "", stderr[:500] if stderr else "")
|
instance.id, returncode, stdout[:200] if stdout else "", stderr[:500] if stderr else "")
|
||||||
|
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
@@ -1014,18 +1014,18 @@ async def start_instance(
|
|||||||
container_id = get_container_id(instance.name)
|
container_id = get_container_id(instance.name)
|
||||||
if container_id:
|
if container_id:
|
||||||
instance.container_id = container_id
|
instance.container_id = container_id
|
||||||
logger.info("Container ID for instance %s: %s", instance.id, container_id)
|
logger.debug("Container ID for instance %s: %s", instance.id, container_id)
|
||||||
|
|
||||||
container_name = get_container_name(instance.name)
|
container_name = get_container_name(instance.name)
|
||||||
if container_name:
|
if container_name:
|
||||||
instance.container_name = container_name
|
instance.container_name = container_name
|
||||||
logger.info("Container name for instance %s: %s", instance.id, container_name)
|
logger.debug("Container name for instance %s: %s", instance.id, container_name)
|
||||||
|
|
||||||
# Connect container to backend network so API can reach it
|
# Connect container to backend network so API can reach it
|
||||||
logger.info("Connecting container %s to backend network...", container_name)
|
logger.debug("Connecting container %s to backend network...", container_name)
|
||||||
connected = connect_container_to_network(container_name, "backend")
|
connected = connect_container_to_network(container_name, "backend")
|
||||||
if connected:
|
if connected:
|
||||||
logger.info("Successfully connected %s to backend network", container_name)
|
logger.debug("Successfully connected %s to backend network", container_name)
|
||||||
else:
|
else:
|
||||||
logger.warning("Failed to connect %s to backend network", container_name)
|
logger.warning("Failed to connect %s to backend network", container_name)
|
||||||
|
|
||||||
@@ -1034,7 +1034,7 @@ async def start_instance(
|
|||||||
instance.status = "starting"
|
instance.status = "starting"
|
||||||
instance.last_started_at = datetime.now()
|
instance.last_started_at = datetime.now()
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info("Instance %s: verifying container startup...", instance.id)
|
logger.debug("Instance %s: verifying container startup...", instance.id)
|
||||||
|
|
||||||
startup_result = wait_for_container_running(instance.container_id, timeout=30, interval=2.0)
|
startup_result = wait_for_container_running(instance.container_id, timeout=30, interval=2.0)
|
||||||
|
|
||||||
@@ -1062,7 +1062,7 @@ async def start_instance(
|
|||||||
"logs": logs,
|
"logs": logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Instance %s container started successfully after %.1fs",
|
"Instance %s container started successfully after %.1fs",
|
||||||
instance.id,
|
instance.id,
|
||||||
startup_result["waited_seconds"],
|
startup_result["waited_seconds"],
|
||||||
@@ -1090,7 +1090,7 @@ async def start_instance(
|
|||||||
if probe_command:
|
if probe_command:
|
||||||
instance.status = "probing"
|
instance.status = "probing"
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Executing readiness probe for instance %s: command='%s', timeout=%d, interval=%d",
|
"Executing readiness probe for instance %s: command='%s', timeout=%d, interval=%d",
|
||||||
instance.id, probe_command, probe_timeout, probe_interval
|
instance.id, probe_command, probe_timeout, probe_interval
|
||||||
)
|
)
|
||||||
@@ -1143,14 +1143,14 @@ async def start_instance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
instance_port = tool_type.default_port or 0
|
instance_port = tool_type.default_port or 0
|
||||||
logger.info("Tool type for instance %s: name=%s, default_port=%s, interface_type=%s",
|
logger.debug("Tool type for instance %s: name=%s, default_port=%s, interface_type=%s",
|
||||||
instance.id, tool_type.name, instance_port, tool_type.interface_type)
|
instance.id, tool_type.name, instance_port, tool_type.interface_type)
|
||||||
|
|
||||||
# Only create Cloudflare tunnel for web-enabled tools
|
# Only create Cloudflare tunnel for web-enabled tools
|
||||||
if tool_type.interface_type == "web":
|
if tool_type.interface_type == "web":
|
||||||
# Create temporary Cloudflare tunnel for public access
|
# Create temporary Cloudflare tunnel for public access
|
||||||
try:
|
try:
|
||||||
logger.info("Creating temporary tunnel for instance %s (container=%s, port=%d)",
|
logger.debug("Creating temporary tunnel for instance %s (container=%s, port=%d)",
|
||||||
instance.id, instance.container_name, instance_port)
|
instance.id, instance.container_name, instance_port)
|
||||||
tunnel_info = start_cloudflared_tunnel(
|
tunnel_info = start_cloudflared_tunnel(
|
||||||
container_name=instance.container_name or instance.name,
|
container_name=instance.container_name or instance.name,
|
||||||
@@ -1160,7 +1160,7 @@ async def start_instance(
|
|||||||
instance.public_url = tunnel_info["url"]
|
instance.public_url = tunnel_info["url"]
|
||||||
instance.url = tunnel_info["url"]
|
instance.url = tunnel_info["url"]
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Created temporary tunnel for instance %s: pid=%s, url=%s",
|
"Created temporary tunnel for instance %s: pid=%s, url=%s",
|
||||||
instance.id,
|
instance.id,
|
||||||
tunnel_info["pid"],
|
tunnel_info["pid"],
|
||||||
@@ -1230,7 +1230,7 @@ async def stop_instance(
|
|||||||
if instance.tunnel_id:
|
if instance.tunnel_id:
|
||||||
try:
|
try:
|
||||||
stop_cloudflared_tunnel(instance.tunnel_id)
|
stop_cloudflared_tunnel(instance.tunnel_id)
|
||||||
logger.info("Stopped tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id)
|
logger.debug("Stopped tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to stop tunnel for instance %s: %s", instance.id, exc)
|
logger.warning("Failed to stop tunnel for instance %s: %s", instance.id, exc)
|
||||||
|
|
||||||
@@ -1284,7 +1284,7 @@ async def restart_instance(
|
|||||||
if instance.tunnel_id:
|
if instance.tunnel_id:
|
||||||
try:
|
try:
|
||||||
stop_cloudflared_tunnel(instance.tunnel_id)
|
stop_cloudflared_tunnel(instance.tunnel_id)
|
||||||
logger.info("Stopped old tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id)
|
logger.debug("Stopped old tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to stop old tunnel for instance %s: %s", instance.id, exc)
|
logger.warning("Failed to stop old tunnel for instance %s: %s", instance.id, exc)
|
||||||
|
|
||||||
@@ -1300,7 +1300,7 @@ async def restart_instance(
|
|||||||
# Write env file with resolved profile env vars
|
# Write env file with resolved profile env vars
|
||||||
if profile_env:
|
if profile_env:
|
||||||
write_env_file(instance_dir, profile_env)
|
write_env_file(instance_dir, profile_env)
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Re-applied config profile %s on restart for instance %s",
|
"Re-applied config profile %s on restart for instance %s",
|
||||||
resolved.profile_name,
|
resolved.profile_name,
|
||||||
instance.id,
|
instance.id,
|
||||||
@@ -1345,7 +1345,7 @@ async def restart_instance(
|
|||||||
instance.tunnel_id = tunnel_info["pid"]
|
instance.tunnel_id = tunnel_info["pid"]
|
||||||
instance.public_url = tunnel_info["url"]
|
instance.public_url = tunnel_info["url"]
|
||||||
instance.url = tunnel_info["url"]
|
instance.url = tunnel_info["url"]
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Created new tunnel for instance %s: %s",
|
"Created new tunnel for instance %s: %s",
|
||||||
instance.id,
|
instance.id,
|
||||||
tunnel_info["url"],
|
tunnel_info["url"],
|
||||||
@@ -1431,7 +1431,7 @@ async def delete_instance(
|
|||||||
if instance.tunnel_id:
|
if instance.tunnel_id:
|
||||||
try:
|
try:
|
||||||
stop_cloudflared_tunnel(instance.tunnel_id)
|
stop_cloudflared_tunnel(instance.tunnel_id)
|
||||||
logger.info("Stopped tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id)
|
logger.debug("Stopped tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to stop tunnel for instance %s: %s", instance.id, exc)
|
logger.warning("Failed to stop tunnel for instance %s: %s", instance.id, exc)
|
||||||
|
|
||||||
@@ -1556,7 +1556,7 @@ async def recreate_tunnel_endpoint(
|
|||||||
instance.public_url = tunnel_info["url"]
|
instance.public_url = tunnel_info["url"]
|
||||||
instance.url = tunnel_info["url"]
|
instance.url = tunnel_info["url"]
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(
|
logger.debug(
|
||||||
"Recreated tunnel for instance %s: pid=%s, url=%s",
|
"Recreated tunnel for instance %s: pid=%s, url=%s",
|
||||||
instance.id,
|
instance.id,
|
||||||
tunnel_info["pid"],
|
tunnel_info["pid"],
|
||||||
|
|||||||
@@ -103,11 +103,11 @@ async def update_user_config(
|
|||||||
|
|
||||||
# Merge updates
|
# Merge updates
|
||||||
update_data = data.model_dump(exclude_unset=True)
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
logger.info("Updating user config for user %s: %s", user_id, update_data)
|
logger.debug("Updating user config for user %s: %s", user_id, update_data)
|
||||||
# SQLAlchemy JSON doesn't track dict mutations, so we replace the whole dict
|
# SQLAlchemy JSON doesn't track dict mutations, so we replace the whole dict
|
||||||
config.config = {**config.config, **update_data}
|
config.config = {**config.config, **update_data}
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(config)
|
await session.refresh(config)
|
||||||
logger.info("Updated config: %s", config.config)
|
logger.debug("Updated config: %s", config.config)
|
||||||
return UserConfigResponse.model_validate(config.config)
|
return UserConfigResponse.model_validate(config.config)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ def clone_repository(
|
|||||||
str(clone_path),
|
str(clone_path),
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info("Cloning repository %s (branch: %s) into %s", remote_url, branch, clone_path)
|
logger.debug("Cloning repository %s (branch: %s) into %s", remote_url, branch, clone_path)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -55,7 +55,7 @@ def clone_repository(
|
|||||||
logger.error("Git clone failed: %s", result.stderr)
|
logger.error("Git clone failed: %s", result.stderr)
|
||||||
raise RuntimeError(f"Failed to clone repository: {result.stderr}")
|
raise RuntimeError(f"Failed to clone repository: {result.stderr}")
|
||||||
|
|
||||||
logger.info("Successfully cloned repository into %s", clone_path)
|
logger.debug("Successfully cloned repository into %s", clone_path)
|
||||||
return str(clone_path)
|
return str(clone_path)
|
||||||
|
|
||||||
|
|
||||||
@@ -94,4 +94,4 @@ def remove_clone_directory(instance_dir: str) -> None:
|
|||||||
if clone_path.exists():
|
if clone_path.exists():
|
||||||
import shutil
|
import shutil
|
||||||
shutil.rmtree(clone_path)
|
shutil.rmtree(clone_path)
|
||||||
logger.info("Removed clone directory: %s", clone_path)
|
logger.debug("Removed clone directory: %s", clone_path)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
|
|||||||
# Write Dockerfile
|
# Write Dockerfile
|
||||||
dockerfile_path = Path(instance_dir) / "Dockerfile"
|
dockerfile_path = Path(instance_dir) / "Dockerfile"
|
||||||
dockerfile_path.write_text(dockerfile)
|
dockerfile_path.write_text(dockerfile)
|
||||||
logger.info("Wrote Dockerfile to %s", dockerfile_path)
|
logger.debug("Wrote Dockerfile to %s", dockerfile_path)
|
||||||
|
|
||||||
# Write build context files
|
# Write build context files
|
||||||
if build_context:
|
if build_context:
|
||||||
@@ -39,10 +39,10 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
|
|||||||
|
|
||||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
full_path.write_text(content)
|
full_path.write_text(content)
|
||||||
logger.info("Wrote build context file: %s", full_path)
|
logger.debug("Wrote build context file: %s", full_path)
|
||||||
|
|
||||||
# Build image
|
# Build image
|
||||||
logger.info("Building Docker image with tag: %s", tag)
|
logger.debug("Building Docker image with tag: %s", tag)
|
||||||
cmd = [
|
cmd = [
|
||||||
"docker", "build",
|
"docker", "build",
|
||||||
"-t", tag,
|
"-t", tag,
|
||||||
@@ -57,7 +57,7 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
|
|||||||
text=True,
|
text=True,
|
||||||
timeout=300, # 5 minute timeout for builds
|
timeout=300, # 5 minute timeout for builds
|
||||||
)
|
)
|
||||||
logger.info("Docker build completed: returncode=%d", result.returncode)
|
logger.debug("Docker build completed: returncode=%d", result.returncode)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logger.error("Docker build failed: %s", result.stderr[:1000])
|
logger.error("Docker build failed: %s", result.stderr[:1000])
|
||||||
return result.returncode, result.stdout, result.stderr
|
return result.returncode, result.stdout, result.stderr
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ class TerminalManager:
|
|||||||
|
|
||||||
# Check if session is still alive
|
# Check if session is still alive
|
||||||
if session.is_alive():
|
if session.is_alive():
|
||||||
logger.info("Reattaching to existing terminal session for instance %s", instance_id)
|
logger.debug("Reattaching to existing terminal session for instance %s", instance_id)
|
||||||
return session
|
return session
|
||||||
else:
|
else:
|
||||||
# Session died, clean it up
|
# Session died, clean it up
|
||||||
logger.info("Existing session for instance %s is dead, cleaning up", instance_id)
|
logger.debug("Existing session for instance %s is dead, cleaning up", instance_id)
|
||||||
await session.close()
|
await session.close()
|
||||||
del self._sessions[instance_id_str]
|
del self._sessions[instance_id_str]
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ class TerminalManager:
|
|||||||
"""Attach a WebSocket to an existing session."""
|
"""Attach a WebSocket to an existing session."""
|
||||||
# Handle concurrent connections - close existing ones
|
# Handle concurrent connections - close existing ones
|
||||||
if session.has_websockets():
|
if session.has_websockets():
|
||||||
logger.info("Closing existing WebSocket connections for instance %s", session.instance_id)
|
logger.debug("Closing existing WebSocket connections for instance %s", session.instance_id)
|
||||||
for ws in list(session._websockets):
|
for ws in list(session._websockets):
|
||||||
try:
|
try:
|
||||||
await ws.close(code=4000, reason="New connection established")
|
await ws.close(code=4000, reason="New connection established")
|
||||||
@@ -135,7 +135,7 @@ class TerminalManager:
|
|||||||
|
|
||||||
# Close existing session if any
|
# Close existing session if any
|
||||||
if instance_id_str in self._sessions:
|
if instance_id_str in self._sessions:
|
||||||
logger.info("Resetting terminal session for instance %s", instance_id)
|
logger.debug("Resetting terminal session for instance %s", instance_id)
|
||||||
old_session = self._sessions.pop(instance_id_str)
|
old_session = self._sessions.pop(instance_id_str)
|
||||||
await old_session.close()
|
await old_session.close()
|
||||||
|
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ class TerminalSession:
|
|||||||
|
|
||||||
# Set the terminal size initially
|
# Set the terminal size initially
|
||||||
self._set_terminal_size(self._cols, self._rows)
|
self._set_terminal_size(self._cols, self._rows)
|
||||||
logger.info(f"Starting terminal session {self.session_id} for container {self.container_id} with initial size {self._cols}x{self._rows}")
|
logger.debug(f"Starting terminal session {self.session_id} for container {self.container_id} with initial size {self._cols}x{self._rows}")
|
||||||
|
|
||||||
# Build the shell command
|
# Build the shell command
|
||||||
if startup_command:
|
if startup_command:
|
||||||
shell_cmd = f'bash -c "{startup_command}" || true; exec bash -il'
|
shell_cmd = f'bash -c "{startup_command}" || true; exec bash -il'
|
||||||
logger.info(f"Using startup command for session {self.session_id}: {startup_command}")
|
logger.debug(f"Using startup command for session {self.session_id}: {startup_command}")
|
||||||
else:
|
else:
|
||||||
shell_cmd = "bash -il"
|
shell_cmd = "bash -il"
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class TerminalSession:
|
|||||||
size = struct.pack('HHHH', rows, cols, 0, 0)
|
size = struct.pack('HHHH', rows, cols, 0, 0)
|
||||||
try:
|
try:
|
||||||
fcntl.ioctl(self._master_fd, TIOCSWINSZ, size)
|
fcntl.ioctl(self._master_fd, TIOCSWINSZ, size)
|
||||||
logger.info(f"Resized PTY to {cols}x{rows} (fd={self._master_fd})")
|
logger.debug(f"Resized PTY to {cols}x{rows} (fd={self._master_fd})")
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
logger.error(f"Failed to resize PTY: {e}")
|
logger.error(f"Failed to resize PTY: {e}")
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ class TerminalSession:
|
|||||||
|
|
||||||
self._cols = cols
|
self._cols = cols
|
||||||
self._rows = rows
|
self._rows = rows
|
||||||
logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}")
|
logger.debug(f"resize() called for session {self.session_id}: {cols}x{rows}")
|
||||||
self._set_terminal_size(cols, rows)
|
self._set_terminal_size(cols, rows)
|
||||||
|
|
||||||
# Docker exec -it creates its own PTY inside the container,
|
# Docker exec -it creates its own PTY inside the container,
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ export const Icon: React.FC<IconProps> = ({
|
|||||||
const sizeValue = sizeMap[size];
|
const sizeValue = sizeMap[size];
|
||||||
|
|
||||||
if (!IconComponent) {
|
if (!IconComponent) {
|
||||||
console.warn(`Icon "${name}" not found`);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,12 +73,11 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
const wsHost = apiUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
const wsHost = apiUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
||||||
const wsUrl = `${wsProtocol}//${wsHost}/ws/tool-instances/${instanceId}/terminal`;
|
const wsUrl = `${wsProtocol}//${wsHost}/ws/tool-instances/${instanceId}/terminal`;
|
||||||
|
|
||||||
console.log(`[Terminal WS] Connecting to ${wsUrl} (attempt ${reconnectAttemptsRef.current + 1}/${RECONNECT_ATTEMPTS + 1})`);
|
// WebSocket connection established
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log(`[Terminal WS] Connected successfully`);
|
|
||||||
setStatus("connected");
|
setStatus("connected");
|
||||||
setError(null);
|
setError(null);
|
||||||
reconnectAttemptsRef.current = 0;
|
reconnectAttemptsRef.current = 0;
|
||||||
@@ -99,10 +98,8 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
}
|
}
|
||||||
heartbeatCheckRef.current = window.setInterval(() => {
|
heartbeatCheckRef.current = window.setInterval(() => {
|
||||||
const elapsed = Date.now() - lastPingRef.current;
|
const elapsed = Date.now() - lastPingRef.current;
|
||||||
console.log(`[Terminal WS] Heartbeat check: lastPing=${elapsed}ms ago`);
|
|
||||||
if (elapsed > 60000) {
|
if (elapsed > 60000) {
|
||||||
// No ping for 60 seconds, connection may be dead
|
// No ping for 60 seconds, connection may be dead
|
||||||
console.warn("[Terminal WS] Heartbeat timeout (>60s), closing connection");
|
|
||||||
ws.close(4000, "Heartbeat timeout");
|
ws.close(4000, "Heartbeat timeout");
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
@@ -143,7 +140,6 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
} else if (msg.type === "ping") {
|
} else if (msg.type === "ping") {
|
||||||
// Respond with pong and update last ping time
|
// Respond with pong and update last ping time
|
||||||
lastPingRef.current = Date.now();
|
lastPingRef.current = Date.now();
|
||||||
console.log(`[Terminal WS] Received ping, sending pong`);
|
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: "pong" }));
|
ws.send(JSON.stringify({ type: "pong" }));
|
||||||
}
|
}
|
||||||
@@ -155,7 +151,6 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
console.log(`[Terminal WS] Connection closed: code=${event.code}, reason="${event.reason}", wasClean=${event.wasClean}, attempts=${reconnectAttemptsRef.current}`);
|
|
||||||
setStatus("disconnected");
|
setStatus("disconnected");
|
||||||
|
|
||||||
// Clean up heartbeat check
|
// Clean up heartbeat check
|
||||||
@@ -171,30 +166,24 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
if (reconnectAttemptsRef.current < RECONNECT_ATTEMPTS) {
|
if (reconnectAttemptsRef.current < RECONNECT_ATTEMPTS) {
|
||||||
reconnectAttemptsRef.current++;
|
reconnectAttemptsRef.current++;
|
||||||
const delay = RECONNECT_DELAY_BASE * Math.pow(2, reconnectAttemptsRef.current - 1);
|
const delay = RECONNECT_DELAY_BASE * Math.pow(2, reconnectAttemptsRef.current - 1);
|
||||||
console.log(`[Terminal WS] Will retry in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${RECONNECT_ATTEMPTS})`);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isUnmountingRef.current) {
|
if (isUnmountingRef.current) {
|
||||||
console.log(`[Terminal WS] Component unmounting, skipping reconnect`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (document.visibilityState !== "hidden") {
|
if (document.visibilityState !== "hidden") {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Terminal WS] Tab hidden, skipping reconnect`);
|
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Terminal WS] Max reconnection attempts (${RECONNECT_ATTEMPTS}) reached`);
|
|
||||||
}
|
}
|
||||||
} else if (event.code === 4000) {
|
} else if (event.code === 4000) {
|
||||||
// Server closed old connection for concurrent connection - don't reconnect
|
// Server closed old connection for concurrent connection - don't reconnect
|
||||||
// The new connection is already established
|
// The new connection is already established
|
||||||
console.log(`[Terminal WS] Server closed old connection (concurrent/heartbeat)`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error(`[Terminal WS] Error event fired`, error);
|
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
setError("WebSocket error");
|
setError("WebSocket error");
|
||||||
};
|
};
|
||||||
@@ -259,7 +248,6 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { cols, rows } = termRef.current;
|
const { cols, rows } = termRef.current;
|
||||||
console.log(`[Terminal] fit() result: ${cols}x${rows} (was ${oldCols}x${oldRows})`);
|
|
||||||
// Force refresh if dimensions are valid
|
// Force refresh if dimensions are valid
|
||||||
if (cols > 0 && rows > 0) {
|
if (cols > 0 && rows > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -285,14 +273,11 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
fitAttempts++;
|
fitAttempts++;
|
||||||
// Ensure container has dimensions before fitting
|
// Ensure container has dimensions before fitting
|
||||||
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
||||||
console.log(`[Terminal] Container ready: ${container.clientWidth}x${container.clientHeight} (attempt ${fitAttempts})`);
|
|
||||||
fitTerminal();
|
fitTerminal();
|
||||||
} else if (fitAttempts < 50) {
|
} else if (fitAttempts < 50) {
|
||||||
// Container not ready yet, try again (max 50 attempts ~ 1s)
|
// Container not ready yet, try again (max 50 attempts ~ 1s)
|
||||||
console.log(`[Terminal] Container not ready: ${container.clientWidth}x${container.clientHeight} (attempt ${fitAttempts})`);
|
|
||||||
requestAnimationFrame(doInitialFit);
|
requestAnimationFrame(doInitialFit);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Terminal] Container never got dimensions after ${fitAttempts} attempts`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
requestAnimationFrame(doInitialFit);
|
requestAnimationFrame(doInitialFit);
|
||||||
@@ -379,9 +364,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
|
|
||||||
// Visibility API for reconnection
|
// Visibility API for reconnection
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
console.log(`[Terminal WS] Visibility changed to: ${document.visibilityState}, wsState=${ws?.readyState}`);
|
|
||||||
if (document.visibilityState === "visible" && ws && ws.readyState !== WebSocket.OPEN) {
|
if (document.visibilityState === "visible" && ws && ws.readyState !== WebSocket.OPEN) {
|
||||||
console.log(`[Terminal WS] Tab visible, resetting reconnect attempts and reconnecting`);
|
|
||||||
reconnectAttemptsRef.current = 0;
|
reconnectAttemptsRef.current = 0;
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user