DispatcherLoop implementation complete (closes #1049)
Did a full review of state and locking logic, seems airtight now. - command processing itself is unimplemented, we log a TODO message for now - likewise, builder is not implemented - need to add the deadlock safeguard #1054
This commit is contained in:
parent
b0b662f200
commit
458fda4058
4 changed files with 64 additions and 51 deletions
|
|
@ -361,9 +361,9 @@ namespace lib {
|
|||
|
||||
template<class X>
|
||||
bool
|
||||
wait (X& instance, bool (X::*method)(void), ulong timedwait=0)
|
||||
wait (X& instance, bool (X::*method)(void), ulong timedwait=0) ///////////////////////TICKET #1051 : add support for lambdas
|
||||
{
|
||||
BoolMethodPredicate<X> invokeMethod(instance, method);
|
||||
BoolMethodPredicate<X> invokeMethod(instance, method); ///////////////////////TICKET #1057 : const correctness, allow use of const member functions
|
||||
return IMPL::wait(invokeMethod, timeout_.setOffset(timedwait));
|
||||
}
|
||||
|
||||
|
|
@ -463,7 +463,7 @@ namespace lib {
|
|||
|
||||
template<typename X>
|
||||
bool
|
||||
wait (X& instance, bool (X::*predicate)(void), ulong timeout=0)
|
||||
wait (X& instance, bool (X::*predicate)(void), ulong timeout=0) //////////////////////TICKET #1051 : enable use of lambdas
|
||||
{
|
||||
return mon_.wait(instance,predicate,timeout);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,8 +73,6 @@
|
|||
** is a private detail of the performing thread. The lock is acquired solely for checking or leaving
|
||||
** the wait state and when fetching the next command from queue.
|
||||
**
|
||||
** @todo as of 12/2016, implementation has been drafted and is very much WIP
|
||||
**
|
||||
** @see ProcDispatcher
|
||||
** @see DispatcherLooper_test
|
||||
** @see CommandQueue_test
|
||||
|
|
@ -94,9 +92,9 @@
|
|||
|
||||
#include <memory>
|
||||
|
||||
using backend::Thread;
|
||||
using lib::Sync;
|
||||
using lib::RecursiveLock_Waitable;
|
||||
using backend::Thread;
|
||||
using std::unique_ptr;
|
||||
|
||||
namespace proc {
|
||||
|
|
@ -104,10 +102,10 @@ namespace control {
|
|||
|
||||
namespace error = lumiera::error;
|
||||
|
||||
/**
|
||||
* PImpl within ProcDispatcher
|
||||
* to implement the _Session Loop Thread._
|
||||
* During the lifetime of this object
|
||||
|
||||
/********************************************************************//**
|
||||
* PImpl within ProcDispatcher to implement the _Session Loop Thread._
|
||||
* During the lifetime of this object...
|
||||
* - the SessionCommandService is offered to enqueue commands
|
||||
* - the Session Loop thread dispatches commands and triggers the Builder
|
||||
* @see DispatcherLooper_test
|
||||
|
|
@ -117,8 +115,7 @@ namespace control {
|
|||
, public CommandDispatch
|
||||
, public Sync<RecursiveLock_Waitable>
|
||||
{
|
||||
bool canDispatch_{false};
|
||||
|
||||
|
||||
/** manage the primary public Session interface */
|
||||
unique_ptr<SessionCommandService> commandService_;
|
||||
|
||||
|
|
@ -165,18 +162,18 @@ namespace control {
|
|||
activateCommandProecssing()
|
||||
{
|
||||
Lock sync(this);
|
||||
canDispatch_ = true;
|
||||
looper_.enableProcessing(true);
|
||||
INFO (command, "Session command processing activated.");
|
||||
TODO ("implement command processing queue");
|
||||
sync.notifyAll();
|
||||
}
|
||||
|
||||
void
|
||||
deactivateCommandProecssing()
|
||||
{
|
||||
Lock sync(this);
|
||||
canDispatch_ = false;
|
||||
looper_.enableProcessing(false);
|
||||
INFO (command, "Session command interface closed.");
|
||||
TODO ("implement command processing queue");
|
||||
sync.notifyAll();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -191,8 +188,8 @@ namespace control {
|
|||
void
|
||||
awaitStateProcessed() const
|
||||
{
|
||||
Lock blockWaiting(this, &DispatcherLoop::stateIsSynched);
|
||||
//////////////////////////////////////////TODO eternal sleep.... find out who will wake us!!!!
|
||||
Lock blockWaiting(unConst(this), &DispatcherLoop::stateIsSynched); ///////////////////////TICKET #1057 : const correctness on wait predicate
|
||||
// wake-up typically by updateState()
|
||||
}
|
||||
|
||||
size_t
|
||||
|
|
@ -272,14 +269,14 @@ namespace control {
|
|||
updateState() ///< at end of loop body...
|
||||
{
|
||||
looper_.markStateProcessed();
|
||||
if (looper_.isDisabled())
|
||||
if (looper_.isDisabled()) // otherwise wake-up would not be safe
|
||||
Lock(this).notifyAll();
|
||||
}
|
||||
|
||||
bool
|
||||
stateIsSynched()
|
||||
{
|
||||
if (looper_.hasPendingChanges() and calledFromWithinSessionThread())
|
||||
if (calledFromWithinSessionThread())
|
||||
throw error::Fatal("Possible Deadlock. "
|
||||
"Attempt to synchronise to a command processing check point "
|
||||
"from within the (single) session thread."
|
||||
|
|
@ -324,11 +321,12 @@ namespace control {
|
|||
|
||||
|
||||
|
||||
|
||||
/** storage for Singleton access */
|
||||
lib::Depend<ProcDispatcher> ProcDispatcher::instance;
|
||||
|
||||
|
||||
|
||||
/* ======== ProcDispatcher implementation ======== */
|
||||
|
||||
/** starting the ProcDispatcher means to start the session subsystem.
|
||||
* @return `false` when _starting_ failed since it is already running...
|
||||
|
|
@ -426,6 +424,12 @@ namespace control {
|
|||
}
|
||||
|
||||
|
||||
/** halt further processing of session commands
|
||||
* @note the processing itself runs in a separate thread, thus any currently
|
||||
* ongoing command or builder execution will be completed prior to this setting
|
||||
* to take effect. If the intention is to halt processing because the session is
|
||||
* about to dismantled, it is mandatory to awaitDeactivation()
|
||||
*/
|
||||
void
|
||||
ProcDispatcher::deactivate()
|
||||
{
|
||||
|
|
@ -472,8 +476,6 @@ namespace control {
|
|||
return not runningLoop_
|
||||
or 0 == runningLoop_->size();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5222,7 +5222,7 @@ Besides, they provide an __inward interface__ for the [[ProcNode]]s, enabling th
|
|||
[img[Asset Classess|uml/fig131077.png]]
|
||||
{{red{Note 3/2010}}} it is very unlikely we'll organise the processing nodes as a class hierarchy. Rather it looks like we'll get several submodules/special capabilities configured in within the Builder</pre>
|
||||
</div>
|
||||
<div title="ProcDispatcher" creator="Ichthyostega" modifier="Ichthyostega" created="201612140406" modified="201612162302" tags="def spec SessionLogic draft" changecount="5">
|
||||
<div title="ProcDispatcher" creator="Ichthyostega" modifier="Ichthyostega" created="201612140406" modified="201701052235" tags="def spec SessionLogic draft" changecount="11">
|
||||
<pre>//The guard and coordinator of any operation within the session subsystem.//
|
||||
The session and related components work effectively single threaded. Any tangible operation on the session data structure has to be enqueued as [[command|CommandHandling]] into the dispatcher. Moreover, the [[Builder]] is triggered from the ProcDispatcher; and while the Builder is running, any command processing is halted. The Builder in turn creates or reshapes the processing nodes network, and the changed network is brought into operation with a //transactional switch// -- while render processes on this processing network operate unaffected and essentially multi-threaded.
|
||||
|
||||
|
|
@ -5233,19 +5233,18 @@ To function properly as action coordinator of the session subsystem, the dispatc
|
|||
;enqueue
|
||||
:accept and enqueue command messages concurrently, any time, without blocking the caller
|
||||
:*FIFO for //regular commands//
|
||||
:*LIFO for //priority requests//
|
||||
:*LIFO for //priority requests// {{red{unimplemented 1/17}}}
|
||||
;process
|
||||
:dequeue and process entries sequentially
|
||||
;sleep
|
||||
:work continuously until queue is empty, then enter wait state
|
||||
;check point
|
||||
:invoke a well defined check point reliably non blocking ("ensure to make progress")
|
||||
:arrive at a well defined check point reliably non blocking ("ensure to make progress")
|
||||
:* necessary to know when internal state is consistent
|
||||
:* when?
|
||||
:** after each command
|
||||
:** regularily whle idle
|
||||
:* why?
|
||||
:** ensure the Builder runs regularly
|
||||
:** respond reliably to shutdown request
|
||||
:** after builder run
|
||||
:** after wake-up
|
||||
;manage
|
||||
:care for rectifying entries in the queue
|
||||
:* ensure they //match// current session, discard obsoleted requests
|
||||
|
|
@ -5263,9 +5262,9 @@ Initially the command queue is empty and the ProcDispatcher can be considered id
|
|||
|
||||
So the logic to trigger builder runs has to take some leeway into account. Due to the typical interactive working style of an editing application, session commands might be trickling in in strikes of similar commands, intermingled with tiny pauses. For this reason, the ProcDispatcher implements some //hysteresis,// as far as triggering the builder runs is concerned. The builder is fired in idle state, but only after passing some //latency period.// On the other hand, massive UI activities (especially during a builder run) may have flooded the queue, thus sending the session into an extended period of command processing. From the user's view angle, the application looks non responsive in such a case, albeit not frozen, since the UI can still enqueue further commands and thus retains the ability to react locally on user interaction. To mitigate this problem, the builder should be started anyway after some extended period of command processing, even if the queue is not yet emptied. Each builder run produces a structural diff message sent towards the UI and thus causes user visible changes within the session's UI representation. This somewhat stuttering response conveys to the user a tangible sensation of ongoing activity, while communicating at the same time, at least subconsciously some degree of operational overload. {{red{note 12/2016 builder is not implemented, so consider this planning}}}
|
||||
|
||||
Any change to the circumstances determining the ProcDispatcher's behaviour need to be imparted actively through the public interface -- the dispatcher is not designed to be a state listener or observer. Any such state change notifications are synchronised and cause a wakeup notification to the session thread. For this purpose, enqueuing of further commands counts as state change and is lock protected. Beyond that, any other activities, like //processing// of commands or builder runs, are performed within the session thread without blocking other threads; the locking on the ProcDispatcher is only ever short term to ensure consistent internal state. Clients need to be prepared for the effect of actions to appear asynchronously and with some delay. Especially this means that session switch or shutdown has to await completion of any session command or builder run currently in progress.
|
||||
Any change to the circumstances determining the ProcDispatcher's behaviour needs to be imparted actively through the public interface -- the dispatcher is not designed to be a state listener or observer. Any such state change notifications are synchronised and cause a wakeup notification to the session thread. For this purpose, enqueuing of further commands counts as state change and is lock protected. Beyond that, any other activities, like //processing// of commands or builder runs, are performed within the session thread without blocking other threads; the locking on the ProcDispatcher is only ever short term to ensure consistent internal state. Clients need to be prepared for the effect of actions to appear asynchronously and with some delay. Especially this means that session switch or shutdown has to await completion of any session command or builder run currently in progress.
|
||||
|
||||
When the session is closed or dismantled, further processing in the ProcDispatcher will be disabled, after completing the current command or builder run. This disabled state can be reversed when a new session instance becomes operative. And while the dispatcher will then continue to empty the command queue, most commands in queue will probably be obsoleted and dropped, because of referring to a deceased session instance. Moreover, the lifecycle of the session instances has to be distinguished from the lifecycle of the "session subsystem" as such. When the latter is terminated, be it by a fatal error in some builder run, or be it due to general shutdown of the application, the ProcDispatcher will be asked to terminate the session thread after completing the current activity in progress. Such an event will also discard any further commands waiting in the dispatcher's queue.
|
||||
When the session is closed or dismantled, further processing in the ProcDispatcher will be disabled, after completing the current command or builder run. This disabled state can be reversed when a new session instance becomes operative. And while the dispatcher will then continue to empty the command queue, most commands in queue will probably be obsoleted and dropped, because of referring to a deceased session instance. Moreover, the lifecycle of the session instances has to be distinguished from the lifecycle of the SessionSubsystem as such. When the latter is terminated, be it by a fatal error in some builder run, or be it due to general shutdown of the application, the ProcDispatcher will be asked to terminate the session thread after completing the current activity in progress. Such an event will also discard any further commands waiting in the dispatcher's queue.
|
||||
</pre>
|
||||
</div>
|
||||
<div title="ProcLayer" modifier="Ichthyostega" created="200708100333" modified="200708100338" tags="def">
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@
|
|||
</body>
|
||||
</html></richcontent>
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node CREATED="1481599413419" HGAP="34" ID="ID_673133356" MODIFIED="1483473316679" TEXT="Ticket #318" VSHIFT="-7">
|
||||
<node COLOR="#338800" CREATED="1481599413419" HGAP="34" ID="ID_673133356" MODIFIED="1483654943383" TEXT="Ticket #318" VSHIFT="-7">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head>
|
||||
|
||||
|
|
@ -437,15 +437,15 @@
|
|||
</body>
|
||||
</html>
|
||||
</richcontent>
|
||||
<icon BUILTIN="pencil"/>
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1483473104426" ID="ID_1606817357" MODIFIED="1483473167947" TEXT="#701 Session interface Support facility">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1483473212107" ID="ID_600424316" MODIFIED="1483473247077" TEXT="#699 entrance to session subsystem">
|
||||
<icon BUILTIN="help"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1483473153851" ID="ID_868700871" MODIFIED="1483473169722" TEXT="#1049 draft SessionLoop component">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1483473153851" ID="ID_868700871" MODIFIED="1483654523247" TEXT="#1049 draft SessionLoop component">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node CREATED="1483642220941" ID="ID_517844262" MODIFIED="1483645251383" TEXT="integrate API functions for enqueuing commands">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
|
|
@ -455,8 +455,12 @@
|
|||
<node CREATED="1483642220941" ID="ID_704571897" MODIFIED="1483648766529" TEXT="care for waking (notification) from sleep state">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node CREATED="1483642220940" ID="ID_1519554797" MODIFIED="1483642272822" TEXT="care for syncing the disabled state"/>
|
||||
<node CREATED="1483642220941" ID="ID_377776032" MODIFIED="1483642220941" TEXT="deadlock safeguard"/>
|
||||
<node CREATED="1483642220940" ID="ID_1519554797" MODIFIED="1483654058917" TEXT="care for syncing the disabled state">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1483642220941" ID="ID_377776032" MODIFIED="1483654517016" TEXT="deadlock safeguard">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node CREATED="1482464327133" HGAP="51" ID="ID_996095134" MODIFIED="1483584055342" TEXT="was es ist" VSHIFT="-2">
|
||||
|
|
@ -630,9 +634,9 @@
|
|||
<node CREATED="1481778397600" ID="ID_1997192349" MODIFIED="1481778409498" TEXT="bereits wenn Anhalten signalisiert wird"/>
|
||||
<node CREATED="1481778409942" ID="ID_100284739" MODIFIED="1481778419113" TEXT="zur Sicherheit nochmal im dtor"/>
|
||||
</node>
|
||||
<node CREATED="1481777114203" ID="ID_1156396729" MODIFIED="1481777121886" TEXT="Lock, um Deadlock zu vermeitden"/>
|
||||
<node CREATED="1481777114203" ID="ID_1156396729" MODIFIED="1483654726680" TEXT="Lock, um Deadlock zu vermeiden"/>
|
||||
<node CREATED="1481777122306" ID="ID_1506554988" MODIFIED="1481777133445" TEXT="alles im dtor -> noexcept"/>
|
||||
<node CREATED="1481777210447" ID="ID_1392452935" MODIFIED="1483582206339" TEXT="sicherstellen daß shutdown nicht blockt">
|
||||
<node CREATED="1481777210447" ID="ID_1392452935" MODIFIED="1483654905825" TEXT="sicherstellen daß shutdown nicht blockt">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head>
|
||||
|
||||
|
|
@ -662,7 +666,7 @@
|
|||
</body>
|
||||
</html>
|
||||
</richcontent>
|
||||
<arrowlink COLOR="#5a97df" DESTINATION="ID_1789585729" ENDARROW="Default" ENDINCLINATION="6300;584;" ID="Arrow_ID_1930701586" STARTARROW="None" STARTINCLINATION="8475;0;"/>
|
||||
<arrowlink COLOR="#5a97df" DESTINATION="ID_1789585729" ENDARROW="Default" ENDINCLINATION="3216;0;" ID="Arrow_ID_1930701586" STARTARROW="None" STARTINCLINATION="8475;0;"/>
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
</node>
|
||||
|
|
@ -11221,8 +11225,8 @@
|
|||
<node CREATED="1481688529539" ID="ID_1972961763" MODIFIED="1481688540109" TEXT="SessionManager ist zuständig"/>
|
||||
<node CREATED="1481688544921" ID="ID_1934560784" MODIFIED="1481688561386" TEXT="wenn Session geladen, ist ProcDispatcher freigegeben"/>
|
||||
<node CREATED="1481688562830" ID="ID_708961458" MODIFIED="1481688582024" TEXT="SessionSubsystem startet processing loop im ProcDispatcher"/>
|
||||
<node CREATED="1481777252497" ID="ID_1789585729" MODIFIED="1481778497944" TEXT="Shutdown">
|
||||
<linktarget COLOR="#5a97df" DESTINATION="ID_1789585729" ENDARROW="Default" ENDINCLINATION="6300;584;" ID="Arrow_ID_1930701586" SOURCE="ID_1392452935" STARTARROW="None" STARTINCLINATION="8475;0;"/>
|
||||
<node CREATED="1481777252497" ID="ID_1789585729" MODIFIED="1483654905825" TEXT="Shutdown">
|
||||
<linktarget COLOR="#5a97df" DESTINATION="ID_1789585729" ENDARROW="Default" ENDINCLINATION="3216;0;" ID="Arrow_ID_1930701586" SOURCE="ID_1392452935" STARTARROW="None" STARTINCLINATION="8475;0;"/>
|
||||
<node CREATED="1481777297635" ID="ID_483368010" MODIFIED="1481777305432" TEXT="Deadlock-Gefahr">
|
||||
<icon BUILTIN="messagebox_warning"/>
|
||||
</node>
|
||||
|
|
@ -11234,9 +11238,13 @@
|
|||
</node>
|
||||
<node CREATED="1481777355747" ID="ID_1804595708" MODIFIED="1481777364326" TEXT="Subsystem->end">
|
||||
<node CREATED="1481777368330" ID="ID_1933237483" MODIFIED="1481777374277" TEXT="Halte-Nachricht"/>
|
||||
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1481777374801" ID="ID_1844558240" MODIFIED="1481826297245" TEXT="sicherstellen, daß Loop anhält">
|
||||
<node COLOR="#338800" CREATED="1481777374801" ID="ID_1844558240" MODIFIED="1483654878632" TEXT="sicherstellen, daß Loop anhält">
|
||||
<arrowlink COLOR="#f70841" DESTINATION="ID_1748096848" ENDARROW="Default" ENDINCLINATION="-353;-77;" ID="Arrow_ID_506637666" STARTARROW="None" STARTINCLINATION="-200;38;"/>
|
||||
<icon BUILTIN="flag-pink"/>
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node CREATED="1483654823454" ID="ID_397283548" MODIFIED="1483654831113" TEXT="shutdown -> requireAction"/>
|
||||
<node CREATED="1483654831773" ID="ID_911489519" MODIFIED="1483654845495" TEXT="shutdown-Trigger macht Notify"/>
|
||||
<node CREATED="1483654846083" ID="ID_1926442698" MODIFIED="1483654852935" TEXT="Loop-Thread wacht auf"/>
|
||||
<node CREATED="1483654853850" ID="ID_934617670" MODIFIED="1483654862885" TEXT="...und fällt aus der Loop heraus"/>
|
||||
</node>
|
||||
<node CREATED="1481777397542" ID="ID_1147135932" MODIFIED="1481778447806" TEXT="DispatcherLoop stirbt"/>
|
||||
<node CREATED="1481777410972" ID="ID_457965735" MODIFIED="1481777418982" TEXT="sigTerm"/>
|
||||
|
|
@ -11347,13 +11355,17 @@
|
|||
<icon BUILTIN="messagebox_warning"/>
|
||||
</node>
|
||||
</node>
|
||||
<node CREATED="1481918517018" HGAP="11" ID="ID_916331172" MODIFIED="1482197469326" TEXT="Sperre" VSHIFT="8">
|
||||
<icon BUILTIN="bell"/>
|
||||
<node CREATED="1481918524673" ID="ID_1705856370" MODIFIED="1481918546498" TEXT="verhindert Command- und Builder-start"/>
|
||||
<node CREATED="1481918547182" ID="ID_920627724" MODIFIED="1481920718126" TEXT="bestehende Commands / Builder-Läufe werden noch abgeschlossen"/>
|
||||
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1481920722100" ID="ID_1378897740" MODIFIED="1482464539361" TEXT="SessionManager muß auf Sperre warten">
|
||||
<node CREATED="1481918517018" HGAP="11" ID="ID_916331172" MODIFIED="1483654611468" TEXT="Sperre" VSHIFT="8">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node CREATED="1481918524673" ID="ID_1705856370" MODIFIED="1483654636642" TEXT="verhindert Command- und Builder-start">
|
||||
<icon BUILTIN="info"/>
|
||||
</node>
|
||||
<node CREATED="1481918547182" ID="ID_920627724" MODIFIED="1483654595721" TEXT="bestehende Commands / Builder-Läufe werden noch abgeschlossen">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node COLOR="#338800" CREATED="1481920722100" ID="ID_1378897740" MODIFIED="1483654575032" TEXT="SessionManager muß auf Sperre warten">
|
||||
<linktarget COLOR="#5a97df" DESTINATION="ID_1378897740" ENDARROW="Default" ENDINCLINATION="9540;0;" ID="Arrow_ID_1446486242" SOURCE="ID_284397306" STARTARROW="None" STARTINCLINATION="9540;0;"/>
|
||||
<icon BUILTIN="flag-pink"/>
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1481926040310" ID="ID_667460598" MODIFIED="1481926061115" TEXT="Deadlock wenn der Session-Thread selber auf diese Sperre wartet">
|
||||
<icon BUILTIN="flag-pink"/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue