LCOV - code coverage report
Current view: top level - src/controls - WebServerControl.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: fef103 Lines: 195 252 77.4 %
Date: 2025-09-03 20:01:23 Functions: 22 24 91.7 %
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       14547 : WebServerControl::validParams()
      52             : {
      53       14547 :   InputParameters params = Control::validParams();
      54       29094 :   params.addClassDescription("Starts a webserver for sending/receiving JSON messages to get data "
      55             :                              "and control a running MOOSE calculation");
      56       58188 :   params.addParam<unsigned int>("port",
      57             :                                 "The port to listen on; must provide either this or 'file_socket'");
      58       43641 :   params.addParam<FileName>(
      59             :       "file_socket",
      60             :       "The path to the unix file socket to listen on; must provide either this or 'port'");
      61       14547 :   return params;
      62           0 : }
      63             : 
      64         145 : WebServerControl::WebServerControl(const InputParameters & parameters)
      65         145 :   : Control(parameters), _currently_waiting(false), _terminate_requested(false)
      66             : {
      67         290 :   const auto has_port = isParamValid("port");
      68         290 :   const auto has_file_socket = isParamValid("file_socket");
      69         145 :   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         141 :   if (has_port && has_file_socket)
      73           8 :     paramError("port", "Cannot provide both 'port' and 'file_socket'");
      74             : 
      75         137 :   if (processor_id() == 0)
      76          94 :     startServer();
      77         137 : }
      78             : 
      79         274 : WebServerControl::~WebServerControl()
      80             : {
      81         137 :   if (_server)
      82             :   {
      83          94 :     _server->shutdown();
      84          94 :     _server_thread->join();
      85             :   }
      86         274 : }
      87             : 
      88             : void
      89          94 : 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         412 :       [&error](const auto & msg, const std::string & name, const std::string & description)
     106             :   {
     107             :     using result = std::variant<std::string, HttpResponse>;
     108         412 :     const auto it = msg.find(name);
     109         412 :     if (it == msg.end())
     110             :       return result(
     111           0 :           error("The entry '" + name + "' is missing which should contain the " + description));
     112         412 :     const auto & value = it->second;
     113         412 :     if (!value.isString())
     114           0 :       return result(error("The entry '" + name + "' which should contain the " + description));
     115         412 :     return result(value.toString());
     116          94 :   };
     117             : 
     118             :   // Helper for getting a string name from a json value with error checking
     119         284 :   const auto get_name = [&get_string](const auto & msg, const std::string & description)
     120         852 :   { 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         284 :   const auto require_waiting = [&error](auto & control)
     125             :   {
     126             :     using result = std::optional<HttpResponse>;
     127         284 :     if (!control.currentlyWaiting())
     128           0 :       return result(error("This control is not currently waiting for data"));
     129         284 :     return result{};
     130          94 :   };
     131             : 
     132         284 :   const auto require_parameters = [&error](const auto & msg, const std::set<std::string> & params)
     133             :   {
     134             :     using result = std::optional<HttpResponse>;
     135         824 :     for (const auto & key_value_pair : msg)
     136         540 :       if (!params.count(key_value_pair.first))
     137           0 :         return result(error("The key '" + key_value_pair.first + "' is unused"));
     138         284 :     return result{};
     139          94 :   };
     140             : 
     141          94 :   _server = std::make_unique<HttpServer>();
     142             : 
     143             :   // GET /check, returns code 200
     144        1639 :   _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         282 :   _server->when("/waiting")
     151          94 :       ->requested(
     152         862 :           [this](const HttpRequest & /*req*/)
     153             :           {
     154         862 :             miniJson::Json::_object res_json;
     155         862 :             if (this->_currently_waiting.load())
     156             :             {
     157        2586 :               res_json["waiting"] = true;
     158        1724 :               res_json["execute_on_flag"] =
     159        2586 :                   static_cast<std::string>(this->_fe_problem.getCurrentExecuteOnFlag());
     160             :             }
     161             :             else
     162           0 :               res_json["waiting"] = false;
     163             : 
     164        1724 :             return HttpResponse{200, res_json};
     165         862 :           });
     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         282 :   _server->when("/get/postprocessor")
     172          94 :       ->posted(
     173          28 :           [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
     174             :           {
     175          28 :             const auto & msg = req.json().toObject();
     176             : 
     177             :             // Get the postprocessor name
     178          56 :             const auto name_result = get_name(msg, "postprocessor to retrieve");
     179          28 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     180           0 :               return *response;
     181          28 :             const auto & name = std::get<std::string>(name_result);
     182             : 
     183             :             // Should only have a name
     184          28 :             if (const auto response = require_parameters(msg, {"name"}))
     185          28 :               return *response;
     186             :             // Should be waiting for data
     187          28 :             if (const auto response = require_waiting(*this))
     188          28 :               return *response;
     189             :             // Postprocessor should exist
     190          28 :             if (!this->hasPostprocessorByName(name))
     191           0 :               return error("The postprocessor '" + name + "' was not found");
     192             : 
     193          28 :             miniJson::Json::_object res_json;
     194          84 :             res_json["value"] = getPostprocessorValueByName(name);
     195          28 :             return HttpResponse{200, res_json};
     196          28 :           });
     197             : 
     198             :   // POST /get/reporter, with data:
     199             :   //   'name' (string): The name of the Reporter value (object_name/value_name)
     200             :   // Returns code 200 on success and JSON:
     201             :   //   'value' (double): The postprocessor value
     202         282 :   _server->when("/get/reporter")
     203          94 :       ->posted(
     204         128 :           [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
     205             :           {
     206         128 :             const auto & msg = req.json().toObject();
     207             : 
     208             :             // Should only have name and type
     209         128 :             if (const auto response = require_parameters(msg, {"name"}))
     210         128 :               return *response;
     211             :             // Should be waiting for data
     212         128 :             if (const auto response = require_waiting(*this))
     213         128 :               return *response;
     214             : 
     215             :             // Get the reporter name
     216         256 :             const auto name_result = get_name(msg, "reporter value to retrieve");
     217         128 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     218           0 :               return *response;
     219         128 :             const auto & name = std::get<std::string>(name_result);
     220         128 :             if (!ReporterName::isValidName(name))
     221           0 :               return error(name + " is not a valid reporter name.");
     222         128 :             const auto rname = ReporterName(name);
     223             : 
     224             :             // Reporter should exist
     225         128 :             if (!this->hasReporterValueByName(rname))
     226           0 :               return error("The reporter value '" + name + "' was not found");
     227             : 
     228             :             // Grab the reporter value in nlohmann::json format, then convert to miniJson
     229         128 :             nlohmann::json njson;
     230         128 :             getReporterContextBaseByName(rname).store(njson);
     231         384 :             miniJson::Json::_object res_json = {{"value", toMiniJson(njson)}};
     232             : 
     233         128 :             return HttpResponse{200, res_json};
     234         256 :           });
     235             : 
     236             :   // POST /set/controllable, with data:
     237             :   //   'name' (string): The path to the controllable data
     238             :   //   'value': The data to set
     239             :   //   'type' (string): The C++ type of the controllable data to set
     240             :   // Returns code 201 on success and JSON:
     241             :   //   'error' (string): The error (only set if an error occurred)
     242         282 :   _server->when("/set/controllable")
     243          94 :       ->posted(
     244         128 :           [this, &error, &get_string, &get_name, &require_waiting, &require_parameters](
     245             :               const HttpRequest & req)
     246             :           {
     247         128 :             const auto & msg = req.json().toObject();
     248             : 
     249             :             // Should only have a name, type, and value
     250         128 :             if (const auto response = require_parameters(msg, {"name", "type", "value"}))
     251         128 :               return *response;
     252             :             // Should be waiting for data
     253         128 :             if (const auto response = require_waiting(*this))
     254         128 :               return *response;
     255             : 
     256             :             // Get the parameter type
     257         512 :             const auto type_result = get_string(msg, "type", "type of the parameter");
     258         128 :             if (const auto response = std::get_if<HttpResponse>(&type_result))
     259           0 :               return *response;
     260         128 :             const auto & type = std::get<std::string>(type_result);
     261         128 :             if (!Moose::WebServerControlTypeRegistry::isRegistered(type))
     262           0 :               return error("The type '" + type +
     263           0 :                            "' is not registered for setting a controllable parameter");
     264             : 
     265             :             // Get the parameter name
     266         256 :             const auto name_result = get_name(msg, "name of the parameter to control");
     267         128 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     268           0 :               return *response;
     269         128 :             const auto & name = std::get<std::string>(name_result);
     270             :             // Parameter should exist
     271         128 :             if (!this->hasControllableParameterByName(name))
     272           0 :               return error("The controllable parameter '" + name + "' was not found");
     273             : 
     274             :             // Get the parameter value
     275         128 :             const auto value_it = msg.find("value");
     276         128 :             if (value_it == msg.end())
     277             :               return error(
     278           0 :                   "The entry 'value' is missing which should contain the value of the parameter");
     279         128 :             const auto & json_value = value_it->second;
     280             : 
     281             :             // Build the value (also does the parsing)
     282             :             {
     283         128 :               std::unique_ptr<ValueBase> value;
     284         128 :               std::lock_guard<std::mutex> lock(this->_controlled_values_mutex);
     285             :               try
     286             :               {
     287         128 :                 value = Moose::WebServerControlTypeRegistry::build(type, name, json_value);
     288             :               }
     289           0 :               catch (ValueBase::Exception & e)
     290             :               {
     291           0 :                 return error("While parsing 'value': " + std::string(e.what()));
     292           0 :               }
     293         128 :               _controlled_values.emplace_back(std::move(value));
     294         128 :             }
     295             : 
     296         128 :             return HttpResponse{201};
     297         128 :           });
     298             : 
     299             :   // GET /continue, Returns code 200
     300         282 :   _server->when("/continue")
     301          94 :       ->requested(
     302         203 :           [this, &error](const HttpRequest &)
     303             :           {
     304         203 :             if (this->_currently_waiting.load())
     305             :             {
     306         203 :               this->_currently_waiting.store(false);
     307         203 :               return HttpResponse{200};
     308             :             }
     309             : 
     310             :             // Not currently waiting
     311           0 :             return error("The control is not currently waiting");
     312             :           });
     313             : 
     314             :   // GET /terminate, Returns code 200 and tell FEProblemBase to terminate solve
     315         282 :   _server->when("/terminate")
     316          94 :       ->requested(
     317           8 :           [this, &error](const HttpRequest &)
     318             :           {
     319           8 :             if (this->_currently_waiting.load())
     320             :             {
     321           8 :               this->_terminate_requested.store(true);
     322           8 :               this->_currently_waiting.store(false);
     323           8 :               return HttpResponse{200};
     324             :             }
     325             : 
     326             :             // Not currently waiting
     327           0 :             return error("The control is not currently waiting");
     328             :           });
     329             : 
     330          94 :   _server_thread = std::make_unique<std::thread>(
     331          94 :       [this]
     332             :       {
     333         282 :         if (this->isParamValid("port"))
     334             :         {
     335          14 :           const uint16_t port = this->getParam<unsigned int>("port");
     336             :           try
     337             :           {
     338           7 :             _server->startListening(port);
     339             :           }
     340           0 :           catch (...)
     341             :           {
     342           0 :             this->mooseError("Failed to start the webserver; it is likely that the port ",
     343             :                              port,
     344             :                              " is not available");
     345           0 :           }
     346             :         }
     347         261 :         else if (this->isParamValid("file_socket"))
     348             :         {
     349         174 :           const auto & file_socket = this->getParam<FileName>("file_socket");
     350          87 :           _server->startListening(file_socket);
     351             :         }
     352         188 :       });
     353          94 : }
     354             : 
     355             : void
     356         306 : WebServerControl::execute()
     357             : {
     358             :   // If simulation is requested to terminate, do not go through this control
     359         306 :   if (_fe_problem.isSolveTerminationRequested())
     360           0 :     return;
     361             : 
     362             :   // Needed to broadcast all of the types and names of data that we have received on rank 0
     363             :   // so that we can construct the same objects on the other ranks to receive the data and
     364             :   // set the same values
     365         306 :   std::vector<std::pair<std::string, std::string>> name_and_types;
     366             : 
     367             :   // Need to also broadcast whether or not to terminate the solve on the timestep
     368         306 :   bool terminate_solve = false; // Set value to avoid compiler warnings
     369             : 
     370             :   // Wait for the server on rank 0 to be done
     371         306 :   if (processor_id() == 0)
     372             :   {
     373        1055 :     TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
     374             : 
     375         211 :     _currently_waiting.store(true);
     376             : 
     377             :     // While waiting, yield so the server has time to run
     378    39902410 :     while (_currently_waiting.load())
     379    39902199 :       std::this_thread::yield();
     380             : 
     381         339 :     for (const auto & value_ptr : _controlled_values)
     382         128 :       name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
     383             : 
     384         211 :     terminate_solve = _terminate_requested.load();
     385         211 :     _terminate_requested.store(false);
     386         211 :   }
     387             : 
     388             :   // All processes need to wait
     389         306 :   _communicator.barrier();
     390             : 
     391             :   // Construct the values on other processors to be received into so that
     392             :   // they're parallel consistent
     393         306 :   comm().broadcast(name_and_types);
     394         306 :   if (processor_id() != 0)
     395         157 :     for (const auto & [name, type] : name_and_types)
     396          62 :       _controlled_values.emplace_back(Moose::WebServerControlTypeRegistry::build(type, name));
     397             : 
     398             :   // Set all of the values
     399         496 :   for (auto & value_ptr : _controlled_values)
     400             :   {
     401             :     try
     402             :     {
     403         190 :       value_ptr->setControllableValue(*this);
     404             :     }
     405           0 :     catch (...)
     406             :     {
     407           0 :       mooseError("Error setting '",
     408           0 :                  value_ptr->type(),
     409             :                  "' typed value for parameter '",
     410           0 :                  value_ptr->name(),
     411             :                  "'; it is likely that the parameter has a different type");
     412           0 :     }
     413             :   }
     414             : 
     415         306 :   _controlled_values.clear();
     416             : 
     417             :   // Set solve terminate on all ranks, if requested
     418         306 :   _communicator.broadcast(terminate_solve);
     419         306 :   if (terminate_solve)
     420          11 :     _fe_problem.terminateSolve();
     421         306 : }
     422             : 
     423             : std::string
     424           0 : WebServerControl::stringifyJSONType(const miniJson::JsonType & json_type)
     425             : {
     426           0 :   if (json_type == miniJson::JsonType::kNull)
     427           0 :     return "empty";
     428           0 :   if (json_type == miniJson::JsonType::kBool)
     429           0 :     return "bool";
     430           0 :   if (json_type == miniJson::JsonType::kNumber)
     431           0 :     return "number";
     432           0 :   if (json_type == miniJson::JsonType::kString)
     433           0 :     return "string";
     434           0 :   if (json_type == miniJson::JsonType::kArray)
     435           0 :     return "array";
     436           0 :   if (json_type == miniJson::JsonType::kObject)
     437           0 :     return "object";
     438           0 :   ::mooseError("WebServerControl::stringifyJSONType(): Unused JSON value type");
     439             : }
     440             : 
     441             : template <>
     442             : miniJson::Json
     443         133 : WebServerControl::toMiniJson(const nlohmann::json & value)
     444             : {
     445         133 :   const auto value_str = value.dump();
     446         133 :   std::string errMsg;
     447         133 :   const auto json_value = miniJson::Json::parse(value_str, errMsg);
     448         133 :   if (!errMsg.empty())
     449           0 :     ::mooseError("Failed parse value into miniJson:\n", errMsg);
     450         266 :   return json_value;
     451         133 : }
     452             : 
     453           6 : WebServerControl::RealEigenMatrixValue::RealEigenMatrixValue(const std::string & name,
     454           6 :                                                              const std::string & type)
     455           6 :   : TypedValueBase<RealEigenMatrix>(name, type)
     456             : {
     457           6 : }
     458             : 
     459          16 : WebServerControl::RealEigenMatrixValue::RealEigenMatrixValue(const std::string & name,
     460             :                                                              const std::string & type,
     461          16 :                                                              const miniJson::Json & json_value)
     462          16 :   : TypedValueBase<RealEigenMatrix>(name, type, getMatrixJSONValue(json_value))
     463             : {
     464          16 : }
     465             : 
     466             : RealEigenMatrix
     467          16 : WebServerControl::RealEigenMatrixValue::getMatrixJSONValue(const miniJson::Json & json_value)
     468             : {
     469          16 :   const auto from_json_type = json_value.getType();
     470          16 :   if (from_json_type != miniJson::JsonType::kArray)
     471           0 :     throw ValueBase::Exception("The value '" + json_value.serialize() + "' of type " +
     472           0 :                                stringifyJSONType(from_json_type) + " is not an array");
     473             : 
     474          16 :   const auto & array_of_array_value = json_value.toArray();
     475          16 :   const auto nrows = array_of_array_value.size();
     476          16 :   if (nrows == 0)
     477           0 :     return RealEigenMatrix::Zero(0, 0);
     478             : 
     479          16 :   RealEigenMatrix matrix;
     480          56 :   for (const auto i : make_range(nrows))
     481             :   {
     482          40 :     if (array_of_array_value[i].getType() != miniJson::JsonType::kArray)
     483             :       throw ValueBase::Exception(
     484           0 :           "Element " + std::to_string(i) + " of '" + json_value.serialize() + "' of type " +
     485           0 :           stringifyJSONType(array_of_array_value[i].getType()) + " is not an array");
     486             : 
     487          40 :     const auto & array_value = array_of_array_value[i].toArray();
     488          40 :     if (i == 0)
     489          16 :       matrix.resize(nrows, array_value.size());
     490          24 :     else if (array_value.size() != (std::size_t)matrix.cols())
     491           0 :       throw ValueBase::Exception("The matrix '" + json_value.serialize() + "' is jagged.");
     492             : 
     493         184 :     for (const auto j : index_range(array_value))
     494         144 :       matrix(i, j) = array_value[j].toDouble();
     495             :   }
     496             : 
     497          16 :   return matrix;
     498          16 : }

Generated by: LCOV version 1.14