1 /*******************************************************************************
2 
3     Handles a set of selected epoll keys. A selected epoll key is an event
4     reported by epoll bundled with its context; the context is the ISelectClient
5     object that contains the file descriptor and the event handler method.
6 
7     Copyright:
8         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
9         All rights reserved.
10 
11     License:
12         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
13         Alternatively, this file may be distributed under the terms of the Tango
14         3-Clause BSD License (see LICENSE_BSD.txt for details).
15 
16 *******************************************************************************/
17 
18 module ocean.io.select.selector.SelectedKeysHandler;
19 
20 
21 import ocean.meta.types.Qualifiers;
22 
23 import ocean.io.select.selector.model.ISelectedKeysHandler;
24 
25 import ocean.io.select.client.model.ISelectClient;
26 
27 import ocean.sys.Epoll;
28 
29 import ocean.io.select.selector.EpollException;
30 
31 import ocean.core.Verify;
32 
33 debug (ISelectClient) import ocean.io.Stdout;
34 
35 version (unittest)
36 {
37     debug = EpollFdSanity;
38 }
39 
40 debug (EpollFdSanity)
41 {
42     import ocean.io.select.selector.EpollFdSanity;
43 }
44 
45 /******************************************************************************/
46 
47 class SelectedKeysHandler: ISelectedKeysHandler
48 {
49     /***************************************************************************
50 
51        Type alias of a callback delegate to remove a client registration. Does
52        not fail/throw if the client is not registered.
53 
54        Params:
55             client = client to unregister
56             remove_from_selected_set = if true, removes the client from the
57                 selected_set that we could be currently iterating. This will allow
58                 safely destroying client instance which may be registered.
59 
60        Should return:
61             0 if everything worked as expected or the error code (errno) as a
62             warning on minor errors, that is, everything except ENOMEM (out of
63             memory) and EINVAL (invalid epoll file descriptor or epoll_ctl()
64             opcode).
65             ENOENT is a minor error that happens regularly when the client was
66             unexpectedly unregistered as it happens when its file descriptor is
67             closed.
68 
69         Should throw:
70             EpollException on the fatal errors ENOMEM and EINVAL.
71 
72     ***************************************************************************/
73 
74     public alias int delegate ( ISelectClient client,
75             bool remove_from_selected_set = false ) UnregisterDg;
76 
77     /***************************************************************************
78 
79        Callback delegate to remove a client registration, see the description
80        for the type alias above.
81 
82     ***************************************************************************/
83 
84     protected UnregisterDg unregister;
85 
86     /***************************************************************************
87 
88         Exception to throw if an error event was reported for a selected key.
89 
90     ***************************************************************************/
91 
92     private EpollException e;
93 
94     /***************************************************************************
95 
96         Constructor.
97 
98         Params:
99             unregister = callback delegate to remove a client registration, must
100                 be available during the lifetime of this instance
101             e = exception to keep and throw if an error event was reported for
102                 a selected key
103 
104     ***************************************************************************/
105 
106     public this ( scope UnregisterDg unregister, EpollException e )
107     {
108         this.unregister = unregister;
109         this.e = e;
110     }
111 
112     /***************************************************************************
113 
114         Handles the clients in selected_set.
115 
116         Params:
117             selected_set = the result list of epoll_wait()
118             unhandled_exception_hook = if not null, will be called each time
119                 event call results in unhandled exception. May both rethrow
120                 and consume exception instance after processing it.
121 
122     ***************************************************************************/
123 
124     override public void opCall ( epoll_event_t[] selected_set,
125         scope bool delegate (Exception) unhandled_exception_hook )
126     {
127         foreach (key; selected_set)
128         {
129             // EpollSelectDispatcher.unregister may invalidate the
130             // slot in the array during the unregistration of the client,
131             // so we'll skip all these.
132             if (key != epoll_event_t.init)
133             {
134                 this.handleSelectedKey(key, unhandled_exception_hook);
135             }
136         }
137     }
138 
139     /***************************************************************************
140 
141         Handles key by calling its handle() method and unregisters it if the
142         handle() call either returns false or throws an exception. In the latter
143         case the exception thrown is reported to the client by calling its
144         error() method.
145 
146         Params:
147             key = an epoll key which contains a client to be handled and the
148                   reported event
149             unhandled_exception_hook = if not null, will be called each time
150                 event call results in unhandled exception. May both rethrow
151                 and consume exception instance after processing it.
152 
153      **************************************************************************/
154 
155     final protected void handleSelectedKey ( epoll_event_t key,
156         scope bool delegate (Exception) unhandled_exception_hook )
157     {
158         debug (EpollFdSanity)
159         {
160             auto registration = FdObjEpollData.decode(key.data.u64);
161             ISelectClient client = cast (ISelectClient) registration.obj;
162 
163             verify(client !is null);
164             verify(registration.verifyFd(client.fileHandle()),
165                     "Warning: the ISelectClient.fileHandle() doesn't match the "
166                     ~ "original fd this ISelectClient was registered for");
167         }
168         else
169         {
170             ISelectClient client = cast (ISelectClient) key.data.ptr;
171         }
172 
173         debug ( ISelectClient ) this.logEvents(client, key.events);
174 
175         // Only handle clients which are registered. Clients may have
176         // already been unregistered (presumably deliberately), as a side-
177         // effect of handling previous clients, so we don't unregister them
178         // again or call their finalizers.
179         if ( client.is_registered )
180         {
181             bool unregister_key = true,
182                  error          = false;
183 
184             try
185             {
186                 this.checkKeyError(client, key.events);
187 
188                 unregister_key = !client.handle(key.events);
189 
190                 debug (ISelectClient) this.logHandled( client, unregister_key);
191             }
192             catch (Exception e)
193             {
194                 debug (ISelectClient) this.logException(client, e);
195 
196                 this.clientError(client, key.events, e);
197                 error = true;
198 
199                 if (   unhandled_exception_hook !is null
200                     && cast(EpollException)e is null)
201                 {
202                     unhandled_exception_hook(e);
203                 }
204             }
205 
206             if (unregister_key)
207             {
208                 this.unregisterAndFinalize(client,
209                                            error? client.FinalizeStatus.Error :
210                                                   client.FinalizeStatus.Success);
211             }
212         }
213     }
214 
215     /***************************************************************************
216 
217         Checks if a selection key error has occurred by checking events and
218         querying a socket error.
219 
220         Hangup states are not checked here, for the following reasons:
221             1. The hangup event is not an error on its own and may be expected
222                to happen, e.g. when short term connections are used. In that
223                case it is also possible and expected that hangup combined with
224                the read event when the remote closed the connection after having
225                data sent, and that data have not been read from the socket yet.
226             2. Experience shows that, when epoll reports a combination of read
227                and hangup event, it will keep reporting that combination even if
228                there are actually no data pending to read from the socket. In
229                that case the only way of determining whether there are data
230                pending is calling read() and comparing the return value against
231                EOF. An application that relies on an exception thrown here will
232                then run into an endless turbo event loop.
233             3. Only the application knows whether hangup events are expected or
234                exceptions. If it expects them, it may want its handler to be
235                invoked which will not happen if checkKeyError() throws an
236                exception. If it treats hangup events as exceptions, it will want
237                an exception to be thrown even if it was combined with a read or
238                write event.
239 
240         Params:
241             client = client for which an event was reported
242             events = reported events
243 
244         Throws:
245             EpollException if events contains an error code. The exception
246             thrown, which is an ErrnoIOException and an IOException, contains
247             the errno code as reported by client.error_code.
248 
249      **************************************************************************/
250 
251     private void checkKeyError ( ISelectClient client, Epoll.Event events )
252     {
253         if (events & events.EPOLLERR)
254         {
255             this.e.set(client.error_code).append(" -- error event reported for ");
256             client.fmtInfo((cstring chunk) {this.e.append(chunk);});
257             throw this.e;
258         }
259     }
260 
261     /***************************************************************************
262 
263         Unregisters and finalizes a select client. Any errors which occur while
264         calling the client's finalizer are caught and reported to the client's
265         error() method (see clientError(), below).
266 
267         Params:
268             client = client to finalize
269             status = finalize status to report to the client (e.g. success or
270                      error)
271 
272     ***************************************************************************/
273 
274     final protected void unregisterAndFinalize ( ISelectClient client,
275                                            ISelectClient.FinalizeStatus status )
276     {
277         this.unregister(client);
278 
279         try
280         {
281             client.finalize(status);
282         }
283         catch ( Exception e )
284         {
285             debug (ISelectClient)
286             {
287                 Stderr.format("{} :: Error while finalizing client: '{}'",
288                     client, e.message()).flush();
289                 if ( e.line )
290                 {
291                     Stderr.format("@ {}:{}", e.file, e.line);
292                 }
293                 Stderr.formatln("");
294             }
295             this.clientError(client, Epoll.Event.None, e);
296         }
297     }
298 
299     /***************************************************************************
300 
301         Called when an exception is thrown while handling a client (either the
302         handle() or finalize() method).
303 
304         Calls the client's error() method, and in debug builds ouputs a message.
305 
306         Params:
307             client = client which threw e
308             events = epoll events which fired for client
309             e      = exception thrown by client.handle() or client.finalize()
310 
311       **************************************************************************/
312 
313     private void clientError ( ISelectClient client, Epoll.Event events, Exception e )
314     {
315         debug (ISelectClient)
316         {
317             // FIXME: printing on separate lines for now as a workaround for a
318             // dmd bug with varargs
319             Stderr.formatln("{} :: Error during handle:", client);
320             Stderr.formatln("    '{}'", e.message()).flush();
321             if ( e.line )
322             {
323                 Stderr.formatln("    @ {}:{}", e.file, e.line).flush();
324             }
325         }
326 
327         client.error(e, events);
328     }
329 
330     /***************************************************************************
331 
332         Debug console output functions.
333 
334     ***************************************************************************/
335 
336     debug (ISelectClient):
337 
338     /***************************************************************************
339 
340         Logs that events were reported for client.
341 
342         Params:
343             client = select client for which events were reported
344             events = events reported for client
345 
346     ***************************************************************************/
347 
348     private static void logEvents ( ISelectClient client, epoll_event_t.Event events )
349     {
350         Stderr.format("{} :: Epoll firing with events ", client);
351         foreach ( event, name; epoll_event_t.event_to_name )
352         {
353             if ( events & event )
354             {
355                 Stderr.format("{}", name);
356             }
357         }
358         Stderr.formatln("").flush();
359     }
360 
361     /***************************************************************************
362 
363         Logs that client was handled.
364 
365         Params:
366             client         = handled client
367             unregister_key = true if the client is unregistered or false if it
368                              stays registered
369 
370     ***************************************************************************/
371 
372     private static void logHandled ( ISelectClient client, bool unregister_key )
373     {
374         if ( unregister_key )
375         {
376             Stderr.formatln("{} :: Handled, unregistering fd", client);
377         }
378         else
379         {
380             Stderr.formatln("{} :: Handled, leaving fd registered", client);
381         }
382         Stderr.flush();
383     }
384 
385     /***************************************************************************
386 
387         Logs that an exception was thrown while handing client. This includes
388         an error event reported by epoll.
389 
390         Params:
391             client = client that caused an error
392             e      = caught exception
393 
394     ***************************************************************************/
395 
396     private static void logException ( ISelectClient client, Exception e )
397     {
398         // FIXME: printing on separate lines for now as a workaround
399         // for a dmd bug with varargs
400 
401         version (none)
402         {
403              Stderr.formatln("{} :: ISelectClient handle exception: '{}' @{}:{}",
404                  client, e.message(), e.file, e.line);
405         }
406         else
407         {
408             Stderr.formatln("{} :: ISelectClient handle exception:", client);
409             Stderr.formatln("    '{}'", e.message());
410             Stderr.formatln("    @{}:{}", e.file, e.line);
411         }
412         Stderr.flush();
413     }
414 }