Skip to content

Commit eb86937

Browse files
stream lifecycle subprocess output and bracket cocoindex with lance lines (#114)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c5601b7 commit eb86937

7 files changed

Lines changed: 390 additions & 32 deletions

File tree

java_codebase_rag/cli.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,12 @@ def _cmd_init(args: argparse.Namespace) -> int:
169169
return 2
170170
cfg.index_dir.mkdir(parents=True, exist_ok=True)
171171
env = cfg.subprocess_env()
172-
coco = run_cocoindex_update(env, full_reprocess=False, quiet=bool(args.quiet))
172+
coco = run_cocoindex_update(
173+
env,
174+
full_reprocess=False,
175+
quiet=bool(args.quiet),
176+
lance_project_root=None if args.quiet else cfg.source_root,
177+
)
173178
if coco.returncode != 0:
174179
_emit(
175180
{
@@ -208,7 +213,12 @@ def _cmd_increment(args: argparse.Namespace) -> int:
208213
cfg.apply_to_os_environ()
209214
_emit_increment_kuzu_warning()
210215
env = cfg.subprocess_env()
211-
coco = run_cocoindex_update(env, full_reprocess=False, quiet=bool(args.quiet))
216+
coco = run_cocoindex_update(
217+
env,
218+
full_reprocess=False,
219+
quiet=bool(args.quiet),
220+
lance_project_root=None if args.quiet else cfg.source_root,
221+
)
212222
if coco.returncode != 0:
213223
_emit(
214224
{

java_codebase_rag/cli_progress.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""CLI-owned stderr progress lines (shared by server reprocess path and pipeline helpers)."""
2+
from __future__ import annotations
3+
4+
import asyncio
5+
import sys
6+
from pathlib import Path
7+
8+
9+
def emit_lance_cocoindex_start(project_root: Path) -> None:
10+
root = project_root.expanduser().resolve()
11+
print(
12+
f"[lance] running cocoindex update (project_root={root})",
13+
file=sys.stderr,
14+
flush=True,
15+
)
16+
17+
18+
def emit_lance_cocoindex_finish(*, elapsed_s: float, exit_code: int) -> None:
19+
print(
20+
f"[lance] cocoindex update finished in {elapsed_s:.2f}s (exit={exit_code})",
21+
file=sys.stderr,
22+
flush=True,
23+
)
24+
25+
26+
async def accumulate_and_relay_subprocess_streams(
27+
proc: asyncio.subprocess.Process,
28+
*,
29+
relay: bool,
30+
) -> tuple[bytes, bytes]:
31+
"""Read stdout and stderr until EOF; optionally copy each chunk verbatim to stderr."""
32+
stdout = proc.stdout
33+
stderr = proc.stderr
34+
if stdout is None or stderr is None:
35+
raise RuntimeError("subprocess must be created with stdout=PIPE and stderr=PIPE")
36+
37+
out_buf = bytearray()
38+
err_buf = bytearray()
39+
40+
async def drain(reader: asyncio.StreamReader, target: bytearray) -> None:
41+
while True:
42+
chunk = await reader.read(65536)
43+
if not chunk:
44+
break
45+
target.extend(chunk)
46+
if relay:
47+
sys.stderr.buffer.write(chunk)
48+
sys.stderr.buffer.flush()
49+
50+
await asyncio.gather(drain(stdout, out_buf), drain(stderr, err_buf))
51+
await proc.wait()
52+
return bytes(out_buf), bytes(err_buf)

java_codebase_rag/pipeline.py

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import os
55
import subprocess
66
import sys
7+
import threading
8+
import time
79
from pathlib import Path
810

11+
from java_codebase_rag.cli_progress import emit_lance_cocoindex_finish, emit_lance_cocoindex_start
12+
913
COCOINDEX_TARGET = "java_index_flow_lancedb.py:JavaCodeIndexLance"
1014

1115

@@ -17,11 +21,48 @@ def cocoindex_bin() -> Path:
1721
return Path(sys.executable).parent / "cocoindex"
1822

1923

24+
def _popen_stream_to_stderr(
25+
proc: subprocess.Popen[bytes],
26+
) -> tuple[str, str, int]:
27+
out_buf = bytearray()
28+
err_buf = bytearray()
29+
30+
def drain_out() -> None:
31+
assert proc.stdout is not None
32+
while True:
33+
chunk = proc.stdout.read(65536)
34+
if not chunk:
35+
break
36+
out_buf.extend(chunk)
37+
sys.stderr.buffer.write(chunk)
38+
sys.stderr.buffer.flush()
39+
40+
def drain_err() -> None:
41+
assert proc.stderr is not None
42+
while True:
43+
chunk = proc.stderr.read(65536)
44+
if not chunk:
45+
break
46+
err_buf.extend(chunk)
47+
sys.stderr.buffer.write(chunk)
48+
sys.stderr.buffer.flush()
49+
50+
t_out = threading.Thread(target=drain_out, name="stream-stdout", daemon=True)
51+
t_err = threading.Thread(target=drain_err, name="stream-stderr", daemon=True)
52+
t_out.start()
53+
t_err.start()
54+
t_out.join()
55+
t_err.join()
56+
code = proc.wait()
57+
return out_buf.decode(errors="replace"), err_buf.decode(errors="replace"), code
58+
59+
2060
def run_cocoindex_update(
2161
env: dict[str, str],
2262
*,
2363
full_reprocess: bool,
2464
quiet: bool,
65+
lance_project_root: Path | None = None,
2566
) -> subprocess.CompletedProcess[str]:
2667
exe = cocoindex_bin()
2768
if not exe.is_file():
@@ -47,13 +88,34 @@ def run_cocoindex_update(
4788
cmd.append("-f")
4889
if quiet:
4990
cmd.append("-q")
50-
return subprocess.run(
51-
cmd,
52-
cwd=str(bd),
53-
env=env,
54-
capture_output=True,
55-
text=True,
56-
)
91+
return subprocess.run(
92+
cmd,
93+
cwd=str(bd),
94+
env=env,
95+
capture_output=True,
96+
text=True,
97+
)
98+
99+
emit_lance = lance_project_root is not None
100+
if emit_lance:
101+
emit_lance_cocoindex_start(lance_project_root)
102+
t0 = time.perf_counter()
103+
code = -1
104+
out_s, err_s = "", ""
105+
try:
106+
proc = subprocess.Popen(
107+
cmd,
108+
cwd=str(bd),
109+
env=env,
110+
stdout=subprocess.PIPE,
111+
stderr=subprocess.PIPE,
112+
bufsize=0,
113+
)
114+
out_s, err_s, code = _popen_stream_to_stderr(proc)
115+
finally:
116+
if emit_lance:
117+
emit_lance_cocoindex_finish(elapsed_s=time.perf_counter() - t0, exit_code=code)
118+
return subprocess.CompletedProcess(args=cmd, returncode=code, stdout=out_s, stderr=err_s)
57119

58120

59121
def run_cocoindex_drop(env: dict[str, str], *, quiet: bool) -> subprocess.CompletedProcess[str]:
@@ -103,13 +165,24 @@ def run_build_ast_graph(
103165
]
104166
if verbose:
105167
cmd.append("--verbose")
106-
return subprocess.run(
168+
if not verbose:
169+
return subprocess.run(
170+
cmd,
171+
cwd=str(source_root),
172+
env=env or os.environ.copy(),
173+
capture_output=True,
174+
text=True,
175+
)
176+
proc = subprocess.Popen(
107177
cmd,
108178
cwd=str(source_root),
109179
env=env or os.environ.copy(),
110-
capture_output=True,
111-
text=True,
180+
stdout=subprocess.PIPE,
181+
stderr=subprocess.PIPE,
182+
bufsize=0,
112183
)
184+
out_s, err_s, code = _popen_stream_to_stderr(proc)
185+
return subprocess.CompletedProcess(args=cmd, returncode=code, stdout=out_s, stderr=err_s)
113186

114187

115188
def clip(s: str, n: int) -> str:

server.py

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55
import asyncio
66
import os
77
import sys
8+
import time
89
from pathlib import Path
910
from typing import Any, Literal
1011

1112
import mcp_v2
1213
from index_common import SBERT_MODEL
14+
from java_codebase_rag.cli_progress import (
15+
accumulate_and_relay_subprocess_streams,
16+
emit_lance_cocoindex_finish,
17+
emit_lance_cocoindex_start,
18+
)
1319
from java_codebase_rag.config import emit_legacy_env_hints_if_present, resolved_sbert_model_for_process_env
1420
from kuzu_queries import KuzuGraph, resolve_kuzu_path
1521
from mcp.server.fastmcp import FastMCP
@@ -213,25 +219,55 @@ async def run_refresh_pipeline(*, quiet: bool = False) -> RefreshIndexOutput:
213219
message=f"java_index_flow_lancedb.py not found under {root} nor {bundle_dir}",
214220
phases_run=[],
215221
)
216-
try:
217-
proc = await asyncio.create_subprocess_exec(
218-
str(cocoindex_bin),
219-
"update",
220-
_COCOINDEX_TARGET,
221-
"--full-reprocess",
222-
"-f",
223-
cwd=str(flow_path.parent),
224-
env=_cocoindex_subprocess_env(root),
225-
stdout=asyncio.subprocess.PIPE,
226-
stderr=asyncio.subprocess.PIPE,
227-
)
228-
out_b, err_b = await proc.communicate()
229-
except Exception as exc:
230-
return RefreshIndexOutput(
231-
success=False,
232-
message=f"spawn failed: {exc!s}",
233-
phases_run=[],
234-
)
222+
proc: asyncio.subprocess.Process | None = None
223+
out_b, err_b = b"", b""
224+
if quiet:
225+
try:
226+
proc = await asyncio.create_subprocess_exec(
227+
str(cocoindex_bin),
228+
"update",
229+
_COCOINDEX_TARGET,
230+
"--full-reprocess",
231+
"-f",
232+
cwd=str(flow_path.parent),
233+
env=_cocoindex_subprocess_env(root),
234+
stdout=asyncio.subprocess.PIPE,
235+
stderr=asyncio.subprocess.PIPE,
236+
)
237+
out_b, err_b = await proc.communicate()
238+
except Exception as exc:
239+
return RefreshIndexOutput(
240+
success=False,
241+
message=f"spawn failed: {exc!s}",
242+
phases_run=[],
243+
)
244+
else:
245+
emit_lance_cocoindex_start(root)
246+
t0 = time.perf_counter()
247+
code_c = -1
248+
try:
249+
proc = await asyncio.create_subprocess_exec(
250+
str(cocoindex_bin),
251+
"update",
252+
_COCOINDEX_TARGET,
253+
"--full-reprocess",
254+
"-f",
255+
cwd=str(flow_path.parent),
256+
env=_cocoindex_subprocess_env(root),
257+
stdout=asyncio.subprocess.PIPE,
258+
stderr=asyncio.subprocess.PIPE,
259+
)
260+
out_b, err_b = await accumulate_and_relay_subprocess_streams(proc, relay=True)
261+
code_c = proc.returncode if proc.returncode is not None else -1
262+
except Exception as exc:
263+
return RefreshIndexOutput(
264+
success=False,
265+
message=f"spawn failed: {exc!s}",
266+
phases_run=[],
267+
)
268+
finally:
269+
emit_lance_cocoindex_finish(elapsed_s=time.perf_counter() - t0, exit_code=code_c)
270+
assert proc is not None
235271
out = out_b.decode(errors="replace")
236272
err = err_b.decode(errors="replace")
237273
ok = proc.returncode == 0
@@ -261,7 +297,10 @@ async def run_refresh_pipeline(*, quiet: bool = False) -> RefreshIndexOutput:
261297
stderr=asyncio.subprocess.PIPE,
262298
)
263299
phases_run = ["vectors", "graph"]
264-
gout_b, gerr_b = await gproc.communicate()
300+
if quiet:
301+
gout_b, gerr_b = await gproc.communicate()
302+
else:
303+
gout_b, gerr_b = await accumulate_and_relay_subprocess_streams(gproc, relay=True)
265304
graph_code = gproc.returncode
266305
graph_out = gout_b.decode(errors="replace")
267306
graph_err = gerr_b.decode(errors="replace")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"message": "init completed", "success": true}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"exit_code": 0, "graph_exit_code": 0, "graph_stderr": "", "graph_stdout": "", "message": null, "phases_run": ["vectors", "graph"], "stderr": "", "stdout": "", "success": true}

0 commit comments

Comments
 (0)