mirror of
https://git.rwth-aachen.de/acs/public/villas/node/
synced 2025-03-30 00:00:11 +01:00
wip
This commit is contained in:
parent
3b84db26c6
commit
1e35ccab0e
6 changed files with 169 additions and 111 deletions
|
@ -38,7 +38,14 @@
|
||||||
#include <villas/exceptions.hpp>
|
#include <villas/exceptions.hpp>
|
||||||
|
|
||||||
namespace villas {
|
namespace villas {
|
||||||
namespace node {
|
|
||||||
|
/* Forward declarations */
|
||||||
|
class SuperNode;
|
||||||
|
|
||||||
|
namespace tools {
|
||||||
|
class Relay;
|
||||||
|
}
|
||||||
|
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
const int version = 2;
|
const int version = 2;
|
||||||
|
@ -90,9 +97,6 @@ public:
|
||||||
|
|
||||||
} /* namespace api */
|
} /* namespace api */
|
||||||
|
|
||||||
/* Forward declarations */
|
|
||||||
class SuperNode;
|
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -103,8 +107,6 @@ protected:
|
||||||
std::thread thread;
|
std::thread thread;
|
||||||
std::atomic<bool> running; /**< Atomic flag for signalizing thread termination. */
|
std::atomic<bool> running; /**< Atomic flag for signalizing thread termination. */
|
||||||
|
|
||||||
SuperNode *super_node;
|
|
||||||
|
|
||||||
void run();
|
void run();
|
||||||
void worker();
|
void worker();
|
||||||
|
|
||||||
|
@ -113,20 +115,64 @@ public:
|
||||||
*
|
*
|
||||||
* Save references to list of paths / nodes for command execution.
|
* Save references to list of paths / nodes for command execution.
|
||||||
*/
|
*/
|
||||||
Api(SuperNode *sn);
|
Api();
|
||||||
~Api();
|
~Api();
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
SuperNode * getSuperNode()
|
|
||||||
{
|
|
||||||
return super_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::list<api::Session *> sessions; /**< List of currently active connections */
|
std::list<api::Session *> sessions; /**< List of currently active connections */
|
||||||
villas::QueueSignalled<api::Session *> pending; /**< A queue of api_sessions which have pending requests. */
|
villas::QueueSignalled<api::Session *> pending; /**< A queue of api_sessions which have pending requests. */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
namespace node {
|
||||||
|
|
||||||
|
class Api : public Api {
|
||||||
|
|
||||||
|
protected:
|
||||||
|
SuperNode *super_node;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Initalize the API.
|
||||||
|
*
|
||||||
|
* Save references to list of paths / nodes for command execution.
|
||||||
|
*/
|
||||||
|
Api(SuperNode *sn) :
|
||||||
|
Api(),
|
||||||
|
super_node(sn)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
SuperNode * getSuperNode()
|
||||||
|
{
|
||||||
|
return super_node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} /* namespace node */
|
} /* namespace node */
|
||||||
|
|
||||||
|
namespace relay {
|
||||||
|
|
||||||
|
class Api : public Api {
|
||||||
|
|
||||||
|
protected:
|
||||||
|
tools::Relay *relay;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Initalize the API.
|
||||||
|
*
|
||||||
|
* Save references to list of paths / nodes for command execution.
|
||||||
|
*/
|
||||||
|
Api(tools::Relay *r) :
|
||||||
|
Api(),
|
||||||
|
relay(r)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
tools::Relay * getRelay()
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} /* namespace relay */
|
||||||
|
|
||||||
} /* namespace villas */
|
} /* namespace villas */
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
#define RE_UUID "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
|
#define RE_UUID "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
|
||||||
|
|
||||||
namespace villas {
|
namespace villas {
|
||||||
namespace node {
|
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
/* Forward declarations */
|
/* Forward declarations */
|
||||||
|
@ -120,6 +119,7 @@ public:
|
||||||
toString();
|
toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template<typename R>
|
||||||
class RequestFactory : public plugin::Plugin {
|
class RequestFactory : public plugin::Plugin {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -128,10 +128,10 @@ public:
|
||||||
virtual bool
|
virtual bool
|
||||||
match(const std::string &uri, std::smatch &m) const = 0;
|
match(const std::string &uri, std::smatch &m) const = 0;
|
||||||
|
|
||||||
virtual Request *
|
virtual R *
|
||||||
make(Session *s) = 0;
|
make(Session *s) = 0;
|
||||||
|
|
||||||
static Request *
|
static R *
|
||||||
create(Session *s, const std::string &uri, Session::Method meth, unsigned long ct);
|
create(Session *s, const std::string &uri, Session::Method meth, unsigned long ct);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -175,5 +175,4 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
} /* namespace api */
|
} /* namespace api */
|
||||||
} /* namespace node */
|
|
||||||
} /* namespace villas */
|
} /* namespace villas */
|
||||||
|
|
|
@ -30,10 +30,12 @@
|
||||||
#include <villas/api.hpp>
|
#include <villas/api.hpp>
|
||||||
|
|
||||||
namespace villas {
|
namespace villas {
|
||||||
namespace node {
|
|
||||||
|
|
||||||
/* Forward declarations */
|
/* Forward declarations */
|
||||||
|
namespace node {
|
||||||
class SuperNode;
|
class SuperNode;
|
||||||
|
}
|
||||||
|
|
||||||
class Api;
|
class Api;
|
||||||
class Web;
|
class Web;
|
||||||
|
|
||||||
|
@ -115,9 +117,7 @@ public:
|
||||||
|
|
||||||
static std::string
|
static std::string
|
||||||
methodToString(Method meth);
|
methodToString(Method meth);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} /* namespace api */
|
} /* namespace api */
|
||||||
} /* namespace node */
|
|
||||||
} /* namespace villas */
|
} /* namespace villas */
|
||||||
|
|
|
@ -40,10 +40,9 @@ InvalidMethod::InvalidMethod(Request *req) :
|
||||||
)
|
)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
Api::Api(SuperNode *sn) :
|
Api::Api() :
|
||||||
logger(logging.get("api")),
|
logger(logging.get("api")),
|
||||||
state(State::INITIALIZED),
|
state(State::INITIALIZED),
|
||||||
super_node(sn)
|
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
Api::~Api()
|
Api::~Api()
|
||||||
|
|
|
@ -50,7 +50,8 @@ namespace tools {
|
||||||
|
|
||||||
RelaySession::RelaySession(Relay *r, Identifier sid) :
|
RelaySession::RelaySession(Relay *r, Identifier sid) :
|
||||||
identifier(sid),
|
identifier(sid),
|
||||||
connects(0)
|
connects(0),
|
||||||
|
metadata(json_null())
|
||||||
{
|
{
|
||||||
auto loggerName = fmt::format("relay:{}", sid);
|
auto loggerName = fmt::format("relay:{}", sid);
|
||||||
logger = villas::logging.get(loggerName);
|
logger = villas::logging.get(loggerName);
|
||||||
|
@ -71,7 +72,7 @@ RelaySession::~RelaySession()
|
||||||
sessions.erase(identifier);
|
sessions.erase(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
RelaySession * RelaySession::get(Relay *r, lws *wsi)
|
RelaySession * RelaySession::getOrCreate(Relay *r, lws *wsi)
|
||||||
{
|
{
|
||||||
char uri[64];
|
char uri[64];
|
||||||
|
|
||||||
|
@ -85,19 +86,41 @@ RelaySession * RelaySession::get(Relay *r, lws *wsi)
|
||||||
if (strlen(uri) <= 1)
|
if (strlen(uri) <= 1)
|
||||||
throw InvalidUrlException();
|
throw InvalidUrlException();
|
||||||
|
|
||||||
Identifier sid = uri + 1;
|
std::string name_or_uuid = uri + 1;
|
||||||
|
|
||||||
auto it = sessions.find(sid);
|
auto s = lookup(name_or_uuid);
|
||||||
if (it == sessions.end()) {
|
if (!s) {
|
||||||
auto *rs = new RelaySession(r, sid);
|
s = new RelaySession(r, name_or_uuid);
|
||||||
if (!rs)
|
if (!s)
|
||||||
throw MemoryAllocationError();
|
throw MemoryAllocationError();
|
||||||
|
|
||||||
return rs;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
auto logger = logging.get("villas-relay");
|
auto logger = logging.get("villas-relay");
|
||||||
logger->info("Found existing session: {}", sid);
|
logger->info("Found existing session: {}", s->getIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
RelaySession * RelaySession::lookup(std::string &name_or_uuid)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
uuid_t uuid;
|
||||||
|
|
||||||
|
ret = uuid_parse(name_or_uuid.c_str(), uuid);
|
||||||
|
|
||||||
|
if (ret == 0) { // UUID
|
||||||
|
auto cmp = [uuid] (const std::pair<Identifier, RelaySession *> &p) { return uuid_compare(p.second->uuid, uuid) == 0; };
|
||||||
|
auto it = std::find_if(sessions.begin(), sessions.end(), cmp);
|
||||||
|
if (it == sessions.end())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
else { // Identifier
|
||||||
|
auto it = sessions.find(name_or_uuid);
|
||||||
|
if (it == sessions.end())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
@ -116,12 +139,13 @@ json_t * RelaySession::toJson() const
|
||||||
uuid_string_t uuid_str;
|
uuid_string_t uuid_str;
|
||||||
uuid_unparse_lower(uuid, uuid_str);
|
uuid_unparse_lower(uuid, uuid_str);
|
||||||
|
|
||||||
return json_pack("{ s: s, s: s, s: o, s: I, s: i }",
|
return json_pack("{ s: s, s: s, s: o, s: I, s: i, s: o }",
|
||||||
"identifier", identifier.c_str(),
|
"identifier", identifier.c_str(),
|
||||||
"uuid", uuid_str,
|
"uuid", uuid_str,
|
||||||
"connections", json_connections,
|
"connections", json_connections,
|
||||||
"created", created,
|
"created", created,
|
||||||
"connects", connects
|
"connects", connects,
|
||||||
|
"metadata", metadata
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +161,7 @@ RelayConnection::RelayConnection(Relay *r, lws *w, bool lo) :
|
||||||
frames_sent(0),
|
frames_sent(0),
|
||||||
loopback(lo)
|
loopback(lo)
|
||||||
{
|
{
|
||||||
session = RelaySession::get(r, wsi);
|
session = RelaySession::getOrCreate(r, wsi);
|
||||||
session->connections[wsi] = this;
|
session->connections[wsi] = this;
|
||||||
session->connects++;
|
session->connects++;
|
||||||
|
|
||||||
|
@ -217,6 +241,12 @@ void RelayConnection::read(void *in, size_t len)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HTTPRequest::HTTPRequest(struct lws *w) :
|
||||||
|
wsi(w)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Relay::Relay(int argc, char *argv[]) :
|
Relay::Relay(int argc, char *argv[]) :
|
||||||
Tool(argc, argv, "relay"),
|
Tool(argc, argv, "relay"),
|
||||||
stop(false),
|
stop(false),
|
||||||
|
@ -224,7 +254,9 @@ Relay::Relay(int argc, char *argv[]) :
|
||||||
vhost(nullptr),
|
vhost(nullptr),
|
||||||
loopback(false),
|
loopback(false),
|
||||||
port(8088),
|
port(8088),
|
||||||
protocol("live")
|
protocol("live"),
|
||||||
|
api(),
|
||||||
|
web(&api)
|
||||||
{
|
{
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
|
@ -255,7 +287,7 @@ Relay::Relay(int argc, char *argv[]) :
|
||||||
{
|
{
|
||||||
.name = "http-api",
|
.name = "http-api",
|
||||||
.callback = httpProtocolCallback,
|
.callback = httpProtocolCallback,
|
||||||
.per_session_data_size = 0,
|
.per_session_data_size = sizeof(HTTPRequest),
|
||||||
.rx_buffer_size = 1024
|
.rx_buffer_size = 1024
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -299,76 +331,6 @@ void Relay::loggerCallback(int level, const char *msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int Relay::httpProtocolCallback(lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
|
|
||||||
{
|
|
||||||
int ret;
|
|
||||||
size_t json_len;
|
|
||||||
json_t *json_sessions, *json_body;
|
|
||||||
|
|
||||||
lws_context *ctx = lws_get_context(wsi);
|
|
||||||
void *user_ctx = lws_context_user(ctx);
|
|
||||||
|
|
||||||
Relay *r = reinterpret_cast<Relay *>(user_ctx);
|
|
||||||
|
|
||||||
unsigned char buf[LWS_PRE + 2048], *start = &buf[LWS_PRE], *end = &buf[sizeof(buf) - LWS_PRE - 1], *p = start;
|
|
||||||
|
|
||||||
switch (reason) {
|
|
||||||
case LWS_CALLBACK_HTTP:
|
|
||||||
if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK,
|
|
||||||
"application/json",
|
|
||||||
LWS_ILLEGAL_HTTP_CONTENT_LEN, /* no content len */
|
|
||||||
&p, end))
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
if (lws_finalize_write_http_header(wsi, start, &p, end))
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
/* Write the body separately */
|
|
||||||
lws_callback_on_writable(wsi);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
case LWS_CALLBACK_HTTP_WRITEABLE:
|
|
||||||
|
|
||||||
json_sessions = json_array();
|
|
||||||
for (auto it : RelaySession::sessions) {
|
|
||||||
auto &session = it.second;
|
|
||||||
|
|
||||||
json_array_append(json_sessions, session->toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid_string_t uuid_str;
|
|
||||||
uuid_unparse(r->uuid, uuid_str);
|
|
||||||
|
|
||||||
json_body = json_pack("{ s: o, s: s, s: s, s: s, s: { s: b, s: i, s: s } }",
|
|
||||||
"sessions", json_sessions,
|
|
||||||
"version", PROJECT_VERSION_STR,
|
|
||||||
"hostname", r->hostname.c_str(),
|
|
||||||
"uuid", uuid_str,
|
|
||||||
"options",
|
|
||||||
"loopback", r->loopback,
|
|
||||||
"port", r->port,
|
|
||||||
"protocol", r->protocol.c_str()
|
|
||||||
);
|
|
||||||
|
|
||||||
json_len = json_dumpb(json_body, (char *) buf + LWS_PRE, sizeof(buf) - LWS_PRE, JSON_INDENT(4));
|
|
||||||
|
|
||||||
ret = lws_write(wsi, buf + LWS_PRE, json_len, LWS_WRITE_HTTP_FINAL);
|
|
||||||
if (ret < 0)
|
|
||||||
return ret;
|
|
||||||
|
|
||||||
r->logger->info("Handled API request");
|
|
||||||
|
|
||||||
//if (lws_http_transaction_completed(wsi))
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lws_callback_http_dummy(wsi, reason, user, in, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
int Relay::protocolCallback(lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
|
int Relay::protocolCallback(lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
|
||||||
{
|
{
|
||||||
lws_context *ctx = lws_get_context(wsi);
|
lws_context *ctx = lws_get_context(wsi);
|
||||||
|
@ -409,6 +371,34 @@ int Relay::protocolCallback(lws *wsi, enum lws_callback_reasons reason, void *us
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_t * Relay::toJson()
|
||||||
|
{
|
||||||
|
json_t *json_sessions, *json;
|
||||||
|
|
||||||
|
json_sessions = json_array();
|
||||||
|
for (auto it : RelaySession::sessions) {
|
||||||
|
auto &session = it.second;
|
||||||
|
|
||||||
|
json_array_append(json_sessions, session->toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid_string_t uuid_str;
|
||||||
|
uuid_unparse(uuid, uuid_str);
|
||||||
|
|
||||||
|
json = json_pack("{ s: o, s: s, s: s, s: s, s: { s: b, s: i, s: s } }",
|
||||||
|
"sessions", json_sessions,
|
||||||
|
"version", PROJECT_VERSION_STR,
|
||||||
|
"hostname", hostname.c_str(),
|
||||||
|
"uuid", uuid_str,
|
||||||
|
"options",
|
||||||
|
"loopback", loopback,
|
||||||
|
"port", port,
|
||||||
|
"protocol", protocol.c_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
void Relay::usage()
|
void Relay::usage()
|
||||||
{
|
{
|
||||||
std::cout << "Usage: villas-relay [OPTIONS]" << std::endl
|
std::cout << "Usage: villas-relay [OPTIONS]" << std::endl
|
||||||
|
@ -507,8 +497,14 @@ int Relay::main() {
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.start();
|
||||||
|
web.start();
|
||||||
|
|
||||||
while (!stop)
|
while (!stop)
|
||||||
lws_service(context, 100);
|
sleep(1);
|
||||||
|
|
||||||
|
api.stop();
|
||||||
|
web.stop();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -531,7 +527,7 @@ const std::vector<lws_extension> Relay::extensions = {
|
||||||
|
|
||||||
const lws_http_mount Relay::mount = {
|
const lws_http_mount Relay::mount = {
|
||||||
.mount_next = nullptr, /* linked-list "next" */
|
.mount_next = nullptr, /* linked-list "next" */
|
||||||
.mountpoint = "/api/v1", /* mountpoint URL */
|
.mountpoint = "/api/v2", /* mountpoint URL */
|
||||||
.origin = nullptr, /* protocol */
|
.origin = nullptr, /* protocol */
|
||||||
.def = nullptr,
|
.def = nullptr,
|
||||||
.protocol = "http-api",
|
.protocol = "http-api",
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
#include <uuid/uuid.h>
|
#include <uuid/uuid.h>
|
||||||
|
|
||||||
#include <libwebsockets.h>
|
#include <libwebsockets.h>
|
||||||
|
#include <villas/api.hpp>
|
||||||
#include <villas/log.hpp>
|
#include <villas/log.hpp>
|
||||||
|
|
||||||
namespace villas {
|
namespace villas {
|
||||||
|
@ -75,16 +75,25 @@ protected:
|
||||||
|
|
||||||
int connects;
|
int connects;
|
||||||
|
|
||||||
|
json_t *metadata;
|
||||||
|
|
||||||
static std::map<std::string, RelaySession *> sessions;
|
static std::map<std::string, RelaySession *> sessions;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static RelaySession * get(Relay *r, lws *wsi);
|
static RelaySession * getOrCreate(Relay *r, lws *wsi);
|
||||||
|
|
||||||
|
static RelaySession * lookup(std::string &name_or_uuid);
|
||||||
|
|
||||||
RelaySession(Relay *r, Identifier sid);
|
RelaySession(Relay *r, Identifier sid);
|
||||||
|
|
||||||
~RelaySession();
|
~RelaySession();
|
||||||
|
|
||||||
json_t * toJson() const;
|
json_t * toJson() const;
|
||||||
|
|
||||||
|
Identifier getIdentifier() const
|
||||||
|
{
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class RelayConnection {
|
class RelayConnection {
|
||||||
|
@ -120,6 +129,10 @@ public:
|
||||||
void read(void *in, size_t len);
|
void read(void *in, size_t len);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class RelayRequestFactory : public {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
class Relay : public Tool {
|
class Relay : public Tool {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -143,6 +156,9 @@ protected:
|
||||||
|
|
||||||
uuid_t uuid;
|
uuid_t uuid;
|
||||||
|
|
||||||
|
Web web;
|
||||||
|
Api<RelayRequestFactory> api;
|
||||||
|
|
||||||
/** List of libwebsockets protocols. */
|
/** List of libwebsockets protocols. */
|
||||||
std::vector<lws_protocols> protocols;
|
std::vector<lws_protocols> protocols;
|
||||||
|
|
||||||
|
@ -163,6 +179,8 @@ protected:
|
||||||
|
|
||||||
int main();
|
int main();
|
||||||
|
|
||||||
|
json_t * toJson();
|
||||||
|
|
||||||
void handler(int signal, siginfo_t *sinfo, void *ctx)
|
void handler(int signal, siginfo_t *sinfo, void *ctx)
|
||||||
{
|
{
|
||||||
stop = true;
|
stop = true;
|
||||||
|
|
Loading…
Add table
Reference in a new issue