2023-11-18 14:50:43 +01:00
|
|
|
#include <hex/api/task_manager.hpp>
|
2021-12-16 23:48:52 +01:00
|
|
|
|
2023-11-21 14:38:01 +01:00
|
|
|
#include <hex/api/localization_manager.hpp>
|
2022-09-03 23:56:57 +02:00
|
|
|
#include <hex/helpers/logger.hpp>
|
2021-12-16 23:48:52 +01:00
|
|
|
|
2022-07-29 18:49:43 +02:00
|
|
|
#include <algorithm>
|
|
|
|
|
2023-12-22 23:39:38 +01:00
|
|
|
#include <jthread.hpp>
|
|
|
|
|
2023-06-21 09:29:57 +02:00
|
|
|
#if defined(OS_WINDOWS)
|
|
|
|
#include <windows.h>
|
|
|
|
#include <processthreadsapi.h>
|
|
|
|
#else
|
|
|
|
#include <pthread.h>
|
|
|
|
#endif
|
|
|
|
|
2021-12-16 23:48:52 +01:00
|
|
|
namespace hex {
|
|
|
|
|
2023-07-26 13:50:51 +02:00
|
|
|
namespace {
|
2022-08-17 16:15:36 +02:00
|
|
|
|
2023-07-26 13:50:51 +02:00
|
|
|
std::mutex s_deferredCallsMutex, s_tasksFinishedMutex;
|
|
|
|
|
|
|
|
std::list<std::shared_ptr<Task>> s_tasks, s_taskQueue;
|
|
|
|
std::list<std::function<void()>> s_deferredCalls;
|
|
|
|
std::list<std::function<void()>> s_tasksFinishedCallbacks;
|
|
|
|
|
|
|
|
std::mutex s_queueMutex;
|
|
|
|
std::condition_variable s_jobCondVar;
|
|
|
|
std::vector<std::jthread> s_workers;
|
|
|
|
|
|
|
|
}
|
2022-08-17 16:15:36 +02:00
|
|
|
|
|
|
|
|
2023-06-21 09:29:57 +02:00
|
|
|
static void setThreadName(const std::string &name) {
|
|
|
|
#if defined(OS_WINDOWS)
|
|
|
|
typedef struct tagTHREADNAME_INFO {
|
|
|
|
DWORD dwType;
|
|
|
|
LPCSTR szName;
|
|
|
|
DWORD dwThreadID;
|
|
|
|
DWORD dwFlags;
|
|
|
|
} THREADNAME_INFO;
|
|
|
|
|
|
|
|
THREADNAME_INFO info;
|
|
|
|
info.dwType = 0x1000;
|
|
|
|
info.szName = name.c_str();
|
|
|
|
info.dwThreadID = ::GetCurrentThreadId();
|
|
|
|
info.dwFlags = 0;
|
|
|
|
|
2023-11-10 20:47:08 +01:00
|
|
|
constexpr static DWORD MS_VC_EXCEPTION = 0x406D1388;
|
|
|
|
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), reinterpret_cast<ULONG_PTR*>(&info));
|
2023-06-21 09:29:57 +02:00
|
|
|
#elif defined(OS_LINUX)
|
|
|
|
pthread_setname_np(pthread_self(), name.c_str());
|
2023-10-04 12:00:32 +02:00
|
|
|
#elif defined(OS_WEB)
|
|
|
|
hex::unused(name);
|
2023-06-21 09:29:57 +02:00
|
|
|
#elif defined(OS_MACOS)
|
|
|
|
pthread_setname_np(name.c_str());
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2023-12-19 12:22:28 +01:00
|
|
|
Task::Task(UnlocalizedString unlocalizedName, u64 maxValue, bool background, std::function<void(Task &)> function)
|
2023-11-10 20:47:08 +01:00
|
|
|
: m_unlocalizedName(std::move(unlocalizedName)), m_maxValue(maxValue), m_function(std::move(function)), m_background(background) { }
|
2022-08-17 16:15:36 +02:00
|
|
|
|
|
|
|
Task::Task(hex::Task &&other) noexcept {
|
2022-10-04 23:37:48 +02:00
|
|
|
{
|
2023-12-19 13:10:25 +01:00
|
|
|
std::scoped_lock thisLock(m_mutex);
|
2022-10-04 23:37:48 +02:00
|
|
|
std::scoped_lock otherLock(other.m_mutex);
|
2022-02-01 18:09:40 +01:00
|
|
|
|
2023-12-19 13:10:25 +01:00
|
|
|
m_function = std::move(other.m_function);
|
|
|
|
m_unlocalizedName = std::move(other.m_unlocalizedName);
|
2022-10-04 23:37:48 +02:00
|
|
|
}
|
2022-02-01 18:09:40 +01:00
|
|
|
|
2023-12-19 13:10:25 +01:00
|
|
|
m_maxValue = u64(other.m_maxValue);
|
|
|
|
m_currValue = u64(other.m_currValue);
|
2022-08-17 16:15:36 +02:00
|
|
|
|
2023-12-19 13:10:25 +01:00
|
|
|
m_finished = bool(other.m_finished);
|
|
|
|
m_hadException = bool(other.m_hadException);
|
|
|
|
m_interrupted = bool(other.m_interrupted);
|
|
|
|
m_shouldInterrupt = bool(other.m_shouldInterrupt);
|
2021-12-16 23:48:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Task::~Task() {
|
2022-09-14 13:51:48 +02:00
|
|
|
if (!this->isFinished())
|
|
|
|
this->interrupt();
|
2021-12-16 23:48:52 +01:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
void Task::update(u64 value) {
|
2023-12-06 11:04:35 +01:00
|
|
|
// Update the current progress value of the task
|
2023-12-19 13:10:25 +01:00
|
|
|
m_currValue.store(value, std::memory_order_relaxed);
|
2022-07-29 18:49:43 +02:00
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Check if the task has been interrupted by the main thread and if yes,
|
|
|
|
// throw an exception that is generally not caught by the task
|
2023-12-19 13:10:25 +01:00
|
|
|
if (m_shouldInterrupt.load(std::memory_order_relaxed)) [[unlikely]]
|
2022-08-17 16:15:36 +02:00
|
|
|
throw TaskInterruptor();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Task::setMaxValue(u64 value) {
|
2023-12-19 13:10:25 +01:00
|
|
|
m_maxValue = value;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Task::interrupt() {
|
2023-12-19 13:10:25 +01:00
|
|
|
m_shouldInterrupt = true;
|
2022-09-13 16:05:41 +02:00
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Call the interrupt callback on the current thread if one is set
|
2023-12-19 13:10:25 +01:00
|
|
|
if (m_interruptCallback)
|
|
|
|
m_interruptCallback();
|
2022-09-13 16:05:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void Task::setInterruptCallback(std::function<void()> callback) {
|
2023-12-19 13:10:25 +01:00
|
|
|
m_interruptCallback = std::move(callback);
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
2022-10-04 23:37:48 +02:00
|
|
|
bool Task::isBackgroundTask() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_background;
|
2022-09-19 16:09:22 +02:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
bool Task::isFinished() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_finished;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Task::hadException() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_hadException;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
2022-10-28 14:32:28 +02:00
|
|
|
bool Task::shouldInterrupt() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_shouldInterrupt;
|
2022-10-28 14:32:28 +02:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
bool Task::wasInterrupted() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_interrupted;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void Task::clearException() {
|
2023-12-19 13:10:25 +01:00
|
|
|
m_hadException = false;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
2022-09-03 23:56:57 +02:00
|
|
|
std::string Task::getExceptionMessage() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
std::scoped_lock lock(m_mutex);
|
2022-09-03 23:56:57 +02:00
|
|
|
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_exceptionMessage;
|
2022-09-03 23:56:57 +02:00
|
|
|
}
|
|
|
|
|
2023-12-19 12:22:28 +01:00
|
|
|
const UnlocalizedString &Task::getUnlocalizedName() {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_unlocalizedName;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
u64 Task::getValue() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_currValue;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
u64 Task::getMaxValue() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
return m_maxValue;
|
2022-07-29 18:49:43 +02:00
|
|
|
}
|
|
|
|
|
2021-12-16 23:48:52 +01:00
|
|
|
void Task::finish() {
|
2023-12-19 13:10:25 +01:00
|
|
|
m_finished = true;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void Task::interruption() {
|
2023-12-19 13:10:25 +01:00
|
|
|
m_interrupted = true;
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
2022-09-03 23:56:57 +02:00
|
|
|
void Task::exception(const char *message) {
|
2023-12-19 13:10:25 +01:00
|
|
|
std::scoped_lock lock(m_mutex);
|
2022-02-01 18:09:40 +01:00
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Store information about the caught exception
|
2023-12-19 13:10:25 +01:00
|
|
|
m_exceptionMessage = message;
|
|
|
|
m_hadException = true;
|
2021-12-16 23:48:52 +01:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
|
|
|
|
bool TaskHolder::isRunning() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
auto task = m_task.lock();
|
2023-12-06 11:04:35 +01:00
|
|
|
if (!task)
|
2022-10-05 12:02:40 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
return !task->isFinished();
|
2022-01-09 21:27:59 +01:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
bool TaskHolder::hadException() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
auto task = m_task.lock();
|
2023-12-06 11:04:35 +01:00
|
|
|
if (!task)
|
2022-10-05 12:02:40 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
return !task->hadException();
|
2021-12-16 23:48:52 +01:00
|
|
|
}
|
|
|
|
|
2022-10-28 14:32:28 +02:00
|
|
|
bool TaskHolder::shouldInterrupt() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
auto task = m_task.lock();
|
2023-12-06 11:04:35 +01:00
|
|
|
if (!task)
|
2022-10-28 14:32:28 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
return !task->shouldInterrupt();
|
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
bool TaskHolder::wasInterrupted() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
auto task = m_task.lock();
|
2023-12-06 11:04:35 +01:00
|
|
|
if (!task)
|
2022-10-05 12:02:40 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
return !task->wasInterrupted();
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
2022-01-09 21:27:59 +01:00
|
|
|
|
2023-11-10 20:47:08 +01:00
|
|
|
void TaskHolder::interrupt() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
auto task = m_task.lock();
|
2023-12-06 11:04:35 +01:00
|
|
|
if (!task)
|
2022-10-05 12:02:40 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
task->interrupt();
|
2021-12-16 23:48:52 +01:00
|
|
|
}
|
|
|
|
|
2023-04-08 00:58:53 +02:00
|
|
|
u32 TaskHolder::getProgress() const {
|
2023-12-19 13:10:25 +01:00
|
|
|
auto task = m_task.lock();
|
2023-12-06 11:04:35 +01:00
|
|
|
if (!task)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// If the max value is 0, the task has no progress
|
2023-12-05 14:32:28 +01:00
|
|
|
if (task->getMaxValue() == 0)
|
|
|
|
return 0;
|
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Calculate the progress of the task from 0 to 100
|
2023-11-10 20:47:08 +01:00
|
|
|
return u32((task->getValue() * 100) / task->getMaxValue());
|
2023-04-08 00:58:53 +02:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
|
2022-10-04 23:37:48 +02:00
|
|
|
void TaskManager::init() {
|
2023-06-06 11:29:58 +02:00
|
|
|
const auto threadCount = std::thread::hardware_concurrency();
|
|
|
|
|
|
|
|
log::debug("Initializing task manager thread pool with {} workers.", threadCount);
|
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Create worker threads
|
2023-12-22 23:39:38 +01:00
|
|
|
for (u32 i = 0; i < threadCount; i++) {
|
|
|
|
s_workers.emplace_back([](const std::stop_token &stopToken) {
|
|
|
|
while (true) {
|
|
|
|
std::shared_ptr<Task> task;
|
|
|
|
|
|
|
|
// Set the thread name to "Idle Task" while waiting for a task
|
|
|
|
setThreadName("Idle Task");
|
|
|
|
|
|
|
|
{
|
|
|
|
// Wait for a task to be added to the queue
|
|
|
|
std::unique_lock lock(s_queueMutex);
|
|
|
|
s_jobCondVar.wait(lock, [&] {
|
|
|
|
return !s_taskQueue.empty() || stopToken.stop_requested();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Check if the thread should exit
|
|
|
|
if (stopToken.stop_requested())
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Grab the next task from the queue
|
|
|
|
task = std::move(s_taskQueue.front());
|
|
|
|
s_taskQueue.pop_front();
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Set the thread name to the name of the task
|
|
|
|
setThreadName(Lang(task->m_unlocalizedName));
|
|
|
|
|
|
|
|
// Execute the task
|
|
|
|
task->m_function(*task);
|
|
|
|
|
|
|
|
log::debug("Task '{}' finished", task->m_unlocalizedName.get());
|
|
|
|
} catch (const Task::TaskInterruptor &) {
|
|
|
|
// Handle the task being interrupted by user request
|
|
|
|
task->interruption();
|
|
|
|
} catch (const std::exception &e) {
|
|
|
|
log::error("Exception in task '{}': {}", task->m_unlocalizedName.get(), e.what());
|
|
|
|
|
|
|
|
// Handle the task throwing an uncaught exception
|
|
|
|
task->exception(e.what());
|
|
|
|
} catch (...) {
|
|
|
|
log::error("Exception in task '{}'", task->m_unlocalizedName.get());
|
|
|
|
|
|
|
|
// Handle the task throwing an uncaught exception of unknown type
|
|
|
|
task->exception("Unknown Exception");
|
|
|
|
}
|
|
|
|
|
|
|
|
task->finish();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-10-04 23:37:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void TaskManager::exit() {
|
2023-12-06 11:04:35 +01:00
|
|
|
// Interrupt all tasks
|
|
|
|
for (auto &task : s_tasks) {
|
2022-10-04 23:37:48 +02:00
|
|
|
task->interrupt();
|
2023-12-06 11:04:35 +01:00
|
|
|
}
|
2022-10-04 23:37:48 +02:00
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Ask worker threads to exit after finishing their task
|
2023-07-26 13:50:51 +02:00
|
|
|
for (auto &thread : s_workers)
|
2022-10-04 23:37:48 +02:00
|
|
|
thread.request_stop();
|
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Wake up all the idle worker threads so they can exit
|
2022-10-04 23:37:48 +02:00
|
|
|
s_jobCondVar.notify_all();
|
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
// Wait for all worker threads to exit
|
2023-07-26 13:50:51 +02:00
|
|
|
s_workers.clear();
|
2023-12-06 11:04:35 +01:00
|
|
|
|
|
|
|
s_tasks.clear();
|
|
|
|
s_taskQueue.clear();
|
2024-01-04 21:33:17 +01:00
|
|
|
|
|
|
|
s_deferredCalls.clear();
|
|
|
|
s_tasksFinishedCallbacks.clear();
|
2022-10-04 23:37:48 +02:00
|
|
|
}
|
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
TaskHolder TaskManager::createTask(std::string name, u64 maxValue, bool background, std::function<void(Task&)> function) {
|
|
|
|
std::scoped_lock lock(s_queueMutex);
|
|
|
|
|
|
|
|
// Construct new task
|
|
|
|
auto task = std::make_shared<Task>(std::move(name), maxValue, background, std::move(function));
|
|
|
|
|
2022-10-05 21:25:52 +02:00
|
|
|
s_tasks.emplace_back(task);
|
2023-12-06 11:04:35 +01:00
|
|
|
|
|
|
|
// Add task to the queue for the worker to pick up
|
|
|
|
s_taskQueue.emplace_back(std::move(task));
|
2022-10-05 21:25:52 +02:00
|
|
|
|
2022-10-04 23:37:48 +02:00
|
|
|
s_jobCondVar.notify_one();
|
2022-08-17 16:15:36 +02:00
|
|
|
|
|
|
|
return TaskHolder(s_tasks.back());
|
2022-01-09 21:27:59 +01:00
|
|
|
}
|
|
|
|
|
2022-10-05 21:25:52 +02:00
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
TaskHolder TaskManager::createTask(std::string name, u64 maxValue, std::function<void(Task &)> function) {
|
|
|
|
log::debug("Creating task {}", name);
|
|
|
|
return createTask(std::move(name), maxValue, false, std::move(function));
|
|
|
|
}
|
2022-09-19 16:09:22 +02:00
|
|
|
|
2023-12-06 11:04:35 +01:00
|
|
|
TaskHolder TaskManager::createBackgroundTask(std::string name, std::function<void(Task &)> function) {
|
|
|
|
log::debug("Creating background task {}", name);
|
|
|
|
return createTask(std::move(name), 0, true, std::move(function));
|
2022-09-19 16:09:22 +02:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
void TaskManager::collectGarbage() {
|
2023-04-06 12:44:25 +02:00
|
|
|
{
|
2023-12-06 11:04:35 +01:00
|
|
|
std::scoped_lock lock(s_queueMutex);
|
|
|
|
std::erase_if(s_tasks, [](const auto &task) {
|
|
|
|
return task->isFinished() && !task->hadException();
|
|
|
|
});
|
2023-04-06 12:44:25 +02:00
|
|
|
}
|
2022-12-27 11:35:20 +01:00
|
|
|
|
|
|
|
if (s_tasks.empty()) {
|
2023-12-06 11:04:35 +01:00
|
|
|
std::scoped_lock lock(s_deferredCallsMutex);
|
2022-12-27 11:35:20 +01:00
|
|
|
for (auto &call : s_tasksFinishedCallbacks)
|
|
|
|
call();
|
|
|
|
s_tasksFinishedCallbacks.clear();
|
|
|
|
}
|
2023-08-26 12:54:52 +02:00
|
|
|
|
2021-12-16 23:48:52 +01:00
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
std::list<std::shared_ptr<Task>> &TaskManager::getRunningTasks() {
|
|
|
|
return s_tasks;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t TaskManager::getRunningTaskCount() {
|
2023-12-06 11:04:35 +01:00
|
|
|
std::scoped_lock lock(s_queueMutex);
|
2022-10-04 23:37:48 +02:00
|
|
|
|
|
|
|
return std::count_if(s_tasks.begin(), s_tasks.end(), [](const auto &task){
|
|
|
|
return !task->isBackgroundTask();
|
|
|
|
});
|
2022-08-17 16:15:36 +02:00
|
|
|
}
|
|
|
|
|
2022-12-27 11:35:20 +01:00
|
|
|
size_t TaskManager::getRunningBackgroundTaskCount() {
|
2023-12-06 11:04:35 +01:00
|
|
|
std::scoped_lock lock(s_queueMutex);
|
2022-12-27 11:35:20 +01:00
|
|
|
|
|
|
|
return std::count_if(s_tasks.begin(), s_tasks.end(), [](const auto &task){
|
|
|
|
return task->isBackgroundTask();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
|
|
|
|
void TaskManager::doLater(const std::function<void()> &function) {
|
|
|
|
std::scoped_lock lock(s_deferredCallsMutex);
|
|
|
|
|
|
|
|
s_deferredCalls.push_back(function);
|
|
|
|
}
|
|
|
|
|
|
|
|
void TaskManager::runDeferredCalls() {
|
|
|
|
std::scoped_lock lock(s_deferredCallsMutex);
|
|
|
|
|
|
|
|
for (const auto &call : s_deferredCalls)
|
|
|
|
call();
|
2022-02-01 18:09:40 +01:00
|
|
|
|
2022-08-17 16:15:36 +02:00
|
|
|
s_deferredCalls.clear();
|
2022-02-01 18:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-12-27 11:35:20 +01:00
|
|
|
void TaskManager::runWhenTasksFinished(const std::function<void()> &function) {
|
2022-12-28 23:06:27 +01:00
|
|
|
std::scoped_lock lock(s_tasksFinishedMutex);
|
2022-12-27 11:35:20 +01:00
|
|
|
|
|
|
|
s_tasksFinishedCallbacks.push_back(function);
|
|
|
|
}
|
|
|
|
|
2023-05-20 13:38:12 +02:00
|
|
|
}
|