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 
37 // Registration of the types that we can accept in the web server for controlling parameters
45 
48 {
50  params.addClassDescription("Starts a webserver for sending/receiving JSON messages to get data "
51  "and control a running MOOSE calculation");
52  params.addParam<unsigned int>("port",
53  "The port to listen on; must provide either this or 'file_socket'");
54  params.addParam<FileName>(
55  "file_socket",
56  "The path to the unix file socket to listen on; must provide either this or 'port'");
57  return params;
58 }
59 
61  : Control(parameters), _currently_waiting(false)
62 {
63  const auto has_port = isParamValid("port");
64  const auto has_file_socket = isParamValid("file_socket");
65  if (!has_port && !has_file_socket)
66  mooseError("You must provide either the parameter 'port' or 'file_socket' to designate where "
67  "to listen");
68  if (has_port && has_file_socket)
69  paramError("port", "Cannot provide both 'port' and 'file_socket'");
70 
71  if (processor_id() == 0)
72  startServer();
73 }
74 
76 {
77  if (_server)
78  {
79  _server->shutdown();
80  _server_thread->join();
81  }
82 }
83 
84 void
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  const auto error = [](const std::string & error)
93  {
94  miniJson::Json::_object response;
95  response["error"] = error;
96  return HttpResponse{400, response};
97  };
98 
99  // Helper for getting a string from a json value with error checking
100  const auto get_string =
101  [&error](const auto & msg, const std::string & name, const std::string & description)
102  {
103  using result = std::variant<std::string, HttpResponse>;
104  const auto it = msg.find(name);
105  if (it == msg.end())
106  return result(
107  error("The entry '" + name + "' is missing which should contain the " + description));
108  const auto & value = it->second;
109  if (!value.isString())
110  return result(error("The entry '" + name + "' which should contain the " + description));
111  return result(value.toString());
112  };
113 
114  // Helper for getting a string name from a json value with error checking
115  const auto get_name = [&get_string](const auto & msg, const std::string & description)
116  { 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  const auto require_waiting = [&error](auto & control)
121  {
122  using result = std::optional<HttpResponse>;
123  if (!control.currentlyWaiting())
124  return result(error("This control is not currently waiting for data"));
125  return result{};
126  };
127 
128  const auto require_parameters = [&error](const auto & msg, const std::set<std::string> & params)
129  {
130  using result = std::optional<HttpResponse>;
131  for (const auto & key_value_pair : msg)
132  if (!params.count(key_value_pair.first))
133  return result(error("The key '" + key_value_pair.first + "' is unused"));
134  return result{};
135  };
136 
137  _server = std::make_unique<HttpServer>();
138 
139  // GET /check, returns code 200
140  _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  _server->when("/waiting")
147  ->requested(
148  [this](const HttpRequest & /*req*/)
149  {
150  miniJson::Json::_object res_json;
151  if (this->_currently_waiting.load())
152  {
153  res_json["waiting"] = true;
154  res_json["execute_on_flag"] =
155  static_cast<std::string>(this->_fe_problem.getCurrentExecuteOnFlag());
156  }
157  else
158  res_json["waiting"] = false;
159 
160  return HttpResponse{200, res_json};
161  });
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  _server->when("/get/postprocessor")
168  ->posted(
169  [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
170  {
171  const auto & msg = req.json().toObject();
172 
173  // Get the postprocessor name
174  const auto name_result = get_name(msg, "postprocessor to retrieve");
175  if (const auto response = std::get_if<HttpResponse>(&name_result))
176  return *response;
177  const auto & name = std::get<std::string>(name_result);
178 
179  // Should only have a name
180  if (const auto response = require_parameters(msg, {"name"}))
181  return *response;
182  // Should be waiting for data
183  if (const auto response = require_waiting(*this))
184  return *response;
185  // Postprocessor should exist
186  if (!this->hasPostprocessorByName(name))
187  return error("The postprocessor '" + name + "' was not found");
188 
189  miniJson::Json::_object res_json;
190  res_json["value"] = getPostprocessorValueByName(name);
191  return HttpResponse{200, res_json};
192  });
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  _server->when("/set/controllable")
201  ->posted(
202  [this, &error, &get_string, &get_name, &require_waiting, &require_parameters](
203  const HttpRequest & req)
204  {
205  const auto & msg = req.json().toObject();
206 
207  // Should only have a name, type, and value
208  if (const auto response = require_parameters(msg, {"name", "type", "value"}))
209  return *response;
210  // Should be waiting for data
211  if (const auto response = require_waiting(*this))
212  return *response;
213 
214  // Get the parameter type
215  const auto type_result = get_string(msg, "type", "type of the parameter");
216  if (const auto response = std::get_if<HttpResponse>(&type_result))
217  return *response;
218  const auto & type = std::get<std::string>(type_result);
220  return error("The type '" + type +
221  "' is not registered for setting a controllable parameter");
222 
223  // Get the parameter name
224  const auto name_result = get_name(msg, "name of the parameter to control");
225  if (const auto response = std::get_if<HttpResponse>(&name_result))
226  return *response;
227  const auto & name = std::get<std::string>(name_result);
228  // Parameter should exist
229  if (!this->hasControllableParameterByName(name))
230  return error("The controllable parameter '" + name + "' was not found");
231 
232  // Get the parameter value
233  const auto value_it = msg.find("value");
234  if (value_it == msg.end())
235  return error(
236  "The entry 'value' is missing which should contain the value of the parameter");
237  const auto & json_value = value_it->second;
238 
239  // Build the value (also does the parsing)
240  {
241  std::unique_ptr<ValueBase> value;
242  std::lock_guard<std::mutex> lock(this->_controlled_values_mutex);
243  try
244  {
246  }
247  catch (ValueBase::Exception & e)
248  {
249  return error("While parsing 'value': " + std::string(e.what()));
250  }
251  _controlled_values.emplace_back(std::move(value));
252  }
253 
254  return HttpResponse{201};
255  });
256 
257  // GET /continue, Returns code 200
258  _server->when("/continue")
259  ->requested(
260  [this, &error](const HttpRequest &)
261  {
262  if (this->_currently_waiting.load())
263  {
264  this->_currently_waiting.store(false);
265  return HttpResponse{200};
266  }
267 
268  // Not currently waiting
269  return error("The control is not currently waiting");
270  });
271 
272  _server_thread = std::make_unique<std::thread>(
273  [this]
274  {
275  if (this->isParamValid("port"))
276  {
277  const uint16_t port = this->getParam<unsigned int>("port");
278  try
279  {
280  _server->startListening(port);
281  }
282  catch (...)
283  {
284  this->mooseError("Failed to start the webserver; it is likely that the port ",
285  port,
286  " is not available");
287  }
288  }
289  else if (this->isParamValid("file_socket"))
290  {
291  const auto & file_socket = this->getParam<FileName>("file_socket");
292  _server->startListening(file_socket);
293  }
294  });
295 }
296 
297 void
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  std::vector<std::pair<std::string, std::string>> name_and_types;
304 
305  // Wait for the server on rank 0 to be done
306  if (processor_id() == 0)
307  {
308  TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
309 
310  _currently_waiting.store(true);
311 
312  // While waiting, yield so the server has time to run
313  while (_currently_waiting.load())
314  std::this_thread::yield();
315 
316  for (const auto & value_ptr : _controlled_values)
317  name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
318  }
319 
320  // All processes need to wait
322 
323  // Construct the values on other processors to be received into so that
324  // they're parallel consistent
325  comm().broadcast(name_and_types);
326  if (processor_id() != 0)
327  for (const auto & [name, type] : name_and_types)
329 
330  // Set all of the values
331  for (auto & value_ptr : _controlled_values)
332  {
333  try
334  {
335  value_ptr->setControllableValue(*this);
336  }
337  catch (...)
338  {
339  mooseError("Error setting '",
340  value_ptr->type(),
341  "' typed value for parameter '",
342  value_ptr->name(),
343  "'; it is likely that the parameter has a different type");
344  }
345  }
346 
347  _controlled_values.clear();
348 }
349 
350 std::string
351 WebServerControl::stringifyJSONType(const miniJson::JsonType & json_type)
352 {
353  if (json_type == miniJson::JsonType::kNull)
354  return "empty";
355  if (json_type == miniJson::JsonType::kBool)
356  return "bool";
357  if (json_type == miniJson::JsonType::kNumber)
358  return "number";
359  if (json_type == miniJson::JsonType::kString)
360  return "string";
361  if (json_type == miniJson::JsonType::kArray)
362  return "array";
363  if (json_type == miniJson::JsonType::kObject)
364  return "object";
365  ::mooseError("WebServerControl::stringifyJSONType(): Unused JSON value type");
366 }
std::atomic< bool > _currently_waiting
Whether or not the Control is currently waiting.
static InputParameters validParams()
Class constructor.
Definition: Control.C:16
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 ...
void startServer()
Internal method for starting the server.
const ExecFlagType & getCurrentExecuteOnFlag() const
Return/set the current execution flag.
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
registerWebServerControlScalarBool(bool)
virtual const std::string & name() const
Get the name of the class.
Definition: MooseBase.h:57
bool isParamValid(const std::string &name) const
Test if the supplied parameter is valid.
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.
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:74
bool hasControllableParameterByName(const std::string &name) const
Definition: Control.C:69
const std::string & type() const
Get the type of this class.
Definition: MooseBase.h:51
std::mutex _controlled_values_mutex
Mutex to prevent threaded writes to _controlled_values.
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 ...
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)
virtual const PostprocessorValue & getPostprocessorValueByName(const PostprocessorName &name) const
Retrieve the value of the Postprocessor.
static std::string stringifyJSONType(const miniJson::JsonType &json_type)
std::unique_ptr< std::thread > _server_thread
The server thread.
Base class for Control objects.
Definition: Control.h:33
virtual void execute() override
Execute the control.
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type.
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)
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)
std::unique_ptr< HttpServer > _server
The server.
registerWebServerControlVectorString(std::string)