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 : }
|