Skip to content

Developer Architecture and Extensibility

Hugo edited this page Sep 25, 2026 · 2 revisions

Developer Architecture and Extensibility

Back to Home

Layout

Directory Role
include/App, src/App Front end: configuration, file resolution, shipped defaults, exit policy, runners
include/Config ProgramConfig, one struct per configuration section
include/Process, src/Process Process launching, IPC/HTTP layer, tool implementations
include/ctrace_tools Shared helpers: string splitting, language detection, mangling, colors

High-Level Execution Flow

Process entry point (main.cpp)

  1. buildConfig(argc, argv) produces a ConfigResult: the ProgramConfig, plus any output, warnings, error text and exit code. It never exits the process itself.
  2. Output, warnings and errors are printed; without a config, main returns the reported exit code.
  3. The logger is configured (prefix, minimum level from output.verbose).
  4. Branch: runtime.ipc == "serve" → run_server(config, executable), otherwise → run_cli_analysis(config).
  5. Any escaping exception is caught and logged rather than aborting the process.

CLI path (run_cli_analysis)

  1. Resolve source files (resolveSourceFiles); a resolution failure is a usage error.
  2. Instantiate ToolInvoker (thread-pool size from hardware concurrency, capped at 255).
  3. Run the selected groups: static, dynamic, then explicitly invoked tools.
  4. In SARIF mode, render the merged document and write it to output.report_file.
  5. Compute the verdict with exitCodeFor(fail_on, outcome) and return it.

Server path (run_server + HTTP layer)

  1. Build an HttpServer around an ApiHandler, with the server configuration section.
  2. POST /api requests are translated into the same sectioned config document and run through the same orchestration.
  3. The response carries the normalized diagnostics, the summary, the SARIF document when asked, the same verdict the CLI would exit with (result.gate), and the captured per-tool output.

One Configuration Path

The CLI, the config file and an HTTP request all build the same sectioned JSON document and apply it through the same loader (applyToolConfigObject). There is one validation and normalization path, so a value behaves identically whichever way it arrived. Diagnostics coming out of the loader name the internal section key and are rewritten to the spelling the caller used — the CLI option, or the flat request parameter.

See Developer Build and Configuration.

Shipped Defaults

applyShippedDefaults() runs last, after the config file and the CLI or request, so an explicit value always wins. It fills two things from what sits next to the running binary — located through the OS (_NSGetExecutablePath, /proc/self/exe) rather than argv[0], because an installed binary is usually started through PATH:

  • Stack analyzer models, from the first existing of <exe dir>/../config/models (install prefix, and a build/ directory inside the repo) and <exe dir>/config/models (any build directory; CMake copies the models there). When the analyzer is selected and a default model is missing, a warning names it, so losing a rule family is visible instead of showing up as a clean report.
  • Bundled external tools, from the first executable of <exe dir>/../libexec/coretrace/<tool>/<tool> and <exe dir>/libexec/coretrace/<tool>/<tool>. Tools without a bundled copy keep being resolved through PATH, so a release archive runs without anything installed.

Resolution order for a tool executable is therefore: tools.paths → bundled copy → PATH.

The Diagnostic Model

Every tool normalizes its own output format into one Diagnostic (include/Process/Tools/Diagnostic.hpp):

tool, ruleId, file, line, column, severity (info|warning|error), message, cwe

Counting, gating, SARIF rendering and the HTTP response only ever see this model. A tool that never produces structured findings is reported as uninterpreted — distinct from a tool with zero findings — so output that nothing counted stays visible instead of reading as a clean run.

Verdict

AnalysisOutcome (the severity counters plus "did any tool fail") and FailOn produce the exit code through exitCodeFor() in include/App/ExitPolicy.hpp: 0 clean, 1 usage, 2 findings, 3 incomplete. An incomplete analysis wins over findings — its counters cannot be trusted, and --fail-on none only silences findings, never a broken toolchain. The server returns the same verdict in result.gate rather than exiting.

SARIF

renderSarif() turns the collected diagnostics into one SARIF 2.1.0 log: one run per tool, rules collected per tool, a coretrace/v1 partial fingerprint per result, and the CWE as a result property. Findings that match on file, line and CWE are merged into a single result whose alsoReportedBy property names the other tools and rules; reports without a CWE cannot be matched and are all kept.

Core Architectural Patterns

  • Strategy: IAnalysisTool for pluggable tools, IpcStrategy for the (deprecated) socket transport.
  • Factory: ProcessFactory picks the platform process implementation — WindowsProcess on _WIN32, UnixProcess elsewhere.
  • Template method: Process::execute() enforces prepare → run → cleanup.

Tools are integrated independently of orchestration: orchestration stays centralized in ToolInvoker, tool behavior stays local to its implementation, and process execution and transport are replaceable without touching the tool interface.

Tool Output: One Channel

Tool output does not go through the IPC strategy any more. Each execution receives a ToolOutput sink (include/Process/Tools/ToolOutput.hpp) that:

  • records the tool's report text and errors in a CaptureBuffer when there is one (the HTTP server turns it into result.outputs),
  • mirrors them to stdout/stderr when asked to,
  • collects the structured Diagnostic list for the invoker.

Status and progress messages are not tool output: they go through the logger. CaptureBuffer is mutex-guarded, so async scheduling is safe.

Captured line format: stream (stdout or stderr) and message — a raw string, or parsed JSON when the captured text is valid JSON.

Tool Scheduling (ToolInvoker)

Registered tools:

Group Tools
static cppcheck, flawfinder, tscancode, ikos, ctrace_stack_analyzer, coretrace-python-analyzer
dynamic dyn_tools_1, dyn_tools_2, dyn_tools_3

Execution modes: deferred/sequential (std::launch::deferred) or parallel (std::launch::async) over an internal thread pool.

Language routing

The invoker detects each file's language (ctrace_tools::detectLanguage) and hands a tool only the files it declares through analyzes(). The default implementation covers C and C++; the Python analyzer overrides it for LanguageType::Python. A mixed --input therefore dispatches .py files and C/C++ files to different tools in the same run.

Per-file vs batch

Tools run per file through execute(file, config, output). A tool opts into one run with the whole input list by overriding supportsBatchExecution() and executeBatch(files, config, output); the default executeBatch loops over execute. ctrace_stack_analyzer and coretrace-python-analyzer are the batch-enabled tools.

IPC

  • standardIO (default): no IPC strategy is installed; results go to the ToolOutput sink.
  • socket: deprecated. Installing it prints the deprecation notice and attaches a UnixSocketStrategy to every tool. Only some tools ever wrote to it and nothing in the project reads from it; it will be removed.
  • serve: HTTP server mode. Inside a request the analysis runs in-process, so the runtime IPC is normalized to standardIO and the outputs are captured for the JSON response.

Extending with a New Tool (Checklist)

  1. Implement a class deriving IAnalysisTool (or AnalysisToolBase).
  2. Implement execute(const std::string&, const ProgramConfig&, ToolOutput&) const and name() const.
  3. Normalize the tool's output into Diagnostic values and hand them to the sink with output.diagnostics(...) — that is what makes findings counted, gated and rendered to SARIF. A tool that only writes text is reported as uninterpreted.
  4. Optional: supportsBatchExecution() / executeBatch() when the tool handles a multi-file input efficiently; analyzes() when it is not a C/C++ analyzer.
  5. Register it in the ToolInvoker constructor (registerTool("tool_name", ...)) and add it to the static or dynamic group if it belongs there.
  6. Add the name to SUPPORTED_TOOLS in include/App/SupportedTools.hpp — this is what validates --invoke and params.invoke, and what the error message lists.
  7. Add its path and arguments handling under the tools config section if it is an external binary.
  8. Validate through both front ends: --invoke tool_name and params.invoke.
  9. Update docs/configuration.md and the wiki pages so the behavior stays discoverable.

Current Architecture Caveats

  • ikos is registered and part of the static group, but its output is not normalized into diagnostics, so its findings are not counted and do not affect the verdict; it shows up in uninterpreted_tools. Its CMake integration is commented out of the build.
  • The --invoke help text omits coretrace-python-analyzer, which the validator accepts.
  • output.output_file is carried through the whole configuration path but no tool consumes it.
  • The Unix socket transport (--ipc=socket) is kept working for existing callers only.

Back to Home