LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_probe.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_WRAPPERS_LDES_LDES_PROBE_H
6#define LINE_SOLVERS_WRAPPERS_LDES_LDES_PROBE_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Where the LDES engine is, and whether this machine can run it.
12 *
13 * Its own header, small enough for SolverAUTO to include: LDES leads several of
14 * the reference's rankings, and the chooser needs to know whether the slot has
15 * an engine behind it without taking on the client, the result parser and the
16 * HTTP transport. This is `lqns_probe.h`'s reason, and the same shape.
17 *
18 * THE ANSWER IS CACHED for the life of the process: a binary does not appear or
19 * vanish mid-run, and the probe stats several directories and reads an ELF
20 * header, which SolverAUTO would otherwise repeat on every choice it makes.
21 *
22 * TWO IMAGES, ONE ENGINE. `common/ldes` is an AOT GraalVM build and
23 * `common/ldes.jar` the same engine on a JVM; the AOT image starts faster and
24 * lacks the reflective features the JVM has, so the client tries it first and
25 * falls through. A binary built for ANOTHER CPU is skipped rather than run --
26 * the ELF e_machine says so before exec does -- because the fall-through then
27 * still has the jar to reach, while an exec failure would look like an engine
28 * failure. When either architecture is unknown the binary is used as a best
29 * effort: an unknown is not evidence of a mismatch.
30 */
31
32#include <cstdlib>
33#include <fstream>
34#include <string>
35#include <vector>
36
37#include <sys/stat.h>
38#include <sys/utsname.h>
39#include <unistd.h>
40
41namespace line {
42namespace ldes {
43
44namespace detail {
45
46inline bool is_file(const std::string& p) {
47 struct stat st;
48 return !p.empty() && ::stat(p.c_str(), &st) == 0 && S_ISREG(st.st_mode);
49}
50
51inline bool is_exec(const std::string& p) {
52 return is_file(p) && ::access(p.c_str(), X_OK) == 0;
53}
54
55inline std::string env_or_empty(const char* name) {
56 const char* v = std::getenv(name);
57 return v != nullptr ? std::string(v) : std::string();
58}
59
60/** The directory holding this executable, or empty when it cannot be read. */
61inline std::string exe_dir() {
62 char buf[4096];
63 const ssize_t n = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1);
64 if (n <= 0) return std::string();
65 const std::string p(buf, static_cast<std::size_t>(n));
66 const std::size_t s = p.find_last_of('/');
67 return s == std::string::npos ? std::string() : p.substr(0, s);
68}
69
70inline std::string cwd_path() {
71 char buf[4096];
72 return ::getcwd(buf, sizeof(buf)) != nullptr ? std::string(buf) : std::string();
73}
74
75/**
76 * Where `common/` may be, in preference order.
77 *
78 * The two other clients resolve it from a package path (Python) or from the
79 * `jline.jar` entry on the Java classpath (MATLAB); a C++ binary has neither, so
80 * it looks where it actually sits -- `common/line-cli` in an install, a build
81 * directory otherwise -- and then ascends, trying each ancestor and each
82 * ancestor's `common/`. `$LINE_LDES_DIR` overrides all of it, for a caller
83 * running an engine from somewhere else entirely.
84 */
85inline std::vector<std::string> engine_dirs() {
86 std::vector<std::string> out;
87 // `$LINE_LDES_DIR` is AUTHORITATIVE, not merely first: a caller who named a
88 // directory and named a wrong one is told there is no engine, rather than
89 // silently served the one beside the binary -- which is `$LINE_JAVA`'s rule
90 // in the Python client, and the difference between running the engine you
91 // asked for and running a different one.
92 const std::string forced = env_or_empty("LINE_LDES_DIR");
93 if (!forced.empty()) {
94 out.push_back(forced);
95 return out;
96 }
97 const std::string roots[2] = {exe_dir(), cwd_path()};
98 for (int r = 0; r < 2; ++r) {
99 if (roots[r].empty()) continue;
100 std::string dir = roots[r];
101 for (int k = 0; k < 7; ++k) {
102 out.push_back(dir);
103 out.push_back(dir + "/common");
104 dir += "/..";
105 }
106 }
107 return out;
108}
109
110/** The first candidate directory that holds an engine, or empty. */
111inline std::string find_engine_dir_uncached() {
112 const std::vector<std::string> cand = engine_dirs();
113 for (std::size_t i = 0; i < cand.size(); ++i)
114 if (is_file(cand[i] + "/ldes.jar") || is_file(cand[i] + "/ldes")) return cand[i];
115 return std::string();
116}
117
118// ELF e_machine identifiers (header offset 0x12, two bytes).
119static const int EM_386_ID = 0x03;
120static const int EM_ARM_ID = 0x28;
121static const int EM_X86_64_ID = 0x3E;
122static const int EM_AARCH64_ID = 0xB7;
123
124/** The ELF e_machine of a binary, or 0 when the file is not an ELF. */
125inline int elf_machine(const std::string& path) {
126 std::ifstream in(path.c_str(), std::ios::binary);
127 if (!in) return 0;
128 unsigned char h[20];
129 in.read(reinterpret_cast<char*>(h), 20);
130 if (in.gcount() < 20) return 0;
131 if (!(h[0] == 0x7F && h[1] == 'E' && h[2] == 'L' && h[3] == 'F')) return 0;
132 return h[5] == 1 ? (h[18] | (h[19] << 8)) : ((h[18] << 8) | h[19]);
133}
134
135/** The e_machine this host runs, or 0 when the name is not one we map. */
136inline int host_elf_machine() {
137 struct utsname u;
138 if (::uname(&u) != 0) return 0;
139 const std::string m(u.machine);
140 if (m == "x86_64" || m == "amd64") return EM_X86_64_ID;
141 if (m == "aarch64" || m == "arm64") return EM_AARCH64_ID;
142 if (m == "i386" || m == "i486" || m == "i586" || m == "i686" || m == "x86") return EM_386_ID;
143 if (m.compare(0, 3, "arm") == 0) return EM_ARM_ID;
144 return 0;
145}
146
147/**
148 * A runnable native binary in `dir`, or empty.
149 *
150 * `$LINE_LDES_FORCE_JAR` is the escape hatch for a prebuilt image that is stale
151 * against the sources: the flag exists in the Python client for the same reason.
152 */
153inline std::string native_ldes_path(const std::string& dir) {
154 if (dir.empty()) return std::string();
155 if (!env_or_empty("LINE_LDES_FORCE_JAR").empty()) return std::string();
156 const std::string cand = dir + "/ldes";
157 if (!is_exec(cand)) return std::string();
158 const int host = host_elf_machine();
159 const int bin = elf_machine(cand);
160 if (host != 0 && bin != 0 && host != bin) return std::string();
161 return cand;
162}
163
164/** True when a native binary is present but built for another CPU. */
165inline bool incompatible_native(const std::string& dir) {
166 if (dir.empty()) return false;
167 const std::string cand = dir + "/ldes";
168 if (!is_file(cand)) return false;
169 const int host = host_elf_machine();
170 const int bin = elf_machine(cand);
171 return host != 0 && bin != 0 && host != bin;
172}
173
174/**
175 * A runnable java launcher, or empty.
176 *
177 * `$LINE_JAVA` is AUTHORITATIVE when set: a caller who named a JVM and named a
178 * wrong one is told so rather than silently served another. Then
179 * `$JAVA_HOME/bin/java`, then PATH -- the order the JAR dispatch uses, so one
180 * setting serves every wrapper.
181 */
182inline std::string find_java() {
183 const std::string forced = env_or_empty("LINE_JAVA");
184 if (!forced.empty()) return is_exec(forced) ? forced : std::string();
185 const std::string home = env_or_empty("JAVA_HOME");
186 if (!home.empty() && is_exec(home + "/bin/java")) return home + "/bin/java";
187 const std::string path = env_or_empty("PATH");
188 std::size_t b = 0;
189 while (b <= path.size()) {
190 const std::size_t e = path.find(':', b);
191 const std::string dir = path.substr(b, e == std::string::npos ? std::string::npos : e - b);
192 if (!dir.empty() && is_exec(dir + "/java")) return dir + "/java";
193 if (e == std::string::npos) break;
194 b = e + 1;
195 }
196 return std::string();
197}
198
199/**
200 * The message for a host with neither a usable image nor a JVM.
201 *
202 * IT NAMES WHICH OF THE THREE IT IS -- no engine directory at all, an engine
203 * built for another CPU with no JVM behind it, or a directory with no jar --
204 * because the three want different things done, and a message that blamed the
205 * JVM for a missing jar sends the reader to install one they already have.
206 */
207inline std::string no_backend_message(const std::string& dir) {
208 struct utsname u;
209 const std::string host =
210 ::uname(&u) == 0 ? std::string(u.sysname) + "/" + u.machine : std::string("this host");
211 const bool have_java = !find_java().empty();
212 std::string why;
213 if (dir.empty()) {
214 const std::string forced = env_or_empty("LINE_LDES_DIR");
215 why = forced.empty()
216 ? "no LDES engine was found beside this executable or under the working "
217 "directory (looking for `ldes` or `ldes.jar` in each ancestor and its "
218 "`common/`)"
219 : "$LINE_LDES_DIR names '" + forced +
220 "', which holds neither `ldes` nor `ldes.jar`";
221 } else if (incompatible_native(dir)) {
222 why = "the native LDES binary in '" + dir +
223 "' was built for a different CPU architecture than this host (" + host + ")" +
224 (have_java ? ", and there is no ldes.jar beside it to run instead"
225 : ", and no Java runtime was found to run ldes.jar");
226 } else {
227 why = "'" + dir + "' holds no runnable engine for this host (" + host + ")" +
228 (have_java ? "" : " and no Java runtime was found to run ldes.jar");
229 }
230 return "cannot run SolverLDES: " + why +
231 ". Install a JRE/JDK 8 or newer and put `java` on PATH, or set $LINE_JAVA or "
232 "$JAVA_HOME; point $LINE_LDES_DIR at the directory holding ldes/ldes.jar when it is "
233 "not beside the executable";
234}
235
236} // namespace detail
237
238/** The directory holding the engine, or empty when there is none. */
239inline const std::string& ldes_engine_dir() {
240 static const std::string dir = detail::find_engine_dir_uncached();
241 return dir;
242}
243
244/** True when this machine can run the engine at all, by either image. */
245inline bool ldes_is_available() {
246 static const bool ok = !ldes_engine_dir().empty() &&
247 (!detail::native_ldes_path(ldes_engine_dir()).empty() ||
248 (!detail::find_java().empty() &&
249 detail::is_file(ldes_engine_dir() + "/ldes.jar")));
250 return ok;
251}
252
253} // namespace ldes
254} // namespace line
255
256#endif // LINE_SOLVERS_WRAPPERS_LDES_LDES_PROBE_H
bool ldes_is_available()
True when this machine can run the engine at all, by either image.
Definition ldes_probe.h:245
const std::string & ldes_engine_dir()
The directory holding the engine, or empty when there is none.
Definition ldes_probe.h:239
std::vector< double > stat(const Matrix< double > &A)
Stationary distribution of a stochastic matrix: the left eigenvector for eigenvalue 1,...
Definition mg1.h:217