Repository navigation
fix(cli): exit 1 instead of aborting on uncaught filesystem errors - #179
Conversation
`cgraph seam fuse --out <dir under a read-only parent>` (and the one-shot
build) aborted with exit 134: create_directories threw an uncaught
std::filesystem::filesystem_error, and main only caught FileWriteError.
- atomic_write: add create_output_directories, which throws FileWriteError
("failed to write <dir>: cannot create directory (<reason>)") so output
directory failures read like every other output write. Used by the CLI
output paths, write_exports, and the semantic stat index / cache writers
(whose daemon callers already log FileWriteError as non-fatal).
- cli/main: one std::exception handler prints `cgraph: <what>` and exits 1.
- graphd, cgraph-client, cgraph-mcp mains: same last-resort handler, so
e.g. `graphd --idle-timeout abc` or a deleted cwd exits 1, not 134.
- mcp_server: a throwing forwarder fails only that request (-32603) instead
of escaping and killing the stdio loop.
Graph output unchanged; kIndexVersionKey not bumped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR brief: 0 agent commits · run the full suiteWarning Run the full suite. The graph cannot vouch for this diff, so every test file is selected. Nothing is skipped; this is the safe default, not a failure. 2 files have no graph node (config, asset, or unextracted). If these files cannot change which tests should run, add a pattern for them to All 2 unmapped filesComputed at
What each commit did
Claims checkedno checkpoint on this branch makes a claim the graph can check blastline 0.13.0. Selection is advisory unless your workflow gates on it. How selection works. Intent: 0 of 4 commits. Not checked: symbol changes: no |
…e errors - graphd: exceptions in the serve loop, the initial-build thread, or the enrichment worker are logged with their type and stop the daemon with exit 1. A scope guard joins both workers and releases the endpoint on any exit, so nothing unwinds past a joinable std::thread (std::terminate). The exit-time incremental persist is skipped after a failure. - cgraph-mcp: handle_mcp_request answers a parseable line of the wrong shape (`5`, `[1]`, a non-string method) with -32600, id null, and the stdio loop keeps serving. - New cgraph/cli_support: integer_flag/number_flag/parse_integer replace std::stoi/stoll/stod/stol at every CLI and graphd flag site, so a bad value exits 2 with "<prog>: --depth expects an integer, got 'abc'"; describe_exception puts the demangled type in last-resort handlers. - Tests: daemon_server_test (fatal serve-loop and build-thread failures), mcp_main_test (stdio loop), mcp_server_test, cli_support_test, cli_main_test (--depth abc), atomic_write_test (create_output_directories). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- cli_support_test: assert only "filesystem_error: " and the path; libc++ demangles the type as std::__1::__fs::filesystem::filesystem_error. - mcp_server: an Invalid Request echoes the request's id when it is an object member that is a string, number or null; otherwise null. - cli_support: guard <cxxabi.h> for MSVC, whose type_info::name() is already readable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts with main: - daemon_server.cpp: take main's LocalSocket serve loop (#203) and re-apply this branch's exception safety on top (fatal flag, worker try/catch, ScopeExit worker join + endpoint release, skip the dirty flush after a failure, exit 1). The per-connection close guard is dropped: LocalSocket closes itself. - mcp_server.cpp: keep respond() + the json-exception wrapper and thread main's listed_tools through both; keep main's new public helpers. - mcp/main.cpp, semantic_cache.cpp, tests: keep both sides. Fix the failing and soon-to-fail tests: - cli_support_test: libc++ quotes the path (["/x"]); match "/x" only. - describe_exception: drop MSVC's "class "/"struct " key so every platform names the type the same way (cli_support_test expected that already). - mcp_main_test, atomic_write_test, daemon_server_test: guard the POSIX-only cases (fork/pipe, geteuid, chmod) on Windows, which main now builds in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_output_directories passed error.value() to failure(), which reads it as an errno. On Windows std::filesystem reports Win32 codes in system_category, so ERROR_ACCESS_DENIED (5) printed as EIO. Use error.message() instead; the text is unchanged on POSIX. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tgod009
left a comment
There was a problem hiding this comment.
Independent review of the main merge (e4d09b5) plus 918dc77: NON-BLOCKING, ready to merge.
- daemon_server.cpp resolution: both workers are joined on every exit path (stop_workers is declared after build_thread and enrichment_worker, so it runs first); the endpoint is cleaned exactly once; the listener is reset only after the last accept; nothing from main (#203, #207) was dropped.
- mcp_server.cpp: listed_tools is threaded through respond() and the json-exception wrapper.
- MSVC/Windows: no unguarded POSIX calls; POSIX-only tests are skipped on Windows.
- The one SHOULD (create_output_directories naming Win32 codes as errno) is fixed in 918dc77.
CI on 918dc77: 12/12 green, including windows-default and windows-release. Local ctest: 104/104.
Note, not this PR: the earlier windows-release failure on e4d09b5 ("incremental state re-persisted to graph.json") came from "cannot rename graph.json.tmp into place: Access is denied". The test polls graph.json with an ifstream while the daemon renames over it, and Windows refuses to replace an open file. The rename code and that test loop are unchanged from main.
…fix the graph.json Windows flake (#212) * fix(daemon): do not stop on a file Windows reports as access denied std::filesystem::exists(path) throws when the path cannot be stat'ed. Windows reports a file that an editor is replacing as "Access is denied", so the enrichment walk (classify_cached_file, is_valid_cached_record) and the watcher's incremental update threw, and the daemon stopped: exit 1 since #179, std::terminate before. Seen on windows-default CI as "enrichment refresh failed: ... exists: Access is denied ... docs\guide.md". Use the error_code overloads. A file that cannot be checked counts as absent for this pass, as the neighbouring file_size/last_write_time checks already treat it; the watcher reports it again once written. Regression tests reproduce the throw on POSIX with an unsearchable parent directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(daemon): read daemon-written files with FILE_SHARE_DELETE on Windows daemon_server_test polled graph.json and the stat index with std::ifstream every 20 ms. MSVC opens without FILE_SHARE_DELETE, so the daemon's MoveFileExW replace failed with access denied for its whole 10 s retry window and the test gave up (windows-release flake "incremental state re-persisted to graph.json"). Restart Manager named the test process as the only holder. read_shared opens with FILE_SHARE_DELETE on Windows, so the replace goes through while a read is in flight; POSIX is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(file-cache): a failed exists() is Unreadable, not Deleted Review of #212: classify_cached_file returned Deleted when exists() failed, so a full stat rescan that met a file mid-save on Windows dropped that file's nodes until the next edit. Unreadable makes every caller keep the last verified entry (full_stat_index_rescan refuses, the incremental path and the chunk planner skip). A clean not-found is still Deleted. incremental_update_test now indexes the file before locking it and checks its nodes survive the unreadable Modified event; a mutant that erases indexed files on a failed check fails it. Drop the redundant <windows.h> block in daemon_server_test (unreadable_file.hpp already includes it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Problem
On bin-v0.8.3,
cgraph seam fuse --seam ... --graph ... --out <dir whose parent is read-only>aborts with a core dump:#173 made
maincatchcgraph::FileWriteError, butstd::filesystem::create_directories(and any other exception) still escaped.graphd,cgraph-clientandcgraph-mcphad no handler at all.Fix
atomic_write: newcreate_output_directories(dir). It throwsFileWriteErrorwith the same message style as fix(io): fail loud when an output file cannot be written whole #173:failed to write <dir>: cannot create directory (Permission denied). It is used by the CLI output paths (build, seam gen/discover/fuse),write_exports,write_semantic_stat_indexandwrite_semantic_cache. The daemon callers of the last two already logFileWriteErroras non-fatal (persist_stat_index, the semantic cache write indaemon_server.cpp), so a directory failure there no longer killsgraphdeither.cli/main: a singlestd::exceptionhandler printscgraph: <what>and exits 1. That coversFileWriteError,filesystem_error(itswhat()names the path) and anything else.graphd,cgraph-client,cgraph-mcp: each gets the same last-resort handler (<tool>: <what>, exit 1).mcp_server: if the forwarder throws, only that request fails, with JSON-RPC-32603carrying the message. The stdio loop keeps running. Before this change onlygraph_change_contextwas caught.Graph output is unchanged and
kIndexVersionKeyis not bumped.Evidence
The new
cli_main_testcase was run against the released binary (bin-v0.8.3, revision 269048a; there is nosrc/ortests/diff from 269048a to main):The same test against this branch prints
cli_main_test: ok(exit 0).mcp_server_testwith themcp_server.cpphunk reverted aborts withterminate called ... cannot create directories: Permission denied [/ro/daemon-dir](exit 134). With the fix it exits 0.Manual end-to-end runs, released binary vs this branch:
graphd --root /tmp --idle-timeout abcterminate ... stollgraphd: stollcgraph-client statusfrom a deleted cwdcgraph-client: filesystem error: cannot get current path: No such file or directorycgraph-mcpfrom a deleted cwdcgraph-mcp: filesystem error: cannot get current path: ...Full suite:
ctest --test-dir build/default -j6passed 95 of 96. The one failure iscgraph_file_watcher_test, which is a known local failure on this host.Review round 1
1. graphd exception safety (
src/engine/daemon_server.cpp)graphd: <where> failed: <type>: <what>). The daemon then stops andrun_daemon_serverreturns 1.ScopeExitguard joins both workers, closes the listen socket and runscleanup_daemon_endpointon every exit path. Nothing unwinds past a joinablestd::threadany more.try. Review withgit diff -w.daemon_server_test: the root's parent loses search permission, soweakly_canonical(root)throwsfilesystem_error. This is tested twice: from anupdate(serve loop) and from the initial build (build thread). Before the fix the test aborts (exit 134,terminate called ... status: Permission denied [/tmp/cgraph_daemon_fatal_test/proj]). After the fix it exits 0, and both cases return 1 with the endpoint removed.2. cgraph-mcp malformed lines (
src/mcp/mcp_server.cpp)handle_mcp_requestcatchesnlohmann::json::exceptionand answers -32600 withid: null. It does this per request, so the stdio loop keeps serving.mcp_main_test: it pipes5,[1]and{"jsonrpc":"2.0","id":3,"method":7}into the realcgraph-mcp, followed bytools/list.cgraph-mcp: ... type_error.306 ... cannot use value() with number, exit 1, and no responses.tools/listis still answered.mcp_server_testalso checks these shapes, which abort before the fix.3. Numeric flags (new
cgraph/cli_support)stoi/stoll/stod/stolinsrc/cli/main.cppand graphd's--idle-timeout:parse_integer<T>,integer_flag<T>andnumber_flag. They usefrom_charsfor integers andstrtodfor numbers, because macOS libc++ lacks floating-pointfrom_chars.cgraph: --depth expects an integer, got 'abc'orgraphd: --idle-timeout expects an integer, got 'abc'.stats --sincereusesparse_integer, so5xhis now rejected;stolused to read it as 5.change-contextkeeps its JSON error output and now exits 2 for a bad flag.cli_main_testrunsreport modules --depth abc.what(): stoi).cgraph: stoi).cli_support_testcovers the helpers.graphd --idle-timeout abcrow in the table above: it now exits 2 with the usage message.4. Last-resort handlers
describe_exceptionadds the demangled type, for examplestd::out_of_range: map::at.std::exceptionhandlers incgraph,graphd,cgraph-clientandcgraph-mcp.FileWriteErrorkeeps fix(io): fail loud when an output file cannot be written whole #173's plain message.5.
atomic_write_testcreate_output_directoriesis now covered for an empty path (no-op), an existing directory, nested creation, and a read-only parent. The read-only case must throwFileWriteErrorwith exactlyfailed to write <dir>: cannot create directory (Permission denied).Verification
cgraph_file_watcher_test, which is a known local failure.origin/mainis merged in (already up to date atd526ca8).Review round 2
tests/smoke/cli_support_test.cpp): thefilesystem_errorcase now checks only forfilesystem_error:and[/x]. libc++ names the typestd::__1::__fs::filesystem::filesystem_error. Thestd::out_of_range:check is unchanged. I could only verify on Linux (cli_support_test: ok); the macOS fix is verified by CI only.src/mcp/mcp_server.cpp): an Invalid Request now returns the request'sidwhen the request is an object and itsidis a string, number or null. Otherwise theidis null.mcp_main_testexpectsnull,nulland3for5,[1]and{"jsonrpc":"2.0","id":3,"method":7}.mcp_server_testalso covers a string id and an object id (which gets null).mcp_server.cppthe test fails:FAIL: malformed line 3 answered with Invalid Request, id 3: {..."id":null...}. With the fix it printsmcp_main_test: ok.src/engine/cli_support.cpp):<cxxabi.h>and__cxa_demangleare now guarded by#if !defined(_MSC_VER). MSVC usestypeid(...).name(), which is already readable there.Note on number parsing:
from_charsrejects a leading+thatstoiaccepted.cgraph report modules --depth +5now printscgraph: --depth expects an integer, got '+5'and exits 2.Verification: the full suite passes 97 of 98. The one failure is
cgraph_file_watcher_test, which is a known local failure.origin/mainis merged in (already up to date).🤖 Generated with Claude Code