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#ifdef LINE_LDES_BUILD_DIR
108 // Last: where cmake put the `ldes` target, so a program linking line_mp_api from its own build tree finds it.
109 out.push_back(LINE_LDES_BUILD_DIR);
110#endif
111 return out;
112}
113
114/** The first candidate directory that holds an engine, or empty. */
115inline std::string find_engine_dir_uncached() {
116 const std::vector<std::string> cand = engine_dirs();
117 for (std::size_t i = 0; i < cand.size(); ++i)
118 if (is_file(cand[i] + "/ldes.jar") || is_file(cand[i] + "/ldes")) return cand[i];
119 return std::string();
120}
121
122// ELF e_machine identifiers (header offset 0x12, two bytes).
123static const int EM_386_ID = 0x03;
124static const int EM_ARM_ID = 0x28;
125static const int EM_X86_64_ID = 0x3E;
126static const int EM_AARCH64_ID = 0xB7;
127
128/** The ELF e_machine of a binary, or 0 when the file is not an ELF. */
129inline int elf_machine(const std::string& path) {
130 std::ifstream in(path.c_str(), std::ios::binary);
131 if (!in) return 0;
132 unsigned char h[20];
133 in.read(reinterpret_cast<char*>(h), 20);
134 if (in.gcount() < 20) return 0;
135 if (!(h[0] == 0x7F && h[1] == 'E' && h[2] == 'L' && h[3] == 'F')) return 0;
136 return h[5] == 1 ? (h[18] | (h[19] << 8)) : ((h[18] << 8) | h[19]);
137}
138
139/** The e_machine this host runs, or 0 when the name is not one we map. */
140inline int host_elf_machine() {
141 struct utsname u;
142 if (::uname(&u) != 0) return 0;
143 const std::string m(u.machine);
144 if (m == "x86_64" || m == "amd64") return EM_X86_64_ID;
145 if (m == "aarch64" || m == "arm64") return EM_AARCH64_ID;
146 if (m == "i386" || m == "i486" || m == "i586" || m == "i686" || m == "x86") return EM_386_ID;
147 if (m.compare(0, 3, "arm") == 0) return EM_ARM_ID;
148 return 0;
149}
150
151/**
152 * A runnable native binary in `dir`, or empty.
153 *
154 * `$LINE_LDES_FORCE_JAR` is the escape hatch for a prebuilt image that is stale
155 * against the sources: the flag exists in the Python client for the same reason.
156 */
157inline std::string native_ldes_path(const std::string& dir) {
158 if (dir.empty()) return std::string();
159 if (!env_or_empty("LINE_LDES_FORCE_JAR").empty()) return std::string();
160 const std::string cand = dir + "/ldes";
161 if (!is_exec(cand)) return std::string();
162 const int host = host_elf_machine();
163 const int bin = elf_machine(cand);
164 if (host != 0 && bin != 0 && host != bin) return std::string();
165 return cand;
166}
167
168/** True when a native binary is present but built for another CPU. */
169inline bool incompatible_native(const std::string& dir) {
170 if (dir.empty()) return false;
171 const std::string cand = dir + "/ldes";
172 if (!is_file(cand)) return false;
173 const int host = host_elf_machine();
174 const int bin = elf_machine(cand);
175 return host != 0 && bin != 0 && host != bin;
176}
177
178/**
179 * A runnable java launcher, or empty.
180 *
181 * `$LINE_JAVA` is AUTHORITATIVE when set: a caller who named a JVM and named a
182 * wrong one is told so rather than silently served another. Then
183 * `$JAVA_HOME/bin/java`, then PATH -- the order the JAR dispatch uses, so one
184 * setting serves every wrapper.
185 */
186inline std::string find_java() {
187 const std::string forced = env_or_empty("LINE_JAVA");
188 if (!forced.empty()) return is_exec(forced) ? forced : std::string();
189 const std::string home = env_or_empty("JAVA_HOME");
190 if (!home.empty() && is_exec(home + "/bin/java")) return home + "/bin/java";
191 const std::string path = env_or_empty("PATH");
192 std::size_t b = 0;
193 while (b <= path.size()) {
194 const std::size_t e = path.find(':', b);
195 const std::string dir = path.substr(b, e == std::string::npos ? std::string::npos : e - b);
196 if (!dir.empty() && is_exec(dir + "/java")) return dir + "/java";
197 if (e == std::string::npos) break;
198 b = e + 1;
199 }
200 return std::string();
201}
202
203/**
204 * The message for a host with neither a usable image nor a JVM.
205 *
206 * IT NAMES WHICH OF THE THREE IT IS -- no engine directory at all, an engine
207 * built for another CPU with no JVM behind it, or a directory with no jar --
208 * because the three want different things done, and a message that blamed the
209 * JVM for a missing jar sends the reader to install one they already have.
210 */
211inline std::string no_backend_message(const std::string& dir) {
212 struct utsname u;
213 const std::string host =
214 ::uname(&u) == 0 ? std::string(u.sysname) + "/" + u.machine : std::string("this host");
215 const bool have_java = !find_java().empty();
216 std::string why;
217 if (dir.empty()) {
218 const std::string forced = env_or_empty("LINE_LDES_DIR");
219 why = forced.empty()
220 ? "no LDES engine was found beside this executable or under the working "
221 "directory (looking for `ldes` or `ldes.jar` in each ancestor and its "
222 "`common/`)"
223 : "$LINE_LDES_DIR names '" + forced +
224 "', which holds neither `ldes` nor `ldes.jar`";
225 } else if (incompatible_native(dir)) {
226 why = "the native LDES binary in '" + dir +
227 "' was built for a different CPU architecture than this host (" + host + ")" +
228 (have_java ? ", and there is no ldes.jar beside it to run instead"
229 : ", and no Java runtime was found to run ldes.jar");
230 } else {
231 why = "'" + dir + "' holds no runnable engine for this host (" + host + ")" +
232 (have_java ? "" : " and no Java runtime was found to run ldes.jar");
233 }
234 if (dir.empty() || !have_java)
235 return "cannot run SolverLDES: " + why +
236 ". Build the native engine (`cmake --build <build-dir> --target ldes`) and point "
237 "$LINE_LDES_DIR at the directory holding `ldes` when it is not beside the executable; "
238 "to run `ldes.jar` instead, install a JRE/JDK 8 or newer and put `java` on PATH, or set "
239 "$LINE_JAVA or $JAVA_HOME";
240 return "cannot run SolverLDES: " + why +
241 ". Point $LINE_LDES_DIR at a directory holding an `ldes` built for this host or an `ldes.jar`";
242}
243
244} // namespace detail
245
246/** The directory holding the engine, or empty when there is none. */
247inline const std::string& ldes_engine_dir() {
248 static const std::string dir = detail::find_engine_dir_uncached();
249 return dir;
250}
251
252/** True when this machine can run the engine at all, by either image. */
253inline bool ldes_is_available() {
254 static const bool ok = !ldes_engine_dir().empty() &&
255 (!detail::native_ldes_path(ldes_engine_dir()).empty() ||
256 (!detail::find_java().empty() &&
257 detail::is_file(ldes_engine_dir() + "/ldes.jar")));
258 return ok;
259}
260
261} // namespace ldes
262} // namespace line
263
264#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:253
const std::string & ldes_engine_dir()
The directory holding the engine, or empty when there is none.
Definition ldes_probe.h:247
bool in(const std::string &s, std::initializer_list< const char * > set)
std::vector< double > stat(const Matrix< double > &A)
Stationary distribution of a stochastic matrix: the left eigenvector for eigenvalue 1,...
Definition mg1.h:221
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52