14#include "minijson/minijson.h"
15#include "tinyhttp/http.h"
26 params.
addClassDescription(
"Starts a webserver for sending/receiving JSON messages to get data "
27 "and control a running MOOSE calculation");
28 params.
addParam<
unsigned int>(
"port",
29 "The port to listen on; must provide either this or 'file_socket'. "
30 "Set to zero to have the operating system choose a free port, "
31 "which is then reported in the output and in 'port_file'");
34 "The path to the unix file socket to listen on; must provide either this or 'port'");
35 params.
addParam<FileName>(
"port_file",
36 "A path to write the bound port to once the server is listening. Lets "
37 "a client that set 'port' to zero learn the chosen port");
38 params.
addParam<Real>(
"initial_client_timeout",
40 "Time in seconds to allow the client to begin communicating on init; if "
41 "this time is surpassed the run will be killed");
42 params.
addParam<Real>(
"client_timeout",
44 "Time in seconds to allow the client to communicate; if this time is "
45 "surpassed the run will be killed");
51 _port(queryParam<unsigned
int>(
"port")),
52 _file_socket(queryParam<FileName>(
"file_socket")),
53 _port_file(queryParam<FileName>(
"port_file")),
54 _initial_client_timeout(getParam<Real>(
"initial_client_timeout")),
55 _client_timeout(getParam<Real>(
"client_timeout"))
58 mooseError(
"You must provide either the parameter 'port' or 'file_socket' to designate where "
61 paramError(
"port",
"Cannot provide both 'port' and 'file_socket'");
63 paramError(
"port_file",
"Can only be used together with 'port'");
86 auto server_ptr = std::make_shared<HttpServer>();
98 [server_ptr, console,
this](
const auto port,
const auto file_socket)
100 mooseAssert(server_ptr,
"Null server");
101 auto & server = *server_ptr;
102 mooseAssert(port || file_socket,
"Neither provided");
107 server.startListening(uint16_t(*port));
109 server.startListening(*file_socket);
111 catch (std::exception & e)
113 console <<
"Server failed with exception: " << e.what() << std::endl;
128 while (!
serverError() && server_ptr->boundPort() == 0)
129 std::this_thread::sleep_for(std::chrono::milliseconds(10));
131 mooseError(
"The server failed to start: ", *error);
136 std::ostringstream message;
137 message <<
"Starting server on ";
139 message <<
"port " << server_ptr->boundPort();
157 const auto start = std::chrono::steady_clock::now();
169 mooseError(
"Client sent kill command; exiting");
173 const auto now = std::chrono::steady_clock::now();
174 const auto elapsed = std::chrono::duration<double>(now - start).count();
180 std::this_thread::sleep_for(std::chrono::milliseconds(10));
190 "\"" + info.name +
"\" from " + info.user +
"@" + info.host +
" initialized", start);
206 const auto start = std::chrono::steady_clock::now();
211 std::vector<std::pair<std::string, std::string>> name_and_types;
214 bool terminate_solve =
false;
219 TIME_SECTION(
"execute()", 3,
"WebServerControl waiting for input")
232 mooseError(
"Client sent kill command; exiting");
237 const auto now = std::chrono::steady_clock::now().time_since_epoch();
238 const auto elapsed = std::chrono::duration<double>(now - last).count();
245 std::this_thread::sleep_for(std::chrono::milliseconds(10));
252 name_and_types.emplace_back(value_ptr->name(), value_ptr->type());
266 for (
const auto & [
name,
type] : name_and_types)
274 value_ptr->setControllableValue(*
this);
280 "' typed value for parameter '",
282 "'; it is likely that the parameter has a different type");
294template <WebServerControl::RequestMethod method>
297 const std::string & path,
306 mooseError(
"addServerAction(): Can only call during addServerActions()");
307 auto & server = *server_ptr;
311 const std::weak_ptr<WebServerControl> control_weak_ptr =
312 std::dynamic_pointer_cast<WebServerControl>(
getSharedPtr());
318 [control_weak_ptr, path, options, action](
const HttpRequest & http_request) -> HttpResponse
321 const auto error = [](
const std::string & error,
322 const unsigned int status_code = 400) -> HttpResponse
324 miniJson::Json::_object response;
325 response[
"error"] = error;
326 return HttpResponse{status_code, response};
332 if (
auto control_ptr = control_weak_ptr.lock())
334 auto & control = *control_ptr;
342 if (!http_request.json().isNull())
345 if (json_keys.empty())
346 return error(
"Request should not have JSON");
349 const auto serialized = http_request.json().serialize();
350 const auto deserialized = nlohmann::json::parse(serialized);
353 for (
const auto & key : json_keys)
354 if (!deserialized.contains(key))
355 return error(
"Missing required key '" + key +
"' in JSON");
358 request.setJSON(deserialized, {});
361 else if (json_keys.size())
362 return error(
"Request should have JSON");
366 return error(
"Client has not initialized the control");
370 return error(
"Control is not currently waiting for data");
376 response = action(request, control);
378 catch (
const std::exception & e)
380 return error(e.what());
392 const auto serialized = response.
getJSON().dump();
393 std::string parse_error;
394 const auto json = miniJson::Json::parse(serialized, parse_error);
395 mooseAssert(parse_error.empty(),
"Should be empty");
400 return error(
"Control is no longer available");
403 auto & when = *server.when(
"/" + path);
405 when.requested(full_action);
407 when.posted(full_action);
415 mooseError(
"WebServerControl::getClientInfo(): Client info is not set yet");
444 addServerAction<RequestMethod::GET>(
"check", check, options);
456 control.setCurrentlyWaiting(
false);
460 addServerAction<RequestMethod::GET>(
"continue", set_continue);
481 if (control.isClientInitialized())
485 const auto & json = req.
getJSON();
487 const auto name = convertJSON<std::string>(json,
"name");
488 const auto host = convertJSON<std::string>(json,
"host");
489 const auto user = convertJSON<std::string>(json,
"user");
490 client_info.
data = json;
491 control.setClientInfo(client_info);
494 std::set<std::string> flags_sorted;
495 for (
const auto & it : control.getParam<
ExecFlagEnum>(
"execute_on"))
496 flags_sorted.insert(it);
497 const std::vector<std::string> flags(flags_sorted.begin(), flags_sorted.end());
500 nlohmann::json response_json;
501 response_json[
"control_name"] = control.name();
502 response_json[
"control_type"] = control.type();
503 response_json[
"execute_on_flags"] = flags_sorted;
506 control.setClientInitialized();
508 return Response{200, response_json};
516 addServerAction<RequestMethod::POST>(
"initialize",
initialize, options);
526 control.clientPoke();
533 addServerAction<RequestMethod::GET>(
"poke", poke, options);
545 control.setTerminateRequested();
546 control.setCurrentlyWaiting(
false);
550 addServerAction<RequestMethod::GET>(
"terminate", terminate);
564 nlohmann::json response_json;
565 if (control.isCurrentlyWaiting())
567 response_json[
"waiting"] =
true;
568 response_json[
"execute_on_flag"] =
569 static_cast<std::string
>(control._fe_problem.getCurrentExecuteOnFlag());
572 response_json[
"waiting"] =
false;
574 return Response{200, response_json};
580 addServerAction<RequestMethod::GET>(
"waiting", waiting, options);
588 control.setKillRequested();
592 addServerAction<RequestMethod::GET>(
"kill", kill);
611 nlohmann::json response_json;
612 response_json[
"dt"] = control._fe_problem.dt();
613 return Response{200, response_json};
616 addServerAction<RequestMethod::GET>(
"get/dt", get_dt);
634 const auto name = convertJSON<std::string>(req.
getJSON(),
"name");
637 if (!control.hasPostprocessorByName(
name))
640 nlohmann::json response_json;
641 response_json[
"value"] = control.getPostprocessorValueByName(
name);
642 return Response{200, response_json};
648 addServerAction<RequestMethod::POST>(
"get/postprocessor", get_postprocessor, options);
667 const auto name = convertJSON<std::string>(req.
getJSON(),
"name");
673 if (!control.hasReporterValueByName(rname))
677 nlohmann::json response_json;
678 control.getReporterContextBaseByName(rname).store(response_json[
"value"]);
680 return Response{200, response_json};
686 addServerAction<RequestMethod::POST>(
"get/reporter", get_reporter, options);
700 nlohmann::json response_json;
701 response_json[
"time"] = control._fe_problem.time();
702 return Response{200, response_json};
705 addServerAction<RequestMethod::GET>(
"get/time", get_time);
728 const auto & json = req.
getJSON();
731 const auto type = convertJSON<std::string>(json,
"type");
733 if (!registered_type)
735 "' not registered for setting a controllable parameter");
738 const auto name = convertJSON<std::string>(json,
"name");
740 if (!control.hasControllableParameterByName(
name))
744 const auto value_it = json.find(
"value");
745 if (value_it == json.end())
750 std::unique_ptr<ControlledValueBase> value;
753 value = registered_type->build(
name, *value_it);
755 catch (std::exception & e)
757 return ErrorResponse(
"While parsing 'value': " + std::string(e.what()));
760 std::lock_guard<std::mutex> lock(control._controlled_values_mutex);
761 control._controlled_values.emplace_back(std::move(value));
770 addServerAction<RequestMethod::POST>(
"set/controllable", set_controllable, options);
787 const int64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(
788 std::chrono::steady_clock::now().time_since_epoch())
795 const std::string & message,
796 const std::chrono::time_point<std::chrono::steady_clock> & start)
const
798 const auto now = std::chrono::steady_clock::now();
799 const auto elapsed = std::chrono::duration<double>(now - start).count();
801 std::ostringstream out;
802 out <<
"Client " << message <<
" after " << std::fixed << std::setprecision(2) << elapsed
810 const std::string & timeout_param_name,
811 const std::optional<std::string> & suffix )
const
813 std::ostringstream oss;
814 oss <<
"The client timed out" << (suffix ? (
" " + *suffix) :
"") <<
"\nThe timeout is "
815 << std::fixed << std::setprecision(2) << timeout <<
" seconds and is set by the '"
816 << timeout_param_name <<
"' parameter";
830 server_ptr->shutdown();
840std::optional<std::string>
854 const std::string temporary_path = *
_port_file +
".tmp";
856 std::ofstream out(temporary_path);
858 mooseError(
"Failed to open '", temporary_path,
"' to write the server port");
859 out << port << std::endl;
861 mooseError(
"Failed to write the server port to '", temporary_path,
"'");
863 if (std::rename(temporary_path.c_str(),
_port_file->c_str()) != 0)
869template void WebServerControl::addServerAction<WebServerControl::RequestMethod::GET>(
874template void WebServerControl::addServerAction<WebServerControl::RequestMethod::POST>(
881const nlohmann::json &
885 throw std::runtime_error(
"Request does not contain JSON when it should");
892 : _status_code(status_code), _json(json)
896const nlohmann::json &
900 throw std::runtime_error(
"Response does not contain JSON when it should");
913 const unsigned int status_code )
registerMooseObject("MooseApp", WebServerControl)
void ErrorVector unsigned int
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
Base class for Control objects.
static InputParameters validParams()
Class constructor.
FEProblemBase & _fe_problem
Reference to the FEProblemBase for this object.
A MultiMooseEnum object to hold "execute_on" flags.
const std::string & type() const
Get the type of this class.
std::string typeAndName() const
Get the class's combined type and name; useful in error handling.
const std::string & name() const
Get the name of the class.
void paramError(const std::string ¶m, Args... args) const
Emits an error prefixed with the file and line number of the given param (from the input file) along ...
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type and optionally a file path to the top-level block p...
std::shared_ptr< MooseObject > getSharedPtr()
Get another shared pointer to this object that has the same ownership group.
virtual std::unique_ptr< ControlledValueBase > build(const std::string &name, const nlohmann::json &json_value) const =0
Builds a value with the given type, name name, and JSON value json_value.
static const RegisteredTypeBase & get(const std::string &type)
Get the registration for the given type, erroring if it isn't registered.
static const RegisteredTypeBase * query(const std::string &type)
Query the registration for the given type.
virtual void terminateSolve()
Allow objects to request clean termination of the solve.
virtual bool isSolveTerminationRequested() const
Check of termination has been requested.
The Reporter system is comprised of objects that can contain any number of data values.
static bool isValidName(const std::string &object_and_value_name)
Determines if the inputted string is convertible to a ReporterName.
void broadcast(T &data, const unsigned int root_id=0, const bool identical_sizes=false) const
Starts a webserver that an external process can connect to in order to send JSON messages to control ...
std::atomic< bool > _terminate_requested
Whether or not the solve should be terminated in the next execute() call.
const Real _client_timeout
Time in seconds to allow the client to communicate after init before timing out.
WebServerControl(const InputParameters ¶meters)
void addServerAction(const std::string &path, std::function< Response(const Request &, WebServerControl &)> &&action, const ServerActionOptions &options={})
Adds an action for the server to perform at the given path.
std::optional< std::string > _server_error
The error the server failed with, if it has failed.
virtual void addServerActions()
Entrypoint for controls derived from this one to add additional actions.
void addServerActionsInternal()
Adds the internal actions to the server.
virtual void execute() override final
Execute the control.
bool isKillRequested() const
Get whether or not the client sent the kill command.
static InputParameters validParams()
std::weak_ptr< HttpServer > _server_weak_ptr
Weak pointer to the server; the server itself is owned by the server thread.
std::atomic< int64_t > _last_client_poke
The most recent time we've heard from the client.
bool isTerminateRequested() const
Whether or not the client has called /terminate.
std::string clientTimeoutErrorMessage(const Real timeout, const std::string &timeout_param_name, const std::optional< std::string > &suffix={}) const
Helper for producing an error message about a client timeout.
void writePortFile(const unsigned int port) const
Write the given port to the 'port_file' path.
bool isCurrentlyWaiting() const
Get whether or not the control is currently waiting.
void clientPoke()
Store a client's poke, which is a timing used to determine the client timeout.
std::mutex _client_info_lock
Lock for _client_info as it is written by the server thread.
ClientInfo getClientInfo() const
Get the information sent by the client on initialize.
const FileName *const _port_file
File to write the bound port to once the server is listening, if any.
const unsigned int *const _port
Port to listen on, if any.
const FileName *const _file_socket
File socket to listen on, if any.
std::unique_ptr< std::thread > _server_thread_ptr
The server thread.
std::vector< std::unique_ptr< ControlledValueBase > > _controlled_values
The values received to control; filled on rank 0 from the server and then broadcast.
void setCurrentlyWaiting(const bool value=true)
Set that the control is currently waiting; used by the server.
void stopServer()
Stop the server if it exists and is running.
void setClientInfo(const ClientInfo &info)
Set the ClientInfo object received from the client during /initialize.
std::optional< std::string > serverError() const
Get the error the server failed with, if it has failed.
void startServer(const Moose::PassKey< StartWebServerControlAction >)
Start the server.
void outputMessage(const std::string &message) const
Output a message with the prefix of this control type and name.
bool isClientInitialized() const
Whether or not the client has called /initialize.
std::mutex _server_error_lock
Lock for _server_error as it is written by the server thread.
std::optional< ClientInfo > _client_info
Client information received on /initialize by the server.
const Real _initial_client_timeout
Time in seconds to allow the client to initially communicate before timing out.
void setTerminateRequested(const bool value=true)
Set for the control to terminate the solve; used by /terminate in the server.
void outputClientTiming(const std::string &message, const std::chrono::time_point< std::chrono::steady_clock > &start) const
Output a timing message with the prefix of this control.
const Parallel::Communicator & _communicator
processor_id_type processor_id() const
const Parallel::Communicator & comm() const
void initialize(EquationSystems &es, const std::string &system_name)
Stores the information sent by the client on initialize.
nlohmann::json data
Raw data.
Represents an error response to the client from the server.
ErrorResponse(const std::string &error, const unsigned int status_code=400)
Represents a request from the client.
std::optional< nlohmann::json > _json
The underlying JSON data, if any.
const nlohmann::json & getJSON() const
Represents a response to the client from the server.
const std::string & getError() const
const nlohmann::json & getJSON() const
void setError(const std::string &error)
Set the error message.
unsigned int getStatusCode() const
Options to be passed to addServerAction.
void requireJSONKey(const std::string &key)
Append a key to be required in JSON in the request data.
void requireJSONKeys(std::initializer_list< std::string > &&keys)
Append keys to be required in JSON in the request data.
void setRequireInitialized(const bool value, const Moose::PassKey< WebServerControl >)
Set the require initialized flag; only accessible by the WebServerControl.
const std::set< std::string > getRequiredJSONKeys() const
bool getRequireInitialized() const
bool getRequireWaiting() const
void setRequireWaiting(const bool value, const Moose::PassKey< WebServerControl >)
Set the require waiting flag; only accessible by the WebServerControl.