1 /*
2  * This file is part of Licq, an instant messaging client for UNIX.
3  * Copyright (C) 2013 Licq developers <licq-dev@googlegroups.com>
4  *
5  * Licq is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * Licq is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Licq; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  */
19 
20 #include <licq/mainloop.h>
21 
22 #include <cassert>
23 #include <cerrno>
24 #include <ctime>
25 #include <map>
26 #include <unistd.h>
27 #include <vector>
28 
29 #include <licq/socket.h>
30 
31 using namespace Licq;
32 
33 
34 // Base class dummy functions
35 // These should always be overloaded if used so anything ending up here is a bug
36 
rawFileEvent(int,int,int)37 void MainLoopCallback::rawFileEvent(int /* id */, int /* fd */, int /* revents */)
38 {
39   assert(0);
40 }
41 
socketEvent(int,INetSocket *,int)42 void MainLoopCallback::socketEvent(int /* id */, INetSocket* /* inetSocket */, int /* revents */)
43 {
44   assert(0);
45 }
46 
timeoutEvent(int)47 void MainLoopCallback::timeoutEvent(int /* id */)
48 {
49   assert(0);
50 }
51 
~MainLoopCallback()52 MainLoopCallback::~MainLoopCallback()
53 {
54   // Empty
55 }
56 
57 
58 class MainLoop::Private
59 {
60 public:
61   // Data for each monitored file
62   struct File
63   {
64     int fd;
65     int events;
66     struct pollfd *pfd;
67     MainLoopCallback* callback;
68     INetSocket* inetSocket;
69     bool removed;
70   };
71   typedef std::map<int, File> FileMap;
72 
73   // Data for each timeout
74   struct Timeout
75   {
76     long long last;
77     int timeout;
78     MainLoopCallback* callback;
79     bool once;
80     bool removed;
81   };
82   typedef std::map<int, Timeout> TimeoutMap;
83 
84   void addFile(int fd, MainLoopCallback* callback,
85       INetSocket* inetSocket, int events, int id);
86 
87   static long long getMonotonicClock();
88 
89   bool myIsRunning;
90   bool myFilesHasChanged;
91   bool myTimeoutsHasChanged;
92   FileMap myFiles;
93   TimeoutMap myTimeouts;
94 };
95 
getMonotonicClock()96 long long MainLoop::Private::getMonotonicClock()
97 {
98   // Get monotonic time and convert to milliseconds
99   struct timespec ts;
100 #ifdef CLOCK_MONOTONIC_RAW
101   // CLOCK_MONOTONIC_RAW is Linux specific but isn't affected by NTP adjustments
102   clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
103 #else
104   clock_gettime(CLOCK_MONOTONIC, &ts);
105 #endif
106   return static_cast<long long>(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000;
107 }
108 
MainLoop()109 MainLoop::MainLoop()
110   : myPrivate(new Private())
111 {
112   // Empty
113 }
114 
~MainLoop()115 MainLoop::~MainLoop()
116 {
117   delete myPrivate;
118 }
119 
run()120 void MainLoop::run()
121 {
122   LICQ_D();
123   d->myIsRunning = true;
124   d->myFilesHasChanged = true;
125   d->myTimeoutsHasChanged = true;
126   std::vector<struct pollfd> pfds;
127   Private::FileMap::size_type fdcount = 0;
128   long long earliest;
129   while (d->myIsRunning)
130   {
131     if (d->myFilesHasChanged)
132     {
133       pfds.resize(d->myFiles.size());
134       fdcount = 0;
135       for (Private::FileMap::iterator i = d->myFiles.begin(); i != d->myFiles.end(); )
136       {
137         if (i->second.removed) {
138           // Actually erase files marked for removal
139           d->myFiles.erase(i++);
140           continue;
141         }
142         struct pollfd* p = &pfds[fdcount++];
143         p->fd = i->second.fd;
144         p->events = i->second.events;
145         i->second.pfd = p;
146         ++i;
147       }
148 
149       d->myFilesHasChanged = false;
150     }
151 
152     if (d->myTimeoutsHasChanged)
153     {
154       earliest = 0;
155       for (Private::TimeoutMap::iterator i = d->myTimeouts.begin(); i != d->myTimeouts.end(); )
156       {
157         if (i->second.removed)
158         {
159           // Actually erase timeouts marked for removal
160           d->myTimeouts.erase(i++);
161           continue;
162         }
163 
164         // Find the earliest timeout
165         if (earliest == 0 || i->second.last + i->second.timeout < earliest)
166           earliest = i->second.last + i->second.timeout;
167 
168         ++i;
169       }
170       d->myTimeoutsHasChanged = false;
171     }
172 
173     int timeout; // Timeout in milliseconds
174     if (earliest == 0)
175     {
176       // No timeouts, wait forever
177       timeout = -1;
178     }
179     else
180     {
181       // Make timeout relative to current timestamp
182       timeout = earliest - Private::getMonotonicClock();
183       if (timeout < 0)
184         timeout = 0;
185     }
186 
187     int pollret = poll(&pfds.front(), fdcount, timeout);
188 
189     if (pollret < 0)
190     {
191       assert(errno == EINTR);
192       continue;
193     }
194 
195     if (pollret > 0)
196     {
197       // Check for file events to handle
198       for (Private::FileMap::iterator i = d->myFiles.begin(); i != d->myFiles.end(); ++i)
199       {
200         Private::File& f(i->second);
201 
202         if (f.removed)
203           continue;
204 
205         if (f.pfd == NULL)
206           /* File has been added after poll was called, just ignore it */
207           continue;
208 
209         int revents = f.pfd->revents;
210         if (revents == 0)
211           continue;
212 
213         if (f.inetSocket != NULL)
214           f.callback->socketEvent(i->first, f.inetSocket, revents);
215         else
216           f.callback->rawFileEvent(i->first, f.fd, revents);
217       }
218     }
219 
220     if (earliest > 0)
221     {
222       long long now = Private::getMonotonicClock();
223       if (now >= earliest)
224       {
225         // Check for expired timeouts
226         for (Private::TimeoutMap::iterator i = d->myTimeouts.begin(); i != d->myTimeouts.end(); ++i)
227         {
228           Private::Timeout& t(i->second);
229 
230           if (t.removed)
231             continue;
232 
233           if (t.last + t.timeout > now)
234             // Not yet
235             continue;
236 
237           if (t.once)
238             // Timeout isn't reoccuring, mark it for removal
239             t.removed = true;
240           else
241             // Timeout is reoccuring, update timestamp
242             t.last += t.timeout;
243 
244           d->myTimeoutsHasChanged = true;
245           t.callback->timeoutEvent(i->first);
246         }
247       }
248     }
249   }
250 }
251 
quit()252 void MainLoop::quit()
253 {
254   LICQ_D();
255 
256   d->myIsRunning = false;
257 }
258 
addFile(int fd,MainLoopCallback * callback,INetSocket * inetSocket,int events,int id)259 void MainLoop::Private::addFile(int fd, MainLoopCallback* callback,
260     INetSocket* inetSocket, int events, int id)
261 {
262   assert(callback != NULL);
263   assert(fd > -1);
264   if (id == -1)
265     id = fd;
266   assert(myFiles.count(id) == 0 || myFiles[id].removed);
267 
268   Private::File& f = myFiles[id];
269   f.fd = fd;
270   f.events = events;
271   f.callback = callback;
272   f.inetSocket = inetSocket;
273   f.pfd = NULL;
274   f.removed = false;
275 
276   myFilesHasChanged = true;
277 }
278 
addRawFile(int fd,MainLoopCallback * callback,int events,int id)279 void MainLoop::addRawFile(int fd, MainLoopCallback* callback, int events, int id)
280 {
281   LICQ_D();
282 
283   d->addFile(fd, callback, NULL, events, id);
284 }
285 
removeFile(int id)286 void MainLoop::removeFile(int id)
287 {
288   LICQ_D();
289 
290   d->myFiles[id].removed = true;
291   d->myFilesHasChanged = true;
292 }
293 
removeRawFile(int fd)294 void MainLoop::removeRawFile(int fd)
295 {
296   LICQ_D();
297 
298   for (Private::FileMap::iterator i = d->myFiles.begin(); i != d->myFiles.end(); ++i)
299     if (i->second.fd == fd)
300       i->second.removed = true;
301 
302   d->myFilesHasChanged = true;
303 }
304 
addSocket(INetSocket * inetSocket,MainLoopCallback * callback,int events,int id)305 void MainLoop::addSocket(INetSocket* inetSocket, MainLoopCallback* callback, int events, int id)
306 {
307   LICQ_D();
308 
309   assert(inetSocket != NULL);
310   int fd = inetSocket->Descriptor();
311   d->addFile(fd, callback, inetSocket, events, id);
312 }
313 
removeSocket(INetSocket * inetSocket)314 void MainLoop::removeSocket(INetSocket* inetSocket)
315 {
316   assert(inetSocket != NULL);
317   removeRawFile(inetSocket->Descriptor());
318 }
319 
addTimeout(int timeout,MainLoopCallback * callback,int id,bool once)320 void MainLoop::addTimeout(int timeout, MainLoopCallback* callback, int id, bool once)
321 {
322   LICQ_D();
323 
324   assert(callback != NULL);
325 
326   // Don't allow a timeout of zero unless it's a oneshot
327   assert(timeout > 0 || once);
328 
329   // Allow reusing an entry if it has been marked for removal
330   Private::TimeoutMap::const_iterator iter(d->myTimeouts.find(id));
331   assert(iter == d->myTimeouts.end() || iter->second.removed);
332 
333   Private::Timeout& t(d->myTimeouts[id]);
334   t.last = Private::getMonotonicClock();
335   t.timeout = timeout;
336   t.callback = callback;
337   t.once = once;
338   t.removed = false;
339   d->myTimeoutsHasChanged = true;
340 }
341 
removeTimeout(int id)342 void MainLoop::removeTimeout(int id)
343 {
344   LICQ_D();
345 
346   Private::TimeoutMap::iterator iter(d->myTimeouts.find(id));
347   if (iter == d->myTimeouts.end())
348     return;
349 
350   // Don't break main loop if it currently has an iterator in the list
351   // Just mark the entry as removed and let main loop remove it when it's safe
352   iter->second.removed = true;
353   d->myTimeoutsHasChanged = true;
354 }
355 
removeCallback(const MainLoopCallback * callback,bool closeDelete)356 void MainLoop::removeCallback(const MainLoopCallback* callback, bool closeDelete)
357 {
358   LICQ_D();
359 
360   // Find and remove all files with this callback object
361   for (Private::FileMap::iterator i = d->myFiles.begin(); i != d->myFiles.end(); ++i)
362   {
363     if (i->second.callback == callback && !i->second.removed)
364     {
365       if (closeDelete)
366       {
367         if (i->second.inetSocket != NULL)
368           delete i->second.inetSocket;
369         else
370           close(i->second.fd);
371       }
372       i->second.removed = true;
373       d->myFilesHasChanged = true;
374     }
375   }
376 
377   // Find and remove all timeouts with this callback object
378   for (Private::TimeoutMap::iterator i = d->myTimeouts.begin(); i != d->myTimeouts.end(); ++i)
379   {
380     if (i->second.callback == callback)
381     {
382       i->second.removed = true;
383       d->myTimeoutsHasChanged = true;
384     }
385   }
386 }
387 
getSocketFromFd(int fd)388 INetSocket* MainLoop::getSocketFromFd(int fd)
389 {
390   LICQ_D();
391 
392   for (Private::FileMap::iterator i = d->myFiles.begin(); i != d->myFiles.end(); ++i)
393     if (i->second.fd == fd)
394       return i->second.inetSocket;
395 
396   return NULL;
397 }
398