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 : #include "tinyhttp/http.h"
16 :
17 : registerMooseObject("MooseApp", WebServerControl);
18 :
19 : InputParameters
20 15257 : WebServerControl::validParams()
21 : {
22 15257 : InputParameters params = Control::validParams();
23 30514 : params.addClassDescription("Starts a webserver for sending/receiving JSON messages to get data "
24 : "and control a running MOOSE calculation");
25 61028 : params.addParam<unsigned int>("port",
26 : "The port to listen on; must provide either this or 'file_socket'");
27 61028 : params.addParam<FileName>(
28 : "file_socket",
29 : "The path to the unix file socket to listen on; must provide either this or 'port'");
30 45771 : params.addParam<Real>("initial_client_timeout",
31 30514 : 10,
32 : "Time in seconds to allow the client to begin communicating on init; if "
33 : "this time is surpassed the run will be killed");
34 30514 : params.addParam<Real>("client_timeout",
35 30514 : 10,
36 : "Time in seconds to allow the client to communicate; if this time is "
37 : "surpassed the run will be killed");
38 15257 : return params;
39 0 : }
40 :
41 199 : WebServerControl::WebServerControl(const InputParameters & parameters)
42 : : Control(parameters),
43 199 : _port(queryParam<unsigned int>("port")),
44 398 : _file_socket(queryParam<FileName>("file_socket")),
45 398 : _initial_client_timeout(getParam<Real>("initial_client_timeout")),
46 796 : _client_timeout(getParam<Real>("client_timeout"))
47 : {
48 199 : if (!_port && !_file_socket)
49 4 : mooseError("You must provide either the parameter 'port' or 'file_socket' to designate where "
50 : "to listen");
51 195 : if (_port && _file_socket)
52 8 : paramError("port", "Cannot provide both 'port' and 'file_socket'");
53 191 : }
54 :
55 147 : WebServerControl::~WebServerControl()
56 : {
57 : // Stop server if running; make sure this DOESN'T throw!
58 147 : stopServer();
59 147 : }
60 :
61 : void
62 191 : WebServerControl::startServer(const Moose::PassKey<StartWebServerControlAction>)
63 : {
64 : // Only start on rank 0
65 191 : if (processor_id() == 0)
66 : {
67 : mooseAssert(_server_weak_ptr.expired(), "Server is already started");
68 : mooseAssert(!_server_thread_ptr, "Server thread is already listening");
69 : mooseAssert(_port || _file_socket, "Neither set");
70 :
71 : // Setup and start the server
72 : {
73 : // Instantiate the server but don't start it, we need
74 : // to setup all of the actions
75 145 : auto server_ptr = std::make_shared<HttpServer>();
76 : // Give the control a weak_ptr to the server, as it will
77 : // really be owned by the server thread
78 145 : _server_weak_ptr = server_ptr;
79 :
80 : // Add all of the actions
81 145 : addServerActionsInternal();
82 :
83 : // Post message about server start
84 : {
85 145 : std::ostringstream message;
86 145 : message << "Starting server on ";
87 145 : if (_port)
88 7 : message << "port " << *_port;
89 138 : else if (_file_socket)
90 138 : message << "file socket " << *_file_socket;
91 145 : outputMessage(message.str());
92 145 : }
93 :
94 : // Start the server thread, giving the thread the server
95 : // shared_ptr, as the control only has a weak_ptr to the server
96 145 : auto console = _console;
97 145 : _server_thread_ptr = std::make_unique<std::thread>(
98 290 : [server_ptr, console](const auto port, const auto file_socket)
99 : {
100 : mooseAssert(server_ptr, "Null server");
101 145 : auto & server = *server_ptr;
102 : mooseAssert(port || file_socket, "Neither provided");
103 :
104 : try
105 : {
106 145 : if (port)
107 7 : server.startListening(uint16_t(*port));
108 : else
109 138 : server.startListening(*file_socket);
110 : }
111 0 : catch (std::exception & e)
112 : {
113 0 : console << "Server failed with exception: " << e.what() << std::endl;
114 : }
115 141 : },
116 145 : _port,
117 290 : _file_socket);
118 145 : }
119 :
120 : // Wait for the client to call /initialize
121 : {
122 : // Output that we're waiting for the client
123 145 : outputMessage("Waiting for client to initialize...");
124 :
125 : // To output initialization time
126 145 : const auto start = std::chrono::steady_clock::now();
127 :
128 413 : while (!isClientInitialized())
129 : {
130 : // Kill command sent before initialize
131 272 : if (isKillRequested())
132 : {
133 0 : stopServer();
134 0 : mooseError("Client sent kill command; exiting");
135 : }
136 :
137 : // Make sure we haven't reached the timeout
138 272 : const auto now = std::chrono::steady_clock::now();
139 272 : const auto elapsed = std::chrono::duration<double>(now - start).count();
140 272 : if (elapsed > _initial_client_timeout)
141 12 : mooseError(clientTimeoutErrorMessage(
142 4 : _initial_client_timeout, "initial_client_timeout", "during initialization"));
143 :
144 : // Poll until the next check
145 268 : std::this_thread::sleep_for(std::chrono::milliseconds(10));
146 : }
147 :
148 : // Assign a start time for the timeout thread, considering
149 : // client initialization to be a poke
150 141 : clientPoke();
151 :
152 : // Output that the client has initialized
153 141 : const auto info = getClientInfo();
154 141 : outputClientTiming(
155 282 : "\"" + info.name + "\" from " + info.user + "@" + info.host + " initialized", start);
156 141 : }
157 : }
158 :
159 : // Let the remaining ranks wait until rank 0 is done
160 187 : _communicator.barrier();
161 187 : }
162 :
163 : void
164 386 : WebServerControl::execute()
165 : {
166 : // If simulation is requested to terminate, do not go through this control
167 386 : if (_fe_problem.isSolveTerminationRequested())
168 0 : return;
169 :
170 : // For outputting the time spent waiting
171 386 : const auto start = std::chrono::steady_clock::now();
172 :
173 : // Needed to broadcast all of the types and names of data that we have received on rank 0
174 : // so that we can construct the same objects on the other ranks to receive the data and
175 : // set the same values
176 386 : std::vector<std::pair<std::string, std::string>> name_and_types;
177 :
178 : // Need to also broadcast whether or not to terminate the solve on the timestep
179 386 : bool terminate_solve = false; // Set value to avoid compiler warnings
180 :
181 : // Wait for the server on rank 0 to be done
182 386 : if (processor_id() == 0)
183 : {
184 1395 : TIME_SECTION("execute()", 3, "WebServerControl waiting for input")
185 279 : outputMessage("Waiting for client to continue...");
186 :
187 279 : setCurrentlyWaiting(true);
188 :
189 : // While waiting, yield so the server has time to run. Check for client
190 : // timeouts and a kill command.
191 830 : while (isCurrentlyWaiting())
192 : {
193 : // Kill command sent
194 591 : if (isKillRequested())
195 : {
196 36 : stopServer();
197 36 : mooseError("Client sent kill command; exiting");
198 : }
199 :
200 : // Check client timeout
201 1110 : const auto last = std::chrono::milliseconds(_last_client_poke.load());
202 555 : const auto now = std::chrono::steady_clock::now().time_since_epoch();
203 555 : const auto elapsed = std::chrono::duration<double>(now - last).count();
204 555 : if (elapsed > _client_timeout)
205 : {
206 4 : stopServer();
207 8 : mooseError(clientTimeoutErrorMessage(_client_timeout, "client_timeout"));
208 : }
209 :
210 551 : std::this_thread::sleep_for(std::chrono::milliseconds(10));
211 : }
212 :
213 : // Output waiting time
214 239 : outputClientTiming("continued", start);
215 :
216 367 : for (const auto & value_ptr : _controlled_values)
217 128 : name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
218 :
219 239 : terminate_solve = isTerminateRequested();
220 239 : setTerminateRequested(false);
221 239 : _terminate_requested.store(false);
222 239 : }
223 :
224 : // All processes need to wait
225 346 : _communicator.barrier();
226 :
227 : // Construct the values on other processors to be received into so that
228 : // they're parallel consistent
229 346 : comm().broadcast(name_and_types);
230 346 : if (processor_id() != 0)
231 169 : for (const auto & [name, type] : name_and_types)
232 62 : _controlled_values.emplace_back(Moose::WebServerControlTypeRegistry::get(type).build(name));
233 :
234 : // Set all of the values
235 536 : for (auto & value_ptr : _controlled_values)
236 : {
237 : try
238 : {
239 190 : value_ptr->setControllableValue(*this);
240 : }
241 0 : catch (...)
242 : {
243 0 : mooseError("Error setting '",
244 0 : value_ptr->type(),
245 : "' typed value for parameter '",
246 0 : value_ptr->name(),
247 : "'; it is likely that the parameter has a different type");
248 0 : }
249 : }
250 :
251 346 : _controlled_values.clear();
252 :
253 : // Set solve terminate on all ranks, if requested
254 346 : _communicator.broadcast(terminate_solve);
255 346 : if (terminate_solve)
256 11 : _fe_problem.terminateSolve();
257 346 : }
258 :
259 : template <WebServerControl::RequestMethod method>
260 : void
261 1740 : WebServerControl::addServerAction(
262 : const std::string & path,
263 : std::function<WebServerControl::Response(const WebServerControl::Request &,
264 : WebServerControl &)> && action,
265 : const WebServerControl::ServerActionOptions & options)
266 : {
267 : static_assert(method == RequestMethod::GET || method == RequestMethod::POST, "Unknown method");
268 :
269 1740 : auto server_ptr = _server_weak_ptr.lock();
270 1740 : if (!server_ptr || _server_thread_ptr)
271 0 : mooseError("addServerAction(): Can only call during addServerActions()");
272 1740 : auto & server = *server_ptr;
273 :
274 : // Capture a weak pointer to the WebServerControl so that this action
275 : // knows if the control is available or not
276 1740 : const std::weak_ptr<WebServerControl> control_weak_ptr =
277 : std::dynamic_pointer_cast<WebServerControl>(getSharedPtr());
278 :
279 : // Build the action that we'll actually pass to the web server.
280 : // Here, is very important that we capture everything by value
281 : // so that we don't rely on state from the control.
282 1740 : auto full_action =
283 2754 : [control_weak_ptr, path, options, action](const HttpRequest & http_request) -> HttpResponse
284 : {
285 : // Helper for returning an error
286 32 : const auto error = [](const std::string & error,
287 : const unsigned int status_code = 400) -> HttpResponse
288 : {
289 32 : miniJson::Json::_object response;
290 96 : response["error"] = error;
291 64 : return HttpResponse{status_code, response};
292 32 : };
293 :
294 : // Only do work here if we have access to the control;
295 : // if this lock fails, it means that it has been destructed
296 : // and there isn't anything we can do
297 5508 : if (auto control_ptr = control_weak_ptr.lock())
298 : {
299 2754 : auto & control = *control_ptr;
300 :
301 : // Setup the request to be passed to the user function,
302 : // reformatting the HttpRequest as our own request
303 : // so we can change the underlying library if needed
304 2754 : Request request;
305 2754 : const auto & json_keys = options.getRequiredJSONKeys();
306 : // Load JSON data if it exists into nlohmann::json
307 2754 : if (!http_request.json().isNull())
308 : {
309 : // JSON data exists but was not needed
310 513 : if (json_keys.empty())
311 0 : return error("Request should not have JSON");
312 :
313 : // Serialize from minijson to nlohmann::json
314 513 : const auto serialized = http_request.json().serialize();
315 513 : const auto deserialized = nlohmann::json::parse(serialized);
316 :
317 : // Check for required key(s)
318 1620 : for (const auto & key : json_keys)
319 1107 : if (!deserialized.contains(key))
320 0 : return error("Missing required key '" + key + "' in JSON");
321 :
322 : // And set in the request
323 513 : request.setJSON(deserialized, {});
324 513 : }
325 : // JSON data does not exist but it was needed
326 2241 : else if (json_keys.size())
327 0 : return error("Request should have JSON");
328 :
329 : // Option requires initialization
330 2754 : if (options.getRequireInitialized() && !control.isClientInitialized())
331 0 : return error("Client has not initialized the control");
332 :
333 : // Option requires waiting
334 2754 : if (options.getRequireWaiting() && !control.isCurrentlyWaiting())
335 0 : return error("Control is not currently waiting for data");
336 :
337 : // Call the action method to act on the request
338 2754 : Response response;
339 : try
340 : {
341 2754 : response = action(request, control);
342 : }
343 0 : catch (const std::exception & e)
344 : {
345 0 : return error(e.what());
346 : }
347 :
348 : // Has an error, return that instead
349 2754 : if (response.hasError())
350 32 : return error(response.getError(), response.getStatusCode());
351 :
352 : // No JSON data, just a status code
353 2722 : if (!response.hasJSON())
354 783 : return HttpResponse{response.getStatusCode()};
355 :
356 : // Has JSON data, convert from nlohmann::json to miniJson
357 1939 : const auto serialized = response.getJSON().dump();
358 1939 : std::string parse_error;
359 1939 : const auto json = miniJson::Json::parse(serialized, parse_error);
360 : mooseAssert(parse_error.empty(), "Should be empty");
361 1939 : return HttpResponse{response.getStatusCode(), json};
362 2754 : }
363 :
364 : // Failed to capture weak_ptr to WebServerControl
365 0 : return error("Control is no longer available");
366 : };
367 :
368 1740 : auto & when = *server.when("/" + path);
369 : if constexpr (method == RequestMethod::GET)
370 1160 : when.requested(full_action);
371 : else
372 580 : when.posted(full_action);
373 1740 : }
374 :
375 : WebServerControl::ClientInfo
376 141 : WebServerControl::getClientInfo() const
377 : {
378 141 : std::lock_guard lock(_client_info_lock);
379 141 : if (!_client_info)
380 0 : mooseError("WebServerControl::getClientInfo(): Client info is not set yet");
381 282 : return *_client_info;
382 141 : }
383 :
384 : void
385 949 : WebServerControl::outputMessage(const std::string & message) const
386 : {
387 949 : _console << typeAndName() << ": " << message << std::endl;
388 949 : }
389 :
390 : void
391 145 : WebServerControl::addServerActionsInternal()
392 : {
393 : //
394 : // -- General actions ------------------------------------------------------------
395 : //
396 :
397 : // GET /check: Helper for checking if the server is running
398 : // Requires waiting: no
399 : // Return code: 200
400 : // Return JSON data: none
401 : {
402 243 : const auto check = [](const Request &, WebServerControl &) -> Response
403 243 : { return Response{200}; };
404 :
405 145 : ServerActionOptions options;
406 145 : options.setRequireWaiting(false, {});
407 145 : options.setRequireInitialized(false, {});
408 :
409 435 : addServerAction<RequestMethod::GET>("check", check, options);
410 145 : }
411 :
412 : // GET /continue: Tell the simulation to continue
413 : // Requires waiting: yes
414 : // Return code: 200
415 : // Return JSON data:
416 : // 'error': string, optional
417 : // The error (only set if an error occurred)
418 : {
419 231 : const auto set_continue = [](const Request &, WebServerControl & control) -> Response
420 : {
421 231 : control.setCurrentlyWaiting(false);
422 231 : return Response{200};
423 : };
424 :
425 435 : addServerAction<RequestMethod::GET>("continue", set_continue);
426 : }
427 :
428 : // GET /initialize: Initializes the communication with the client
429 : // Requires waiting: no
430 : // POST JSON data:
431 : // 'name': string
432 : // The name of the client
433 : // 'host': string
434 : // The name the client host
435 : // 'user': string
436 : // The name of the client user
437 : // Return code: 200
438 : // Return JSON data:
439 : // 'flags': list[string]
440 : // The execute on flags
441 : // 'error': string, optional
442 : // The error (only set if an error occurred)
443 : {
444 141 : const auto initialize = [](const Request & req, WebServerControl & control) -> Response
445 : {
446 141 : if (control.isClientInitialized())
447 0 : return ErrorResponse("Initialize has already been called");
448 :
449 : // Store the information received in the client info
450 141 : const auto & json = req.getJSON();
451 141 : ClientInfo client_info;
452 141 : const auto name = convertJSON<std::string>(json, "name");
453 141 : const auto host = convertJSON<std::string>(json, "host");
454 141 : const auto user = convertJSON<std::string>(json, "user");
455 141 : client_info.data = json;
456 141 : control.setClientInfo(client_info);
457 :
458 : // Capture the sorted exceute on flags
459 141 : std::set<std::string> flags_sorted;
460 594 : for (const auto & it : control.getParam<ExecFlagEnum>("execute_on"))
461 171 : flags_sorted.insert(it);
462 141 : const std::vector<std::string> flags(flags_sorted.begin(), flags_sorted.end());
463 :
464 : // Send back context about the control
465 141 : nlohmann::json response_json;
466 141 : response_json["control_name"] = control.name();
467 141 : response_json["control_type"] = control.type();
468 141 : response_json["execute_on_flags"] = flags_sorted;
469 :
470 : // Set initialized
471 141 : control.setClientInitialized();
472 :
473 141 : return Response{200, response_json};
474 141 : };
475 :
476 145 : ServerActionOptions options;
477 145 : options.setRequireWaiting(false, {});
478 145 : options.setRequireInitialized(false, {});
479 580 : options.requireJSONKeys({"host", "name", "user"});
480 :
481 290 : addServerAction<RequestMethod::POST>("initialize", initialize, options);
482 145 : }
483 :
484 : // GET /poke: "Poke" the server; used for checking timeouts
485 : // Requires waiting: no
486 : // Return code: 200
487 : // Return JSON data: none
488 : {
489 137 : const auto poke = [](const Request &, WebServerControl & control) -> Response
490 : {
491 137 : control.clientPoke();
492 137 : return Response{200};
493 : };
494 :
495 145 : ServerActionOptions options;
496 145 : options.setRequireWaiting(false, {});
497 :
498 435 : addServerAction<RequestMethod::GET>("poke", poke, options);
499 145 : }
500 :
501 : // GET /terminate: Tell FEProblemBase to terminate the solve
502 : // Requires waiting: yes
503 : // Return code: 200
504 : // Return JSON data:
505 : // 'error': string, optional
506 : // The error (only set if an error occurred)
507 : {
508 8 : const auto terminate = [](const Request &, WebServerControl & control) -> Response
509 : {
510 8 : control.setTerminateRequested();
511 8 : control.setCurrentlyWaiting(false);
512 8 : return Response{200};
513 : };
514 :
515 435 : addServerAction<RequestMethod::GET>("terminate", terminate);
516 : }
517 :
518 : // GET /waiting: Check if waiting and the waiting exec flag if it exists
519 : // Requires waiting: no
520 : // Return code: 200
521 : // Return JSON data:
522 : // 'waiting': bool
523 : // Whether or not the control is waiting
524 : // 'execute_on_flag': string, optional
525 : // Only exists if waiting=true, the execute flag that is being waited on
526 : {
527 1530 : const auto waiting = [](const Request &, WebServerControl & control) -> Response
528 : {
529 1530 : nlohmann::json response_json;
530 1530 : if (control.isCurrentlyWaiting())
531 : {
532 1044 : response_json["waiting"] = true;
533 1044 : response_json["execute_on_flag"] =
534 2088 : static_cast<std::string>(control._fe_problem.getCurrentExecuteOnFlag());
535 : }
536 : else
537 486 : response_json["waiting"] = false;
538 :
539 3060 : return Response{200, response_json};
540 1530 : };
541 :
542 145 : ServerActionOptions options;
543 145 : options.setRequireWaiting(false, {});
544 :
545 435 : addServerAction<RequestMethod::GET>("waiting", waiting, options);
546 145 : }
547 :
548 : // GET /kill: Tell the client poll thread to kill
549 : // Requires waiting: no
550 : {
551 36 : const auto kill = [](const Request &, WebServerControl & control) -> Response
552 : {
553 36 : control.setKillRequested();
554 36 : return Response{200};
555 : };
556 :
557 435 : addServerAction<RequestMethod::GET>("kill", kill);
558 : }
559 :
560 : //
561 : // -- Get actions ----------------------------------------------------------------
562 : //
563 :
564 : // GET /get/dt: Get current simulation timestep size
565 : // Requires waiting: yes
566 : // Return code: 200
567 : // Return JSON data:
568 : // 'dt': double
569 : // Current timestep size
570 : // Return JSON data:
571 : // 'error': string, optional
572 : // The error (only set if an error occurred)
573 : {
574 28 : const auto get_dt = [](const Request &, WebServerControl & control) -> Response
575 : {
576 28 : nlohmann::json response_json;
577 28 : response_json["dt"] = control._fe_problem.dt();
578 56 : return Response{200, response_json};
579 28 : };
580 :
581 435 : addServerAction<RequestMethod::GET>("get/dt", get_dt);
582 : }
583 :
584 : // POST /get/postprocessor: Get a postprocessor value
585 : // Requires waiting: yes
586 : // POST JSON data:
587 : // 'name': string
588 : // The name of the Postprocessor
589 : // Return code: 200
590 : // Return JSON data:
591 : // 'value': double
592 : // The postprocessor value
593 : // 'error': string, optional
594 : // The error (only set if an error occurred)
595 : {
596 88 : const auto get_postprocessor = [](const Request & req, WebServerControl & control) -> Response
597 : {
598 : // Get the postprocessor name
599 88 : const auto name = convertJSON<std::string>(req.getJSON(), "name");
600 :
601 : // Postprocessor should exist
602 88 : if (!control.hasPostprocessorByName(name))
603 4 : return ErrorResponse("Postprocessor '" + name + "' not found");
604 :
605 84 : nlohmann::json response_json;
606 84 : response_json["value"] = control.getPostprocessorValueByName(name);
607 84 : return Response{200, response_json};
608 88 : };
609 :
610 145 : ServerActionOptions options;
611 145 : options.requireJSONKey("name");
612 :
613 435 : addServerAction<RequestMethod::POST>("get/postprocessor", get_postprocessor, options);
614 145 : }
615 :
616 : // POST /get/reporter: Get a Reporter value
617 : // Requires waiting: yes
618 : // POST JSON data:
619 : // 'name': string
620 : // The name of the Reporter value (object_name/value_name)
621 : // Return code: 200
622 : // Return JSON data:
623 : // 'value': any
624 : // The reporter value
625 : // Return JSON data:
626 : // 'error': string, optional
627 : // The error (only set if an error occurred)
628 : {
629 128 : const auto get_reporter = [](const Request & req, WebServerControl & control) -> Response
630 : {
631 : // Get the reporter name
632 128 : const auto name = convertJSON<std::string>(req.getJSON(), "name");
633 128 : if (!ReporterName::isValidName(name))
634 0 : return ErrorResponse("Name '" + name + "' not a valid reporter value name");
635 128 : const auto rname = ReporterName(name);
636 :
637 : // Reporter should exist
638 128 : if (!control.hasReporterValueByName(rname))
639 0 : return ErrorResponse("Reporter value '" + name + "' was not found");
640 :
641 : // Store the reporter value
642 128 : nlohmann::json response_json;
643 128 : control.getReporterContextBaseByName(rname).store(response_json["value"]);
644 :
645 128 : return Response{200, response_json};
646 128 : };
647 :
648 145 : ServerActionOptions options;
649 145 : options.requireJSONKey("name");
650 :
651 435 : addServerAction<RequestMethod::POST>("get/reporter", get_reporter, options);
652 145 : }
653 :
654 : // GET /get/time: Get current simulation time
655 : // Requires waiting: yes
656 : // Return code: 200
657 : // Return JSON data:
658 : // 'time': double
659 : // Current time
660 : // 'error': string, optional
661 : // The error (only set if an error occurred)
662 : {
663 28 : const auto get_time = [](const Request &, WebServerControl & control) -> Response
664 : {
665 28 : nlohmann::json response_json;
666 28 : response_json["time"] = control._fe_problem.time();
667 56 : return Response{200, response_json};
668 28 : };
669 :
670 435 : addServerAction<RequestMethod::GET>("get/time", get_time);
671 : }
672 :
673 : //
674 : // -- Set actions ----------------------------------------------------------------
675 : //
676 :
677 : // POST /set/controllable: Get a controllable parameter
678 : // Requires waiting: yes
679 : // POST JSON data:
680 : // 'name': string
681 : // The path to the controllable data
682 : // 'value': any
683 : // The value to set
684 : // 'type: string
685 : // The C++ type of the controllable parameter
686 : // Return code: 201
687 : // Return JSON data:
688 : // 'error': string, optional
689 : // The error (only set if an error occurred)
690 : {
691 156 : const auto set_controllable = [](const Request & req, WebServerControl & control) -> Response
692 : {
693 156 : const auto & json = req.getJSON();
694 :
695 : // Get the parameter type
696 156 : const auto type = convertJSON<std::string>(json, "type");
697 156 : const auto registered_type = Moose::WebServerControlTypeRegistry::query(type);
698 156 : if (!registered_type)
699 8 : return ErrorResponse("Type '" + type +
700 4 : "' not registered for setting a controllable parameter");
701 :
702 : // Get the parameter name
703 152 : const auto name = convertJSON<std::string>(json, "name");
704 : // Parameter should exist
705 152 : if (!control.hasControllableParameterByName(name))
706 4 : return ErrorResponse("Controllable parameter '" + name + "' not found");
707 :
708 : // 'value' must exist
709 148 : const auto value_it = json.find("value");
710 148 : if (value_it == json.end())
711 0 : return ErrorResponse("Missing 'value' entry");
712 :
713 : // Build the value (also does the parsing)
714 : {
715 148 : std::unique_ptr<ControlledValueBase> value;
716 : try
717 : {
718 148 : value = registered_type->build(name, *value_it);
719 : }
720 20 : catch (std::exception & e)
721 : {
722 20 : return ErrorResponse("While parsing 'value': " + std::string(e.what()));
723 20 : }
724 :
725 128 : std::lock_guard<std::mutex> lock(control._controlled_values_mutex);
726 128 : control._controlled_values.emplace_back(std::move(value));
727 148 : }
728 :
729 128 : return Response{201};
730 156 : };
731 :
732 145 : ServerActionOptions options;
733 580 : options.requireJSONKeys({"name", "type", "value"});
734 :
735 290 : addServerAction<RequestMethod::POST>("set/controllable", set_controllable, options);
736 145 : }
737 :
738 : // Let derived classes add actions
739 145 : addServerActions();
740 1015 : }
741 :
742 : void
743 141 : WebServerControl::setClientInfo(const WebServerControl::ClientInfo & info)
744 : {
745 141 : std::lock_guard lock(_client_info_lock);
746 141 : _client_info = info;
747 141 : }
748 :
749 : void
750 278 : WebServerControl::clientPoke()
751 : {
752 278 : const int64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(
753 278 : std::chrono::steady_clock::now().time_since_epoch())
754 278 : .count();
755 278 : _last_client_poke.store(now);
756 278 : }
757 :
758 : void
759 380 : WebServerControl::outputClientTiming(
760 : const std::string & message,
761 : const std::chrono::time_point<std::chrono::steady_clock> & start) const
762 : {
763 380 : const auto now = std::chrono::steady_clock::now();
764 380 : const auto elapsed = std::chrono::duration<double>(now - start).count();
765 :
766 380 : std::ostringstream out;
767 380 : out << "Client " << message << " after " << std::fixed << std::setprecision(2) << elapsed
768 380 : << " seconds";
769 380 : outputMessage(out.str());
770 380 : }
771 :
772 : std::string
773 8 : WebServerControl::clientTimeoutErrorMessage(
774 : const Real timeout,
775 : const std::string & timeout_param_name,
776 : const std::optional<std::string> & suffix /* = {} */) const
777 : {
778 8 : std::ostringstream oss;
779 20 : oss << "The client timed out" << (suffix ? (" " + *suffix) : "") << "\nThe timeout is "
780 8 : << std::fixed << std::setprecision(2) << timeout << " seconds and is set by the '"
781 8 : << timeout_param_name << "' parameter";
782 16 : return oss.str();
783 8 : }
784 :
785 : void
786 187 : WebServerControl::stopServer()
787 : {
788 : // Only have something to do here if the server still exists
789 : // and the server thread was set; make sure this doesn't
790 : // throw because it is used in the destructor
791 187 : if (auto server_ptr = _server_weak_ptr.lock(); server_ptr && _server_thread_ptr)
792 : {
793 : try
794 : {
795 141 : server_ptr->shutdown();
796 141 : _server_thread_ptr->join();
797 141 : _server_thread_ptr.reset();
798 : }
799 0 : catch (...)
800 : {
801 0 : }
802 187 : }
803 187 : }
804 :
805 : /// Explicitly instantiate the addServerAction method for the valid request types
806 : ///@{
807 : template void WebServerControl::addServerAction<WebServerControl::RequestMethod::GET>(
808 : const std::string &,
809 : std::function<WebServerControl::Response(const WebServerControl::Request &,
810 : WebServerControl &)> &&,
811 : const WebServerControl::ServerActionOptions &);
812 : template void WebServerControl::addServerAction<WebServerControl::RequestMethod::POST>(
813 : const std::string &,
814 : std::function<WebServerControl::Response(const WebServerControl::Request &,
815 : WebServerControl &)> &&,
816 : const WebServerControl::ServerActionOptions & options);
817 : ///@}
818 :
819 : const nlohmann::json &
820 513 : WebServerControl::Request::getJSON() const
821 : {
822 513 : if (!hasJSON())
823 0 : throw std::runtime_error("Request does not contain JSON when it should");
824 513 : return *_json;
825 : }
826 :
827 815 : WebServerControl::Response::Response(const unsigned int status_code) : _status_code(status_code) {}
828 :
829 1939 : WebServerControl::Response::Response(const unsigned int status_code, const nlohmann::json & json)
830 1939 : : _status_code(status_code), _json(json)
831 : {
832 1939 : }
833 :
834 : const nlohmann::json &
835 1939 : WebServerControl::Response::getJSON() const
836 : {
837 1939 : if (!_json)
838 0 : throw std::runtime_error("Response does not contain JSON when it should");
839 1939 : return *_json;
840 : }
841 :
842 : const std::string &
843 32 : WebServerControl::Response::getError() const
844 : {
845 32 : if (!_error)
846 0 : ::mooseError("Does not have an error");
847 32 : return *_error;
848 : }
849 :
850 32 : WebServerControl::ErrorResponse::ErrorResponse(const std::string & error,
851 32 : const unsigned int status_code /* = 400 */)
852 32 : : WebServerControl::Response(status_code)
853 : {
854 32 : setError(error);
855 32 : }
|