https://mooseframework.inl.gov
WebServerControl.C
Go to the documentation of this file.
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 
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
49 
52 {
54  params.addClassDescription("Starts a webserver for sending/receiving JSON messages to get data "
55  "and control a running MOOSE calculation");
56  params.addParam<unsigned int>("port",
57  "The port to listen on; must provide either this or 'file_socket'");
58  params.addParam<FileName>(
59  "file_socket",
60  "The path to the unix file socket to listen on; must provide either this or 'port'");
61  return params;
62 }
63 
65  : Control(parameters), _currently_waiting(false), _terminate_requested(false)
66 {
67  const auto has_port = isParamValid("port");
68  const auto has_file_socket = isParamValid("file_socket");
69  if (!has_port && !has_file_socket)
70  mooseError("You must provide either the parameter 'port' or 'file_socket' to designate where "
71  "to listen");
72  if (has_port && has_file_socket)
73  paramError("port", "Cannot provide both 'port' and 'file_socket'");
74 
75  if (processor_id() == 0)
76  startServer();
77 }
78 
80 {
81  if (_server)
82  {
83  _server->shutdown();
84  _server_thread->join();
85  }
86 }
87 
88 void
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  const auto error = [](const std::string & error)
97  {
98  miniJson::Json::_object response;
99  response["error"] = error;
100  return HttpResponse{400, response};
101  };
102 
103  // Helper for getting a string from a json value with error checking
104  const auto get_string =
105  [&error](const auto & msg, const std::string & name, const std::string & description)
106  {
107  using result = std::variant<std::string, HttpResponse>;
108  const auto it = msg.find(name);
109  if (it == msg.end())
110  return result(
111  error("The entry '" + name + "' is missing which should contain the " + description));
112  const auto & value = it->second;
113  if (!value.isString())
114  return result(error("The entry '" + name + "' which should contain the " + description));
115  return result(value.toString());
116  };
117 
118  // Helper for getting a string name from a json value with error checking
119  const auto get_name = [&get_string](const auto & msg, const std::string & description)
120  { 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  const auto require_waiting = [&error](auto & control)
125  {
126  using result = std::optional<HttpResponse>;
127  if (!control.currentlyWaiting())
128  return result(error("This control is not currently waiting for data"));
129  return result{};
130  };
131 
132  const auto require_parameters = [&error](const auto & msg, const std::set<std::string> & params)
133  {
134  using result = std::optional<HttpResponse>;
135  for (const auto & key_value_pair : msg)
136  if (!params.count(key_value_pair.first))
137  return result(error("The key '" + key_value_pair.first + "' is unused"));
138  return result{};
139  };
140 
141  _server = std::make_unique<HttpServer>();
142 
143  // GET /check, returns code 200
144  _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  _server->when("/waiting")
151  ->requested(
152  [this](const HttpRequest & /*req*/)
153  {
154  miniJson::Json::_object res_json;
155  if (this->_currently_waiting.load())
156  {
157  res_json["waiting"] = true;
158  res_json["execute_on_flag"] =
159  static_cast<std::string>(this->_fe_problem.getCurrentExecuteOnFlag());
160  }
161  else
162  res_json["waiting"] = false;
163 
164  return HttpResponse{200, res_json};
165  });
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  _server->when("/get/postprocessor")
172  ->posted(
173  [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
174  {
175  const auto & msg = req.json().toObject();
176 
177  // Get the postprocessor name
178  const auto name_result = get_name(msg, "postprocessor to retrieve");
179  if (const auto response = std::get_if<HttpResponse>(&name_result))
180  return *response;
181  const auto & name = std::get<std::string>(name_result);
182 
183  // Should only have a name
184  if (const auto response = require_parameters(msg, {"name"}))
185  return *response;
186  // Should be waiting for data
187  if (const auto response = require_waiting(*this))
188  return *response;
189  // Postprocessor should exist
190  if (!this->hasPostprocessorByName(name))
191  return error("The postprocessor '" + name + "' was not found");
192 
193  miniJson::Json::_object res_json;
194  res_json["value"] = getPostprocessorValueByName(name);
195  return HttpResponse{200, res_json};
196  });
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  _server->when("/get/reporter")
203  ->posted(
204  [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
205  {
206  const auto & msg = req.json().toObject();
207 
208  // Should only have name and type
209  if (const auto response = require_parameters(msg, {"name"}))
210  return *response;
211  // Should be waiting for data
212  if (const auto response = require_waiting(*this))
213  return *response;
214 
215  // Get the reporter name
216  const auto name_result = get_name(msg, "reporter value to retrieve");
217  if (const auto response = std::get_if<HttpResponse>(&name_result))
218  return *response;
219  const auto & name = std::get<std::string>(name_result);
221  return error(name + " is not a valid reporter name.");
222  const auto rname = ReporterName(name);
223 
224  // Reporter should exist
225  if (!this->hasReporterValueByName(rname))
226  return error("The reporter value '" + name + "' was not found");
227 
228  // Grab the reporter value in nlohmann::json format, then convert to miniJson
229  nlohmann::json njson;
230  getReporterContextBaseByName(rname).store(njson);
231  miniJson::Json::_object res_json = {{"value", toMiniJson(njson)}};
232 
233  return HttpResponse{200, res_json};
234  });
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  _server->when("/set/controllable")
243  ->posted(
244  [this, &error, &get_string, &get_name, &require_waiting, &require_parameters](
245  const HttpRequest & req)
246  {
247  const auto & msg = req.json().toObject();
248 
249  // Should only have a name, type, and value
250  if (const auto response = require_parameters(msg, {"name", "type", "value"}))
251  return *response;
252  // Should be waiting for data
253  if (const auto response = require_waiting(*this))
254  return *response;
255 
256  // Get the parameter type
257  const auto type_result = get_string(msg, "type", "type of the parameter");
258  if (const auto response = std::get_if<HttpResponse>(&type_result))
259  return *response;
260  const auto & type = std::get<std::string>(type_result);
262  return error("The type '" + type +
263  "' is not registered for setting a controllable parameter");
264 
265  // Get the parameter name
266  const auto name_result = get_name(msg, "name of the parameter to control");
267  if (const auto response = std::get_if<HttpResponse>(&name_result))
268  return *response;
269  const auto & name = std::get<std::string>(name_result);
270  // Parameter should exist
271  if (!this->hasControllableParameterByName(name))
272  return error("The controllable parameter '" + name + "' was not found");
273 
274  // Get the parameter value
275  const auto value_it = msg.find("value");
276  if (value_it == msg.end())
277  return error(
278  "The entry 'value' is missing which should contain the value of the parameter");
279  const auto & json_value = value_it->second;
280 
281  // Build the value (also does the parsing)
282  {
283  std::unique_ptr<ValueBase> value;
284  std::lock_guard<std::mutex> lock(this->_controlled_values_mutex);
285  try
286  {
288  }
289  catch (ValueBase::Exception & e)
290  {
291  return error("While parsing 'value': " + std::string(e.what()));
292  }
293  _controlled_values.emplace_back(std::move(value));
294  }
295 
296  return HttpResponse{201};
297  });
298 
299  // GET /continue, Returns code 200
300  _server->when("/continue")
301  ->requested(
302  [this, &error](const HttpRequest &)
303  {
304  if (this->_currently_waiting.load())
305  {
306  this->_currently_waiting.store(false);
307  return HttpResponse{200};
308  }
309 
310  // Not currently waiting
311  return error("The control is not currently waiting");
312  });
313 
314  // GET /terminate, Returns code 200 and tell FEProblemBase to terminate solve
315  _server->when("/terminate")
316  ->requested(
317  [this, &error](const HttpRequest &)
318  {
319  if (this->_currently_waiting.load())
320  {
321  this->_terminate_requested.store(true);
322  this->_currently_waiting.store(false);
323  return HttpResponse{200};
324  }
325 
326  // Not currently waiting
327  return error("The control is not currently waiting");
328  });
329 
330  _server_thread = std::make_unique<std::thread>(
331  [this]
332  {
333  if (this->isParamValid("port"))
334  {
335  const uint16_t port = this->getParam<unsigned int>("port");
336  try
337  {
338  _server->startListening(port);
339  }
340  catch (...)
341  {
342  this->mooseError("Failed to start the webserver; it is likely that the port ",
343  port,
344  " is not available");
345  }
346  }
347  else if (this->isParamValid("file_socket"))
348  {
349  const auto & file_socket = this->getParam<FileName>("file_socket");
350  _server->startListening(file_socket);
351  }
352  });
353 }
354 
355 void
357 {
358  // If simulation is requested to terminate, do not go through this control
360  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  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  bool terminate_solve = false; // Set value to avoid compiler warnings
369 
370  // Wait for the server on rank 0 to be done
371  if (processor_id() == 0)
372  {
373  TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
374 
375  _currently_waiting.store(true);
376 
377  // While waiting, yield so the server has time to run
378  while (_currently_waiting.load())
379  std::this_thread::yield();
380 
381  for (const auto & value_ptr : _controlled_values)
382  name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
383 
384  terminate_solve = _terminate_requested.load();
385  _terminate_requested.store(false);
386  }
387 
388  // All processes need to wait
390 
391  // Construct the values on other processors to be received into so that
392  // they're parallel consistent
393  comm().broadcast(name_and_types);
394  if (processor_id() != 0)
395  for (const auto & [name, type] : name_and_types)
397 
398  // Set all of the values
399  for (auto & value_ptr : _controlled_values)
400  {
401  try
402  {
403  value_ptr->setControllableValue(*this);
404  }
405  catch (...)
406  {
407  mooseError("Error setting '",
408  value_ptr->type(),
409  "' typed value for parameter '",
410  value_ptr->name(),
411  "'; it is likely that the parameter has a different type");
412  }
413  }
414 
415  _controlled_values.clear();
416 
417  // Set solve terminate on all ranks, if requested
418  _communicator.broadcast(terminate_solve);
419  if (terminate_solve)
421 }
422 
423 std::string
424 WebServerControl::stringifyJSONType(const miniJson::JsonType & json_type)
425 {
426  if (json_type == miniJson::JsonType::kNull)
427  return "empty";
428  if (json_type == miniJson::JsonType::kBool)
429  return "bool";
430  if (json_type == miniJson::JsonType::kNumber)
431  return "number";
432  if (json_type == miniJson::JsonType::kString)
433  return "string";
434  if (json_type == miniJson::JsonType::kArray)
435  return "array";
436  if (json_type == miniJson::JsonType::kObject)
437  return "object";
438  ::mooseError("WebServerControl::stringifyJSONType(): Unused JSON value type");
439 }
440 
441 template <>
442 miniJson::Json
443 WebServerControl::toMiniJson(const nlohmann::json & value)
444 {
445  const auto value_str = value.dump();
446  std::string errMsg;
447  const auto json_value = miniJson::Json::parse(value_str, errMsg);
448  if (!errMsg.empty())
449  ::mooseError("Failed parse value into miniJson:\n", errMsg);
450  return json_value;
451 }
452 
454  const std::string & type)
456 {
457 }
458 
460  const std::string & type,
461  const miniJson::Json & json_value)
462  : TypedValueBase<RealEigenMatrix>(name, type, getMatrixJSONValue(json_value))
463 {
464 }
465 
468 {
469  const auto from_json_type = json_value.getType();
470  if (from_json_type != miniJson::JsonType::kArray)
471  throw ValueBase::Exception("The value '" + json_value.serialize() + "' of type " +
472  stringifyJSONType(from_json_type) + " is not an array");
473 
474  const auto & array_of_array_value = json_value.toArray();
475  const auto nrows = array_of_array_value.size();
476  if (nrows == 0)
477  return RealEigenMatrix::Zero(0, 0);
478 
479  RealEigenMatrix matrix;
480  for (const auto i : make_range(nrows))
481  {
482  if (array_of_array_value[i].getType() != miniJson::JsonType::kArray)
483  throw ValueBase::Exception(
484  "Element " + std::to_string(i) + " of '" + json_value.serialize() + "' of type " +
485  stringifyJSONType(array_of_array_value[i].getType()) + " is not an array");
486 
487  const auto & array_value = array_of_array_value[i].toArray();
488  if (i == 0)
489  matrix.resize(nrows, array_value.size());
490  else if (array_value.size() != (std::size_t)matrix.cols())
491  throw ValueBase::Exception("The matrix '" + json_value.serialize() + "' is jagged.");
492 
493  for (const auto j : index_range(array_value))
494  matrix(i, j) = array_value[j].toDouble();
495  }
496 
497  return matrix;
498 }
std::string name(const ElemQuality q)
std::atomic< bool > _currently_waiting
Whether or not the Control is currently waiting.
static InputParameters validParams()
Class constructor.
Definition: Control.C:16
void paramError(const std::string &param, Args... args) const
Emits an error prefixed with the file and line number of the given param (from the input file) along ...
Definition: MooseBase.h:435
static bool isRegistered(const std::string &type)
Starts a webserver that an external process can connect to in order to send JSON messages to control ...
static RealEigenMatrix getMatrixJSONValue(const miniJson::Json &json_value)
void startServer()
Internal method for starting the server.
const ExecFlagType & getCurrentExecuteOnFlag() const
Return/set the current execution flag.
RealEigenMatrixValue(const std::string &name, const std::string &type)
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system...
const Parallel::Communicator & comm() const
WebServerControl(const InputParameters &parameters)
Common exception for parsing related errors in converting JSON to a value.
const Parallel::Communicator & _communicator
const ReporterContextBase & getReporterContextBaseByName(const ReporterName &reporter_name) const
Get the reporter context to allow non-typed operations with the data.
registerWebServerControlScalarBool(bool)
virtual bool isSolveTerminationRequested() const
Check of termination has been requested.
Definition: Problem.h:43
const std::string & name() const
Get the name of the class.
Definition: MooseBase.h:99
virtual void store(nlohmann::json &json) const =0
Called by JSONOutput::outputReporters to invoke storage of values for output.
Base class for a controllable value with a given type and name.
static miniJson::Json toMiniJson(const T &value)
Convert values to a miniJson::Json node.
Real value(unsigned n, unsigned alpha, unsigned beta, Real x)
std::vector< std::unique_ptr< ValueBase > > _controlled_values
The values received to control; filled on rank 0 from the server and then broadcast.
virtual void terminateSolve()
Allow objects to request clean termination of the solve.
Definition: Problem.h:37
bool hasPostprocessorByName(const PostprocessorName &name) const
Determine if the Postprocessor data exists.
registerWebServerControlScalarString(std::string)
FEProblemBase & _fe_problem
Reference to the FEProblemBase for this object.
Definition: Control.h:76
bool hasControllableParameterByName(const std::string &name) const
Definition: Control.C:70
const std::string & type() const
Get the type of this class.
Definition: MooseBase.h:89
std::mutex _controlled_values_mutex
Mutex to prevent threaded writes to _controlled_values.
Eigen::Matrix< Real, Eigen::Dynamic, Eigen::Dynamic > RealEigenMatrix
Definition: MooseTypes.h:149
virtual const char * what() const noexcept override final
void broadcast(T &data, const unsigned int root_id=0, const bool identical_sizes=false) const
registerWebServerControlVectorNumber(Real)
static bool isValidName(const std::string &object_and_value_name)
Determines if the inputted string is convertible to a ReporterName.
Definition: ReporterName.C:35
registerWebServerControlRealEigenMatrix()
virtual const PostprocessorValue & getPostprocessorValueByName(const PostprocessorName &name) const
Retrieve the value of the Postprocessor.
static std::string stringifyJSONType(const miniJson::JsonType &json_type)
std::atomic< bool > _terminate_requested
std::unique_ptr< std::thread > _server_thread
The server thread.
Base class for Control objects.
Definition: Control.h:34
IntRange< T > make_range(T beg, T end)
virtual void execute() override
Execute the control.
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type and optionally a file path to the top-level block p...
Definition: MooseBase.h:267
void addClassDescription(const std::string &doc_string)
This method adds a description of the class that will be displayed in the input file syntax dump...
void addParam(const std::string &name, const S &value, const std::string &doc_string)
These methods add an optional parameter and a documentation string to the InputParameters object...
registerMooseObject("MooseApp", WebServerControl)
bool isParamValid(const std::string &name) const
Test if the supplied parameter is valid.
Definition: MooseBase.h:195
static InputParameters validParams()
processor_id_type processor_id() const
static std::unique_ptr< ValueBase > build(const std::string &type, const std::string &name)
Builds a value with the type type, name name, and a default value.
registerWebServerControlScalarNumber(Real)
auto index_range(const T &sizable)
bool hasReporterValueByName(const ReporterName &reporter_name) const
The Reporter system is comprised of objects that can contain any number of data values.
Definition: ReporterName.h:30
std::unique_ptr< HttpServer > _server
The server.
registerWebServerControlVectorString(std::string)