ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
external_tool.cpp
Go to the documentation of this file.
1/**
2 * @file external_tool.cpp
3 * @brief Implementation of the external-tool helpers.
4 *
5 * Reads the @c provsql.tool_search_path GUC (exposed as
6 * @c provsql_tool_search_path) and uses it both to extend @c $PATH around
7 * @c system() and to drive the pre-flight @c find_external_tool() lookup.
8 */
9extern "C" {
10#include "postgres.h"
11#include "provsql_utils.h"
12#include "miscadmin.h"
13
14#include <signal.h>
15#include <stdlib.h>
16#include <stdio.h>
17#include <errno.h>
18#include <unistd.h>
19#include <sys/wait.h>
20}
21
22#include "external_tool.h"
23#include "ToolRegistry.h"
24#include "provsql_config.h"
25
26#include <string>
27#include <unordered_map>
28
29// PATH that /bin/sh resolves binaries against when the environment has no
30// PATH set. PostgreSQL backends inherit no PATH from systemd, so
31// getenv("PATH") is NULL inside the server; without an explicit fallback,
32// setting PATH to "<GUC>" alone would mask /usr/local/bin and friends
33// (dash's compiled-in default), making the GUC-set case strictly narrower
34// than the GUC-empty case. Matches dash's _PATH_STDPATH on Debian/Ubuntu
35// and bash's default on macOS.
36static const char *DEFAULT_PATH =
37 "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
38
39/*
40 * Run @p cmdline via @c /bin/sh @c -c in its OWN process group, polling for
41 * a query cancel / backend termination while it runs. Returns a wait(2)
42 * status (so @c WIFEXITED / @c WIFSIGNALED / @c WEXITSTATUS decode exactly
43 * as the old @c system() return did), or -1 if @c fork failed.
44 *
45 * Why not @c system(): it runs the child in the BACKEND's process group,
46 * and statement_timeout / pg_cancel_backend deliver SIGINT to that group
47 * (PostgreSQL backends @c setsid, and @c StatementTimeoutHandler does
48 * @c kill(-MyProcPid, SIGINT)). A well-behaved tool dies on it, but a tool
49 * that catches/ignores SIGINT -- or that forks a worker into another
50 * process group, as KCBox/Panini does -- survives, so the timeout is
51 * silently not honoured (a single long OBDD compile runs unbounded). Here
52 * the child leads its own process group, and when a cancel/terminate is
53 * pending we @c SIGKILL that whole group: uncatchable, and it reaches any
54 * forked workers. The pending interrupt is then raised by the
55 * @c CHECK_FOR_INTERRUPTS() in @c run_external_tool, with the child reaped.
56 */
57static int run_in_own_pgroup(const std::string &cmdline)
58{
59#ifdef PROVSQL_NO_SUBPROCESS
60 /* No subprocesses in the WASM sandbox. Mirror fork()'s failure return
61 so find_external_tool() reports "not found" and callers fall back to
62 the in-process compiler. */
63 (void) cmdline;
64 return -1;
65#else
66 fflush(NULL); /* flush stdio before fork, like system() */
67
68 pid_t child = fork();
69 if (child < 0)
70 return -1; /* mirror system()'s failure return */
71
72 if (child == 0) {
73 /* Child: lead a new process group so a later killpg targets this whole
74 * subtree (including tools that fork their own workers) and never the
75 * backend. exec resets caught signals to default, so SIGINT / SIGTERM
76 * / SIGKILL terminate the tool normally. */
77 setpgid(0, 0);
78 execl("/bin/sh", "sh", "-c", cmdline.c_str(), (char *) NULL);
79 _exit(127); /* exec failed: shell-style "not found" */
80 }
81
82 /* Parent: close the setpgid race (whichever process runs first wins). */
83 setpgid(child, child);
84
85 int status = 0;
86 for (;;) {
87 pid_t w = waitpid(child, &status, WNOHANG);
88 if (w == child)
89 break; /* tool finished; status is set */
90 if (w < 0 && errno != EINTR) {
91 status = -1; /* unexpected; surface as failure */
92 break;
93 }
94 /* A pending query cancel (statement_timeout, pg_cancel_backend) or
95 * backend termination (pg_terminate_backend / SIGTERM) must stop the
96 * tool now. Kill its whole process group hard and reap it. We do NOT
97 * call CHECK_FOR_INTERRUPTS() while the child is alive: a throw there
98 * would leak the running child as an orphan -- the very bug being
99 * fixed. The caller's CHECK_FOR_INTERRUPTS() (below) raises the
100 * pending interrupt once the child is reaped. */
101 if (QueryCancelPending || ProcDiePending) {
102 killpg(child, SIGKILL);
103 pid_t r;
104 do { r = waitpid(child, &status, 0); } while (r < 0 && errno == EINTR);
105 break;
106 }
107 pg_usleep(10000); /* 10 ms; an arriving signal wakes us via EINTR */
108 }
109 return status;
110#endif /* PROVSQL_NO_SUBPROCESS */
111}
112
113int run_external_tool(const std::string &cmdline) {
114 bool override_path = (provsql_tool_search_path != NULL
115 && provsql_tool_search_path[0] != '\0');
116 std::string saved_path;
117 bool had_path = false;
118
119 if (override_path) {
120 const char *cur = getenv("PATH");
121 if (cur != NULL) {
122 saved_path = cur;
123 had_path = true;
124 }
125 std::string new_path(provsql_tool_search_path);
126 new_path += ':';
127 new_path += had_path ? saved_path : DEFAULT_PATH;
128 setenv("PATH", new_path.c_str(), 1);
129 }
130
131 int rv = run_in_own_pgroup(cmdline);
132
133 if (override_path) {
134 if (had_path)
135 setenv("PATH", saved_path.c_str(), 1);
136 else
137 unsetenv("PATH");
138 }
139
140 /* If a cancel/terminate fired while the tool ran, run_in_own_pgroup has
141 * already killed and reaped it; raise the interrupt now (cleanly, with no
142 * child left running) so it surfaces as query-cancelled rather than being
143 * masked by a downstream "tool killed by signal" error. A no-op when no
144 * interrupt is pending. */
145 CHECK_FOR_INTERRUPTS();
146
147 return rv;
148}
149
150std::string find_external_tool(const std::string &name) {
151 // Path-like names (containing '/') are tested directly without any
152 // search-path walk; this matches POSIX execvp semantics.
153 if (name.find('/') != std::string::npos)
154 return access(name.c_str(), X_OK) == 0 ? name : "";
155
156 // Per-session positive-result cache, keyed on (tool_search_path, name).
157 // find_external_tool runs on every compilation() / Ganak() / ... call and
158 // each miss forks /bin/sh for `command -v`; memoizing the (common)
159 // successful lookups removes that fork on the hot path. Keying on the
160 // current provsql.tool_search_path means a runtime change to that
161 // PGC_USERSET GUC simply misses and re-probes -- no explicit invalidation.
162 // We cache positives only and re-probe on miss, mirroring the deliberate
163 // "don't cache failed lookups so pooled backends self-heal" choice for
164 // get_constants(): a tool installed mid-session is picked up on the next
165 // call rather than being remembered as absent. The embedded '\0' separates
166 // the two fields unambiguously (a C-string GUC value cannot contain it).
167 static std::unordered_map<std::string, std::string> tool_cache;
168 std::string key =
170 + '\0' + name;
171 auto cached = tool_cache.find(key);
172 if (cached != tool_cache.end())
173 return cached->second;
174
175 // Delegate the search to /bin/sh via `command -v`, routed through
176 // run_external_tool() so the GUC override is honoured. This reuses
177 // exactly the PATH resolution that the eventual tool invocation will
178 // see, including the shell's compiled-in default when the environment
179 // has no PATH (typical inside a PostgreSQL backend).
180 //
181 // Single-quoting `name` defends against shell metacharacters; the
182 // five tool names provsql actually uses ("d4", "c2d", "minic2d",
183 // "dsharp", "weightmc", "graph-easy") contain none.
184 std::string check = "command -v '" + name + "' >/dev/null 2>&1";
185 // run_external_tool runs the probe in its own process group and raises
186 // any pending statement_timeout / cancel itself (so it does not surface
187 // as a spurious "tool not found").
188 int rv = run_external_tool(check);
189
190 if (rv == 0) {
191 tool_cache[key] = name;
192 return name;
193 }
194 return "";
195}
196
197std::string format_external_tool_status(int rv, const std::string &tool) {
198 if (rv == 0)
199 return "";
200 if (rv == -1)
201 return tool + " could not be invoked (system() returned -1)";
202 if (WIFSIGNALED(rv))
203 return tool + " terminated by signal "
204 + std::to_string(WTERMSIG(rv));
205 if (WIFEXITED(rv)) {
206 int code = WEXITSTATUS(rv);
207 if (code == 127)
208 return tool + " was not found at runtime (shell exit 127); "
209 "install it or add its directory to provsql.tool_search_path";
210 if (code == 126)
211 return tool + " is not executable (shell exit 126); "
212 "check permissions on the binary";
213 return tool + " exited with status " + std::to_string(code);
214 }
215 return tool + " failed with raw status " + std::to_string(rv);
216}
217
219 if (rec.kind == "kcmcp")
220 return !rec.endpoint.empty();
221 if (!rec.binary.empty() && find_external_tool(rec.binary).empty())
222 return false;
223 for (const std::string &dep : rec.dependencies)
224 if (find_external_tool(dep).empty())
225 return false;
226 return true;
227}
In-memory catalog of the external tools ProvSQL can invoke.
int run_external_tool(const std::string &cmdline)
Run a shell command line in its own process group, optionally extending PATH, interruptible by query ...
std::string format_external_tool_status(int rv, const std::string &tool)
Decode a system() return value into a human-readable message.
static int run_in_own_pgroup(const std::string &cmdline)
bool toolAvailable(const provsql::ToolRecord &rec)
True iff a registry tool can currently be used.
std::string find_external_tool(const std::string &name)
Locate an external tool by name.
static const char * DEFAULT_PATH
Helpers for invoking external command-line tools.
char * provsql_tool_search_path
Colon-separated directory list prepended to PATH when invoking external tools (d4,...
Definition provsql.c:92
Build-configuration switches shared across the C and C++ sources.
Core types, constants, and utilities shared across ProvSQL.
One registered external tool.
std::vector< std::string > dependencies
std::string kind
"cli" (spawn a binary) or "kcmcp" (talk to a socket server at endpoint).
std::string endpoint
KCMCP server address for kind "kcmcp": "unix:/path" or "host:port".