Line data Source code
1 : /*
2 : * Copyright (C) 2025 aeml
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "core/Logger.hpp"
19 : #include "core/Clock.hpp"
20 : #include "core/FixedTimestepLoop.hpp"
21 : #include "ecs/World.hpp"
22 : #include "simlab/Scenario.hpp"
23 : #include "simlab/HeadlessMetrics.hpp"
24 : #include "physics/Systems.hpp"
25 :
26 : #include <atomic>
27 : #include <cstdlib>
28 : #include <fstream>
29 : #include <string>
30 : #include <string_view>
31 : #include <iostream>
32 : #include <vector>
33 : #include <thread>
34 : #include <filesystem>
35 : #include <algorithm>
36 : #include <ctime>
37 : #include <stdexcept>
38 : #include <utility>
39 :
40 19 : int main(int argc, char** argv)
41 : {
42 19 : core::Logger logger;
43 19 : logger.Info("AtlasCore starting up...");
44 :
45 19 : ecs::World world;
46 19 : const auto& options = simlab::ScenarioRegistry::All();
47 :
48 19 : auto envTruthy = [](const char* value) {
49 19 : if (!value || !*value) return false;
50 0 : switch (value[0])
51 : {
52 0 : case '1':
53 : case 'y': case 'Y':
54 : case 't': case 'T':
55 0 : return true;
56 0 : default:
57 0 : return false;
58 : }
59 : };
60 :
61 38 : auto getEnvValue = [](const char* key) -> std::string {
62 : #ifdef _WIN32
63 : char* buffer = nullptr;
64 : size_t len = 0;
65 : if (_dupenv_s(&buffer, &len, key) == 0 && buffer)
66 : {
67 : std::string value(buffer);
68 : free(buffer);
69 : return value;
70 : }
71 : return {};
72 : #else
73 38 : const char* value = std::getenv(key);
74 38 : return value ? std::string(value) : std::string{};
75 : #endif
76 : };
77 :
78 19 : std::string headlessEnvValue = getEnvValue("ATLASCORE_HEADLESS");
79 19 : const std::string failPhase = getEnvValue("ATLASCORE_FAIL_PHASE");
80 88 : auto maybeFailPhase = [&](std::string_view phase) {
81 88 : if (failPhase == phase)
82 : {
83 0 : throw std::runtime_error(std::string("Injected failure for phase: ") + std::string(phase));
84 : }
85 88 : };
86 19 : bool headless = envTruthy(headlessEnvValue.c_str());
87 19 : std::string scenarioArg;
88 19 : std::string outputPrefix;
89 19 : std::string batchIndexPath;
90 19 : int maxFrames = -1; // Headless auto-termination after N frames if >0
91 94 : for (int i = 1; i < argc; ++i)
92 : {
93 75 : std::string_view arg{argv[i]};
94 75 : if (arg == "--headless")
95 : {
96 18 : headless = true;
97 : }
98 57 : else if (arg.rfind("--frames=", 0) == 0)
99 : {
100 : // Parse --frames=N
101 17 : auto value = std::string(arg.substr(9));
102 17 : try { maxFrames = std::stoi(value); } catch(...) { maxFrames = -1; }
103 17 : if (maxFrames < 0) {
104 0 : logger.Warn("Ignoring invalid --frames value: " + std::string(value));
105 : }
106 17 : }
107 40 : else if (arg.rfind("--output-prefix=", 0) == 0)
108 : {
109 17 : outputPrefix = std::string(arg.substr(16));
110 : }
111 23 : else if (arg.rfind("--batch-index=", 0) == 0)
112 : {
113 5 : batchIndexPath = std::string(arg.substr(14));
114 : }
115 18 : else if (scenarioArg.empty())
116 : {
117 36 : scenarioArg = std::string(arg);
118 : }
119 : }
120 19 : simlab::SetHeadlessRendering(headless);
121 19 : if (headless)
122 : {
123 36 : logger.Info("Headless renderer enabled");
124 : }
125 :
126 : // Determine scenario by CLI arg or interactive menu
127 19 : std::size_t menuChoiceIndex = 0;
128 19 : if (scenarioArg.empty())
129 : {
130 1 : std::cout << "========================================\n";
131 1 : std::cout << " AtlasCore Simulation Framework \n";
132 1 : std::cout << "========================================\n";
133 1 : std::cout << "Select a simulation to run:\n";
134 :
135 9 : for (std::size_t i = 0; i < options.size(); ++i)
136 : {
137 8 : std::cout << " [" << (i + 1) << "] " << options[i].title << " (" << options[i].key << ")\n";
138 : }
139 :
140 1 : std::cout << "\nEnter choice number (default 1): ";
141 1 : std::string line;
142 1 : std::getline(std::cin, line);
143 1 : if (!line.empty())
144 : {
145 : try
146 : {
147 1 : const auto parsed = std::stoul(line);
148 1 : if (parsed >= 1 && parsed <= options.size())
149 : {
150 1 : menuChoiceIndex = static_cast<std::size_t>(parsed - 1);
151 : }
152 : }
153 0 : catch (...)
154 : {
155 0 : }
156 : }
157 1 : }
158 :
159 19 : auto selection = simlab::ScenarioRegistry::ResolveScenarioSelection(scenarioArg, menuChoiceIndex);
160 19 : if (selection.shouldLogUnknownScenario)
161 : {
162 2 : logger.Error(std::string("Unknown scenario: ") + selection.unknownScenarioKey);
163 : }
164 19 : if (selection.shouldLogSelectedScenario)
165 : {
166 2 : logger.Info(std::string("Running scenario: ") + selection.selectedKey);
167 : }
168 :
169 19 : auto scenario = std::move(selection.scenario);
170 19 : const std::string requestedScenarioKey = selection.requestedKey;
171 19 : const std::string selectedScenarioKey = selection.selectedKey;
172 19 : const bool fallbackUsed = selection.fallbackUsed;
173 19 : if (!scenario)
174 : {
175 0 : logger.Error("No scenario available to run.");
176 0 : return 1;
177 : }
178 :
179 19 : std::atomic<bool> running{true};
180 19 : std::atomic<bool> quitRequestedByInput{false};
181 19 : std::atomic<bool> quitRequestedByEof{false};
182 :
183 19 : constexpr double fixedDtSeconds = 1.0 / 60.0;
184 19 : const bool boundedFrames = maxFrames > 0;
185 19 : const auto requestedFrames = boundedFrames ? static_cast<std::size_t>(maxFrames) : 0u;
186 19 : const auto runConfigHash = simlab::HashHeadlessRunConfig(selectedScenarioKey,
187 : fixedDtSeconds,
188 : requestedFrames,
189 : headless);
190 :
191 32 : auto formatTimestampUtc = []() -> std::string {
192 32 : const auto now = std::chrono::system_clock::now();
193 32 : const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
194 32 : std::tm utcTime{};
195 : #if defined(_WIN32)
196 : gmtime_s(&utcTime, &nowTime);
197 : #else
198 32 : gmtime_r(&nowTime, &utcTime);
199 : #endif
200 32 : char buffer[32]{};
201 32 : if (std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &utcTime) == 0)
202 : {
203 0 : return {};
204 : }
205 64 : return std::string(buffer);
206 : };
207 :
208 44 : auto toAbsolutePathString = [](const std::string& pathText) -> std::string {
209 44 : std::error_code ec;
210 44 : const auto absolutePath = std::filesystem::absolute(std::filesystem::path(pathText), ec);
211 88 : return ec ? pathText : absolutePath.string();
212 44 : };
213 :
214 19 : core::FixedTimestepLoop loop{static_cast<float>(fixedDtSeconds)};
215 :
216 19 : std::thread quitThread;
217 :
218 19 : const auto interactiveQuit = simlab::PrepareInteractiveQuitHandling(headless,
219 : maxFrames,
220 : running,
221 : quitRequestedByInput,
222 : quitRequestedByEof);
223 :
224 19 : simlab::HeadlessRunSummaryAccumulator headlessSummaryAccumulator;
225 19 : auto headlessState = simlab::BuildHeadlessLocalState(batchIndexPath);
226 19 : auto& outcome = headlessState.outcome;
227 : const auto startupConfig = simlab::BuildHeadlessStartupCoordinatorConfig(
228 : headless,
229 : outputPrefix,
230 : batchIndexPath,
231 : requestedScenarioKey,
232 : selectedScenarioKey,
233 : fallbackUsed,
234 : fixedDtSeconds,
235 : boundedFrames,
236 : requestedFrames,
237 : runConfigHash,
238 : headlessState.startupFailureSummaryPath,
239 : headlessState.startupFailureManifestPath,
240 38 : formatTimestampUtc(),
241 : ATLASCORE_BUILD_GIT_COMMIT,
242 : ATLASCORE_BUILD_GIT_DIRTY != 0,
243 38 : ATLASCORE_BUILD_TYPE);
244 :
245 : auto startup = simlab::CoordinateHeadlessStartup(world,
246 19 : *scenario,
247 : outcome,
248 : startupConfig,
249 38 : maybeFailPhase);
250 :
251 : const auto startupLogging = simlab::PrepareHeadlessStartupLogging(startup,
252 : batchIndexPath,
253 : headlessState.startupFailureSummaryPath,
254 19 : headlessState.startupFailureManifestPath);
255 19 : simlab::LogHeadlessStartupMessages(logger, startupLogging);
256 :
257 19 : simlab::ApplyHeadlessStartupResult(headlessState, std::move(startup));
258 :
259 19 : if (outcome.runStatus == "startup_failure")
260 : {
261 6 : logger.Info("AtlasCore shutting down.");
262 6 : return outcome.exitCode;
263 : }
264 :
265 13 : if (interactiveQuit.enabled)
266 : {
267 0 : quitThread = std::thread([interactiveQuit, &logger]() {
268 0 : simlab::LogInteractiveQuitPrompt(logger, interactiveQuit);
269 0 : simlab::RunInteractiveQuitInputLoop(interactiveQuit, std::cin);
270 0 : });
271 : }
272 :
273 13 : simlab::HeadlessRuntimeFrameState runtimeFrameState{};
274 13 : const auto runtimeFramePreparation = simlab::PrepareHeadlessRuntimeFrame(headless,
275 : boundedFrames,
276 : maxFrames,
277 : headlessState.outputStream,
278 : headlessState.metricsStream,
279 : headlessState.outputWriteStatus,
280 : headlessState.outputFailureCategory,
281 : headlessState.metricsWriteStatus,
282 : headlessState.metricsFailureCategory);
283 : try
284 : {
285 16 : loop.Run(
286 26 : [&](float dt)
287 : {
288 48 : const bool shouldStop = simlab::RunHeadlessRuntimeFrame(world,
289 24 : *scenario,
290 : dt,
291 : runtimeFrameState,
292 24 : runtimeFramePreparation.config,
293 : headlessSummaryAccumulator,
294 : std::cout,
295 24 : runtimeFramePreparation.artifacts,
296 : maybeFailPhase);
297 21 : if (shouldStop)
298 : {
299 10 : running.store(false);
300 : }
301 : // Otherwise runs until Enter is pressed.
302 21 : },
303 : running);
304 : }
305 3 : catch (const std::exception& ex)
306 : {
307 3 : const std::string_view failurePhase = runtimeFrameState.currentFailurePhase.empty()
308 3 : ? std::string_view{failPhase}
309 3 : : std::string_view{runtimeFrameState.currentFailurePhase};
310 3 : outcome.MarkRuntimeFailure(failurePhase, ex.what());
311 3 : running.store(false);
312 3 : }
313 :
314 13 : if (quitThread.joinable())
315 : {
316 0 : quitThread.join();
317 : }
318 :
319 : const auto finalizationPreparation = simlab::PrepareHeadlessRunFinalization(
320 : outcome,
321 : requestedScenarioKey,
322 : selectedScenarioKey,
323 : fallbackUsed,
324 : fixedDtSeconds,
325 : boundedFrames,
326 : requestedFrames,
327 : headless,
328 : runConfigHash,
329 13 : quitRequestedByInput.load(),
330 13 : quitRequestedByEof.load(),
331 26 : toAbsolutePathString(headlessState.outputPath),
332 26 : toAbsolutePathString(headlessState.metricsPath),
333 26 : toAbsolutePathString(headlessState.summaryPath),
334 26 : batchIndexPath.empty() ? std::string{} : toAbsolutePathString(batchIndexPath),
335 : headlessState.batchIndexAppendStatus,
336 : headlessState.batchIndexFailureCategory,
337 : headlessState.outputWriteStatus,
338 : headlessState.outputFailureCategory,
339 : headlessState.metricsWriteStatus,
340 : headlessState.metricsFailureCategory,
341 : headlessState.summaryWriteStatus,
342 : headlessState.summaryFailureCategory,
343 : headlessState.manifestWriteStatus,
344 : headlessState.manifestFailureCategory,
345 : headlessState.startupFailureSummaryWriteStatus,
346 : headlessState.startupFailureSummaryFailureCategory,
347 : headlessState.startupFailureManifestWriteStatus,
348 : headlessState.startupFailureManifestFailureCategory,
349 26 : formatTimestampUtc(),
350 : ATLASCORE_BUILD_GIT_COMMIT,
351 : ATLASCORE_BUILD_GIT_DIRTY != 0,
352 91 : ATLASCORE_BUILD_TYPE);
353 :
354 13 : const auto finalization = simlab::FinalizeHeadlessRunReports(selectedScenarioKey,
355 : headlessSummaryAccumulator,
356 : finalizationPreparation.context,
357 : outcome,
358 : finalizationPreparation.artifacts,
359 13 : headlessState.summaryStream.is_open() ? static_cast<std::ostream*>(&headlessState.summaryStream) : nullptr,
360 39 : headlessState.manifestStream.is_open() ? static_cast<std::ostream*>(&headlessState.manifestStream) : nullptr);
361 :
362 13 : simlab::ApplyHeadlessRunFinalizationResult(headlessState, finalization);
363 :
364 : const auto finalizationLogging = simlab::PrepareHeadlessFinalizationLogging(batchIndexPath,
365 13 : headlessState.batchIndexFailureCategory);
366 13 : simlab::LogHeadlessFinalizationMessages(logger, finalizationLogging);
367 :
368 13 : logger.Info("AtlasCore shutting down.");
369 13 : return outcome.exitCode;
370 19 : }
|