Skip to content
Merged
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
53 changes: 31 additions & 22 deletions src/cli/main.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "cgraph/atomic_write.hpp"
#include "cgraph/cli_support.hpp"
#include "cgraph/change_context.hpp"
#include "cgraph/client_runtime.hpp"
#include "cgraph/daemon_endpoint.hpp"
Expand Down Expand Up @@ -138,14 +139,14 @@
else if (arg == "--expected-base-content-root") params["expected_base_content_root"] = value;
else if (arg == "--expected-target-content-root") params["expected_target_content_root"] = value;
else if (arg == "--budget" || arg == "--max-depth") {
std::size_t consumed = 0;
const auto number = std::stoll(value, &consumed);
if (consumed != value.size()) throw std::invalid_argument("invalid numeric argument");
params[arg == "--budget" ? "budget" : "max_depth"] = number;
params[arg == "--budget" ? "budget" : "max_depth"] = cgraph::integer_flag<long long>(arg, value);
} else throw std::invalid_argument("unknown argument: " + arg);
}
std::cout << cgraph::change_context_across_workspace(params, cgraph::ClientRequest{}).dump() << '\n';
return 0;
} catch (const cgraph::UsageError& error) {
std::cout << nlohmann::json{{"error", error.what()}}.dump() << '\n';
return 2;
} catch (const std::exception& error) {
std::cout << nlohmann::json{{"error", error.what()}}.dump() << '\n';
return 1;
Expand All @@ -158,7 +159,7 @@

// Sidecar stats.json (durable, diffable) deliberately kept out of graph.json
// so the Graphify node-link parity golden stays byte-identical.
std::filesystem::create_directories(args.output);
cgraph::create_output_directories(args.output);
cgraph::write_file_atomically(args.output / "stats.json", cgraph::build_stats_json(result.stats).dump(2));

std::cerr << "processed " << result.file_count << " files, wrote exports to " << args.output << '\n';
Expand Down Expand Up @@ -209,13 +210,12 @@
return cgraph::parse_iso8601_utc(midnight);
}
if (spec.size() > 1 && (spec.back() == 'h' || spec.back() == 'd')) {
try {
const long n = std::stol(spec.substr(0, spec.size() - 1));
const auto hours = std::chrono::hours(spec.back() == 'd' ? n * 24 : n);
return cgraph::WallClock::now() - hours;
} catch (...) {
const auto n = cgraph::parse_integer<long>(std::string_view(spec).substr(0, spec.size() - 1));
if (!n) {
return std::nullopt;
}
const auto hours = std::chrono::hours(spec.back() == 'd' ? *n * 24 : *n);
return cgraph::WallClock::now() - hours;
}
return cgraph::parse_iso8601_utc(spec);
}
Expand Down Expand Up @@ -329,17 +329,17 @@
} else if (arg == "--group-by" && has_value) {
request.params["group_by"] = argv[++index];
} else if (arg == "--depth" && has_value) {
request.params["depth"] = std::stoi(argv[++index]);
request.params["depth"] = cgraph::integer_flag<int>(arg, argv[++index]);
} else if (arg == "--budget" && has_value) {
request.params["budget"] = std::stoll(argv[++index]);
request.params["budget"] = cgraph::integer_flag<long long>(arg, argv[++index]);
} else if (arg == "--threshold" && has_value) {
request.params["threshold"] = std::stod(argv[++index]);
request.params["threshold"] = cgraph::number_flag(arg, argv[++index]);
} else if (arg == "--min-members" && has_value) {
request.params["min_members"] = std::stoi(argv[++index]);
request.params["min_members"] = cgraph::integer_flag<int>(arg, argv[++index]);
} else if (arg == "--min-tokens" && has_value) {
request.params["min_tokens"] = std::stoi(argv[++index]);
request.params["min_tokens"] = cgraph::integer_flag<int>(arg, argv[++index]);
} else if (arg == "--hops" && has_value) {
request.params["hops"] = std::stoi(argv[++index]);
request.params["hops"] = cgraph::integer_flag<int>(arg, argv[++index]);
} else if (arg == "--include-tests") {
request.params["include_tests"] = true;
} else if (arg == "--daemon" && has_value) {
Expand Down Expand Up @@ -444,7 +444,7 @@
std::cerr << " " << line << '\n';
}

std::filesystem::create_directories(out_dir);
cgraph::create_output_directories(out_dir);
const auto out_file = out_dir / "chunk_00.json";
cgraph::write_file_atomically(out_file, cgraph::to_json(result.fragment).dump(2) + '\n');
std::cerr << "seam gen: wrote " << out_file << " (" << result.fragment.nodes.size()
Expand Down Expand Up @@ -646,7 +646,7 @@
for (const auto& line : result.resolution_log) {
std::cerr << " " << line << '\n';
}
std::filesystem::create_directories(out_dir);
cgraph::create_output_directories(out_dir);
const auto out_file = out_dir / "chunk_00.json";
cgraph::write_file_atomically(out_file, cgraph::to_json(result.fragment).dump(2) + '\n');
std::cerr << "seam discover: wrote " << out_file << " (" << result.fragment.nodes.size() << " nodes, "
Expand Down Expand Up @@ -765,7 +765,7 @@
return 1;
}

std::filesystem::create_directories(out_dir);
cgraph::create_output_directories(out_dir);
cgraph::write_file_atomically(out_dir / "graph.json", cgraph::to_node_link_json(fused.graph).dump(2) + '\n');
cgraph::write_file_atomically(out_dir / "graph.html", cgraph::export_graph_html(fused.graph));
// Marker: tells graphd to serve this dir as a static read-only seam graph.
Expand Down Expand Up @@ -897,7 +897,7 @@
} else if (arg == "--cgraph" && index + 1 < argc) {
config.cgraph_binary = argv[++index];
} else if (arg == "--interval" && index + 1 < argc) {
config.reconcile_interval_seconds = std::stoi(argv[++index]);
config.reconcile_interval_seconds = cgraph::integer_flag<int>(arg, argv[++index]);
} else {
std::cerr << "cgraph daemon: unknown argument: " << arg << '\n';
return false;
Expand All @@ -915,7 +915,7 @@
"On this platform run `graphd --root PATH` directly.\n";
return 1;
#endif
const std::string sub = argc >= 3 ? argv[2] : "";

Check warning on line 918 in src/cli/main.cpp

View workflow job for this annotation

GitHub Actions / windows-latest / windows-default

unreachable code
if (sub != "install" && sub != "sync" && sub != "status" && sub != "uninstall") {
std::cerr << "usage: cgraph daemon <install|sync|status|uninstall> "
"[--search-root DIR] [--exclude DIR] [--launch-agents-dir DIR] "
Expand Down Expand Up @@ -1087,25 +1087,25 @@
"on a schedule (e.g. cron or a systemd timer).\n";
return 1;
#endif
const std::string sub = argc >= 3 ? argv[2] : "";

Check warning on line 1090 in src/cli/main.cpp

View workflow job for this annotation

GitHub Actions / windows-latest / windows-default

unreachable code
if (sub != "install" && sub != "status" && sub != "uninstall") {

Check warning on line 1091 in src/cli/main.cpp

View workflow job for this annotation

GitHub Actions / windows-latest / windows-default

unreachable code
std::cerr << "usage: cgraph drain <install|status|uninstall> [--interval SECONDS] "

Check warning on line 1092 in src/cli/main.cpp

View workflow job for this annotation

GitHub Actions / windows-latest / windows-default

unreachable code
"[--chunk-cap N] [--script PATH] [--launch-agents-dir DIR] [--client PATH]\n";
return 2;
}

cgraph::DrainerConfig config;

Check warning on line 1097 in src/cli/main.cpp

View workflow job for this annotation

GitHub Actions / windows-latest / windows-default

unreachable code
config.launch_agents_dir = cgraph::default_launch_agents_dir();
const auto self = current_executable_path(argv[0]);
config.cgraph_binary = self;
config.client_binary = self.parent_path().parent_path() / "client" / cgraph::executable_file_name("cgraph-client");

for (int index = 3; index < argc; ++index) {

Check warning on line 1103 in src/cli/main.cpp

View workflow job for this annotation

GitHub Actions / windows-latest / windows-default

unreachable code
const std::string arg = argv[index];

Check warning on line 1104 in src/cli/main.cpp

View workflow job for this annotation

GitHub Actions / windows-latest / windows-default

unreachable code
if (arg == "--interval" && index + 1 < argc) {
config.interval_seconds = std::stoi(argv[++index]);
config.interval_seconds = cgraph::integer_flag<int>(arg, argv[++index]);
} else if (arg == "--chunk-cap" && index + 1 < argc) {
config.chunk_cap = std::stoi(argv[++index]);
config.chunk_cap = cgraph::integer_flag<int>(arg, argv[++index]);
} else if (arg == "--script" && index + 1 < argc) {
config.script_path = argv[++index];
} else if (arg == "--launch-agents-dir" && index + 1 < argc) {
Expand Down Expand Up @@ -1242,11 +1242,20 @@
int main(int argc, char** argv) {
// Every output file goes through write_file_atomically: a full disk or any
// other failed write leaves no truncated file behind and must not exit 0.
// A malformed flag value is a usage error (exit 2). Any other exception is
// reported with its type (a filesystem_error's what() also names the
// path(s)) and exits 1; escaping main would abort with a core dump (134).
try {
return run(argc, argv);
} catch (const cgraph::UsageError& error) {
std::cerr << "cgraph: " << error.what() << '\n';
return 2;
} catch (const cgraph::FileWriteError& error) {
std::cerr << "cgraph: " << error.what() << '\n';
return 1;
} catch (const std::exception& error) {
std::cerr << "cgraph: " << cgraph::describe_exception(error) << '\n';
return 1;
}
}

Expand Down
19 changes: 16 additions & 3 deletions src/client/main.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#include "cgraph/cli_support.hpp"
#include "cgraph/client_runtime.hpp"
#include "cgraph/engine.hpp"

#include <cstdlib>
#include <iterator>
#include <chrono>
#include <exception>
#include <iostream>
#include <string>

Expand All @@ -15,9 +17,7 @@ void print_usage() {
" the other services on the far side of each endpoint FILE serves or calls\n";
}

} // namespace

int main(int argc, char** argv) {
int run(int argc, char** argv) {
const auto info = cgraph::build_info();
cgraph::ClientRequest request{
.project_root = std::filesystem::current_path(),
Expand Down Expand Up @@ -93,3 +93,16 @@ int main(int argc, char** argv) {
std::cout << result.response->dump(2) << '\n';
return 0;
}

} // namespace

int main(int argc, char** argv) {
// Report any unhandled failure (e.g. a filesystem_error resolving the
// project root) and exit non-zero instead of aborting with a core dump.
try {
return run(argc, argv);
} catch (const std::exception& error) {
std::cerr << "cgraph-client: " << cgraph::describe_exception(error) << '\n';
return 1;
}
}
27 changes: 23 additions & 4 deletions src/daemon/main.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "cgraph/cli_support.hpp"
#include "cgraph/daemon_lifecycle.hpp"
#include "cgraph/daemon_ops.hpp"
#include "cgraph/daemon_server.hpp"
Expand All @@ -6,6 +7,7 @@
#include "cgraph/seam.hpp"

#include <chrono>
#include <exception>
#include <iostream>
#include <string>

Expand All @@ -16,9 +18,7 @@ void print_usage() {
" [--benchmark-query --graph PATH --query TEXT]\n";
}

} // namespace

int main(int argc, char** argv) {
int run(int argc, char** argv) {
const auto info = cgraph::build_info();
std::filesystem::path graph_path;
std::filesystem::path root;
Expand All @@ -42,7 +42,7 @@ int main(int argc, char** argv) {
continue;
}
if (arg == "--idle-timeout" && index + 1 < argc) {
idle_timeout = std::chrono::seconds(std::stoll(argv[++index]));
idle_timeout = std::chrono::seconds(cgraph::integer_flag<long long>(arg, argv[++index]));
continue;
}
if (arg == "--no-watch") {
Expand Down Expand Up @@ -100,3 +100,22 @@ int main(int argc, char** argv) {
}
return cgraph::run_daemon_server(root, options);
}

} // namespace

int main(int argc, char** argv) {
// A malformed flag value is a usage error (exit 2). Setup failures outside
// the serve loop (e.g. a filesystem_error creating the socket directory) are
// logged with their type and exit 1; an escaped exception would abort with a
// core dump. Failures inside the serve loop and its worker threads are
// handled by run_daemon_server itself, which shuts down cleanly and returns 1.
try {
return run(argc, argv);
} catch (const cgraph::UsageError& error) {
std::cerr << "graphd: " << error.what() << '\n';
return 2;
} catch (const std::exception& error) {
std::cerr << "graphd: " << cgraph::describe_exception(error) << '\n';
return 1;
}
}
1 change: 1 addition & 0 deletions src/engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ add_library(cgraph_engine STATIC
atomic_write.cpp
body_index.cpp
change_context.cpp
cli_support.cpp
daemon_endpoint.cpp
daemon_server.cpp
configured_extractors.cpp
Expand Down
13 changes: 13 additions & 0 deletions src/engine/atomic_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,17 @@ void write_file_atomically(const std::filesystem::path& path, std::string_view c
}
}

void create_output_directories(const std::filesystem::path& directory) {
if (directory.empty()) {
return;
}
std::error_code error;
std::filesystem::create_directories(directory, error);
if (error) {
// error.message(), not failure()'s errno lookup: on Windows the code is a
// Win32 error in system_category, which generic_category would misname.
throw FileWriteError(failure(directory, "cannot create directory (" + error.message() + ")", 0));
}
}

} // namespace cgraph
49 changes: 49 additions & 0 deletions src/engine/cli_support.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "cgraph/cli_support.hpp"

#include <cctype>
#include <cerrno>
#include <cstdlib>
#include <memory>
#include <typeinfo>

#if !defined(_MSC_VER)
#include <cxxabi.h>
#endif

namespace cgraph {

double number_flag(std::string_view flag, std::string_view value) {
// strtod, not from_chars: libc++ on macOS lacks floating-point from_chars.
const std::string text(value);
char* end = nullptr;
errno = 0;
const double parsed = std::strtod(text.c_str(), &end);
if (text.empty() || std::isspace(static_cast<unsigned char>(text.front())) != 0 ||
end != text.c_str() + text.size() || errno == ERANGE) {
throw UsageError(std::string(flag) + " expects a number, got '" + text + "'");
}
return parsed;
}

std::string describe_exception(const std::exception& error) {
#if defined(_MSC_VER)
// MSVC's type_info::name() is already demangled but keyed ("class std::out_of_range");
// drop the key so every platform names the type the same way.
std::string_view name = typeid(error).name();
for (const std::string_view key : {std::string_view{"class "}, std::string_view{"struct "}}) {
if (name.substr(0, key.size()) == key) {
name.remove_prefix(key.size());
break;
}
}
return std::string(name) + ": " + error.what();
#else
const char* mangled = typeid(error).name();
int status = 0;
const std::unique_ptr<char, decltype(&std::free)> demangled(
abi::__cxa_demangle(mangled, nullptr, nullptr, &status), &std::free);
return std::string(status == 0 && demangled ? demangled.get() : mangled) + ": " + error.what();
#endif
}

} // namespace cgraph
Loading
Loading