LCOV - code coverage report
Current view: top level - src - main.cpp (source / functions) Coverage Total Hit
Test: coverage.info Lines: 89.0 % 154 137
Test Date: 2026-04-10 19:03:25 Functions: 87.5 % 8 7

            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 : }
        

Generated by: LCOV version 2.0-1