Moved handling remote timers into cSVDRPClientHandler::ProcessConnections()

This commit is contained in:
Klaus Schmidinger 2018-02-25 13:26:17 +01:00
parent bbbc36a1e6
commit 6600478675
6 changed files with 87 additions and 128 deletions

View File

@ -9162,7 +9162,7 @@ Video Disk Recorder Revision History
a subdirectory. a subdirectory.
- SVDRP peering can now be limited to the default SVDRP host (see MANUAL for details). - SVDRP peering can now be limited to the default SVDRP host (see MANUAL for details).
2018-02-20: Version 2.3.9 2018-02-25: Version 2.3.9
- Updated the Italian OSD texts (thanks to Diego Pierotto). - Updated the Italian OSD texts (thanks to Diego Pierotto).
- Updated the Finnish OSD texts (thanks to Rolf Ahrenberg). - Updated the Finnish OSD texts (thanks to Rolf Ahrenberg).
@ -9283,3 +9283,4 @@ Video Disk Recorder Revision History
SVDRP command CONN instead of using the UDP port with the server's address. SVDRP command CONN instead of using the UDP port with the server's address.
This change requires that all VDRs that shall take part in a peer-to-peer network need This change requires that all VDRs that shall take part in a peer-to-peer network need
to be updated to this version. to be updated to this version.
- Moved handling remote timers into cSVDRPClientHandler::ProcessConnections().

4
menu.c
View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and * See the main source file 'vdr.c' for copyright information and
* how to reach the author. * how to reach the author.
* *
* $Id: menu.c 4.61 2018/02/13 09:25:43 kls Exp $ * $Id: menu.c 4.62 2018/02/25 13:07:09 kls Exp $
*/ */
#include "menu.h" #include "menu.h"
@ -4184,7 +4184,7 @@ eOSState cMenuSetupMisc::ProcessKey(eKeys Key)
else { else {
LOCK_TIMERS_WRITE; LOCK_TIMERS_WRITE;
Timers->SetExplicitModify(); Timers->SetExplicitModify();
if (Timers->DelRemoteTimers()) if (Timers->StoreRemoteTimers(NULL, NULL))
Timers->SetModified(); Timers->SetModified();
} }
} }

72
svdrp.c
View File

@ -10,7 +10,7 @@
* and interact with the Video Disk Recorder - or write a full featured * and interact with the Video Disk Recorder - or write a full featured
* graphical interface that sits on top of an SVDRP connection. * graphical interface that sits on top of an SVDRP connection.
* *
* $Id: svdrp.c 4.27 2018/02/20 13:28:04 kls Exp $ * $Id: svdrp.c 4.28 2018/02/25 13:26:17 kls Exp $
*/ */
#include "svdrp.h" #include "svdrp.h"
@ -317,6 +317,7 @@ private:
cFile file; cFile file;
int fetchFlags; int fetchFlags;
bool connected; bool connected;
bool Send(const char *Command);
void Close(void); void Close(void);
public: public:
cSVDRPClient(const char *Address, int Port, const char *ServerName, int Timeout); cSVDRPClient(const char *Address, int Port, const char *ServerName, int Timeout);
@ -324,12 +325,12 @@ public:
const char *ServerName(void) const { return serverName; } const char *ServerName(void) const { return serverName; }
const char *Connection(void) const { return serverIpAddress.Connection(); } const char *Connection(void) const { return serverIpAddress.Connection(); }
bool HasAddress(const char *Address, int Port) const; bool HasAddress(const char *Address, int Port) const;
bool Send(const char *Command);
bool Process(cStringList *Response = NULL); bool Process(cStringList *Response = NULL);
bool Execute(const char *Command, cStringList *Response = NULL); bool Execute(const char *Command, cStringList *Response = NULL);
bool Connected(void) const { return connected; } bool Connected(void) const { return connected; }
void SetFetchFlag(eSvdrpFetchFlags Flag); void SetFetchFlag(eSvdrpFetchFlags Flag);
bool HasFetchFlag(eSvdrpFetchFlags Flag); bool HasFetchFlag(eSvdrpFetchFlags Flag);
bool GetRemoteTimers(cStringList &Response);
}; };
static cPoller SVDRPClientPoller; static cPoller SVDRPClientPoller;
@ -365,9 +366,6 @@ void cSVDRPClient::Close(void)
SVDRPClientPoller.Del(file, false); SVDRPClientPoller.Del(file, false);
file.Close(); file.Close();
socket.Close(); socket.Close();
LOCK_TIMERS_WRITE;
if (Timers)
Timers->DelRemoteTimers(serverName);
} }
} }
@ -483,6 +481,14 @@ bool cSVDRPClient::HasFetchFlag(eSvdrpFetchFlags Flag)
return Result; return Result;
} }
bool cSVDRPClient::GetRemoteTimers(cStringList &Response)
{
if (HasFetchFlag(sffTimers))
return Execute("LSTT ID", &Response);
return false;
}
// --- cSVDRPServerParams ---------------------------------------------------- // --- cSVDRPServerParams ----------------------------------------------------
class cSVDRPServerParams { class cSVDRPServerParams {
@ -554,20 +560,21 @@ private:
cMutex mutex; cMutex mutex;
int tcpPort; int tcpPort;
cSocket udpSocket; cSocket udpSocket;
cStateKey timersStateKey;
cVector<cSVDRPClient *> clientConnections; cVector<cSVDRPClient *> clientConnections;
void SendDiscover(void);
void HandleClientConnection(void); void HandleClientConnection(void);
void ProcessConnections(void); void ProcessConnections(void);
cSVDRPClient *GetClientForServer(const char *ServerName);
protected: protected:
virtual void Action(void); virtual void Action(void);
public: public:
cSVDRPClientHandler(int TcpPort, int UdpPort); cSVDRPClientHandler(int TcpPort, int UdpPort);
virtual ~cSVDRPClientHandler(); virtual ~cSVDRPClientHandler();
void SendDiscover(void);
void AddClient(cSVDRPServerParams &ServerParams, const char *IpAddress); void AddClient(cSVDRPServerParams &ServerParams, const char *IpAddress);
bool Execute(const char *ServerName, const char *Command, cStringList *Response = NULL); bool Execute(const char *ServerName, const char *Command, cStringList *Response = NULL);
bool GetServerNames(cStringList *ServerNames, eSvdrpFetchFlags FetchFlags = sffNone); bool GetServerNames(cStringList *ServerNames, eSvdrpFetchFlags FetchFlags = sffNone);
bool TriggerFetchingTimers(const char *ServerName); bool TriggerFetchingTimers(const char *ServerName);
cSVDRPClient *GetClientForServer(const char *ServerName);
}; };
static cSVDRPClientHandler *SVDRPClientHandler = NULL; static cSVDRPClientHandler *SVDRPClientHandler = NULL;
@ -575,6 +582,7 @@ static cSVDRPClientHandler *SVDRPClientHandler = NULL;
cSVDRPClientHandler::cSVDRPClientHandler(int TcpPort, int UdpPort) cSVDRPClientHandler::cSVDRPClientHandler(int TcpPort, int UdpPort)
:cThread("SVDRP client handler", true) :cThread("SVDRP client handler", true)
,udpSocket(UdpPort, false) ,udpSocket(UdpPort, false)
,timersStateKey(true)
{ {
tcpPort = TcpPort; tcpPort = TcpPort;
} }
@ -588,7 +596,6 @@ cSVDRPClientHandler::~cSVDRPClientHandler()
cSVDRPClient *cSVDRPClientHandler::GetClientForServer(const char *ServerName) cSVDRPClient *cSVDRPClientHandler::GetClientForServer(const char *ServerName)
{ {
cMutexLock MutexLock(&mutex);
for (int i = 0; i < clientConnections.Size(); i++) { for (int i = 0; i < clientConnections.Size(); i++) {
if (strcmp(clientConnections[i]->ServerName(), ServerName) == 0) if (strcmp(clientConnections[i]->ServerName(), ServerName) == 0)
return clientConnections[i]; return clientConnections[i];
@ -598,17 +605,36 @@ cSVDRPClient *cSVDRPClientHandler::GetClientForServer(const char *ServerName)
void cSVDRPClientHandler::SendDiscover(void) void cSVDRPClientHandler::SendDiscover(void)
{ {
cMutexLock MutexLock(&mutex);
cString Dgram = cString::sprintf("SVDRP:discover name:%s port:%d vdrversion:%d apiversion:%d timeout:%d%s", Setup.SVDRPHostName, tcpPort, VDRVERSNUM, APIVERSNUM, Setup.SVDRPTimeout, (Setup.SVDRPPeering == spmOnly && *Setup.SVDRPDefaultHost) ? *cString::sprintf(" host:%s", Setup.SVDRPDefaultHost) : ""); cString Dgram = cString::sprintf("SVDRP:discover name:%s port:%d vdrversion:%d apiversion:%d timeout:%d%s", Setup.SVDRPHostName, tcpPort, VDRVERSNUM, APIVERSNUM, Setup.SVDRPTimeout, (Setup.SVDRPPeering == spmOnly && *Setup.SVDRPDefaultHost) ? *cString::sprintf(" host:%s", Setup.SVDRPDefaultHost) : "");
udpSocket.SendDgram(Dgram, udpSocket.Port()); udpSocket.SendDgram(Dgram, udpSocket.Port());
} }
void cSVDRPClientHandler::ProcessConnections(void) void cSVDRPClientHandler::ProcessConnections(void)
{ {
cMutexLock MutexLock(&mutex); cString PollTimersCmd;
if (cTimers::GetTimersRead(timersStateKey)) {
PollTimersCmd = cString::sprintf("POLL %s TIMERS", Setup.SVDRPHostName);
timersStateKey.Remove();
}
for (int i = 0; i < clientConnections.Size(); i++) { for (int i = 0; i < clientConnections.Size(); i++) {
if (!clientConnections[i]->Process()) { cSVDRPClient *Client = clientConnections[i];
delete clientConnections[i]; if (Client->Process()) {
cStringList RemoteTimers;
if (Client->GetRemoteTimers(RemoteTimers)) {
cTimers *Timers = cTimers::GetTimersWrite(timersStateKey);
bool TimersModified = Timers->StoreRemoteTimers(Client->ServerName(), &RemoteTimers);
timersStateKey.Remove(TimersModified);
}
if (*PollTimersCmd) {
if (!Client->Execute(PollTimersCmd))
esyslog("ERROR: can't send '%s' to '%s'", *PollTimersCmd, Client->ServerName());
}
}
else {
cTimers *Timers = cTimers::GetTimersWrite(timersStateKey);
bool TimersModified = Timers->StoreRemoteTimers(Client->ServerName(), NULL);
timersStateKey.Remove(TimersModified);
delete Client;
clientConnections.Remove(i); clientConnections.Remove(i);
i--; i--;
} }
@ -617,11 +643,10 @@ void cSVDRPClientHandler::ProcessConnections(void)
void cSVDRPClientHandler::AddClient(cSVDRPServerParams &ServerParams, const char *IpAddress) void cSVDRPClientHandler::AddClient(cSVDRPServerParams &ServerParams, const char *IpAddress)
{ {
cMutexLock MutexLock(&mutex);
for (int i = 0; i < clientConnections.Size(); i++) { for (int i = 0; i < clientConnections.Size(); i++) {
if (clientConnections[i]->HasAddress(IpAddress, ServerParams.Port())) { if (clientConnections[i]->HasAddress(IpAddress, ServerParams.Port()))
dsyslog("SVDRP %s < %s connection to '%s' already exists", Setup.SVDRPHostName, clientConnections[i]->Connection(), clientConnections[i]->ServerName());
return; return;
}
} }
if (Setup.SVDRPPeering == spmOnly && strcmp(ServerParams.Name(), Setup.SVDRPDefaultHost) != 0) if (Setup.SVDRPPeering == spmOnly && strcmp(ServerParams.Name(), Setup.SVDRPDefaultHost) != 0)
return; // we only want to peer with the default host, but this isn't the default host return; // we only want to peer with the default host, but this isn't the default host
@ -1294,8 +1319,7 @@ void cSVDRPServer::CmdCONN(const char *Option)
if (ServerParams.Ok()) { if (ServerParams.Ok()) {
clientName = ServerParams.Name(); clientName = ServerParams.Name();
Reply(250, "OK"); // must finish this transaction before creating the new client Reply(250, "OK"); // must finish this transaction before creating the new client
if (!SVDRPClientHandler->GetClientForServer(ServerParams.Name())) SVDRPClientHandler->AddClient(ServerParams, clientIpAddress.Address());
SVDRPClientHandler->AddClient(ServerParams, clientIpAddress.Address());
} }
else else
Reply(501, "Error in server parameters: %s", ServerParams.Error()); Reply(501, "Error in server parameters: %s", ServerParams.Error());
@ -2601,7 +2625,6 @@ void SetSVDRPGrabImageDir(const char *GrabImageDir)
class cSVDRPServerHandler : public cThread { class cSVDRPServerHandler : public cThread {
private: private:
cMutex mutex;
bool ready; bool ready;
cSocket tcpSocket; cSocket tcpSocket;
cVector<cSVDRPServer *> serverConnections; cVector<cSVDRPServer *> serverConnections;
@ -2613,7 +2636,6 @@ public:
cSVDRPServerHandler(int TcpPort); cSVDRPServerHandler(int TcpPort);
virtual ~cSVDRPServerHandler(); virtual ~cSVDRPServerHandler();
void WaitUntilReady(void); void WaitUntilReady(void);
cSVDRPServer *GetServerForClient(const char *ClientName);
}; };
static cSVDRPServerHandler *SVDRPServerHandler = NULL; static cSVDRPServerHandler *SVDRPServerHandler = NULL;
@ -2641,7 +2663,6 @@ void cSVDRPServerHandler::WaitUntilReady(void)
void cSVDRPServerHandler::ProcessConnections(void) void cSVDRPServerHandler::ProcessConnections(void)
{ {
cMutexLock MutexLock(&mutex);
for (int i = 0; i < serverConnections.Size(); i++) { for (int i = 0; i < serverConnections.Size(); i++) {
if (!serverConnections[i]->Process()) { if (!serverConnections[i]->Process()) {
delete serverConnections[i]; delete serverConnections[i];
@ -2665,7 +2686,6 @@ void cSVDRPServerHandler::Action(void)
ready = true; ready = true;
while (Running()) { while (Running()) {
SVDRPServerPoller.Poll(1000); SVDRPServerPoller.Poll(1000);
cMutexLock MutexLock(&mutex);
HandleServerConnection(); HandleServerConnection();
ProcessConnections(); ProcessConnections();
} }
@ -2674,16 +2694,6 @@ void cSVDRPServerHandler::Action(void)
} }
} }
cSVDRPServer *cSVDRPServerHandler::GetServerForClient(const char *ClientName)
{
cMutexLock MutexLock(&mutex);
for (int i = 0; i < serverConnections.Size(); i++) {
if (serverConnections[i]->ClientName() && strcmp(serverConnections[i]->ClientName(), ClientName) == 0)
return serverConnections[i];
}
return NULL;
}
// --- SVDRP Handler --------------------------------------------------------- // --- SVDRP Handler ---------------------------------------------------------
static cMutex SVDRPHandlerMutex; static cMutex SVDRPHandlerMutex;

View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and * See the main source file 'vdr.c' for copyright information and
* how to reach the author. * how to reach the author.
* *
* $Id: timers.c 4.14 2017/11/12 13:01:22 kls Exp $ * $Id: timers.c 4.15 2018/02/25 13:05:03 kls Exp $
*/ */
#include "timers.h" #include "timers.h"
@ -898,78 +898,48 @@ bool cTimers::DeleteExpired(void)
return TimersModified; return TimersModified;
} }
bool cTimers::GetRemoteTimers(const char *ServerName) bool cTimers::StoreRemoteTimers(const char *ServerName, const cStringList *RemoteTimers)
{ {
//TODO handle only new/deleted/modified timers?
bool Result = false; bool Result = false;
if (ServerName) { // Delete old remote timers:
Result = DelRemoteTimers(ServerName);
cStringList Response;
if (ExecSVDRPCommand(ServerName, "LSTT ID", &Response)) {
for (int i = 0; i < Response.Size(); i++) {
const char *s = Response[i];
int Code = SVDRPCode(s);
if (Code == 250) {
if (const char *v = SVDRPValue(s)) {
int Id = atoi(v);
while (*v && *v != ' ')
v++; // skip id
cTimer *Timer = new cTimer;
if (Timer->Parse(v)) {
Timer->SetRemote(ServerName);
Timer->SetId(Id);
Add(Timer);
Result = true;
}
else {
esyslog("ERROR: %s: error in timer settings: %s", ServerName, v);
delete Timer;
}
}
}
else if (Code != 550)
esyslog("ERROR: %s: %s", ServerName, s);
}
return Result;
}
}
else {
cStringList ServerNames;
if (GetSVDRPServerNames(&ServerNames, sffTimers)) {
for (int i = 0; i < ServerNames.Size(); i++)
Result |= GetRemoteTimers(ServerNames[i]);
}
}
return Result;
}
bool cTimers::DelRemoteTimers(const char *ServerName)
{
bool Deleted = false;
cTimer *Timer = First(); cTimer *Timer = First();
while (Timer) { while (Timer) {
cTimer *t = Next(Timer); cTimer *t = Next(Timer);
if (Timer->Remote() && (!ServerName || strcmp(Timer->Remote(), ServerName) == 0)) { if (Timer->Remote() && (!ServerName || strcmp(Timer->Remote(), ServerName) == 0)) {
Del(Timer); Del(Timer);
Deleted = true; Result = true;
} }
Timer = t; Timer = t;
} }
return Deleted; // Add new remote timers:
} if (ServerName && RemoteTimers) {
for (int i = 0; i < RemoteTimers->Size(); i++) {
void cTimers::TriggerRemoteTimerPoll(const char *ServerName) const char *s = (*RemoteTimers)[i];
{ int Code = SVDRPCode(s);
if (ServerName) { if (Code == 250) {
if (!ExecSVDRPCommand(ServerName, cString::sprintf("POLL %s TIMERS", Setup.SVDRPHostName))) if (const char *v = SVDRPValue(s)) {
esyslog("ERROR: can't send 'POLL %s TIMERS' to '%s'", Setup.SVDRPHostName, ServerName); int Id = atoi(v);
} while (*v && *v != ' ')
else { v++; // skip id
cStringList ServerNames; cTimer *Timer = new cTimer;
if (GetSVDRPServerNames(&ServerNames)) { if (Timer->Parse(v)) {
for (int i = 0; i < ServerNames.Size(); i++) Timer->SetRemote(ServerName);
TriggerRemoteTimerPoll(ServerNames[i]); Timer->SetId(Id);
} Add(Timer);
Result = true;
}
else {
esyslog("ERROR: %s: error in timer settings: %s", ServerName, v);
delete Timer;
}
}
}
else if (Code != 550)
esyslog("ERROR: %s: %s", ServerName, s);
}
} }
return Result;
} }
static bool RemoteTimerError(const cTimer *Timer, cString *Msg) static bool RemoteTimerError(const cTimer *Timer, cString *Msg)

View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and * See the main source file 'vdr.c' for copyright information and
* how to reach the author. * how to reach the author.
* *
* $Id: timers.h 4.9 2017/10/31 09:47:14 kls Exp $ * $Id: timers.h 4.10 2018/02/25 12:54:55 kls Exp $
*/ */
#ifndef __TIMERS_H #ifndef __TIMERS_H
@ -185,21 +185,12 @@ public:
void Add(cTimer *Timer, cTimer *After = NULL); void Add(cTimer *Timer, cTimer *After = NULL);
void Ins(cTimer *Timer, cTimer *Before = NULL); void Ins(cTimer *Timer, cTimer *Before = NULL);
void Del(cTimer *Timer, bool DeleteObject = true); void Del(cTimer *Timer, bool DeleteObject = true);
bool GetRemoteTimers(const char *ServerName = NULL); bool StoreRemoteTimers(const char *ServerName = NULL, const cStringList *RemoteTimers = NULL);
///< Gets the timers from the given remote machine and adds them to this ///< Stores the given list of RemoteTimers, which come from the VDR ServerName, in
///< list. If no ServerName is given, all timers from all known remote ///< this list. If no ServerName is given, all remote timers from all peer machines
///< machines will be fetched. This function calls DelRemoteTimers() with ///< will be removed from this list. If no RemoteTimers are given, only the remote
///< the given ServerName first. ///< timers from ServerName will be removed from this list.
///< Returns true if any remote timers have been added or deleted ///< Returns true if any remote timers have been added or deleted
bool DelRemoteTimers(const char *ServerName = NULL);
///< Deletes all timers of the given remote machine from this list (leaves
///< them untouched on the remote machine). If no ServerName is given, the
///< timers of all remote machines will be deleted from the list.
///< Returns true if any remote timers have been deleted.
void TriggerRemoteTimerPoll(const char *ServerName = NULL);
///< Sends an SVDRP POLL command to the given remote machine.
///< If no ServerName is given, the POLL command will be sent to all
///< known remote machines.
}; };
bool HandleRemoteTimerModifications(cTimer *NewTimer, cTimer *OldTimer = NULL, cString *Msg = NULL); bool HandleRemoteTimerModifications(cTimer *NewTimer, cTimer *OldTimer = NULL, cString *Msg = NULL);

21
vdr.c
View File

@ -22,7 +22,7 @@
* *
* The project's page is at http://www.tvdr.de * The project's page is at http://www.tvdr.de
* *
* $Id: vdr.c 4.21 2017/11/09 16:15:34 kls Exp $ * $Id: vdr.c 4.22 2018/02/25 13:07:09 kls Exp $
*/ */
#include <getopt.h> #include <getopt.h>
@ -1081,25 +1081,18 @@ int main(int argc, char *argv[])
{ {
// Timers and Recordings: // Timers and Recordings:
bool TimersModified = false; bool TimersModified = false;
bool TriggerRemoteTimerPoll = false;
static cStateKey TimersStateKey(true); static cStateKey TimersStateKey(true);
if (cTimers::GetTimersRead(TimersStateKey)) { if (cTimers::GetTimersRead(TimersStateKey))
TriggerRemoteTimerPoll = true;
TimersStateKey.Remove(); TimersStateKey.Remove();
}
cTimers *Timers = cTimers::GetTimersWrite(TimersStateKey); cTimers *Timers = cTimers::GetTimersWrite(TimersStateKey);
// Get remote timers:
TimersModified |= Timers->GetRemoteTimers();
// Assign events to timers: // Assign events to timers:
static cStateKey SchedulesStateKey; static cStateKey SchedulesStateKey;
if (const cSchedules *Schedules = cSchedules::GetSchedulesRead(SchedulesStateKey)) if (const cSchedules *Schedules = cSchedules::GetSchedulesRead(SchedulesStateKey))
TimersModified |= Timers->SetEvents(Schedules); TimersModified |= Timers->SetEvents(Schedules);
// Must do all following calls with the exact same time! // Must do all following calls with the exact same time!
// Process ongoing recordings: // Process ongoing recordings:
if (cRecordControls::Process(Timers, Now)) { if (cRecordControls::Process(Timers, Now))
TimersModified = true; TimersModified = true;
TriggerRemoteTimerPoll = true;
}
// Must keep the lock on the schedules until after processing the record // Must keep the lock on the schedules until after processing the record
// controls, in order to avoid short interrupts in case the current event // controls, in order to avoid short interrupts in case the current event
// is replaced by a new one (which some broadcasters do, instead of just // is replaced by a new one (which some broadcasters do, instead of just
@ -1113,7 +1106,6 @@ int main(int argc, char *argv[])
else else
LastTimerChannel = Timer->Channel()->Number(); LastTimerChannel = Timer->Channel()->Number();
TimersModified = true; TimersModified = true;
TriggerRemoteTimerPoll = true;
} }
// Make sure timers "see" their channel early enough: // Make sure timers "see" their channel early enough:
static time_t LastTimerCheck = 0; static time_t LastTimerCheck = 0;
@ -1168,13 +1160,8 @@ int main(int argc, char *argv[])
LastTimerCheck = Now; LastTimerCheck = Now;
} }
// Delete expired timers: // Delete expired timers:
if (Timers->DeleteExpired()) { if (Timers->DeleteExpired())
TimersModified = true; TimersModified = true;
TriggerRemoteTimerPoll = true;
}
// Trigger remote timer polls:
if (TriggerRemoteTimerPoll)
Timers->TriggerRemoteTimerPoll();
// Make sure there is enough free disk space for ongoing recordings: // Make sure there is enough free disk space for ongoing recordings:
AssertFreeDiskSpace(Timers->GetMaxPriority()); AssertFreeDiskSpace(Timers->GetMaxPriority());
TimersStateKey.Remove(TimersModified); TimersStateKey.Remove(TimersModified);