Aktualisiere WebSocket-Server und Daemon-Konfiguration

- Ändere die Pfade für SSL-Zertifikate in der Konfigurationsdatei.
- Verbessere die Fehlerbehandlung beim Entfernen alter vorbereiteter Anweisungen in HouseWorker.
- Füge Debug-Ausgaben zur Nachverfolgung von Verbindungen und Nachrichten im WebSocket-Server hinzu.
- Implementiere Timeout-Logik für das Stoppen von Worker- und Watchdog-Threads.
- Optimiere die Signalverarbeitung und Shutdown-Logik in main.cpp für bessere Responsivität.
This commit is contained in:
Torsten Schulz (local)
2025-10-28 08:06:45 +01:00
committed by Torsten (PC)
parent 8fe816dddc
commit bd961a03d4
11 changed files with 364 additions and 41 deletions

View File

@@ -53,10 +53,23 @@ void HouseWorker::performHouseStateChange() {
try {
ConnectionGuard connGuard(pool);
auto &db = connGuard.get();
db.remove("QUERY_UPDATE_BUYABLE_HOUSE_STATE");
db.remove("QUERY_UPDATE_USER_HOUSE_STATE");
// Entferne alte vorbereitete Anweisungen falls sie existieren
try {
db.remove("QUERY_UPDATE_BUYABLE_HOUSE_STATE");
} catch (...) {
// Ignoriere Fehler beim Entfernen
}
try {
db.remove("QUERY_UPDATE_USER_HOUSE_STATE");
} catch (...) {
// Ignoriere Fehler beim Entfernen
}
// Bereite neue Anweisungen vor
db.prepare("QUERY_UPDATE_BUYABLE_HOUSE_STATE", QUERY_UPDATE_BUYABLE_HOUSE_STATE);
db.prepare("QUERY_UPDATE_USER_HOUSE_STATE", QUERY_UPDATE_USER_HOUSE_STATE);
// Führe die Anweisungen aus
db.execute("QUERY_UPDATE_BUYABLE_HOUSE_STATE");
db.execute("QUERY_UPDATE_USER_HOUSE_STATE");
} catch(const std::exception &e) {

View File

@@ -17,6 +17,7 @@
#include <thread>
#include <vector>
#include <memory>
#include <cstdlib>
#include <systemd/sd-daemon.h>
std::atomic<bool> keepRunning(true);
@@ -26,8 +27,10 @@ void handleSignal(int signal) {
std::cerr << "Signal erhalten: " << signal << ". Beende Anwendung..." << std::endl;
if (signal == SIGINT || signal == SIGTERM) {
std::cerr << "Setze Shutdown-Flags..." << std::endl;
keepRunning.store(false, std::memory_order_relaxed);
shutdownRequested.store(true, std::memory_order_relaxed);
std::cerr << "Shutdown-Flags gesetzt. keepRunning=" << keepRunning.load() << ", shutdownRequested=" << shutdownRequested.load() << std::endl;
}
}
@@ -36,7 +39,7 @@ int main() {
std::signal(SIGTERM, handleSignal);
try {
Config config("/etc/yourpart/daemon.conf");
Config config("../daemon.conf");
ConnectionPool pool(
config.get("DB_HOST"),
config.get("DB_PORT"),
@@ -78,24 +81,52 @@ int main() {
sd_notify(0, "READY=1");
// Hauptschleife mit besserer Signal-Behandlung
while (keepRunning && !shutdownRequested) {
std::cerr << "Hauptschleife gestartet. keepRunning=" << keepRunning.load() << ", shutdownRequested=" << shutdownRequested.load() << std::endl;
while (keepRunning.load() && !shutdownRequested.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cerr << "Hauptschleife beendet. keepRunning=" << keepRunning.load() << ", shutdownRequested=" << shutdownRequested.load() << std::endl;
std::cerr << "Starte sauberes Herunterfahren..." << std::endl;
// Stoppe alle Worker
auto shutdownStart = std::chrono::steady_clock::now();
const auto maxShutdownTime = std::chrono::seconds(10);
// Stoppe alle Worker-Threads
std::cerr << "Stoppe Worker-Threads..." << std::endl;
for (auto &worker : workers) {
worker->stopWorkerThread();
if (std::chrono::steady_clock::now() - shutdownStart > maxShutdownTime) {
std::cerr << "Shutdown-Timeout erreicht, erzwinge Beendigung..." << std::endl;
break;
}
}
// Stoppe Watchdog-Threads
std::cerr << "Stoppe Watchdog-Threads..." << std::endl;
for (auto &worker : workers) {
worker->stopWatchdogThread();
if (std::chrono::steady_clock::now() - shutdownStart > maxShutdownTime) {
std::cerr << "Shutdown-Timeout erreicht, erzwinge Beendigung..." << std::endl;
break;
}
}
// Stoppe WebSocket Server
std::cerr << "Stoppe WebSocket-Server..." << std::endl;
websocketServer.stop();
// Stoppe Message Broker
std::cerr << "Stoppe Message Broker..." << std::endl;
broker.stop();
std::cerr << "Anwendung erfolgreich beendet." << std::endl;
auto shutdownDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - shutdownStart);
std::cerr << "Anwendung erfolgreich beendet in " << shutdownDuration.count() << "ms." << std::endl;
// Erzwinge sofortiges Exit nach Shutdown
std::cerr << "Erzwinge Prozess-Beendigung..." << std::endl;
std::_Exit(0);
} catch (const std::exception &e) {
std::cerr << "Fehler: " << e.what() << std::endl;
return 1;

View File

@@ -3,6 +3,7 @@
void MessageBroker::publish(const std::string &message) {
std::lock_guard<std::mutex> lock(mutex);
std::cout << "[MessageBroker] Nachricht gepubliziert: " << message << std::endl;
messageQueue.push(message);
cv.notify_all();
}
@@ -34,6 +35,7 @@ void MessageBroker::processMessages() {
std::string message = std::move(messageQueue.front());
messageQueue.pop();
lock.unlock();
std::cout << "[MessageBroker] Sende Nachricht an " << subscribers.size() << " Subscribers: " << message << std::endl;
for (const auto &callback : subscribers) {
callback(message);
}

View File

@@ -13,20 +13,25 @@ ProduceWorker::~ProduceWorker() {
void ProduceWorker::run() {
auto lastIterationTime = std::chrono::steady_clock::now();
while (runningWorker) {
while (runningWorker.load()) {
setCurrentStep("Check runningWorker Variable");
{
std::lock_guard<std::mutex> lock(activityMutex);
if (!runningWorker) {
break;
}
if (!runningWorker.load()) {
break;
}
setCurrentStep("Calculate elapsed time");
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastIterationTime);
if (elapsed < std::chrono::milliseconds(200)) {
std::this_thread::sleep_for(std::chrono::milliseconds(200) - elapsed);
// Kürzere Sleep-Intervalle für bessere Shutdown-Responsivität
auto sleepTime = std::chrono::milliseconds(200) - elapsed;
for (int i = 0; i < sleepTime.count() && runningWorker.load(); i += 10) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
if (!runningWorker.load()) break;
lastIterationTime = std::chrono::steady_clock::now();
setCurrentStep("Process Productions");
processProductions();

View File

@@ -4,11 +4,18 @@
#include <iostream>
#include <chrono>
#include <cstring>
#include <future>
using json = nlohmann::json;
// Protocols array definition
struct lws_protocols WebSocketServer::protocols[] = {
{
"", // Leeres Protokoll für Standard-WebSocket-Verbindungen
WebSocketServer::wsCallback,
sizeof(WebSocketUserData),
4096
},
{
"yourpart-protocol",
WebSocketServer::wsCallback,
@@ -44,14 +51,40 @@ void WebSocketServer::run() {
serverThread = std::thread([this](){ startServer(); });
messageThread = std::thread([this](){ processMessageQueue(); });
pingThread = std::thread([this](){ pingClients(); });
// Warte kurz bis alle Threads gestartet sind
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
void WebSocketServer::stop() {
running = false;
if (context) lws_cancel_service(context);
if (serverThread.joinable()) serverThread.join();
if (messageThread.joinable()) messageThread.join();
if (pingThread.joinable()) pingThread.join();
// Stoppe Threads mit Timeout
std::vector<std::future<void>> futures;
if (serverThread.joinable()) {
futures.push_back(std::async(std::launch::async, [this]() { serverThread.join(); }));
}
if (messageThread.joinable()) {
futures.push_back(std::async(std::launch::async, [this]() { messageThread.join(); }));
}
if (pingThread.joinable()) {
futures.push_back(std::async(std::launch::async, [this]() { pingThread.join(); }));
}
// Warte auf alle Threads mit Timeout
for (auto& future : futures) {
if (future.wait_for(std::chrono::milliseconds(1000)) == std::future_status::timeout) {
std::cerr << "WebSocket-Thread beendet sich nicht, erzwinge Beendigung..." << std::endl;
}
}
// Force detach alle Threads
if (serverThread.joinable()) serverThread.detach();
if (messageThread.joinable()) messageThread.detach();
if (pingThread.joinable()) pingThread.detach();
if (context) {
lws_context_destroy(context);
context = nullptr;
@@ -64,12 +97,15 @@ void WebSocketServer::startServer() {
info.port = port;
info.protocols = protocols;
// Vereinfachte Server-Optionen für bessere Kompatibilität
info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
// SSL/TLS Konfiguration
if (useSSL) {
if (certPath.empty() || keyPath.empty()) {
throw std::runtime_error("SSL enabled but certificate or key path not provided");
}
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.ssl_cert_filepath = certPath.c_str();
info.ssl_private_key_filepath = keyPath.c_str();
std::cout << "WebSocket SSL Server starting on port " << port << " with certificates: "
@@ -78,23 +114,36 @@ void WebSocketServer::startServer() {
std::cout << "WebSocket Server starting on port " << port << " (no SSL)" << std::endl;
}
// Reduziere Log-Level um weniger Debug-Ausgaben zu haben
setenv("LWS_LOG_LEVEL", "0", 1); // 0 = nur Fehler
// Erhöhe Log-Level für besseres Debugging
setenv("LWS_LOG_LEVEL", "7", 1); // 7 = alle Logs
context = lws_create_context(&info);
if (!context) {
throw std::runtime_error("Failed to create LWS context");
}
std::cout << "WebSocket-Server erfolgreich gestartet auf Port " << port << std::endl;
while (running) {
lws_service(context, 50);
int ret = lws_service(context, 50);
if (ret < 0) {
std::cerr << "WebSocket-Server Fehler: lws_service returned " << ret << std::endl;
break;
}
// Kurze Pause für bessere Shutdown-Responsivität
if (running) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
std::cout << "WebSocket-Server wird beendet..." << std::endl;
}
void WebSocketServer::processMessageQueue() {
while (running) {
std::unique_lock<std::mutex> lock(queueMutex);
queueCV.wait(lock, [this](){ return !messageQueue.empty() || !running; });
while (!messageQueue.empty()) {
queueCV.wait_for(lock, std::chrono::milliseconds(100), [this](){ return !messageQueue.empty() || !running; });
while (!messageQueue.empty() && running) {
std::string msg = std::move(messageQueue.front());
messageQueue.pop();
lock.unlock();
@@ -106,8 +155,52 @@ void WebSocketServer::processMessageQueue() {
void WebSocketServer::pingClients() {
while (running) {
std::this_thread::sleep_for(std::chrono::seconds(30));
lws_callback_on_writable_all_protocol(context, &protocols[0]);
// Kürzere Sleep-Intervalle für bessere Shutdown-Responsivität
for (int i = 0; i < WebSocketUserData::PING_INTERVAL_SECONDS * 10 && running; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!running || !context) continue;
auto now = std::chrono::steady_clock::now();
std::vector<struct lws*> toDisconnect;
// Prüfe alle Verbindungen auf Timeouts
{
std::shared_lock<std::shared_mutex> lock(connectionsMutex);
for (auto& pair : connections) {
auto* ud = reinterpret_cast<WebSocketUserData*>(lws_wsi_user(pair.second));
if (!ud) continue;
// Prüfe ob Pong-Timeout erreicht wurde
auto timeSincePing = std::chrono::duration_cast<std::chrono::seconds>(now - ud->lastPingTime).count();
auto timeSincePong = std::chrono::duration_cast<std::chrono::seconds>(now - ud->lastPongTime).count();
if (!ud->pongReceived && timeSincePing > WebSocketUserData::PONG_TIMEOUT_SECONDS) {
ud->pingTimeoutCount++;
std::cout << "Ping-Timeout für User " << ud->userId << " (Versuch " << ud->pingTimeoutCount << "/" << WebSocketUserData::MAX_PING_TIMEOUTS << ")" << std::endl;
if (ud->pingTimeoutCount >= WebSocketUserData::MAX_PING_TIMEOUTS) {
std::cout << "Verbindung wird getrennt: Zu viele Ping-Timeouts für User " << ud->userId << std::endl;
toDisconnect.push_back(pair.second);
} else {
// Reset für nächsten Versuch
ud->pongReceived = true;
ud->lastPongTime = now;
}
}
}
}
// Trenne problematische Verbindungen
for (auto* wsi : toDisconnect) {
lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION, (unsigned char*)"Ping timeout", 12);
}
// Sende Pings an alle aktiven Verbindungen
if (running) {
lws_callback_on_writable_all_protocol(context, &protocols[0]);
}
}
}
@@ -118,27 +211,51 @@ int WebSocketServer::wsCallback(struct lws *wsi,
auto *ud = reinterpret_cast<WebSocketUserData*>(user);
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
case LWS_CALLBACK_ESTABLISHED: {
ud->pongReceived = true;
std::cout << "WebSocket-Verbindung hergestellt" << std::endl;
ud->lastPingTime = std::chrono::steady_clock::now();
ud->lastPongTime = std::chrono::steady_clock::now();
ud->pingTimeoutCount = 0;
const char* protocolName = lws_get_protocol(wsi)->name;
std::cout << "WebSocket-Verbindung hergestellt (Protokoll: " << (protocolName ? protocolName : "Standard") << ")" << std::endl;
char client_addr[128];
lws_get_peer_simple(wsi, client_addr, sizeof(client_addr));
std::cout << "Client-Adresse: " << client_addr << std::endl;
break;
}
case LWS_CALLBACK_RECEIVE: {
std::string msg(reinterpret_cast<char*>(in), len);
std::cout << "WebSocket-Nachricht empfangen: " << msg << std::endl;
// Pong-Antwort behandeln
if (msg == "pong") {
ud->pongReceived = true;
ud->lastPongTime = std::chrono::steady_clock::now();
ud->pingTimeoutCount = 0;
std::cout << "Pong von Client empfangen" << std::endl;
break;
}
try {
json parsed = json::parse(msg);
std::cout << "[RECEIVE] Nachricht empfangen: " << msg << std::endl;
if (parsed.contains("event") && parsed["event"] == "setUserId") {
if (parsed.contains("data") && parsed["data"].contains("userId")) {
ud->userId = parsed["data"]["userId"].get<std::string>();
std::cout << "User-ID gesetzt: " << ud->userId << std::endl;
std::cout << "[RECEIVE] User-ID gesetzt: " << ud->userId << std::endl;
// Verbindung in der Map speichern
instance->addConnection(ud->userId, wsi);
std::cout << "[RECEIVE] Verbindung gespeichert" << std::endl;
} else {
std::cerr << "[RECEIVE] setUserId-Event ohne data.userId-Feld" << std::endl;
}
} else {
std::cout << "[RECEIVE] Ignoriere Nachricht (kein setUserId-Event)" << std::endl;
}
} catch (const std::exception &e) {
std::cerr << "Fehler beim Parsen der WebSocket-Nachricht: " << e.what() << std::endl;
std::cerr << "[RECEIVE] Fehler beim Parsen der WebSocket-Nachricht: " << e.what() << std::endl;
}
break;
}
@@ -146,15 +263,20 @@ int WebSocketServer::wsCallback(struct lws *wsi,
// Prüfe ob es eine Nachricht zum Senden gibt
if (ud->pendingMessage.empty()) {
// Ping senden
ud->lastPingTime = std::chrono::steady_clock::now();
ud->pongReceived = false;
unsigned char buf[LWS_PRE + 4];
memcpy(buf + LWS_PRE, "ping", 4);
lws_write(wsi, buf + LWS_PRE, 4, LWS_WRITE_TEXT);
// std::cout << "Ping an Client gesendet" << std::endl;
} else {
// Nachricht senden
std::cout << "[WRITEABLE] Sende Nachricht: " << ud->pendingMessage << std::endl;
unsigned char buf[LWS_PRE + ud->pendingMessage.length()];
memcpy(buf + LWS_PRE, ud->pendingMessage.c_str(), ud->pendingMessage.length());
lws_write(wsi, buf + LWS_PRE, ud->pendingMessage.length(), LWS_WRITE_TEXT);
ud->pendingMessage.clear();
std::cout << "[WRITEABLE] Nachricht erfolgreich gesendet" << std::endl;
}
break;
}
@@ -162,8 +284,18 @@ int WebSocketServer::wsCallback(struct lws *wsi,
// Verbindung aus der Map entfernen
if (!ud->userId.empty()) {
instance->removeConnection(ud->userId);
std::cout << "WebSocket-Verbindung geschlossen für User: " << ud->userId << std::endl;
}
break;
case LWS_CALLBACK_HTTP:
// HTTP-Anfragen ablehnen (nur WebSocket erlaubt)
return -1;
case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
// Protokoll-Filter für bessere Kompatibilität
return 0;
case LWS_CALLBACK_RAW_CONNECTED:
// Raw-Verbindungen behandeln
return 0;
default:
break;
}
@@ -172,30 +304,47 @@ int WebSocketServer::wsCallback(struct lws *wsi,
void WebSocketServer::handleBrokerMessage(const std::string &message) {
try {
std::cout << "[handleBrokerMessage] Nachricht empfangen: " << message << std::endl;
json parsed = json::parse(message);
if (parsed.contains("user_id")) {
int fid = parsed["user_id"].get<int>();
int fid;
if (parsed["user_id"].is_string()) {
fid = std::stoi(parsed["user_id"].get<std::string>());
} else {
fid = parsed["user_id"].get<int>();
}
auto userId = getUserIdFromFalukantUserId(fid);
std::cout << "Broker-Nachricht für Falukant-User " << fid << " -> User-ID " << userId << std::endl;
std::cout << "[handleBrokerMessage] Broker-Nachricht für Falukant-User " << fid << " -> User-ID " << userId << std::endl;
std::shared_lock<std::shared_mutex> lock(connectionsMutex);
std::cout << "[handleBrokerMessage] Aktive Verbindungen: " << connections.size() << std::endl;
auto it = connections.find(userId);
if (it != connections.end()) {
std::cout << "Sende Nachricht an User " << userId << ": " << message << std::endl;
std::cout << "[handleBrokerMessage] Sende Nachricht an User " << userId << ": " << message << std::endl;
// Nachricht in der UserData speichern
auto *ud = reinterpret_cast<WebSocketUserData*>(lws_wsi_user(it->second));
if (ud) {
ud->pendingMessage = message;
std::cout << "[handleBrokerMessage] Nachricht in pendingMessage gespeichert" << std::endl;
} else {
std::cerr << "[handleBrokerMessage] FEHLER: ud ist nullptr!" << std::endl;
}
lws_callback_on_writable(it->second);
} else {
std::cout << "Keine aktive Verbindung für User " << userId << " gefunden" << std::endl;
std::cout << "[handleBrokerMessage] Keine aktive Verbindung für User " << userId << " gefunden" << std::endl;
std::cout << "[handleBrokerMessage] Verfügbare User-IDs in connections:" << std::endl;
for (const auto& pair : connections) {
std::cout << " - " << pair.first << std::endl;
}
}
} else {
std::cout << "[handleBrokerMessage] Nachricht enthält kein user_id-Feld!" << std::endl;
}
} catch (const std::exception &e) {
std::cerr << "Error processing broker message: " << e.what() << std::endl;
std::cerr << "[handleBrokerMessage] Error processing broker message: " << e.what() << std::endl;
}
}
@@ -224,11 +373,11 @@ void WebSocketServer::setWorkers(const std::vector<std::unique_ptr<Worker>> &wor
void WebSocketServer::addConnection(const std::string &userId, struct lws *wsi) {
std::unique_lock<std::shared_mutex> lock(connectionsMutex);
connections[userId] = wsi;
std::cout << "Verbindung für User " << userId << " gespeichert" << std::endl;
std::cout << "[addConnection] Verbindung für User " << userId << " gespeichert (Insgesamt: " << connections.size() << " Verbindungen)" << std::endl;
}
void WebSocketServer::removeConnection(const std::string &userId) {
std::unique_lock<std::shared_mutex> lock(connectionsMutex);
connections.erase(userId);
std::cout << "Verbindung für User " << userId << " entfernt" << std::endl;
std::cout << "[removeConnection] Verbindung für User " << userId << " entfernt (Insgesamt: " << connections.size() << " Verbindungen)" << std::endl;
}

View File

@@ -15,11 +15,18 @@
#include <unordered_map>
#include <vector>
#include <memory>
#include <chrono>
struct WebSocketUserData {
std::string userId;
bool pongReceived = true;
std::string pendingMessage;
std::chrono::steady_clock::time_point lastPingTime;
std::chrono::steady_clock::time_point lastPongTime;
int pingTimeoutCount = 0;
static constexpr int MAX_PING_TIMEOUTS = 3;
static constexpr int PING_INTERVAL_SECONDS = 30;
static constexpr int PONG_TIMEOUT_SECONDS = 10;
};
class Worker; // forward

View File

@@ -5,6 +5,7 @@
#include <mutex>
#include <chrono>
#include <iostream>
#include <future>
#include <nlohmann/json.hpp>
#include "connection_pool.h"
@@ -39,7 +40,16 @@ public:
void stopWorkerThread() {
runningWorker.store(false);
if (workerThread.joinable()) {
workerThread.join();
// Timeout für Thread-Beendigung
auto future = std::async(std::launch::async, [this]() {
workerThread.join();
});
if (future.wait_for(std::chrono::milliseconds(500)) == std::future_status::timeout) {
std::cerr << "[" << workerName << "] Worker-Thread beendet sich nicht, erzwinge Beendigung..." << std::endl;
// Thread wird beim Destruktor automatisch detached
workerThread.detach();
}
}
}
@@ -55,7 +65,15 @@ public:
void stopWatchdogThread() {
runningWatchdog.store(false);
if (watchdogThread.joinable()) {
watchdogThread.join();
// Timeout für Watchdog-Thread-Beendigung
auto future = std::async(std::launch::async, [this]() {
watchdogThread.join();
});
if (future.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout) {
std::cerr << "[" << workerName << "] Watchdog-Thread beendet sich nicht, erzwinge Beendigung..." << std::endl;
watchdogThread.detach();
}
}
}
@@ -75,7 +93,13 @@ protected:
void watchdog() {
try {
while (runningWatchdog.load()) {
std::this_thread::sleep_for(watchdogInterval);
// Kürzere Sleep-Intervalle für bessere Shutdown-Responsivität
for (int i = 0; i < 10 && runningWatchdog.load(); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!runningWatchdog.load()) break;
bool isActive = false;
{
std::lock_guard<std::mutex> lock(activityMutex);
@@ -86,7 +110,9 @@ protected:
std::cerr << "[" << workerName << "] Watchdog: Keine Aktivität! Starte Worker neu...\n";
std::cerr << "[" << workerName << "] Letzte Aktivität: " << getCurrentStep() << "\n";
stopWorkerThread();
startWorkerThread();
if (runningWatchdog.load()) { // Nur neu starten wenn nicht shutdown
startWorkerThread();
}
}
}
} catch (const std::exception &e) {