Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions apps/backend/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,56 @@
sys.path.insert(0, str(_PARENT_DIR))


from .analytics_commands import handle_analytics_command
from .batch_commands import (
handle_batch_cleanup_command,
handle_batch_create_command,
handle_batch_status_command,
)
from .build_commands import handle_build_command
from .followup_commands import handle_followup_command
from .migration_commands import (
handle_migration_command,
handle_migration_status_command,
)
from .predictive_scan_commands import (
handle_predictive_scan_check_command,
handle_predictive_scan_command,
handle_predictive_scan_status_command,
)
from .qa_commands import (
handle_qa_command,
handle_qa_status_command,
handle_review_status_command,
)
from .scheduler_commands import (
handle_schedule_cancel_command,
handle_schedule_command,
handle_schedule_start_command,
handle_schedule_status_command,
handle_schedule_stop_command,
)
from .server_commands import handle_server_command
from .security_commands import handle_security_audit_command
from .spec_commands import print_specs_list
from .utils import (
DEFAULT_MODEL,
find_spec,
get_project_dir,
print_banner,
setup_environment,
)
from .workspace_commands import (
handle_cleanup_worktrees_command,
handle_create_pr_command,
handle_discard_command,
handle_list_worktrees_command,
handle_merge_analytics_export_command,
handle_merge_analytics_list_command,
handle_merge_analytics_summary_command,
handle_merge_command,
handle_review_command,
)

Check failure on line 68 in apps/backend/cli/main.py

View workflow job for this annotation

GitHub Actions / Python (Ruff)

Ruff (I001)

apps/backend/cli/main.py:19:1: I001 Import block is un-sorted or un-formatted


def parse_args() -> argparse.Namespace:
Expand Down Expand Up @@ -543,6 +544,34 @@
help="Output format for security audit report (default: both)",
)

# Server daemon commands
parser.add_argument(
"--server",
type=str,
default=None,
choices=["start", "stop", "status", "logs"],
metavar="CMD",
help="Manage the web-backend server daemon (start|stop|status|logs)",
)
parser.add_argument(
"--server-host",
type=str,
default="0.0.0.0",
help="Host for server start (default: 0.0.0.0)",
)
parser.add_argument(
"--server-port",
type=int,
default=8000,
help="Port for server start (default: 8000)",
)
parser.add_argument(
"--server-log-lines",
type=int,
default=50,
help="Number of log lines to show with 'server logs' (default: 50)",
)

return parser.parse_args()


Expand Down Expand Up @@ -747,6 +776,17 @@
)
sys.exit(exit_code)

# Handle server daemon commands
if args.server:
handle_server_command(
subcommand=args.server,
project_dir=str(project_dir),
host=args.server_host,
port=args.server_port,
log_lines=args.server_log_lines,
)
return

# Handle security audit command
if args.security_audit:
# Security audit can run with or without a spec
Expand Down
311 changes: 311 additions & 0 deletions apps/backend/cli/server_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
"""
Server Commands
===============

Commands for managing the Auto Code web-backend server daemon.
Supports start, stop, status, and logs subcommands.
"""

import os
import signal
import subprocess
import sys
from pathlib import Path

from ui import print_status

Check failure on line 15 in apps/backend/cli/server_commands.py

View workflow job for this annotation

GitHub Actions / Python (Ruff)

Ruff (I001)

apps/backend/cli/server_commands.py:9:1: I001 Import block is un-sorted or un-formatted


# Default server configuration
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8000
DEFAULT_LOG_LINES = 50

# Relative path from the project root to the web-backend
_WEB_BACKEND_DIR = "apps/web-backend"


def _get_pid_file(project_dir: str) -> Path:
"""Return the path to the server PID file."""
pid_env = os.getenv("AUTO_CLAUDE_PID_FILE")
if pid_env:
return Path(pid_env)
return Path(project_dir) / "auto-claude-web.pid"


def _get_log_file(project_dir: str) -> Path:
"""Return the path to the server log file."""
log_env = os.getenv("AUTO_CLAUDE_LOG_FILE")
if log_env:
return Path(log_env)
return Path(project_dir) / ".auto-claude" / "server" / "server.log"


def _read_pid(pid_file: Path) -> int | None:
"""Read a PID from the given file; returns None if missing or invalid."""
if not pid_file.exists():
return None
try:
return int(pid_file.read_text(encoding="utf-8").strip())
except (OSError, ValueError):
return None


def _is_running(pid_file: Path) -> bool:
"""Check whether the process described by the PID file is still running."""
pid = _read_pid(pid_file)
if pid is None:
return False
try:
os.kill(pid, 0)
return True
except ProcessLookupError:
# Stale PID file
try:
pid_file.unlink()
except OSError:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI about 1 month ago

In general, to fix an “empty except” you either (a) add an explanatory comment stating clearly why the exception is being ignored, or (b) perform some minimal handling such as logging, or (c) narrow the exception type and re-raise or propagate as appropriate. Here, we don’t want to change _is_running’s behavior—the function should still return False if the process doesn’t exist, regardless of whether PID file cleanup succeeds. The safest fix is to add explicit handling while keeping the return value the same.

The single best approach here is to keep the except OSError block but replace pass with either a short comment and/or a debug log. Since we’re told not to assume logging infrastructure beyond what’s visible in the snippet, and to avoid changing imports aside from well-known libraries, the minimal, non-invasive change is to add a comment explaining that failure to delete the PID file is intentionally ignored. This satisfies CodeQL’s requirement (the “does nothing but pass and there is no explanatory comment” part) without altering behavior.

Concretely, in apps/backend/cli/server_commands.py, inside _is_running, change the except OSError: block that currently just passes so that it includes an explanatory comment before pass, e.g. # Best-effort cleanup; ignore errors when removing stale PID file. No new imports or method definitions are needed.

Suggested changeset 1
apps/backend/cli/server_commands.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/backend/cli/server_commands.py b/apps/backend/cli/server_commands.py
--- a/apps/backend/cli/server_commands.py
+++ b/apps/backend/cli/server_commands.py
@@ -63,6 +63,7 @@
         try:
             pid_file.unlink()
         except OSError:
+            # Best-effort cleanup: ignore errors when removing a stale PID file.
             pass
         return False
     except PermissionError:
EOF
@@ -63,6 +63,7 @@
try:
pid_file.unlink()
except OSError:
# Best-effort cleanup: ignore errors when removing a stale PID file.
pass
return False
except PermissionError:
Copilot is powered by AI and may make mistakes. Always verify output.
pass
return False
except PermissionError:
# Process exists but belongs to a different user
return True
except OSError:
return False


def handle_server_start_command(
project_dir: str,
host: str = DEFAULT_HOST,
port: int = DEFAULT_PORT,
) -> bool:
"""
Start the Auto Code web-backend server as a background process.

Args:
project_dir: Project root directory
host: Host to bind (default: 0.0.0.0)
port: Port to listen on (default: 8000)

Returns:
True if the server started successfully
"""
pid_file = _get_pid_file(project_dir)
log_file = _get_log_file(project_dir)

if _is_running(pid_file):
pid = _read_pid(pid_file)
print_status(f"Server is already running (PID {pid})", "warning")
return False

web_backend_dir = Path(project_dir) / _WEB_BACKEND_DIR
if not web_backend_dir.exists():
print_status(
f"Web backend directory not found: {web_backend_dir}", "error"
)
return False

# Ensure log directory exists
log_file.parent.mkdir(parents=True, exist_ok=True)

env = os.environ.copy()
env["AUTO_CLAUDE_PID_FILE"] = str(pid_file)

try:
log_fd = open(log_file, "a", encoding="utf-8") # noqa: WPS515
except OSError as exc:
print_status(f"Cannot open log file {log_file}: {exc}", "error")
return False

try:
proc = subprocess.Popen(
[
sys.executable,
"-m",
"uvicorn",
"main:app",
"--host",
host,
"--port",
str(port),
],
cwd=str(web_backend_dir),
env=env,
stdout=log_fd,
stderr=log_fd,
start_new_session=True,
)
except FileNotFoundError:
log_fd.close()
print_status(
"uvicorn not found. Install with: pip install uvicorn", "error"
)
return False
except OSError as exc:
log_fd.close()
print_status(f"Failed to start server: {exc}", "error")
return False

log_fd.close()

# Write PID file from the parent process so CLI can track it
try:
pid_file.parent.mkdir(parents=True, exist_ok=True)
pid_file.write_text(str(proc.pid), encoding="utf-8")
except OSError as exc:
print_status(f"Warning: could not write PID file: {exc}", "warning")

print_status(
f"Server started (PID {proc.pid}) on {host}:{port}", "success"
)
print_status(f"Logs: {log_file}", "info")
return True


def handle_server_stop_command(project_dir: str) -> bool:
"""
Stop the running Auto Code web-backend server.

Args:
project_dir: Project root directory

Returns:
True if the server was stopped (or was not running)
"""
pid_file = _get_pid_file(project_dir)

if not _is_running(pid_file):
print_status("Server is not running", "info")
return True

pid = _read_pid(pid_file)
if pid is None:
print_status("Could not read server PID", "error")
return False

try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
print_status(f"Process {pid} not found; cleaning up PID file", "warning")
try:
pid_file.unlink()
except OSError:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI about 1 month ago

In general, empty except blocks should either (a) be removed so the exception can propagate, or (b) explicitly document and/or log why the exception is being ignored and, if appropriate, perform compensating actions. The goal is to avoid silently discarding information about errors.

For this specific case in handle_server_stop_command (apps/backend/cli/server_commands.py, around lines 188–191), we want to keep the “best-effort cleanup” behavior—i.e., a failure to delete the PID file should not cause the stop command itself to fail—but we should not ignore the exception silently. The most consistent fix with the existing code is:

  • Catch OSError as exc.
  • Use the already-imported print_status helper to log a warning that we could not remove the PID file, including the exception message for debugging.
  • Preserve the existing control flow: after handling the OSError, still return True from the outer except ProcessLookupError block.

No new imports or helper methods are needed; print_status is already available and used for status, warning, and error messages elsewhere in the file.

Suggested changeset 1
apps/backend/cli/server_commands.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/backend/cli/server_commands.py b/apps/backend/cli/server_commands.py
--- a/apps/backend/cli/server_commands.py
+++ b/apps/backend/cli/server_commands.py
@@ -187,8 +187,11 @@
         print_status(f"Process {pid} not found; cleaning up PID file", "warning")
         try:
             pid_file.unlink()
-        except OSError:
-            pass
+        except OSError as exc:
+            print_status(
+                f"Failed to remove PID file {pid_file}: {exc}",
+                "warning",
+            )
         return True
     except PermissionError:
         print_status(
EOF
@@ -187,8 +187,11 @@
print_status(f"Process {pid} not found; cleaning up PID file", "warning")
try:
pid_file.unlink()
except OSError:
pass
except OSError as exc:
print_status(
f"Failed to remove PID file {pid_file}: {exc}",
"warning",
)
return True
except PermissionError:
print_status(
Copilot is powered by AI and may make mistakes. Always verify output.
pass
return True
except PermissionError:
print_status(
f"Permission denied sending SIGTERM to PID {pid}", "error"
)
return False
except OSError as exc:
print_status(f"Failed to stop server: {exc}", "error")
return False

print_status(f"SIGTERM sent to server (PID {pid})", "success")
return True


def handle_server_status_command(project_dir: str) -> bool:
"""
Show the current status of the Auto Code web-backend server.

Args:
project_dir: Project root directory

Returns:
True if the server is running, False if stopped
"""
pid_file = _get_pid_file(project_dir)
log_file = _get_log_file(project_dir)
running = _is_running(pid_file)

print_status("Auto Code Server Status", "info")
print()

if running:
pid = _read_pid(pid_file)
print(f" Status : \033[32mRunning\033[0m")

Check failure on line 225 in apps/backend/cli/server_commands.py

View workflow job for this annotation

GitHub Actions / Python (Ruff)

Ruff (F541)

apps/backend/cli/server_commands.py:225:15: F541 f-string without any placeholders

Check warning on line 225 in apps/backend/cli/server_commands.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add replacement fields or use a normal string instead of an f-string.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0aZbZoBnk7Uu5RP7tR&open=AZ0aZbZoBnk7Uu5RP7tR&pullRequest=181
print(f" PID : {pid}")
else:
print(f" Status : \033[31mStopped\033[0m")

Check failure on line 228 in apps/backend/cli/server_commands.py

View workflow job for this annotation

GitHub Actions / Python (Ruff)

Ruff (F541)

apps/backend/cli/server_commands.py:228:15: F541 f-string without any placeholders

Check warning on line 228 in apps/backend/cli/server_commands.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add replacement fields or use a normal string instead of an f-string.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0aZbZoBnk7Uu5RP7tS&open=AZ0aZbZoBnk7Uu5RP7tS&pullRequest=181

print(f" PID file : {pid_file}")
print(f" Log file : {log_file}")
print()

return running


def handle_server_logs_command(
project_dir: str,
lines: int = DEFAULT_LOG_LINES,
) -> bool:
"""
Display the tail of the server log file.

Args:
project_dir: Project root directory
lines: Number of log lines to show (default: 50)

Returns:
True if successful
"""
log_file = _get_log_file(project_dir)

if not log_file.exists():
print_status(f"Log file not found: {log_file}", "warning")
return False

try:
content = log_file.read_text(encoding="utf-8", errors="replace")
except OSError as exc:
print_status(f"Cannot read log file: {exc}", "error")
return False

all_lines = content.splitlines()
tail = all_lines[-lines:] if len(all_lines) > lines else all_lines

print_status(
f"Last {len(tail)} lines from {log_file}", "info"
)
print()
for line in tail:
print(line)

return True


def handle_server_command(
subcommand: str,
project_dir: str,
host: str = DEFAULT_HOST,
port: int = DEFAULT_PORT,
log_lines: int = DEFAULT_LOG_LINES,
) -> bool:
"""
Dispatch a server subcommand to the appropriate handler.

Args:
subcommand: One of start, stop, status, logs
project_dir: Project root directory
host: Host to bind when starting (default: 0.0.0.0)
port: Port when starting (default: 8000)
log_lines: Number of log lines to show with 'logs' subcommand

Returns:
True if the operation succeeded
"""
handlers = {
"start": lambda: handle_server_start_command(project_dir, host, port),
"stop": lambda: handle_server_stop_command(project_dir),
"status": lambda: handle_server_status_command(project_dir),
"logs": lambda: handle_server_logs_command(project_dir, log_lines),
}

handler = handlers.get(subcommand)
if handler is None:
valid = ", ".join(handlers.keys())
print_status(
f"Unknown server subcommand '{subcommand}'. Valid: {valid}", "error"
)
return False

return handler()
17 changes: 17 additions & 0 deletions apps/web-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ GITLAB_CLIENT_SECRET=your-gitlab-client-secret

# OAuth redirect URI (must match provider configuration)
OAUTH_REDIRECT_URI=http://localhost:8000/api/git/callback

# =============================================================================
# HEADLESS SERVER MODE CONFIGURATION
# =============================================================================

# Enable headless mode for CI/CD and automated server operation (no UI required)
# When true, the server operates without requiring a frontend connection
HEADLESS_MODE=false

# Maximum number of concurrent agent sessions allowed
# Increase for high-throughput CI/CD environments, decrease for resource-constrained servers
MAX_CONCURRENT_AGENTS=5

# Timeout in seconds for individual agent sessions
# Agents that exceed this limit will be terminated
# Default: 3600 (1 hour)
AGENT_TIMEOUT_SECONDS=3600
Loading
Loading