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 15169 : WebServerControl::validParams()
52 : {
53 15169 : InputParameters params = Control::validParams();
54 30338 : params.addClassDescription("Starts a webserver for sending/receiving JSON messages to get data "
55 : "and control a running MOOSE calculation");
56 60676 : params.addParam<unsigned int>("port",
57 : "The port to listen on; must provide either this or 'file_socket'");
58 45507 : params.addParam<FileName>(
59 : "file_socket",
60 : "The path to the unix file socket to listen on; must provide either this or 'port'");
61 15169 : return params;
62 0 : }
63 :
64 155 : WebServerControl::WebServerControl(const InputParameters & parameters)
65 155 : : Control(parameters), _currently_waiting(false), _terminate_requested(false)
66 : {
67 310 : const auto has_port = isParamValid("port");
68 310 : const auto has_file_socket = isParamValid("file_socket");
69 155 : 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 151 : if (has_port && has_file_socket)
73 8 : paramError("port", "Cannot provide both 'port' and 'file_socket'");
74 :
75 147 : if (processor_id() == 0)
76 101 : startServer();
77 147 : }
78 :
79 294 : WebServerControl::~WebServerControl()
80 : {
81 147 : if (_server)
82 : {
83 101 : _server->shutdown();
84 101 : _server_thread->join();
85 : }
86 294 : }
87 :
88 : void
89 101 : 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 468 : [&error](const auto & msg, const std::string & name, const std::string & description)
106 : {
107 : using result = std::variant<std::string, HttpResponse>;
108 468 : const auto it = msg.find(name);
109 468 : if (it == msg.end())
110 : return result(
111 0 : error("The entry '" + name + "' is missing which should contain the " + description));
112 468 : const auto & value = it->second;
113 468 : if (!value.isString())
114 0 : return result(error("The entry '" + name + "' which should contain the " + description));
115 468 : return result(value.toString());
116 101 : };
117 :
118 : // Helper for getting a string name from a json value with error checking
119 340 : const auto get_name = [&get_string](const auto & msg, const std::string & description)
120 1020 : { 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 396 : const auto require_waiting = [&error](auto & control)
125 : {
126 : using result = std::optional<HttpResponse>;
127 396 : if (!control.currentlyWaiting())
128 0 : return result(error("This control is not currently waiting for data"));
129 396 : return result{};
130 101 : };
131 :
132 340 : const auto require_parameters = [&error](const auto & msg, const std::set<std::string> & params)
133 : {
134 : using result = std::optional<HttpResponse>;
135 936 : for (const auto & key_value_pair : msg)
136 596 : if (!params.count(key_value_pair.first))
137 0 : return result(error("The key '" + key_value_pair.first + "' is unused"));
138 340 : return result{};
139 101 : };
140 :
141 101 : _server = std::make_unique<HttpServer>();
142 :
143 : // GET /check, returns code 200
144 2024 : _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 303 : _server->when("/waiting")
151 101 : ->requested(
152 1086 : [this](const HttpRequest & /*req*/)
153 : {
154 1086 : miniJson::Json::_object res_json;
155 1086 : if (this->_currently_waiting.load())
156 : {
157 3258 : res_json["waiting"] = true;
158 2172 : res_json["execute_on_flag"] =
159 3258 : static_cast<std::string>(this->_fe_problem.getCurrentExecuteOnFlag());
160 : }
161 : else
162 0 : res_json["waiting"] = false;
163 :
164 2172 : return HttpResponse{200, res_json};
165 1086 : });
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 303 : _server->when("/get/postprocessor")
172 101 : ->posted(
173 84 : [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
174 : {
175 84 : const auto & msg = req.json().toObject();
176 :
177 : // Get the postprocessor name
178 168 : const auto name_result = get_name(msg, "postprocessor to retrieve");
179 84 : if (const auto response = std::get_if<HttpResponse>(&name_result))
180 0 : return *response;
181 84 : const auto & name = std::get<std::string>(name_result);
182 :
183 : // Should only have a name
184 84 : if (const auto response = require_parameters(msg, {"name"}))
185 84 : return *response;
186 : // Should be waiting for data
187 84 : if (const auto response = require_waiting(*this))
188 84 : return *response;
189 : // Postprocessor should exist
190 84 : if (!this->hasPostprocessorByName(name))
191 0 : return error("The postprocessor '" + name + "' was not found");
192 :
193 84 : miniJson::Json::_object res_json;
194 252 : res_json["value"] = getPostprocessorValueByName(name);
195 84 : return HttpResponse{200, res_json};
196 84 : });
197 :
198 : // GET /get/time
199 : // Returns code 200 on success and JSON:
200 : // 'time' (double): The current time
201 303 : _server->when("/get/time")
202 101 : ->requested(
203 28 : [this, &require_waiting](const HttpRequest & /*req*/)
204 : {
205 : // Should be waiting for data
206 28 : if (const auto response = require_waiting(*this))
207 28 : return *response;
208 :
209 28 : miniJson::Json::_object res_json;
210 84 : res_json["time"] = _fe_problem.time();
211 :
212 28 : return HttpResponse{200, res_json};
213 28 : });
214 :
215 : // GET /get/dt
216 : // Returns code 200 on success and JSON:
217 : // 'dt' (double): The current time step size
218 202 : _server->when("/get/dt")->requested(
219 28 : [this, &require_waiting](const HttpRequest & /*req*/)
220 : {
221 : // Should be waiting for data
222 28 : if (const auto response = require_waiting(*this))
223 28 : return *response;
224 :
225 28 : miniJson::Json::_object res_json;
226 84 : res_json["dt"] = _fe_problem.dt();
227 :
228 28 : return HttpResponse{200, res_json};
229 28 : });
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 303 : _server->when("/get/reporter")
236 101 : ->posted(
237 128 : [this, &error, &get_name, &require_waiting, &require_parameters](const HttpRequest & req)
238 : {
239 128 : const auto & msg = req.json().toObject();
240 :
241 : // Should only have name and type
242 128 : if (const auto response = require_parameters(msg, {"name"}))
243 128 : return *response;
244 : // Should be waiting for data
245 128 : if (const auto response = require_waiting(*this))
246 128 : return *response;
247 :
248 : // Get the reporter name
249 256 : const auto name_result = get_name(msg, "reporter value to retrieve");
250 128 : if (const auto response = std::get_if<HttpResponse>(&name_result))
251 0 : return *response;
252 128 : const auto & name = std::get<std::string>(name_result);
253 128 : if (!ReporterName::isValidName(name))
254 0 : return error(name + " is not a valid reporter name.");
255 128 : const auto rname = ReporterName(name);
256 :
257 : // Reporter should exist
258 128 : if (!this->hasReporterValueByName(rname))
259 0 : return error("The reporter value '" + name + "' was not found");
260 :
261 : // Grab the reporter value in nlohmann::json format, then convert to miniJson
262 128 : nlohmann::json njson;
263 128 : getReporterContextBaseByName(rname).store(njson);
264 384 : miniJson::Json::_object res_json = {{"value", toMiniJson(njson)}};
265 :
266 128 : return HttpResponse{200, res_json};
267 256 : });
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 303 : _server->when("/set/controllable")
276 101 : ->posted(
277 128 : [this, &error, &get_string, &get_name, &require_waiting, &require_parameters](
278 : const HttpRequest & req)
279 : {
280 128 : const auto & msg = req.json().toObject();
281 :
282 : // Should only have a name, type, and value
283 128 : if (const auto response = require_parameters(msg, {"name", "type", "value"}))
284 128 : return *response;
285 : // Should be waiting for data
286 128 : if (const auto response = require_waiting(*this))
287 128 : return *response;
288 :
289 : // Get the parameter type
290 512 : const auto type_result = get_string(msg, "type", "type of the parameter");
291 128 : if (const auto response = std::get_if<HttpResponse>(&type_result))
292 0 : return *response;
293 128 : const auto & type = std::get<std::string>(type_result);
294 128 : if (!Moose::WebServerControlTypeRegistry::isRegistered(type))
295 0 : return error("The type '" + type +
296 0 : "' is not registered for setting a controllable parameter");
297 :
298 : // Get the parameter name
299 256 : const auto name_result = get_name(msg, "name of the parameter to control");
300 128 : if (const auto response = std::get_if<HttpResponse>(&name_result))
301 0 : return *response;
302 128 : const auto & name = std::get<std::string>(name_result);
303 : // Parameter should exist
304 128 : if (!this->hasControllableParameterByName(name))
305 0 : return error("The controllable parameter '" + name + "' was not found");
306 :
307 : // Get the parameter value
308 128 : const auto value_it = msg.find("value");
309 128 : if (value_it == msg.end())
310 : return error(
311 0 : "The entry 'value' is missing which should contain the value of the parameter");
312 128 : const auto & json_value = value_it->second;
313 :
314 : // Build the value (also does the parsing)
315 : {
316 128 : std::unique_ptr<ValueBase> value;
317 128 : std::lock_guard<std::mutex> lock(this->_controlled_values_mutex);
318 : try
319 : {
320 128 : value = Moose::WebServerControlTypeRegistry::build(type, name, json_value);
321 : }
322 0 : catch (ValueBase::Exception & e)
323 : {
324 0 : return error("While parsing 'value': " + std::string(e.what()));
325 0 : }
326 128 : _controlled_values.emplace_back(std::move(value));
327 128 : }
328 :
329 128 : return HttpResponse{201};
330 128 : });
331 :
332 : // GET /continue, Returns code 200
333 303 : _server->when("/continue")
334 101 : ->requested(
335 231 : [this, &error](const HttpRequest &)
336 : {
337 231 : if (this->_currently_waiting.load())
338 : {
339 231 : this->_currently_waiting.store(false);
340 231 : return HttpResponse{200};
341 : }
342 :
343 : // Not currently waiting
344 0 : return error("The control is not currently waiting");
345 : });
346 :
347 : // GET /terminate, Returns code 200 and tell FEProblemBase to terminate solve
348 303 : _server->when("/terminate")
349 101 : ->requested(
350 8 : [this, &error](const HttpRequest &)
351 : {
352 8 : if (this->_currently_waiting.load())
353 : {
354 8 : this->_terminate_requested.store(true);
355 8 : this->_currently_waiting.store(false);
356 8 : return HttpResponse{200};
357 : }
358 :
359 : // Not currently waiting
360 0 : return error("The control is not currently waiting");
361 : });
362 :
363 101 : _server_thread = std::make_unique<std::thread>(
364 101 : [this]
365 : {
366 303 : if (this->isParamValid("port"))
367 : {
368 14 : const uint16_t port = this->getParam<unsigned int>("port");
369 : try
370 : {
371 7 : _server->startListening(port);
372 : }
373 0 : catch (...)
374 : {
375 0 : this->mooseError("Failed to start the webserver; it is likely that the port ",
376 : port,
377 : " is not available");
378 0 : }
379 : }
380 282 : else if (this->isParamValid("file_socket"))
381 : {
382 188 : const auto & file_socket = this->getParam<FileName>("file_socket");
383 94 : _server->startListening(file_socket);
384 : }
385 202 : });
386 101 : }
387 :
388 : void
389 346 : WebServerControl::execute()
390 : {
391 : // If simulation is requested to terminate, do not go through this control
392 346 : if (_fe_problem.isSolveTerminationRequested())
393 0 : 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 346 : 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 346 : bool terminate_solve = false; // Set value to avoid compiler warnings
402 :
403 : // Wait for the server on rank 0 to be done
404 346 : if (processor_id() == 0)
405 : {
406 1195 : TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
407 :
408 239 : _currently_waiting.store(true);
409 :
410 : // While waiting, yield so the server has time to run
411 37350634 : while (_currently_waiting.load())
412 37350395 : std::this_thread::yield();
413 :
414 367 : for (const auto & value_ptr : _controlled_values)
415 128 : name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
416 :
417 239 : terminate_solve = _terminate_requested.load();
418 239 : _terminate_requested.store(false);
419 239 : }
420 :
421 : // All processes need to wait
422 346 : _communicator.barrier();
423 :
424 : // Construct the values on other processors to be received into so that
425 : // they're parallel consistent
426 346 : comm().broadcast(name_and_types);
427 346 : if (processor_id() != 0)
428 169 : for (const auto & [name, type] : name_and_types)
429 62 : _controlled_values.emplace_back(Moose::WebServerControlTypeRegistry::build(type, name));
430 :
431 : // Set all of the values
432 536 : for (auto & value_ptr : _controlled_values)
433 : {
434 : try
435 : {
436 190 : value_ptr->setControllableValue(*this);
437 : }
438 0 : catch (...)
439 : {
440 0 : mooseError("Error setting '",
441 0 : value_ptr->type(),
442 : "' typed value for parameter '",
443 0 : value_ptr->name(),
444 : "'; it is likely that the parameter has a different type");
445 0 : }
446 : }
447 :
448 346 : _controlled_values.clear();
449 :
450 : // Set solve terminate on all ranks, if requested
451 346 : _communicator.broadcast(terminate_solve);
452 346 : if (terminate_solve)
453 11 : _fe_problem.terminateSolve();
454 346 : }
455 :
456 : std::string
457 0 : WebServerControl::stringifyJSONType(const miniJson::JsonType & json_type)
458 : {
459 0 : if (json_type == miniJson::JsonType::kNull)
460 0 : return "empty";
461 0 : if (json_type == miniJson::JsonType::kBool)
462 0 : return "bool";
463 0 : if (json_type == miniJson::JsonType::kNumber)
464 0 : return "number";
465 0 : if (json_type == miniJson::JsonType::kString)
466 0 : return "string";
467 0 : if (json_type == miniJson::JsonType::kArray)
468 0 : return "array";
469 0 : if (json_type == miniJson::JsonType::kObject)
470 0 : return "object";
471 0 : ::mooseError("WebServerControl::stringifyJSONType(): Unused JSON value type");
472 : }
473 :
474 : template <>
475 : miniJson::Json
476 138 : WebServerControl::toMiniJson(const nlohmann::json & value)
477 : {
478 138 : const auto value_str = value.dump();
479 138 : std::string errMsg;
480 138 : const auto json_value = miniJson::Json::parse(value_str, errMsg);
481 138 : if (!errMsg.empty())
482 0 : ::mooseError("Failed parse value into miniJson:\n", errMsg);
483 276 : return json_value;
484 138 : }
485 :
486 6 : WebServerControl::RealEigenMatrixValue::RealEigenMatrixValue(const std::string & name,
487 6 : const std::string & type)
488 6 : : TypedValueBase<RealEigenMatrix>(name, type)
489 : {
490 6 : }
491 :
492 16 : WebServerControl::RealEigenMatrixValue::RealEigenMatrixValue(const std::string & name,
493 : const std::string & type,
494 16 : const miniJson::Json & json_value)
495 16 : : TypedValueBase<RealEigenMatrix>(name, type, getMatrixJSONValue(json_value))
496 : {
497 16 : }
498 :
499 : RealEigenMatrix
500 16 : WebServerControl::RealEigenMatrixValue::getMatrixJSONValue(const miniJson::Json & json_value)
501 : {
502 16 : const auto from_json_type = json_value.getType();
503 16 : if (from_json_type != miniJson::JsonType::kArray)
504 0 : throw ValueBase::Exception("The value '" + json_value.serialize() + "' of type " +
505 0 : stringifyJSONType(from_json_type) + " is not an array");
506 :
507 16 : const auto & array_of_array_value = json_value.toArray();
508 16 : const auto nrows = array_of_array_value.size();
509 16 : if (nrows == 0)
510 0 : return RealEigenMatrix::Zero(0, 0);
511 :
512 16 : RealEigenMatrix matrix;
513 56 : for (const auto i : make_range(nrows))
514 : {
515 40 : if (array_of_array_value[i].getType() != miniJson::JsonType::kArray)
516 : throw ValueBase::Exception(
517 0 : "Element " + std::to_string(i) + " of '" + json_value.serialize() + "' of type " +
518 0 : stringifyJSONType(array_of_array_value[i].getType()) + " is not an array");
519 :
520 40 : const auto & array_value = array_of_array_value[i].toArray();
521 40 : if (i == 0)
522 16 : matrix.resize(nrows, array_value.size());
523 24 : else if (array_value.size() != (std::size_t)matrix.cols())
524 0 : throw ValueBase::Exception("The matrix '" + json_value.serialize() + "' is jagged.");
525 :
526 184 : for (const auto j : index_range(array_value))
527 144 : matrix(i, j) = array_value[j].toDouble();
528 : }
529 :
530 16 : return matrix;
531 16 : }
|