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:
Alex Blank
2026-05-27 22:16:59 +02:00
parent 1883825b18
commit d6ea5fb1fd
11 changed files with 77 additions and 95 deletions
+10 -10
View File
@@ -48,7 +48,7 @@ async def login(next: str = "/") -> RedirectResponse:
redirect_uri=redirect_uri,
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.set_cookie("auth_state", state, 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="/"),
session: AsyncSession = Depends(get_db_session),
) -> 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:
logger.warning("State mismatch: cookie=%s, param=%s", auth_state, state)
@@ -71,7 +71,7 @@ async def callback(
settings = Settings()
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:
try:
@@ -92,7 +92,7 @@ async def callback(
access_token=token_payload["access_token"],
client=client,
)
logger.info("User info fetched successfully")
logger.debug("User info fetched successfully")
except Exception as 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")
@@ -100,19 +100,19 @@ async def callback(
authentik_id = str(user_info.get("sub", ""))
email = str(user_info.get("email", f"{authentik_id}@authentik.local"))
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:
user = await session.scalar(select(User).where(User.authentik_id == authentik_id))
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)
session.add(user)
await session.commit()
await session.refresh(user)
logger.info("New user created: id=%s", user.id)
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.name = name
await session.commit()
@@ -165,20 +165,20 @@ async def me(
session_cookie: str | None = Cookie(default=None, alias="session"),
session: AsyncSession = Depends(get_db_session),
) -> 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:
logger.warning("Auth /me: missing session cookie")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing session")
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)
try:
payload = decode_session_cookie(settings=settings, cookie_value=session_cookie)
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:
logger.warning("Auth /me: invalid session: %s", exc)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc))
+4 -4
View File
@@ -420,7 +420,7 @@ async def create_config_profile(
)
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)
@@ -521,7 +521,7 @@ async def update_config_profile(
)
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)
@@ -541,7 +541,7 @@ async def delete_config_profile(
await session.delete(profile)
await session.commit()
logger.info("Deleted config profile %s", profile_id)
logger.debug("Deleted config profile %s", profile_id)
return None
@@ -626,7 +626,7 @@ async def update_profile_includes(
)
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))
+12 -12
View File
@@ -44,9 +44,9 @@ async def terminal_websocket(
Returns:
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()
logger.info("Terminal WebSocket accepted for instance %s", instance_id)
logger.debug("Terminal WebSocket accepted for instance %s", instance_id)
try:
# Parse instance_id
@@ -80,13 +80,13 @@ async def terminal_websocket(
await websocket.close(code=4004, reason="Instance not running")
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
tool_type = await db_session.get(ToolType, instance.tool_type_id)
startup_command = tool_type.startup_command if tool_type else None
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
try:
@@ -95,15 +95,15 @@ async def terminal_websocket(
instance.container_id,
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
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
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
session_ref = SessionRef(session)
@@ -112,7 +112,7 @@ async def terminal_websocket(
read_task = asyncio.create_task(_read_loop(session_ref, websocket))
write_task = asyncio.create_task(_write_loop(session_ref, websocket, instance_id))
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)
done, pending = await asyncio.wait(
@@ -120,7 +120,7 @@ async def terminal_websocket(
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
for task in pending:
@@ -134,7 +134,7 @@ async def terminal_websocket(
try:
if 'session' in locals():
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:
pass
@@ -183,11 +183,11 @@ async def _write_loop(session_ref: SessionRef, websocket, instance_id: str) -> N
if msg_type == "resize":
cols = ctrl.get("cols", 80)
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)
elif msg_type == "reset":
# 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"})
# Reset the session
+33 -33
View File
@@ -120,7 +120,7 @@ async def _resolve_single_git_mount(
)
return []
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:
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),
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:
logger.warning("Clone failed for git mount %s: %s", remote_url, exc)
return []
@@ -151,7 +151,7 @@ async def _resolve_single_git_mount(
# Repo exists - pull latest updates
try:
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:
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:
success = await asyncio.to_thread(_checkout_branch, repo_path, branch)
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:
logger.warning(
"Branch %s not found in %s, using current branch",
@@ -198,7 +198,7 @@ async def _resolve_single_git_mount(
"target": final_target,
"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
@@ -461,7 +461,7 @@ async def create_instance(
Returns:
Dictionary with instance details.
"""
logger.info(
logger.debug(
"Creating instance: project_id=%s, repo_id=%s, tool_type_id=%s, display_name=%s",
project_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"):
logger.error("Cloned repository at %s appears empty", repo_path)
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:
logger.exception("Failed to verify cloned repository: %s", exc)
raise HTTPException(
@@ -574,7 +574,7 @@ async def create_instance(
if result.returncode != 0:
logger.error("Failed to create branch %s: %s", data.new_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:
logger.exception("Failed to create local branch: %s", exc)
raise HTTPException(
@@ -895,7 +895,7 @@ async def start_instance(
config_result = await session.execute(config_query)
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:
if config.config_type == "env":
@@ -942,7 +942,7 @@ async def start_instance(
working_directory = profile_hints["working_directory"]
if profile_hints.get("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)",
resolved.profile_name,
instance.id,
@@ -958,18 +958,18 @@ async def start_instance(
detail=f"Config profile cycle detected: {exc}",
)
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
env_file_path = None
if 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:
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
if instance.clone_mode == "clone":
@@ -984,21 +984,21 @@ async def start_instance(
"target": "/root/.ssh",
"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:
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)
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)
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
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(
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 "")
if returncode != 0:
@@ -1014,18 +1014,18 @@ async def start_instance(
container_id = get_container_id(instance.name)
if 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)
if 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
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")
if connected:
logger.info("Successfully connected %s to backend network", container_name)
logger.debug("Successfully connected %s to backend network", container_name)
else:
logger.warning("Failed to connect %s to backend network", container_name)
@@ -1034,7 +1034,7 @@ async def start_instance(
instance.status = "starting"
instance.last_started_at = datetime.now()
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)
@@ -1062,7 +1062,7 @@ async def start_instance(
"logs": logs,
}
logger.info(
logger.debug(
"Instance %s container started successfully after %.1fs",
instance.id,
startup_result["waited_seconds"],
@@ -1090,7 +1090,7 @@ async def start_instance(
if probe_command:
instance.status = "probing"
await session.commit()
logger.info(
logger.debug(
"Executing readiness probe for instance %s: command='%s', timeout=%d, interval=%d",
instance.id, probe_command, probe_timeout, probe_interval
)
@@ -1143,14 +1143,14 @@ async def start_instance(
}
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)
# Only create Cloudflare tunnel for web-enabled tools
if tool_type.interface_type == "web":
# Create temporary Cloudflare tunnel for public access
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)
tunnel_info = start_cloudflared_tunnel(
container_name=instance.container_name or instance.name,
@@ -1160,7 +1160,7 @@ async def start_instance(
instance.public_url = tunnel_info["url"]
instance.url = tunnel_info["url"]
await session.commit()
logger.info(
logger.debug(
"Created temporary tunnel for instance %s: pid=%s, url=%s",
instance.id,
tunnel_info["pid"],
@@ -1230,7 +1230,7 @@ async def stop_instance(
if instance.tunnel_id:
try:
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:
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:
try:
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:
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
if profile_env:
write_env_file(instance_dir, profile_env)
logger.info(
logger.debug(
"Re-applied config profile %s on restart for instance %s",
resolved.profile_name,
instance.id,
@@ -1345,7 +1345,7 @@ async def restart_instance(
instance.tunnel_id = tunnel_info["pid"]
instance.public_url = tunnel_info["url"]
instance.url = tunnel_info["url"]
logger.info(
logger.debug(
"Created new tunnel for instance %s: %s",
instance.id,
tunnel_info["url"],
@@ -1431,7 +1431,7 @@ async def delete_instance(
if instance.tunnel_id:
try:
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:
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.url = tunnel_info["url"]
await session.commit()
logger.info(
logger.debug(
"Recreated tunnel for instance %s: pid=%s, url=%s",
instance.id,
tunnel_info["pid"],
+2 -2
View File
@@ -103,11 +103,11 @@ async def update_user_config(
# Merge updates
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
config.config = {**config.config, **update_data}
await session.commit()
await session.refresh(config)
logger.info("Updated config: %s", config.config)
logger.debug("Updated config: %s", config.config)
return UserConfigResponse.model_validate(config.config)
+3 -3
View File
@@ -42,7 +42,7 @@ def clone_repository(
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(
cmd,
capture_output=True,
@@ -55,7 +55,7 @@ def clone_repository(
logger.error("Git clone failed: %s", 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)
@@ -94,4 +94,4 @@ def remove_clone_directory(instance_dir: str) -> None:
if clone_path.exists():
import shutil
shutil.rmtree(clone_path)
logger.info("Removed clone directory: %s", clone_path)
logger.debug("Removed clone directory: %s", clone_path)
+4 -4
View File
@@ -24,7 +24,7 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
# Write Dockerfile
dockerfile_path = Path(instance_dir) / "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
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.write_text(content)
logger.info("Wrote build context file: %s", full_path)
logger.debug("Wrote build context file: %s", full_path)
# Build image
logger.info("Building Docker image with tag: %s", tag)
logger.debug("Building Docker image with tag: %s", tag)
cmd = [
"docker", "build",
"-t", tag,
@@ -57,7 +57,7 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
text=True,
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:
logger.error("Docker build failed: %s", result.stderr[:1000])
return result.returncode, result.stdout, result.stderr
+4 -4
View File
@@ -72,11 +72,11 @@ class TerminalManager:
# Check if session is still 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
else:
# 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()
del self._sessions[instance_id_str]
@@ -97,7 +97,7 @@ class TerminalManager:
"""Attach a WebSocket to an existing session."""
# Handle concurrent connections - close existing ones
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):
try:
await ws.close(code=4000, reason="New connection established")
@@ -135,7 +135,7 @@ class TerminalManager:
# Close existing session if any
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)
await old_session.close()
+4 -4
View File
@@ -60,12 +60,12 @@ class TerminalSession:
# Set the terminal size initially
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
if startup_command:
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:
shell_cmd = "bash -il"
@@ -102,7 +102,7 @@ class TerminalSession:
size = struct.pack('HHHH', rows, cols, 0, 0)
try:
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:
logger.error(f"Failed to resize PTY: {e}")
@@ -159,7 +159,7 @@ class TerminalSession:
self._cols = cols
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)
# Docker exec -it creates its own PTY inside the container,
-1
View File
@@ -150,7 +150,6 @@ export const Icon: React.FC<IconProps> = ({
const sizeValue = sizeMap[size];
if (!IconComponent) {
console.warn(`Icon "${name}" not found`);
return null;
}
+1 -18
View File
@@ -73,12 +73,11 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
const wsHost = apiUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
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);
wsRef.current = ws;
ws.onopen = () => {
console.log(`[Terminal WS] Connected successfully`);
setStatus("connected");
setError(null);
reconnectAttemptsRef.current = 0;
@@ -99,10 +98,8 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
}
heartbeatCheckRef.current = window.setInterval(() => {
const elapsed = Date.now() - lastPingRef.current;
console.log(`[Terminal WS] Heartbeat check: lastPing=${elapsed}ms ago`);
if (elapsed > 60000) {
// No ping for 60 seconds, connection may be dead
console.warn("[Terminal WS] Heartbeat timeout (>60s), closing connection");
ws.close(4000, "Heartbeat timeout");
}
}, 30000);
@@ -143,7 +140,6 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
} else if (msg.type === "ping") {
// Respond with pong and update last ping time
lastPingRef.current = Date.now();
console.log(`[Terminal WS] Received ping, sending pong`);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "pong" }));
}
@@ -155,7 +151,6 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
};
ws.onclose = (event) => {
console.log(`[Terminal WS] Connection closed: code=${event.code}, reason="${event.reason}", wasClean=${event.wasClean}, attempts=${reconnectAttemptsRef.current}`);
setStatus("disconnected");
// Clean up heartbeat check
@@ -171,30 +166,24 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
if (reconnectAttemptsRef.current < RECONNECT_ATTEMPTS) {
reconnectAttemptsRef.current++;
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(() => {
if (isUnmountingRef.current) {
console.log(`[Terminal WS] Component unmounting, skipping reconnect`);
return;
}
if (document.visibilityState !== "hidden") {
connectWebSocket();
} else {
console.log(`[Terminal WS] Tab hidden, skipping reconnect`);
}
}, delay);
} else {
console.log(`[Terminal WS] Max reconnection attempts (${RECONNECT_ATTEMPTS}) reached`);
}
} else if (event.code === 4000) {
// Server closed old connection for concurrent connection - don't reconnect
// The new connection is already established
console.log(`[Terminal WS] Server closed old connection (concurrent/heartbeat)`);
}
};
ws.onerror = (error) => {
console.error(`[Terminal WS] Error event fired`, error);
setStatus("error");
setError("WebSocket error");
};
@@ -259,7 +248,6 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
return;
}
const { cols, rows } = termRef.current;
console.log(`[Terminal] fit() result: ${cols}x${rows} (was ${oldCols}x${oldRows})`);
// Force refresh if dimensions are valid
if (cols > 0 && rows > 0) {
try {
@@ -285,14 +273,11 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
fitAttempts++;
// Ensure container has dimensions before fitting
if (container.clientWidth > 0 && container.clientHeight > 0) {
console.log(`[Terminal] Container ready: ${container.clientWidth}x${container.clientHeight} (attempt ${fitAttempts})`);
fitTerminal();
} else if (fitAttempts < 50) {
// 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);
} else {
console.warn(`[Terminal] Container never got dimensions after ${fitAttempts} attempts`);
}
};
requestAnimationFrame(doInitialFit);
@@ -379,9 +364,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
// Visibility API for reconnection
const handleVisibilityChange = () => {
console.log(`[Terminal WS] Visibility changed to: ${document.visibilityState}, wsState=${ws?.readyState}`);
if (document.visibilityState === "visible" && ws && ws.readyState !== WebSocket.OPEN) {
console.log(`[Terminal WS] Tab visible, resetting reconnect attempts and reconnecting`);
reconnectAttemptsRef.current = 0;
connectWebSocket();
}