LCOV - code coverage report
Current view: top level - src/controls - WebServerControl.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: #31730 (e8b711) with base e0c998 Lines: 212 269 78.8 %
Date: 2025-10-29 16:49:47 Functions: 24 26 92.3 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://mooseframework.inl.gov
       3             : //*
       4             : //* All rights reserved, see COPYRIGHT for full restrictions
       5             : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
       6             : //*
       7             : //* Licensed under LGPL 2.1, please see LICENSE for details
       8             : //* https://www.gnu.org/licenses/lgpl-2.1.html
       9             : 
      10             : #include "WebServerControl.h"
      11             : #include "FEProblemBase.h"
      12             : #include "MooseApp.h"
      13             : 
      14             : #include "minijson/minijson.h"
      15             : 
      16             : registerMooseObject("MooseApp", WebServerControl);
      17             : 
      18             : #define registerWebServerControlCombine1(X, Y) X##Y
      19             : #define registerWebServerControlCombine(X, Y) registerWebServerControlCombine1(X, Y)
      20             : #define registerWebServerControlScalar(T, json_type)                                               \
      21             :   static char registerWebServerControlCombine(wsc_scalar, __COUNTER__) =                           \
      22             :       WebServerControl::registerScalarType<T, json_type>(#T)
      23             : #define registerWebServerControlVector(T, json_type)                                               \
      24             :   static char registerWebServerControlCombine(wsc_vector, __COUNTER__) =                           \
      25             :       WebServerControl::registerVectorType<T, json_type>(#T)
      26             : #define registerWebServerControlScalarBool(T)                                                      \
      27             :   registerWebServerControlScalar(T, miniJson::JsonType::kBool)
      28             : #define registerWebServerControlScalarNumber(T)                                                    \
      29             :   registerWebServerControlScalar(T, miniJson::JsonType::kNumber)
      30             : #define registerWebServerControlScalarString(T)                                                    \
      31             :   registerWebServerControlScalar(T, miniJson::JsonType::kString)
      32             : #define registerWebServerControlVectorNumber(T)                                                    \
      33             :   registerWebServerControlVector(T, miniJson::JsonType::kNumber)
      34             : #define registerWebServerControlVectorString(T)                                                    \
      35             :   registerWebServerControlVector(T, miniJson::JsonType::kString)
      36             : #define registerWebServerControlRealEigenMatrix()                                                  \
      37             :   static char registerWebServerControlCombine(wsc_matrix, __COUNTER__) =                           \
      38             :       WebServerControl::registerRealEigenMatrix()
      39             : 
      40             : // Registration of the types that we can accept in the web server for controlling parameters
      41             : registerWebServerControlScalarBool(bool);
      42             : registerWebServerControlScalarNumber(Real);
      43             : registerWebServerControlScalarNumber(int);
      44             : registerWebServerControlScalarString(std::string);
      45             : registerWebServerControlVectorNumber(Real);
      46             : registerWebServerControlVectorNumber(int);
      47             : registerWebServerControlVectorString(std::string);
      48             : registerWebServerControlRealEigenMatrix();
      49             : 
      50             : InputParameters
      51       15169 : WebServerControl::validParams()
      52             : {
      53       15169 :   InputParameters params = Control::validParams();
      54       30338 :   params.addClassDescription("Starts a webserver for sending/receiving JSON messages to get data "
      55             :                              "and control a running MOOSE calculation");
      56       60676 :   params.addParam<unsigned int>("port",
      57             :                                 "The port to listen on; must provide either this or 'file_socket'");
      58       45507 :   params.addParam<FileName>(
      59             :       "file_socket",
      60             :       "The path to the unix file socket to listen on; must provide either this or 'port'");
      61       15169 :   return params;
      62           0 : }
      63             : 
      64         155 : WebServerControl::WebServerControl(const InputParameters & parameters)
      65         155 :   : Control(parameters), _currently_waiting(false), _terminate_requested(false)
      66             : {
      67         310 :   const auto has_port = isParamValid("port");
      68         310 :   const auto has_file_socket = isParamValid("file_socket");
      69         155 :   if (!has_port && !has_file_socket)
      70           4 :     mooseError("You must provide either the parameter 'port' or 'file_socket' to designate where "
      71             :                "to listen");
      72         151 :   if (has_port && has_file_socket)
      73           8 :     paramError("port", "Cannot provide both 'port' and 'file_socket'");
      74             : 
      75         147 :   if (processor_id() == 0)
      76         101 :     startServer();
      77         147 : }
      78             : 
      79         294 : WebServerControl::~WebServerControl()
      80             : {
      81         147 :   if (_server)
      82             :   {
      83         101 :     _server->shutdown();
      84         101 :     _server_thread->join();
      85             :   }
      86         294 : }
      87             : 
      88             : void
      89         101 : WebServerControl::startServer()
      90             : {
      91             :   mooseAssert(processor_id() == 0, "Should only be started on rank 0");
      92             :   mooseAssert(!_server, "Server is already started");
      93             :   mooseAssert(!_server_thread, "Server thread is already listening");
      94             : 
      95             :   // Helper for returning an error response
      96           0 :   const auto error = [](const std::string & error)
      97             :   {
      98           0 :     miniJson::Json::_object response;
      99           0 :     response["error"] = error;
     100           0 :     return HttpResponse{400, response};
     101           0 :   };
     102             : 
     103             :   // Helper for getting a string from a json value with error checking
     104             :   const auto get_string =
     105         468 :       [&error](const auto & msg, const std::string & name, const std::string & description)
     106             :   {
     107             :     using result = std::variant<std::string, HttpResponse>;
     108         468 :     const auto it = msg.find(name);
     109         468 :     if (it == msg.end())
     110             :       return result(
     111           0 :           error("The entry '" + name + "' is missing which should contain the " + description));
     112         468 :     const auto & value = it->second;
     113         468 :     if (!value.isString())
     114           0 :       return result(error("The entry '" + name + "' which should contain the " + description));
     115         468 :     return result(value.toString());
     116         101 :   };
     117             : 
     118             :   // Helper for getting a string name from a json value with error checking
     119         340 :   const auto get_name = [&get_string](const auto & msg, const std::string & description)
     120        1020 :   { return get_string(msg, "name", "name of the " + description); };
     121             : 
     122             :   // Helper for requiring that the control is waiting
     123             :   // Note that this is very hard to test unless we want to add sleeps
     124         396 :   const auto require_waiting = [&error](auto & control)
     125             :   {
     126             :     using result = std::optional<HttpResponse>;
     127         396 :     if (!control.currentlyWaiting())
     128           0 :       return result(error("This control is not currently waiting for data"));
     129         396 :     return result{};
     130         101 :   };
     131             : 
     132         340 :   const auto require_parameters = [&error](const auto & msg, const std::set<std::string> & params)
     133             :   {
     134             :     using result = std::optional<HttpResponse>;
     135         936 :     for (const auto & key_value_pair : msg)
     136         596 :       if (!params.count(key_value_pair.first))
     137           0 :         return result(error("The key '" + key_value_pair.first + "' is unused"));
     138         340 :     return result{};
     139         101 :   };
     140             : 
     141         101 :   _server = std::make_unique<HttpServer>();
     142             : 
     143             :   // GET /check, returns code 200
     144        2024 :   _server->when("/check")->requested([](const HttpRequest & /*req*/) { return HttpResponse{200}; });
     145             : 
     146             :   // GET /waiting, returns code 200 on success and JSON:
     147             :   //  'waiting' (bool): Whether or not the control is waiting
     148             :   //  'execute_on_flag' (string): Only exists if waiting=true, the execute
     149             :   //                              flag that is being waited on
     150         303 :   _server->when("/waiting")
     151         101 :       ->requested(
     152        1086 :           [this](const HttpRequest & /*req*/)
     153             :           {
     154        1086 :             miniJson::Json::_object res_json;
     155        1086 :             if (this->_currently_waiting.load())
     156             :             {
     157        3258 :               res_json["waiting"] = true;
     158        2172 :               res_json["execute_on_flag"] =
     159        3258 :                   static_cast<std::string>(this->_fe_problem.getCurrentExecuteOnFlag());
     160             :             }
     161             :             else
     162           0 :               res_json["waiting"] = false;
     163             : 
     164        2172 :             return HttpResponse{200, res_json};
     165        1086 :           });
     166             : 
     167             :   // POST /get/postprocessor, with data:
     168             :   //   'name' (string): The name of the Postprocessor
     169             :   // Returns code 200 on success and JSON:
     170             :   //   'value' (double): The postprocessor value
     171         303 :   _server->when("/get/postprocessor")
     172         101 :       ->posted(
     173          84 :           [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
     174             :           {
     175          84 :             const auto & msg = req.json().toObject();
     176             : 
     177             :             // Get the postprocessor name
     178         168 :             const auto name_result = get_name(msg, "postprocessor to retrieve");
     179          84 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     180           0 :               return *response;
     181          84 :             const auto & name = std::get<std::string>(name_result);
     182             : 
     183             :             // Should only have a name
     184          84 :             if (const auto response = require_parameters(msg, {"name"}))
     185          84 :               return *response;
     186             :             // Should be waiting for data
     187          84 :             if (const auto response = require_waiting(*this))
     188          84 :               return *response;
     189             :             // Postprocessor should exist
     190          84 :             if (!this->hasPostprocessorByName(name))
     191           0 :               return error("The postprocessor '" + name + "' was not found");
     192             : 
     193          84 :             miniJson::Json::_object res_json;
     194         252 :             res_json["value"] = getPostprocessorValueByName(name);
     195          84 :             return HttpResponse{200, res_json};
     196          84 :           });
     197             : 
     198             :   // GET /get/time
     199             :   // Returns code 200 on success and JSON:
     200             :   //   'time' (double): The current time
     201         303 :   _server->when("/get/time")
     202         101 :       ->requested(
     203          28 :           [this, &require_waiting](const HttpRequest & /*req*/)
     204             :           {
     205             :             // Should be waiting for data
     206          28 :             if (const auto response = require_waiting(*this))
     207          28 :               return *response;
     208             : 
     209          28 :             miniJson::Json::_object res_json;
     210          84 :             res_json["time"] = _fe_problem.time();
     211             : 
     212          28 :             return HttpResponse{200, res_json};
     213          28 :           });
     214             : 
     215             :   // GET /get/dt
     216             :   // Returns code 200 on success and JSON:
     217             :   //   'dt' (double): The current time step size
     218         202 :   _server->when("/get/dt")->requested(
     219          28 :       [this, &require_waiting](const HttpRequest & /*req*/)
     220             :       {
     221             :         // Should be waiting for data
     222          28 :         if (const auto response = require_waiting(*this))
     223          28 :           return *response;
     224             : 
     225          28 :         miniJson::Json::_object res_json;
     226          84 :         res_json["dt"] = _fe_problem.dt();
     227             : 
     228          28 :         return HttpResponse{200, res_json};
     229          28 :       });
     230             : 
     231             :   // POST /get/reporter, with data:
     232             :   //   'name' (string): The name of the Reporter value (object_name/value_name)
     233             :   // Returns code 200 on success and JSON:
     234             :   //   'value' (double): The postprocessor value
     235         303 :   _server->when("/get/reporter")
     236         101 :       ->posted(
     237         128 :           [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
     238             :           {
     239         128 :             const auto & msg = req.json().toObject();
     240             : 
     241             :             // Should only have name and type
     242         128 :             if (const auto response = require_parameters(msg, {"name"}))
     243         128 :               return *response;
     244             :             // Should be waiting for data
     245         128 :             if (const auto response = require_waiting(*this))
     246         128 :               return *response;
     247             : 
     248             :             // Get the reporter name
     249         256 :             const auto name_result = get_name(msg, "reporter value to retrieve");
     250         128 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     251           0 :               return *response;
     252         128 :             const auto & name = std::get<std::string>(name_result);
     253         128 :             if (!ReporterName::isValidName(name))
     254           0 :               return error(name + " is not a valid reporter name.");
     255         128 :             const auto rname = ReporterName(name);
     256             : 
     257             :             // Reporter should exist
     258         128 :             if (!this->hasReporterValueByName(rname))
     259           0 :               return error("The reporter value '" + name + "' was not found");
     260             : 
     261             :             // Grab the reporter value in nlohmann::json format, then convert to miniJson
     262         128 :             nlohmann::json njson;
     263         128 :             getReporterContextBaseByName(rname).store(njson);
     264         384 :             miniJson::Json::_object res_json = {{"value", toMiniJson(njson)}};
     265             : 
     266         128 :             return HttpResponse{200, res_json};
     267         256 :           });
     268             : 
     269             :   // POST /set/controllable, with data:
     270             :   //   'name' (string): The path to the controllable data
     271             :   //   'value': The data to set
     272             :   //   'type' (string): The C++ type of the controllable data to set
     273             :   // Returns code 201 on success and JSON:
     274             :   //   'error' (string): The error (only set if an error occurred)
     275         303 :   _server->when("/set/controllable")
     276         101 :       ->posted(
     277         128 :           [this, &error, &get_string, &get_name, &require_waiting, &require_parameters](
     278             :               const HttpRequest & req)
     279             :           {
     280         128 :             const auto & msg = req.json().toObject();
     281             : 
     282             :             // Should only have a name, type, and value
     283         128 :             if (const auto response = require_parameters(msg, {"name", "type", "value"}))
     284         128 :               return *response;
     285             :             // Should be waiting for data
     286         128 :             if (const auto response = require_waiting(*this))
     287         128 :               return *response;
     288             : 
     289             :             // Get the parameter type
     290         512 :             const auto type_result = get_string(msg, "type", "type of the parameter");
     291         128 :             if (const auto response = std::get_if<HttpResponse>(&type_result))
     292           0 :               return *response;
     293         128 :             const auto & type = std::get<std::string>(type_result);
     294         128 :             if (!Moose::WebServerControlTypeRegistry::isRegistered(type))
     295           0 :               return error("The type '" + type +
     296           0 :                            "' is not registered for setting a controllable parameter");
     297             : 
     298             :             // Get the parameter name
     299         256 :             const auto name_result = get_name(msg, "name of the parameter to control");
     300         128 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     301           0 :               return *response;
     302         128 :             const auto & name = std::get<std::string>(name_result);
     303             :             // Parameter should exist
     304         128 :             if (!this->hasControllableParameterByName(name))
     305           0 :               return error("The controllable parameter '" + name + "' was not found");
     306             : 
     307             :             // Get the parameter value
     308         128 :             const auto value_it = msg.find("value");
     309         128 :             if (value_it == msg.end())
     310             :               return error(
     311           0 :                   "The entry 'value' is missing which should contain the value of the parameter");
     312         128 :             const auto & json_value = value_it->second;
     313             : 
     314             :             // Build the value (also does the parsing)
     315             :             {
     316         128 :               std::unique_ptr<ValueBase> value;
     317         128 :               std::lock_guard<std::mutex> lock(this->_controlled_values_mutex);
     318             :               try
     319             :               {
     320         128 :                 value = Moose::WebServerControlTypeRegistry::build(type, name, json_value);
     321             :               }
     322           0 :               catch (ValueBase::Exception & e)
     323             :               {
     324           0 :                 return error("While parsing 'value': " + std::string(e.what()));
     325           0 :               }
     326         128 :               _controlled_values.emplace_back(std::move(value));
     327         128 :             }
     328             : 
     329         128 :             return HttpResponse{201};
     330         128 :           });
     331             : 
     332             :   // GET /continue, Returns code 200
     333         303 :   _server->when("/continue")
     334         101 :       ->requested(
     335         231 :           [this, &error](const HttpRequest &)
     336             :           {
     337         231 :             if (this->_currently_waiting.load())
     338             :             {
     339         231 :               this->_currently_waiting.store(false);
     340         231 :               return HttpResponse{200};
     341             :             }
     342             : 
     343             :             // Not currently waiting
     344           0 :             return error("The control is not currently waiting");
     345             :           });
     346             : 
     347             :   // GET /terminate, Returns code 200 and tell FEProblemBase to terminate solve
     348         303 :   _server->when("/terminate")
     349         101 :       ->requested(
     350           8 :           [this, &error](const HttpRequest &)
     351             :           {
     352           8 :             if (this->_currently_waiting.load())
     353             :             {
     354           8 :               this->_terminate_requested.store(true);
     355           8 :               this->_currently_waiting.store(false);
     356           8 :               return HttpResponse{200};
     357             :             }
     358             : 
     359             :             // Not currently waiting
     360           0 :             return error("The control is not currently waiting");
     361             :           });
     362             : 
     363         101 :   _server_thread = std::make_unique<std::thread>(
     364         101 :       [this]
     365             :       {
     366         303 :         if (this->isParamValid("port"))
     367             :         {
     368          14 :           const uint16_t port = this->getParam<unsigned int>("port");
     369             :           try
     370             :           {
     371           7 :             _server->startListening(port);
     372             :           }
     373           0 :           catch (...)
     374             :           {
     375           0 :             this->mooseError("Failed to start the webserver; it is likely that the port ",
     376             :                              port,
     377             :                              " is not available");
     378           0 :           }
     379             :         }
     380         282 :         else if (this->isParamValid("file_socket"))
     381             :         {
     382         188 :           const auto & file_socket = this->getParam<FileName>("file_socket");
     383          94 :           _server->startListening(file_socket);
     384             :         }
     385         202 :       });
     386         101 : }
     387             : 
     388             : void
     389         346 : WebServerControl::execute()
     390             : {
     391             :   // If simulation is requested to terminate, do not go through this control
     392         346 :   if (_fe_problem.isSolveTerminationRequested())
     393           0 :     return;
     394             : 
     395             :   // Needed to broadcast all of the types and names of data that we have received on rank 0
     396             :   // so that we can construct the same objects on the other ranks to receive the data and
     397             :   // set the same values
     398         346 :   std::vector<std::pair<std::string, std::string>> name_and_types;
     399             : 
     400             :   // Need to also broadcast whether or not to terminate the solve on the timestep
     401         346 :   bool terminate_solve = false; // Set value to avoid compiler warnings
     402             : 
     403             :   // Wait for the server on rank 0 to be done
     404         346 :   if (processor_id() == 0)
     405             :   {
     406        1195 :     TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
     407             : 
     408         239 :     _currently_waiting.store(true);
     409             : 
     410             :     // While waiting, yield so the server has time to run
     411    37350634 :     while (_currently_waiting.load())
     412    37350395 :       std::this_thread::yield();
     413             : 
     414         367 :     for (const auto & value_ptr : _controlled_values)
     415         128 :       name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
     416             : 
     417         239 :     terminate_solve = _terminate_requested.load();
     418         239 :     _terminate_requested.store(false);
     419         239 :   }
     420             : 
     421             :   // All processes need to wait
     422         346 :   _communicator.barrier();
     423             : 
     424             :   // Construct the values on other processors to be received into so that
     425             :   // they're parallel consistent
     426         346 :   comm().broadcast(name_and_types);
     427         346 :   if (processor_id() != 0)
     428         169 :     for (const auto & [name, type] : name_and_types)
     429          62 :       _controlled_values.emplace_back(Moose::WebServerControlTypeRegistry::build(type, name));
     430             : 
     431             :   // Set all of the values
     432         536 :   for (auto & value_ptr : _controlled_values)
     433             :   {
     434             :     try
     435             :     {
     436         190 :       value_ptr->setControllableValue(*this);
     437             :     }
     438           0 :     catch (...)
     439             :     {
     440           0 :       mooseError("Error setting '",
     441           0 :                  value_ptr->type(),
     442             :                  "' typed value for parameter '",
     443           0 :                  value_ptr->name(),
     444             :                  "'; it is likely that the parameter has a different type");
     445           0 :     }
     446             :   }
     447             : 
     448         346 :   _controlled_values.clear();
     449             : 
     450             :   // Set solve terminate on all ranks, if requested
     451         346 :   _communicator.broadcast(terminate_solve);
     452         346 :   if (terminate_solve)
     453          11 :     _fe_problem.terminateSolve();
     454         346 : }
     455             : 
     456             : std::string
     457           0 : WebServerControl::stringifyJSONType(const miniJson::JsonType & json_type)
     458             : {
     459           0 :   if (json_type == miniJson::JsonType::kNull)
     460           0 :     return "empty";
     461           0 :   if (json_type == miniJson::JsonType::kBool)
     462           0 :     return "bool";
     463           0 :   if (json_type == miniJson::JsonType::kNumber)
     464           0 :     return "number";
     465           0 :   if (json_type == miniJson::JsonType::kString)
     466           0 :     return "string";
     467           0 :   if (json_type == miniJson::JsonType::kArray)
     468           0 :     return "array";
     469           0 :   if (json_type == miniJson::JsonType::kObject)
     470           0 :     return "object";
     471           0 :   ::mooseError("WebServerControl::stringifyJSONType(): Unused JSON value type");
     472             : }
     473             : 
     474             : template <>
     475             : miniJson::Json
     476         138 : WebServerControl::toMiniJson(const nlohmann::json & value)
     477             : {
     478         138 :   const auto value_str = value.dump();
     479         138 :   std::string errMsg;
     480         138 :   const auto json_value = miniJson::Json::parse(value_str, errMsg);
     481         138 :   if (!errMsg.empty())
     482           0 :     ::mooseError("Failed parse value into miniJson:\n", errMsg);
     483         276 :   return json_value;
     484         138 : }
     485             : 
     486           6 : WebServerControl::RealEigenMatrixValue::RealEigenMatrixValue(const std::string & name,
     487           6 :                                                              const std::string & type)
     488           6 :   : TypedValueBase<RealEigenMatrix>(name, type)
     489             : {
     490           6 : }
     491             : 
     492          16 : WebServerControl::RealEigenMatrixValue::RealEigenMatrixValue(const std::string & name,
     493             :                                                              const std::string & type,
     494          16 :                                                              const miniJson::Json & json_value)
     495          16 :   : TypedValueBase<RealEigenMatrix>(name, type, getMatrixJSONValue(json_value))
     496             : {
     497          16 : }
     498             : 
     499             : RealEigenMatrix
     500          16 : WebServerControl::RealEigenMatrixValue::getMatrixJSONValue(const miniJson::Json & json_value)
     501             : {
     502          16 :   const auto from_json_type = json_value.getType();
     503          16 :   if (from_json_type != miniJson::JsonType::kArray)
     504           0 :     throw ValueBase::Exception("The value '" + json_value.serialize() + "' of type " +
     505           0 :                                stringifyJSONType(from_json_type) + " is not an array");
     506             : 
     507          16 :   const auto & array_of_array_value = json_value.toArray();
     508          16 :   const auto nrows = array_of_array_value.size();
     509          16 :   if (nrows == 0)
     510           0 :     return RealEigenMatrix::Zero(0, 0);
     511             : 
     512          16 :   RealEigenMatrix matrix;
     513          56 :   for (const auto i : make_range(nrows))
     514             :   {
     515          40 :     if (array_of_array_value[i].getType() != miniJson::JsonType::kArray)
     516             :       throw ValueBase::Exception(
     517           0 :           "Element " + std::to_string(i) + " of '" + json_value.serialize() + "' of type " +
     518           0 :           stringifyJSONType(array_of_array_value[i].getType()) + " is not an array");
     519             : 
     520          40 :     const auto & array_value = array_of_array_value[i].toArray();
     521          40 :     if (i == 0)
     522          16 :       matrix.resize(nrows, array_value.size());
     523          24 :     else if (array_value.size() != (std::size_t)matrix.cols())
     524           0 :       throw ValueBase::Exception("The matrix '" + json_value.serialize() + "' is jagged.");
     525             : 
     526         184 :     for (const auto j : index_range(array_value))
     527         144 :       matrix(i, j) = array_value[j].toDouble();
     528             :   }
     529             : 
     530          16 :   return matrix;
     531          16 : }

Generated by: LCOV version 1.14