LCOV - code coverage report
Current view: top level - src/controls - WebServerControl.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: 2bf808 Lines: 131 176 74.4 %
Date: 2025-07-17 01:28:37 Functions: 16 18 88.9 %
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             : 
      37             : // Registration of the types that we can accept in the web server for controlling parameters
      38             : registerWebServerControlScalarBool(bool);
      39             : registerWebServerControlScalarNumber(Real);
      40             : registerWebServerControlScalarNumber(int);
      41             : registerWebServerControlScalarString(std::string);
      42             : registerWebServerControlVectorNumber(Real);
      43             : registerWebServerControlVectorNumber(int);
      44             : registerWebServerControlVectorString(std::string);
      45             : 
      46             : InputParameters
      47       14481 : WebServerControl::validParams()
      48             : {
      49       14481 :   InputParameters params = Control::validParams();
      50       14481 :   params.addClassDescription("Starts a webserver for sending/receiving JSON messages to get data "
      51             :                              "and control a running MOOSE calculation");
      52       14481 :   params.addParam<unsigned int>("port",
      53             :                                 "The port to listen on; must provide either this or 'file_socket'");
      54       14481 :   params.addParam<FileName>(
      55             :       "file_socket",
      56             :       "The path to the unix file socket to listen on; must provide either this or 'port'");
      57       14481 :   return params;
      58           0 : }
      59             : 
      60         112 : WebServerControl::WebServerControl(const InputParameters & parameters)
      61         112 :   : Control(parameters), _currently_waiting(false)
      62             : {
      63         112 :   const auto has_port = isParamValid("port");
      64         112 :   const auto has_file_socket = isParamValid("file_socket");
      65         112 :   if (!has_port && !has_file_socket)
      66           4 :     mooseError("You must provide either the parameter 'port' or 'file_socket' to designate where "
      67             :                "to listen");
      68         108 :   if (has_port && has_file_socket)
      69           4 :     paramError("port", "Cannot provide both 'port' and 'file_socket'");
      70             : 
      71         104 :   if (processor_id() == 0)
      72          70 :     startServer();
      73         104 : }
      74             : 
      75         208 : WebServerControl::~WebServerControl()
      76             : {
      77         104 :   if (_server)
      78             :   {
      79          70 :     _server->shutdown();
      80          70 :     _server_thread->join();
      81             :   }
      82         208 : }
      83             : 
      84             : void
      85          70 : WebServerControl::startServer()
      86             : {
      87             :   mooseAssert(processor_id() == 0, "Should only be started on rank 0");
      88             :   mooseAssert(!_server, "Server is already started");
      89             :   mooseAssert(!_server_thread, "Server thread is already listening");
      90             : 
      91             :   // Helper for returning an error response
      92           0 :   const auto error = [](const std::string & error)
      93             :   {
      94           0 :     miniJson::Json::_object response;
      95           0 :     response["error"] = error;
      96           0 :     return HttpResponse{400, response};
      97           0 :   };
      98             : 
      99             :   // Helper for getting a string from a json value with error checking
     100             :   const auto get_string =
     101         252 :       [&error](const auto & msg, const std::string & name, const std::string & description)
     102             :   {
     103             :     using result = std::variant<std::string, HttpResponse>;
     104         252 :     const auto it = msg.find(name);
     105         252 :     if (it == msg.end())
     106             :       return result(
     107           0 :           error("The entry '" + name + "' is missing which should contain the " + description));
     108         252 :     const auto & value = it->second;
     109         252 :     if (!value.isString())
     110           0 :       return result(error("The entry '" + name + "' which should contain the " + description));
     111         252 :     return result(value.toString());
     112          70 :   };
     113             : 
     114             :   // Helper for getting a string name from a json value with error checking
     115         140 :   const auto get_name = [&get_string](const auto & msg, const std::string & description)
     116         140 :   { return get_string(msg, "name", "name of the " + description); };
     117             : 
     118             :   // Helper for requiring that the control is waiting
     119             :   // Note that this is very hard to test unless we want to add sleeps
     120         140 :   const auto require_waiting = [&error](auto & control)
     121             :   {
     122             :     using result = std::optional<HttpResponse>;
     123         140 :     if (!control.currentlyWaiting())
     124           0 :       return result(error("This control is not currently waiting for data"));
     125         140 :     return result{};
     126          70 :   };
     127             : 
     128         140 :   const auto require_parameters = [&error](const auto & msg, const std::set<std::string> & params)
     129             :   {
     130             :     using result = std::optional<HttpResponse>;
     131         504 :     for (const auto & key_value_pair : msg)
     132         364 :       if (!params.count(key_value_pair.first))
     133           0 :         return result(error("The key '" + key_value_pair.first + "' is unused"));
     134         140 :     return result{};
     135          70 :   };
     136             : 
     137          70 :   _server = std::make_unique<HttpServer>();
     138             : 
     139             :   // GET /check, returns code 200
     140         889 :   _server->when("/check")->requested([](const HttpRequest & /*req*/) { return HttpResponse{200}; });
     141             : 
     142             :   // GET /waiting, returns code 200 on success and JSON:
     143             :   //  'waiting' (bool): Whether or not the control is waiting
     144             :   //  'execute_on_flag' (string): Only exists if waiting=true, the execute
     145             :   //                              flag that is being waited on
     146         140 :   _server->when("/waiting")
     147          70 :       ->requested(
     148        1386 :           [this](const HttpRequest & /*req*/)
     149             :           {
     150         462 :             miniJson::Json::_object res_json;
     151         462 :             if (this->_currently_waiting.load())
     152             :             {
     153         462 :               res_json["waiting"] = true;
     154         924 :               res_json["execute_on_flag"] =
     155        1386 :                   static_cast<std::string>(this->_fe_problem.getCurrentExecuteOnFlag());
     156             :             }
     157             :             else
     158           0 :               res_json["waiting"] = false;
     159             : 
     160         924 :             return HttpResponse{200, res_json};
     161         462 :           });
     162             : 
     163             :   // POST /get/postprocessor, with data:
     164             :   //   'name' (string): The name of the Postprocessor
     165             :   // Returns code 200 on success and JSON:
     166             :   //   'value' (double): The postprocessor value
     167         140 :   _server->when("/get/postprocessor")
     168          70 :       ->posted(
     169         168 :           [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
     170             :           {
     171          28 :             const auto & msg = req.json().toObject();
     172             : 
     173             :             // Get the postprocessor name
     174          28 :             const auto name_result = get_name(msg, "postprocessor to retrieve");
     175          28 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     176           0 :               return *response;
     177          28 :             const auto & name = std::get<std::string>(name_result);
     178             : 
     179             :             // Should only have a name
     180          84 :             if (const auto response = require_parameters(msg, {"name"}))
     181          28 :               return *response;
     182             :             // Should be waiting for data
     183          28 :             if (const auto response = require_waiting(*this))
     184          28 :               return *response;
     185             :             // Postprocessor should exist
     186          28 :             if (!this->hasPostprocessorByName(name))
     187           0 :               return error("The postprocessor '" + name + "' was not found");
     188             : 
     189          28 :             miniJson::Json::_object res_json;
     190          28 :             res_json["value"] = getPostprocessorValueByName(name);
     191          28 :             return HttpResponse{200, res_json};
     192          84 :           });
     193             : 
     194             :   // POST /set/controllable, with data:
     195             :   //   'name' (string): The path to the controllable data
     196             :   //   'value': The data to set
     197             :   //   'type' (string): The C++ type of the controllable data to set
     198             :   // Returns code 201 on success and JSON:
     199             :   //   'error' (string): The error (only set if an error occurred)
     200         140 :   _server->when("/set/controllable")
     201          70 :       ->posted(
     202         112 :           [this, &error, &get_string, &get_name, &require_waiting, &require_parameters](
     203         784 :               const HttpRequest & req)
     204             :           {
     205         112 :             const auto & msg = req.json().toObject();
     206             : 
     207             :             // Should only have a name, type, and value
     208         560 :             if (const auto response = require_parameters(msg, {"name", "type", "value"}))
     209         112 :               return *response;
     210             :             // Should be waiting for data
     211         112 :             if (const auto response = require_waiting(*this))
     212         112 :               return *response;
     213             : 
     214             :             // Get the parameter type
     215         112 :             const auto type_result = get_string(msg, "type", "type of the parameter");
     216         112 :             if (const auto response = std::get_if<HttpResponse>(&type_result))
     217           0 :               return *response;
     218         112 :             const auto & type = std::get<std::string>(type_result);
     219         112 :             if (!Moose::WebServerControlTypeRegistry::isRegistered(type))
     220           0 :               return error("The type '" + type +
     221           0 :                            "' is not registered for setting a controllable parameter");
     222             : 
     223             :             // Get the parameter name
     224         112 :             const auto name_result = get_name(msg, "name of the parameter to control");
     225         112 :             if (const auto response = std::get_if<HttpResponse>(&name_result))
     226           0 :               return *response;
     227         112 :             const auto & name = std::get<std::string>(name_result);
     228             :             // Parameter should exist
     229         112 :             if (!this->hasControllableParameterByName(name))
     230           0 :               return error("The controllable parameter '" + name + "' was not found");
     231             : 
     232             :             // Get the parameter value
     233         112 :             const auto value_it = msg.find("value");
     234         112 :             if (value_it == msg.end())
     235             :               return error(
     236           0 :                   "The entry 'value' is missing which should contain the value of the parameter");
     237         112 :             const auto & json_value = value_it->second;
     238             : 
     239             :             // Build the value (also does the parsing)
     240             :             {
     241         112 :               std::unique_ptr<ValueBase> value;
     242         112 :               std::lock_guard<std::mutex> lock(this->_controlled_values_mutex);
     243             :               try
     244             :               {
     245         112 :                 value = Moose::WebServerControlTypeRegistry::build(type, name, json_value);
     246             :               }
     247           0 :               catch (ValueBase::Exception & e)
     248             :               {
     249           0 :                 return error("While parsing 'value': " + std::string(e.what()));
     250           0 :               }
     251         112 :               _controlled_values.emplace_back(std::move(value));
     252         112 :             }
     253             : 
     254         112 :             return HttpResponse{201};
     255         336 :           });
     256             : 
     257             :   // GET /continue, Returns code 200
     258         140 :   _server->when("/continue")
     259          70 :       ->requested(
     260         294 :           [this, &error](const HttpRequest &)
     261             :           {
     262         147 :             if (this->_currently_waiting.load())
     263             :             {
     264         147 :               this->_currently_waiting.store(false);
     265         147 :               return HttpResponse{200};
     266             :             }
     267             : 
     268             :             // Not currently waiting
     269           0 :             return error("The control is not currently waiting");
     270             :           });
     271             : 
     272          70 :   _server_thread = std::make_unique<std::thread>(
     273          70 :       [this]
     274             :       {
     275          70 :         if (this->isParamValid("port"))
     276             :         {
     277           7 :           const uint16_t port = this->getParam<unsigned int>("port");
     278             :           try
     279             :           {
     280           7 :             _server->startListening(port);
     281             :           }
     282           0 :           catch (...)
     283             :           {
     284           0 :             this->mooseError("Failed to start the webserver; it is likely that the port ",
     285             :                              port,
     286             :                              " is not available");
     287           0 :           }
     288             :         }
     289          63 :         else if (this->isParamValid("file_socket"))
     290             :         {
     291          63 :           const auto & file_socket = this->getParam<FileName>("file_socket");
     292          63 :           _server->startListening(file_socket);
     293             :         }
     294         140 :       });
     295          70 : }
     296             : 
     297             : void
     298         218 : WebServerControl::execute()
     299             : {
     300             :   // Needed to broadcast all of the types and names of data that we have received on rank 0
     301             :   // so that we can construct the same objects on the other ranks to receive the data and
     302             :   // set the same values
     303         218 :   std::vector<std::pair<std::string, std::string>> name_and_types;
     304             : 
     305             :   // Wait for the server on rank 0 to be done
     306         218 :   if (processor_id() == 0)
     307             :   {
     308         147 :     TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
     309             : 
     310         147 :     _currently_waiting.store(true);
     311             : 
     312             :     // While waiting, yield so the server has time to run
     313    20434588 :     while (_currently_waiting.load())
     314    20434441 :       std::this_thread::yield();
     315             : 
     316         259 :     for (const auto & value_ptr : _controlled_values)
     317         112 :       name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
     318         147 :   }
     319             : 
     320             :   // All processes need to wait
     321         218 :   _communicator.barrier();
     322             : 
     323             :   // Construct the values on other processors to be received into so that
     324             :   // they're parallel consistent
     325         218 :   comm().broadcast(name_and_types);
     326         218 :   if (processor_id() != 0)
     327         127 :     for (const auto & [name, type] : name_and_types)
     328          56 :       _controlled_values.emplace_back(Moose::WebServerControlTypeRegistry::build(type, name));
     329             : 
     330             :   // Set all of the values
     331         386 :   for (auto & value_ptr : _controlled_values)
     332             :   {
     333             :     try
     334             :     {
     335         168 :       value_ptr->setControllableValue(*this);
     336             :     }
     337           0 :     catch (...)
     338             :     {
     339           0 :       mooseError("Error setting '",
     340           0 :                  value_ptr->type(),
     341             :                  "' typed value for parameter '",
     342           0 :                  value_ptr->name(),
     343             :                  "'; it is likely that the parameter has a different type");
     344           0 :     }
     345             :   }
     346             : 
     347         218 :   _controlled_values.clear();
     348         218 : }
     349             : 
     350             : std::string
     351           0 : WebServerControl::stringifyJSONType(const miniJson::JsonType & json_type)
     352             : {
     353           0 :   if (json_type == miniJson::JsonType::kNull)
     354           0 :     return "empty";
     355           0 :   if (json_type == miniJson::JsonType::kBool)
     356           0 :     return "bool";
     357           0 :   if (json_type == miniJson::JsonType::kNumber)
     358           0 :     return "number";
     359           0 :   if (json_type == miniJson::JsonType::kString)
     360           0 :     return "string";
     361           0 :   if (json_type == miniJson::JsonType::kArray)
     362           0 :     return "array";
     363           0 :   if (json_type == miniJson::JsonType::kObject)
     364           0 :     return "object";
     365           0 :   ::mooseError("WebServerControl::stringifyJSONType(): Unused JSON value type");
     366             : }

Generated by: LCOV version 1.14