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