Skip to content

Commit 8867e56

Browse files
committed
feat: add --daemon to codegen run
1 parent a87d17d commit 8867e56

File tree

19 files changed

+367
-144
lines changed

19 files changed

+367
-144
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ dependencies = [
7474
"httpx>=0.28.1",
7575
"docker>=6.1.3",
7676
"urllib3>=2.0.0",
77+
"colorlog>=6.9.0",
7778
]
7879

7980
license = { text = "Apache-2.0" }

src/codegen/cli/commands/run/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@
1414
@requires_init
1515
@click.argument("label", required=True)
1616
@click.option("--web", is_flag=True, help="Run the function on the web service instead of locally")
17+
@click.option("--daemon", is_flag=True, help="Run the function against a running daemon")
1718
@click.option("--diff-preview", type=int, help="Show a preview of the first N lines of the diff")
1819
@click.option("--arguments", type=str, help="Arguments as a json string to pass as the function's 'arguments' parameter")
1920
def run_command(
2021
session: CodegenSession,
2122
label: str,
2223
web: bool = False,
24+
daemon: bool = False,
2325
diff_preview: int | None = None,
2426
arguments: str | None = None,
2527
):
2628
"""Run a codegen function by its label."""
29+
if web and daemon:
30+
msg = "Cannot enable run on both the web and daemon"
31+
raise ValueError(msg)
32+
2733
# Ensure venv is initialized
2834
venv = VenvManager(session.codegen_dir)
2935
if not venv.is_initialized():
@@ -54,6 +60,10 @@ def run_command(
5460
from codegen.cli.commands.run.run_cloud import run_cloud
5561

5662
run_cloud(session, codemod, diff_preview=diff_preview)
63+
elif daemon:
64+
from codegen.cli.commands.run.run_daemon import run_daemon
65+
66+
run_daemon(session, codemod, diff_preview=diff_preview)
5767
else:
5868
from codegen.cli.commands.run.run_local import run_local
5969

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import rich
2+
import rich_click as click
3+
from rich.panel import Panel
4+
5+
from codegen.cli.auth.session import CodegenSession
6+
from codegen.cli.commands.start.docker_container import DockerContainer
7+
from codegen.cli.errors import ServerError
8+
from codegen.cli.rich.codeblocks import format_command
9+
from codegen.cli.rich.spinners import create_spinner
10+
from codegen.runner.clients.docker_client import DockerClient
11+
from codegen.runner.enums.warmup_state import WarmupState
12+
13+
14+
def run_daemon(session: CodegenSession, function, diff_preview: int | None = None):
15+
"""Run a function on the cloud service.
16+
17+
Args:
18+
session: The current codegen session
19+
function: The function to run
20+
diff_preview: Number of lines of diff to preview (None for all)
21+
"""
22+
with create_spinner(f"Running {function.name}...") as status:
23+
try:
24+
client = _get_docker_client(session)
25+
run_output = client.run_function(function, commit=not diff_preview)
26+
rich.print(f"✅ Ran {function.name} successfully")
27+
28+
if run_output.logs:
29+
rich.print("")
30+
panel = Panel(run_output.logs, title="[bold]Logs[/bold]", border_style="blue", padding=(1, 2), expand=False)
31+
rich.print(panel)
32+
33+
if run_output.error:
34+
rich.print("")
35+
panel = Panel(run_output.error, title="[bold]Error[/bold]", border_style="red", padding=(1, 2), expand=False)
36+
rich.print(panel)
37+
38+
if run_output.observation:
39+
# Only show diff preview if requested
40+
if diff_preview:
41+
rich.print("") # Add some spacing
42+
43+
# Split and limit diff to requested number of lines
44+
diff_lines = run_output.observation.splitlines()
45+
truncated = len(diff_lines) > diff_preview
46+
limited_diff = "\n".join(diff_lines[:diff_preview])
47+
48+
if truncated:
49+
limited_diff += "\n\n...\n\n[yellow]diff truncated to {diff_preview} lines, view the full change set on your local file system after using run with `--apply-local`[/yellow]"
50+
51+
panel = Panel(limited_diff, title="[bold]Diff Preview[/bold]", border_style="blue", padding=(1, 2), expand=False)
52+
rich.print(panel)
53+
else:
54+
rich.print("")
55+
rich.print("[yellow] No changes were produced by this codemod[/yellow]")
56+
57+
if diff_preview:
58+
rich.print("[green]✓ Changes have been applied to your local filesystem[/green]")
59+
rich.print("[yellow]→ Don't forget to commit your changes:[/yellow]")
60+
rich.print(format_command("git add ."))
61+
rich.print(format_command("git commit -m 'Applied codemod changes'"))
62+
63+
except ServerError as e:
64+
status.stop()
65+
raise click.ClickException(str(e))
66+
67+
68+
def _get_docker_client(session: CodegenSession) -> DockerClient:
69+
repo_name = session.config.repository.name
70+
if (container := DockerContainer.get(repo_name)) is None:
71+
msg = f"Codegen runner does not exist for {repo_name}. Please run 'codegen start' from {session.config.repository.path}."
72+
raise click.ClickException(msg)
73+
74+
if not container.is_running():
75+
msg = f"Codegen runner for {repo_name} is not running. Please run 'codegen start' from {session.config.repository.path}."
76+
raise click.ClickException(msg)
77+
78+
client = DockerClient(container)
79+
if not client.is_running():
80+
msg = "Codebase server is not running. Please stop the container and restart."
81+
raise click.ClickException(msg)
82+
83+
if client.server_info().warmup_state != WarmupState.COMPLETED:
84+
msg = "Runner has not finished parsing the codebase. Please wait a moment and try again."
85+
raise click.ClickException(msg)
86+
87+
return client

src/codegen/cli/commands/start/docker_container.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,74 @@
1+
from functools import cached_property
2+
13
import docker
4+
from docker import DockerClient
5+
from docker.errors import APIError, NotFound
6+
from docker.models.containers import Container
27

38

49
class DockerContainer:
5-
_client: docker.DockerClient
6-
host: str | None
7-
port: int | None
8-
name: str
10+
_client: DockerClient
11+
_container: Container | None
912

10-
def __init__(self, client: docker.DockerClient, name: str, port: int | None = None, host: str | None = None):
13+
def __init__(self, client: DockerClient, container: Container) -> None:
1114
self._client = client
12-
self.host = host
13-
self.port = port
14-
self.name = name
15+
self._container = container
16+
17+
@classmethod
18+
def get(cls, name: str) -> "DockerContainer | None":
19+
try:
20+
client = docker.from_env()
21+
container = client.containers.get(name)
22+
return cls(client=client, container=container)
23+
except NotFound:
24+
return None
25+
26+
@cached_property
27+
def name(self) -> str:
28+
return self._container.name
29+
30+
@cached_property
31+
def host(self) -> str | None:
32+
if not self.is_running():
33+
return None
34+
35+
host_config = next(iter(self._container.ports.values()))[0]
36+
return host_config["HostIp"]
37+
38+
@cached_property
39+
def port(self) -> int | None:
40+
if not self.is_running():
41+
return None
42+
43+
host_config = next(iter(self._container.ports.values()))[0]
44+
return host_config["HostPort"]
1545

1646
def is_running(self) -> bool:
1747
try:
18-
container = self._client.containers.get(self.name)
19-
return container.status == "running"
20-
except docker.errors.NotFound:
48+
return self._container.status == "running"
49+
except NotFound:
2150
return False
2251

2352
def start(self) -> bool:
2453
try:
25-
container = self._client.containers.get(self.name)
26-
container.start()
54+
self._container.start()
55+
return True
56+
except (NotFound, APIError):
57+
return False
58+
59+
def stop(self) -> bool:
60+
try:
61+
self._container.stop()
62+
return True
63+
except (NotFound, APIError):
64+
return False
65+
66+
def remove(self) -> bool:
67+
try:
68+
self.stop()
69+
self._container.remove()
2770
return True
28-
except (docker.errors.NotFound, docker.errors.APIError):
71+
except (NotFound, APIError):
2972
return False
3073

3174
def __str__(self) -> str:

src/codegen/cli/commands/start/docker_fleet.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import docker
2+
from docker.errors import NotFound
23

34
from codegen.cli.commands.start.docker_container import DockerContainer
45

@@ -15,19 +16,14 @@ def __init__(self, containers: list[DockerContainer]):
1516
def load(cls) -> "DockerFleet":
1617
try:
1718
client = docker.from_env()
18-
containers = client.containers.list(all=True, filters={"ancestor": CODEGEN_RUNNER_IMAGE})
19-
codegen_containers = []
20-
for container in containers:
19+
filters = {"ancestor": CODEGEN_RUNNER_IMAGE}
20+
containers = []
21+
for container in client.containers.list(all=True, filters=filters):
2122
if container.attrs["Config"]["Image"] == CODEGEN_RUNNER_IMAGE:
22-
if container.status == "running":
23-
host_config = next(iter(container.ports.values()))[0]
24-
codegen_container = DockerContainer(client=client, host=host_config["HostIp"], port=host_config["HostPort"], name=container.name)
25-
else:
26-
codegen_container = DockerContainer(client=client, name=container.name)
27-
codegen_containers.append(codegen_container)
28-
29-
return cls(containers=codegen_containers)
30-
except docker.errors.NotFound:
23+
containers.append(DockerContainer(client=client, container=container))
24+
25+
return cls(containers=containers)
26+
except NotFound:
3127
return cls(containers=[])
3228

3329
@property

src/codegen/cli/commands/start/main.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from rich.panel import Panel
99

1010
from codegen.cli.commands.start.docker_container import DockerContainer
11-
from codegen.cli.commands.start.docker_fleet import CODEGEN_RUNNER_IMAGE, DockerFleet
11+
from codegen.cli.commands.start.docker_fleet import CODEGEN_RUNNER_IMAGE
1212
from codegen.configs.models.secrets import SecretsConfig
1313
from codegen.git.repo_operator.local_git_repo import LocalGitRepo
1414
from codegen.git.schemas.repo_config import RepoConfig
@@ -20,13 +20,19 @@
2020
@click.command(name="start")
2121
@click.option("--platform", "-t", type=click.Choice(["linux/amd64", "linux/arm64", "linux/amd64,linux/arm64"]), default="linux/amd64,linux/arm64", help="Target platform(s) for the Docker image")
2222
@click.option("--port", "-p", type=int, default=None, help="Port to run the server on")
23-
def start_command(port: int | None, platform: str):
23+
@click.option("--detached", "-d", is_flag=True, help="Run the server in detached mode")
24+
@click.option("--skip-build", is_flag=True, help="Skip building the Docker image")
25+
@click.option("--force", "-f", is_flag=True, help="Force start the server even if it is already running")
26+
def start_command(port: int | None, platform: str, detached: bool = False, skip_build: bool = False, force: bool = False) -> None:
2427
"""Starts a local codegen server"""
2528
repo_path = Path.cwd().resolve()
2629
repo_config = RepoConfig.from_repo_path(str(repo_path))
27-
fleet = DockerFleet.load()
28-
if (container := fleet.get(repo_config.name)) is not None:
29-
return _handle_existing_container(repo_config, container)
30+
if (container := DockerContainer.get(repo_config.name)) is not None:
31+
if force:
32+
rich.print(f"[yellow]Removing existing runner {repo_config.name} to force restart[/yellow]")
33+
container.remove()
34+
else:
35+
return _handle_existing_container(repo_config, container, force)
3036

3137
codegen_version = version("codegen")
3238
rich.print(f"[bold green]Codegen version:[/bold green] {codegen_version}")
@@ -35,10 +41,11 @@ def start_command(port: int | None, platform: str):
3541
port = get_free_port()
3642

3743
try:
38-
rich.print("[bold blue]Building Docker image...[/bold blue]")
39-
_build_docker_image(codegen_root, platform)
44+
if not skip_build:
45+
rich.print("[bold blue]Building Docker image...[/bold blue]")
46+
_build_docker_image(codegen_root, platform)
4047
rich.print("[bold blue]Starting Docker container...[/bold blue]")
41-
_run_docker_container(repo_config, port)
48+
_run_docker_container(repo_config, port, detached)
4249
rich.print(Panel(f"[green]Server started successfully![/green]\nAccess the server at: [bold]http://{_default_host}:{port}[/bold]", box=ROUNDED, title="Codegen Server"))
4350
# TODO: memory snapshot here
4451
except subprocess.CalledProcessError as e:
@@ -49,7 +56,7 @@ def start_command(port: int | None, platform: str):
4956
raise click.Abort()
5057

5158

52-
def _handle_existing_container(repo_config: RepoConfig, container: DockerContainer) -> None:
59+
def _handle_existing_container(repo_config: RepoConfig, container: DockerContainer, force: bool) -> None:
5360
if container.is_running():
5461
rich.print(
5562
Panel(
@@ -86,19 +93,26 @@ def _build_docker_image(codegen_root: Path, platform: str) -> None:
8693
subprocess.run(build_cmd, check=True)
8794

8895

89-
def _run_docker_container(repo_config: RepoConfig, port: int) -> None:
96+
def _run_docker_container(repo_config: RepoConfig, port: int, detached: bool) -> None:
9097
container_repo_path = f"/app/git/{repo_config.name}"
9198
name_args = ["--name", f"{repo_config.name}"]
9299
envvars = {
93100
"REPOSITORY_LANGUAGE": repo_config.language.value,
94101
"REPOSITORY_OWNER": LocalGitRepo(repo_config.repo_path).owner,
95102
"REPOSITORY_PATH": container_repo_path,
96103
"GITHUB_TOKEN": SecretsConfig().github_token,
104+
"PYTHONUNBUFFERED": "1", # Ensure Python output is unbuffered
97105
}
98106
envvars_args = [arg for k, v in envvars.items() for arg in ("--env", f"{k}={v}")]
99107
mount_args = ["-v", f"{repo_config.repo_path}:{container_repo_path}"]
100-
entry_point = f"uv run --frozen uvicorn codegen.runner.sandbox.server:app --host {_default_host} --port {port}"
101-
run_cmd = ["docker", "run", "-d", "-p", f"{port}:{port}", *name_args, *mount_args, *envvars_args, CODEGEN_RUNNER_IMAGE, entry_point]
108+
entry_point = f"uv run --frozen uvicorn codegen.runner.servers.local_daemon:app --host {_default_host} --port {port}"
109+
port_args = ["-p", f"{port}:{port}"]
110+
detached_args = ["-d"] if detached else []
111+
run_cmd = ["docker", "run", *detached_args, *port_args, *name_args, *mount_args, *envvars_args, CODEGEN_RUNNER_IMAGE, entry_point]
102112

103113
rich.print(f"run_cmd: {str.join(' ', run_cmd)}")
104114
subprocess.run(run_cmd, check=True)
115+
116+
if detached:
117+
rich.print("[yellow]Container started in detached mode. To view logs, run:[/yellow]")
118+
rich.print(f"[bold]docker logs -f {repo_config.name}[/bold]")

src/codegen/git/repo_operator/repo_operator.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,20 @@ def viz_file_path(self) -> str:
113113
return os.path.join(self.viz_path, "graph.json")
114114

115115
def _set_bot_email(self, git_cli: GitCLI) -> None:
116+
logging.info(f"****** Setting bot email to {CODEGEN_BOT_EMAIL} ******")
116117
with git_cli.config_writer("repository") as writer:
117118
if not writer.has_section("user"):
118119
writer.add_section("user")
119120
writer.set("user", "email", CODEGEN_BOT_EMAIL)
121+
logging.info(f"****** [DONE] Setting bot email to {CODEGEN_BOT_EMAIL} ******")
120122

121123
def _set_bot_username(self, git_cli: GitCLI) -> None:
124+
logging.info(f"****** Setting bot USERNAME to {CODEGEN_BOT_NAME} ******")
122125
with git_cli.config_writer("repository") as writer:
123126
if not writer.has_section("user"):
124127
writer.add_section("user")
125128
writer.set("user", "name", CODEGEN_BOT_NAME)
129+
logging.info(f"****** [DONE] Setting bot USERNAME to {CODEGEN_BOT_NAME} ******")
126130

127131
def _unset_bot_email(self, git_cli: GitCLI) -> None:
128132
with git_cli.config_writer("repository") as writer:
@@ -136,6 +140,7 @@ def _unset_bot_username(self, git_cli: GitCLI) -> None:
136140

137141
@cached_property
138142
def git_cli(self) -> GitCLI:
143+
logger.info("**** Initializing git_cli!!!*****")
139144
git_cli = GitCLI(self.repo_path)
140145
username = None
141146
user_level = None

0 commit comments

Comments
 (0)