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  // GET /get/time
199  // Returns code 200 on success and JSON:
200  // 'time' (double): The current time
201  _server->when("/get/time")
202  ->requested(
203  [this, &require_waiting](const HttpRequest & /*req*/)
204  {
205  // Should be waiting for data
206  if (const auto response = require_waiting(*this))
207  return *response;
208 
209  miniJson::Json::_object res_json;
210  res_json["time"] = _fe_problem.time();
211 
212  return HttpResponse{200, res_json};
213  });
214 
215  // GET /get/dt
216  // Returns code 200 on success and JSON:
217  // 'dt' (double): The current time step size
218  _server->when("/get/dt")->requested(
219  [this, &require_waiting](const HttpRequest & /*req*/)
220  {
221  // Should be waiting for data
222  if (const auto response = require_waiting(*this))
223  return *response;
224 
225  miniJson::Json::_object res_json;
226  res_json["dt"] = _fe_problem.dt();
227 
228  return HttpResponse{200, res_json};
229  });
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  _server->when("/get/reporter")
236  ->posted(
237  [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
238  {
239  const auto & msg = req.json().toObject();
240 
241  // Should only have name and type
242  if (const auto response = require_parameters(msg, {"name"}))
243  return *response;
244  // Should be waiting for data
245  if (const auto response = require_waiting(*this))
246  return *response;
247 
248  // Get the reporter name
249  const auto name_result = get_name(msg, "reporter value to retrieve");
250  if (const auto response = std::get_if<HttpResponse>(&name_result))
251  return *response;
252  const auto & name = std::get<std::string>(name_result);
254  return error(name + " is not a valid reporter name.");
255  const auto rname = ReporterName(name);
256 
257  // Reporter should exist
258  if (!this->hasReporterValueByName(rname))
259  return error("The reporter value '" + name + "' was not found");
260 
261  // Grab the reporter value in nlohmann::json format, then convert to miniJson
262  nlohmann::json njson;
263  getReporterContextBaseByName(rname).store(njson);
264  miniJson::Json::_object res_json = {{"value", toMiniJson(njson)}};
265 
266  return HttpResponse{200, res_json};
267  });
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  _server->when("/set/controllable")
276  ->posted(
277  [this, &error, &get_string, &get_name, &require_waiting, &require_parameters](
278  const HttpRequest & req)
279  {
280  const auto & msg = req.json().toObject();
281 
282  // Should only have a name, type, and value
283  if (const auto response = require_parameters(msg, {"name", "type", "value"}))
284  return *response;
285  // Should be waiting for data
286  if (const auto response = require_waiting(*this))
287  return *response;
288 
289  // Get the parameter type
290  const auto type_result = get_string(msg, "type", "type of the parameter");
291  if (const auto response = std::get_if<HttpResponse>(&type_result))
292  return *response;
293  const auto & type = std::get<std::string>(type_result);
295  return error("The type '" + type +
296  "' is not registered for setting a controllable parameter");
297 
298  // Get the parameter name
299  const auto name_result = get_name(msg, "name of the parameter to control");
300  if (const auto response = std::get_if<HttpResponse>(&name_result))
301  return *response;
302  const auto & name = std::get<std::string>(name_result);
303  // Parameter should exist
304  if (!this->hasControllableParameterByName(name))
305  return error("The controllable parameter '" + name + "' was not found");
306 
307  // Get the parameter value
308  const auto value_it = msg.find("value");
309  if (value_it == msg.end())
310  return error(
311  "The entry 'value' is missing which should contain the value of the parameter");
312  const auto & json_value = value_it->second;
313 
314  // Build the value (also does the parsing)
315  {
316  std::unique_ptr<ValueBase> value;
317  std::lock_guard<std::mutex> lock(this->_controlled_values_mutex);
318  try
319  {
321  }
322  catch (ValueBase::Exception & e)
323  {
324  return error("While parsing 'value': " + std::string(e.what()));
325  }
326  _controlled_values.emplace_back(std::move(value));
327  }
328 
329  return HttpResponse{201};
330  });
331 
332  // GET /continue, Returns code 200
333  _server->when("/continue")
334  ->requested(
335  [this, &error](const HttpRequest &)
336  {
337  if (this->_currently_waiting.load())
338  {
339  this->_currently_waiting.store(false);
340  return HttpResponse{200};
341  }
342 
343  // Not currently waiting
344  return error("The control is not currently waiting");
345  });
346 
347  // GET /terminate, Returns code 200 and tell FEProblemBase to terminate solve
348  _server->when("/terminate")
349  ->requested(
350  [this, &error](const HttpRequest &)
351  {
352  if (this->_currently_waiting.load())
353  {
354  this->_terminate_requested.store(true);
355  this->_currently_waiting.store(false);
356  return HttpResponse{200};
357  }
358 
359  // Not currently waiting
360  return error("The control is not currently waiting");
361  });
362 
363  _server_thread = std::make_unique<std::thread>(
364  [this]
365  {
366  if (this->isParamValid("port"))
367  {
368  const uint16_t port = this->getParam<unsigned int>("port");
369  try
370  {
371  _server->startListening(port);
372  }
373  catch (...)
374  {
375  this->mooseError("Failed to start the webserver; it is likely that the port ",
376  port,
377  " is not available");
378  }
379  }
380  else if (this->isParamValid("file_socket"))
381  {
382  const auto & file_socket = this->getParam<FileName>("file_socket");
383  _server->startListening(file_socket);
384  }
385  });
386 }
387 
388 void
390 {
391  // If simulation is requested to terminate, do not go through this control
393  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  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  bool terminate_solve = false; // Set value to avoid compiler warnings
402 
403  // Wait for the server on rank 0 to be done
404  if (processor_id() == 0)
405  {
406  TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
407 
408  _currently_waiting.store(true);
409 
410  // While waiting, yield so the server has time to run
411  while (_currently_waiting.load())
412  std::this_thread::yield();
413 
414  for (const auto & value_ptr : _controlled_values)
415  name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
416 
417  terminate_solve = _terminate_requested.load();
418  _terminate_requested.store(false);
419  }
420 
421  // All processes need to wait
423 
424  // Construct the values on other processors to be received into so that
425  // they're parallel consistent
426  comm().broadcast(name_and_types);
427  if (processor_id() != 0)
428  for (const auto & [name, type] : name_and_types)
430 
431  // Set all of the values
432  for (auto & value_ptr : _controlled_values)
433  {
434  try
435  {
436  value_ptr->setControllableValue(*this);
437  }
438  catch (...)
439  {
440  mooseError("Error setting '",
441  value_ptr->type(),
442  "' typed value for parameter '",
443  value_ptr->name(),
444  "'; it is likely that the parameter has a different type");
445  }
446  }
447 
448  _controlled_values.clear();
449 
450  // Set solve terminate on all ranks, if requested
451  _communicator.broadcast(terminate_solve);
452  if (terminate_solve)
454 }
455 
456 std::string
457 WebServerControl::stringifyJSONType(const miniJson::JsonType & json_type)
458 {
459  if (json_type == miniJson::JsonType::kNull)
460  return "empty";
461  if (json_type == miniJson::JsonType::kBool)
462  return "bool";
463  if (json_type == miniJson::JsonType::kNumber)
464  return "number";
465  if (json_type == miniJson::JsonType::kString)
466  return "string";
467  if (json_type == miniJson::JsonType::kArray)
468  return "array";
469  if (json_type == miniJson::JsonType::kObject)
470  return "object";
471  ::mooseError("WebServerControl::stringifyJSONType(): Unused JSON value type");
472 }
473 
474 template <>
475 miniJson::Json
476 WebServerControl::toMiniJson(const nlohmann::json & value)
477 {
478  const auto value_str = value.dump();
479  std::string errMsg;
480  const auto json_value = miniJson::Json::parse(value_str, errMsg);
481  if (!errMsg.empty())
482  ::mooseError("Failed parse value into miniJson:\n", errMsg);
483  return json_value;
484 }
485 
487  const std::string & type)
489 {
490 }
491 
493  const std::string & type,
494  const miniJson::Json & json_value)
495  : TypedValueBase<RealEigenMatrix>(name, type, getMatrixJSONValue(json_value))
496 {
497 }
498 
501 {
502  const auto from_json_type = json_value.getType();
503  if (from_json_type != miniJson::JsonType::kArray)
504  throw ValueBase::Exception("The value '" + json_value.serialize() + "' of type " +
505  stringifyJSONType(from_json_type) + " is not an array");
506 
507  const auto & array_of_array_value = json_value.toArray();
508  const auto nrows = array_of_array_value.size();
509  if (nrows == 0)
510  return RealEigenMatrix::Zero(0, 0);
511 
512  RealEigenMatrix matrix;
513  for (const auto i : make_range(nrows))
514  {
515  if (array_of_array_value[i].getType() != miniJson::JsonType::kArray)
516  throw ValueBase::Exception(
517  "Element " + std::to_string(i) + " of '" + json_value.serialize() + "' of type " +
518  stringifyJSONType(array_of_array_value[i].getType()) + " is not an array");
519 
520  const auto & array_value = array_of_array_value[i].toArray();
521  if (i == 0)
522  matrix.resize(nrows, array_value.size());
523  else if (array_value.size() != (std::size_t)matrix.cols())
524  throw ValueBase::Exception("The matrix '" + json_value.serialize() + "' is jagged.");
525 
526  for (const auto j : index_range(array_value))
527  matrix(i, j) = array_value[j].toDouble();
528  }
529 
530  return matrix;
531 }
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
virtual Real & time() const
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:439
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:103
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:93
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:271
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:199
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.
virtual Real & dt() const
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)