ModSelect.cc
Go to the documentation of this file.
1 /*
2  * Copyright (C) 1996-2025 The Squid Software Foundation and contributors
3  *
4  * Squid software is distributed under GPLv2+ license and includes
5  * contributions from numerous individuals and organizations.
6  * Please see the COPYING and CONTRIBUTORS files for details.
7  */
8 
9 /* DEBUG: section 05 Socket Functions */
10 
11 #include "squid.h"
12 
13 #if USE_SELECT
14 
15 #include "anyp/PortCfg.h"
16 #include "comm/Connection.h"
17 #include "comm/Loops.h"
18 #include "compat/select.h"
19 #include "fde.h"
20 #include "globals.h"
21 #include "ICP.h"
22 #include "mgr/Registration.h"
23 #include "SquidConfig.h"
24 #include "StatCounters.h"
25 #include "StatHist.h"
26 #include "Store.h"
27 
28 #include <cerrno>
29 #if HAVE_SYS_STAT_H
30 #include <sys/stat.h>
31 #endif
32 
33 static int MAX_POLL_TIME = 1000; /* see also Comm::QuickPollRequired() */
34 
35 #ifndef howmany
36 #define howmany(x, y) (((x)+((y)-1))/(y))
37 #endif
38 #ifndef NBBY
39 #define NBBY 8
40 #endif
41 #define FD_MASK_BYTES sizeof(fd_mask)
42 #define FD_MASK_BITS (FD_MASK_BYTES*NBBY)
43 
44 /* STATIC */
45 static int examine_select(fd_set *, fd_set *);
46 static int fdIsTcpListener(int fd);
47 static int fdIsUdpListener(int fd);
48 static int fdIsDns(int fd);
50 static int comm_check_incoming_select_handlers(int nfds, int *fds);
51 static void comm_select_dns_incoming(void);
52 static void commUpdateReadBits(int fd, PF * handler);
53 static void commUpdateWriteBits(int fd, PF * handler);
54 
55 static struct timeval zero_tv;
56 static fd_set global_readfds;
57 static fd_set global_writefds;
58 static int nreadfds;
59 static int nwritefds;
60 
61 void
62 Comm::SetSelect(int fd, unsigned int type, PF * handler, void *client_data, time_t timeout)
63 {
64  fde *F = &fd_table[fd];
65  assert(fd >= 0);
66  assert(F->flags.open || (!handler && !client_data && !timeout));
67  debugs(5, 5, "FD " << fd << ", type=" << type <<
68  ", handler=" << handler << ", client_data=" << client_data <<
69  ", timeout=" << timeout);
70 
71  if (type & COMM_SELECT_READ) {
72  F->read_handler = handler;
73  F->read_data = client_data;
74  commUpdateReadBits(fd, handler);
75  }
76 
77  if (type & COMM_SELECT_WRITE) {
78  F->write_handler = handler;
79  F->write_data = client_data;
80  commUpdateWriteBits(fd, handler);
81  }
82 
83  if (timeout)
84  F->timeout = squid_curtime + timeout;
85 }
86 
87 static int
89 {
90  if (icpIncomingConn != nullptr && fd == icpIncomingConn->fd)
91  return 1;
92 
93  if (icpOutgoingConn != nullptr && fd == icpOutgoingConn->fd)
94  return 1;
95 
96  return 0;
97 }
98 
99 static int
100 fdIsDns(int fd)
101 {
102  if (fd == DnsSocketA)
103  return 1;
104 
105  if (fd == DnsSocketB)
106  return 1;
107 
108  return 0;
109 }
110 
111 static int
113 {
114  for (AnyP::PortCfgPointer s = HttpPortList; s != nullptr; s = s->next) {
115  if (s->listenConn != nullptr && s->listenConn->fd == fd)
116  return 1;
117  }
118 
119  return 0;
120 }
121 
122 static int
124 {
125  int i;
126  int fd;
127  int maxfd = 0;
128  PF *hdl = nullptr;
129  fd_set read_mask;
130  fd_set write_mask;
131  FD_ZERO(&read_mask);
132  FD_ZERO(&write_mask);
134 
135  for (i = 0; i < nfds; ++i) {
136  fd = fds[i];
137 
138  if (fd_table[fd].read_handler) {
139  FD_SET(fd, &read_mask);
140 
141  if (fd > maxfd)
142  maxfd = fd;
143  }
144 
145  if (fd_table[fd].write_handler) {
146  FD_SET(fd, &write_mask);
147 
148  if (fd > maxfd)
149  maxfd = fd;
150  }
151  }
152 
153  if (maxfd++ == 0)
154  return -1;
155 
156  getCurrentTime();
157 
159 
160  if (xselect(maxfd, &read_mask, &write_mask, nullptr, &zero_tv) < 1)
162 
163  for (i = 0; i < nfds; ++i) {
164  fd = fds[i];
165 
166  if (FD_ISSET(fd, &read_mask)) {
167  if ((hdl = fd_table[fd].read_handler) != nullptr) {
168  fd_table[fd].read_handler = nullptr;
169  commUpdateReadBits(fd, nullptr);
170  hdl(fd, fd_table[fd].read_data);
171  } else {
172  debugs(5, DBG_IMPORTANT, "comm_select_incoming: FD " << fd << " NULL read handler");
173  }
174  }
175 
176  if (FD_ISSET(fd, &write_mask)) {
177  if ((hdl = fd_table[fd].write_handler) != nullptr) {
178  fd_table[fd].write_handler = nullptr;
179  commUpdateWriteBits(fd, nullptr);
180  hdl(fd, fd_table[fd].write_data);
181  } else {
182  debugs(5, DBG_IMPORTANT, "comm_select_incoming: FD " << fd << " NULL write handler");
183  }
184  }
185  }
186 
188 }
189 
190 static void
192 {
193  int nfds = 0;
194  int fds[2];
195 
197  fds[nfds] = icpIncomingConn->fd;
198  ++nfds;
199  }
200 
202  fds[nfds] = icpOutgoingConn->fd;
203  ++nfds;
204  }
205 
206  if (statCounter.comm_udp.startPolling(nfds)) {
207  auto n = comm_check_incoming_select_handlers(nfds, fds);
209  }
210 }
211 
212 static void
214 {
215  int nfds = 0;
216  int fds[MAXTCPLISTENPORTS];
217 
218  // XXX: only poll sockets that won't be deferred. But how do we identify them?
219 
220  for (AnyP::PortCfgPointer s = HttpPortList; s != nullptr; s = s->next) {
221  if (Comm::IsConnOpen(s->listenConn)) {
222  fds[nfds] = s->listenConn->fd;
223  ++nfds;
224  }
225  }
226 
227  if (statCounter.comm_tcp.startPolling(nfds)) {
228  auto n = comm_check_incoming_select_handlers(nfds, fds);
230  }
231 }
232 
233 /* Select on all sockets; call handlers for those that are ready. */
235 Comm::DoSelect(int msec)
236 {
237  fd_set readfds;
238  fd_set pendingfds;
239  fd_set writefds;
240 
241  PF *hdl = nullptr;
242  int fd;
243  int maxfd;
244  int num;
245  int pending;
246  int calldns = 0, calludp = 0, calltcp = 0;
247  int maxindex;
248  unsigned int k;
249  int j;
250  fd_mask *fdsp;
251  fd_mask *pfdsp;
252  fd_mask tmask;
253 
254  struct timeval poll_time;
255  double timeout = current_dtime + (msec / 1000.0);
256  fde *F;
257 
258  do {
259  double start;
260  getCurrentTime();
261  start = current_dtime;
262 
263  if (statCounter.comm_udp.check())
265 
266  if (statCounter.comm_dns.check())
268 
269  if (statCounter.comm_tcp.check())
271 
272  calldns = calludp = calltcp = 0;
273 
274  maxfd = Biggest_FD + 1;
275 
276  memcpy(&readfds, &global_readfds,
278 
279  memcpy(&writefds, &global_writefds,
281 
282  /* remove stalled FDs, and deal with pending descriptors */
283  pending = 0;
284 
285  FD_ZERO(&pendingfds);
286 
287  maxindex = howmany(maxfd, FD_MASK_BITS);
288 
289  fdsp = (fd_mask *) & readfds;
290 
291  for (j = 0; j < maxindex; ++j) {
292  if ((tmask = fdsp[j]) == 0)
293  continue; /* no bits here */
294 
295  for (k = 0; k < FD_MASK_BITS; ++k) {
296  if (!EBIT_TEST(tmask, k))
297  continue;
298 
299  /* Found a set bit */
300  fd = (j * FD_MASK_BITS) + k;
301 
302  if (FD_ISSET(fd, &readfds) && fd_table[fd].flags.read_pending) {
303  FD_SET(fd, &pendingfds);
304  ++pending;
305  }
306  }
307  }
308 
309  if (nreadfds + nwritefds == 0) {
311  return Comm::SHUTDOWN;
312  }
313 
314  if (msec > MAX_POLL_TIME)
315  msec = MAX_POLL_TIME;
316 
317  if (pending)
318  msec = 0;
319 
320  for (;;) {
321  poll_time.tv_sec = msec / 1000;
322  poll_time.tv_usec = (msec % 1000) * 1000;
324  num = xselect(maxfd, &readfds, &writefds, nullptr, &poll_time);
325  int xerrno = errno;
327 
328  if (num >= 0 || pending > 0)
329  break;
330 
331  if (ignoreErrno(xerrno))
332  break;
333 
334  debugs(5, DBG_CRITICAL, MYNAME << "select failure: " << xstrerr(xerrno));
335 
336  examine_select(&readfds, &writefds);
337 
338  return Comm::COMM_ERROR;
339 
340  /* NOTREACHED */
341  }
342 
343  if (num < 0 && !pending)
344  continue;
345 
346  getCurrentTime();
347 
348  debugs(5, num ? 5 : 8, "comm_select: " << num << "+" << pending << " FDs ready");
349 
351 
352  if (num == 0 && pending == 0)
353  continue;
354 
355  /* Scan return fd masks for ready descriptors */
356  fdsp = (fd_mask *) & readfds;
357 
358  pfdsp = (fd_mask *) & pendingfds;
359 
360  maxindex = howmany(maxfd, FD_MASK_BITS);
361 
362  for (j = 0; j < maxindex; ++j) {
363  if ((tmask = (fdsp[j] | pfdsp[j])) == 0)
364  continue; /* no bits here */
365 
366  for (k = 0; k < FD_MASK_BITS; ++k) {
367  if (tmask == 0)
368  break; /* no more bits left */
369 
370  if (!EBIT_TEST(tmask, k))
371  continue;
372 
373  /* Found a set bit */
374  fd = (j * FD_MASK_BITS) + k;
375 
376  EBIT_CLR(tmask, k); /* this will be done */
377 
378  if (fdIsUdpListener(fd)) {
379  calludp = 1;
380  continue;
381  }
382 
383  if (fdIsDns(fd)) {
384  calldns = 1;
385  continue;
386  }
387 
388  if (fdIsTcpListener(fd)) {
389  calltcp = 1;
390  continue;
391  }
392 
393  F = &fd_table[fd];
394  debugs(5, 6, "comm_select: FD " << fd << " ready for reading");
395 
396  if (nullptr == (hdl = F->read_handler))
397  (void) 0;
398  else {
399  F->read_handler = nullptr;
400  commUpdateReadBits(fd, nullptr);
401  hdl(fd, F->read_data);
403 
404  if (statCounter.comm_udp.check())
406 
407  if (statCounter.comm_dns.check())
409 
410  if (statCounter.comm_tcp.check())
412  }
413  }
414  }
415 
416  fdsp = (fd_mask *) & writefds;
417 
418  for (j = 0; j < maxindex; ++j) {
419  if ((tmask = fdsp[j]) == 0)
420  continue; /* no bits here */
421 
422  for (k = 0; k < FD_MASK_BITS; ++k) {
423  if (tmask == 0)
424  break; /* no more bits left */
425 
426  if (!EBIT_TEST(tmask, k))
427  continue;
428 
429  /* Found a set bit */
430  fd = (j * FD_MASK_BITS) + k;
431 
432  EBIT_CLR(tmask, k); /* this will be done */
433 
434  if (fdIsUdpListener(fd)) {
435  calludp = 1;
436  continue;
437  }
438 
439  if (fdIsDns(fd)) {
440  calldns = 1;
441  continue;
442  }
443 
444  if (fdIsTcpListener(fd)) {
445  calltcp = 1;
446  continue;
447  }
448 
449  F = &fd_table[fd];
450  debugs(5, 6, "comm_select: FD " << fd << " ready for writing");
451 
452  if ((hdl = F->write_handler)) {
453  F->write_handler = nullptr;
454  commUpdateWriteBits(fd, nullptr);
455  hdl(fd, F->write_data);
457 
458  if (statCounter.comm_udp.check())
460 
461  if (statCounter.comm_dns.check())
463 
464  if (statCounter.comm_tcp.check())
466  }
467  }
468  }
469 
470  if (calludp)
472 
473  if (calldns)
475 
476  if (calltcp)
478 
479  getCurrentTime();
480 
482 
483  return Comm::OK;
484  } while (timeout > current_dtime);
485  debugs(5, 8, "comm_select: time out: " << squid_curtime);
486 
487  return Comm::TIMEOUT;
488 }
489 
490 static void
492 {
493  int nfds = 0;
494  int fds[3];
495 
496  if (DnsSocketA >= 0) {
497  fds[nfds] = DnsSocketA;
498  ++nfds;
499  }
500 
501  if (DnsSocketB >= 0) {
502  fds[nfds] = DnsSocketB;
503  ++nfds;
504  }
505 
506  if (statCounter.comm_dns.startPolling(nfds)) {
507  auto n = comm_check_incoming_select_handlers(nfds, fds);
509  }
510 }
511 
512 void
514 {
515  zero_tv.tv_sec = 0;
516  zero_tv.tv_usec = 0;
517  FD_ZERO(&global_readfds);
518  FD_ZERO(&global_writefds);
519  nreadfds = nwritefds = 0;
520 
521  Mgr::RegisterAction("comm_select_incoming",
522  "comm_incoming() stats",
523  commIncomingStats, 0, 1);
524 }
525 
526 /*
527  * examine_select - debug routine.
528  *
529  * I spend the day chasing this core dump that occurs when both the client
530  * and the server side of a cache fetch simultaneoulsy abort the
531  * connection. While I haven't really studied the code to figure out how
532  * it happens, the snippet below may prevent the cache from exiting:
533  *
534  * Call this from where the select loop fails.
535  */
536 static int
537 examine_select(fd_set * readfds, fd_set * writefds)
538 {
539  int fd = 0;
540  fd_set read_x;
541  fd_set write_x;
542 
543  struct timeval tv;
544  AsyncCall::Pointer ch = nullptr;
545  fde *F = nullptr;
546 
547  struct stat sb;
548  debugs(5, DBG_CRITICAL, "examine_select: Examining open file descriptors...");
549 
550  for (fd = 0; fd < Squid_MaxFD; ++fd) {
551  FD_ZERO(&read_x);
552  FD_ZERO(&write_x);
553  tv.tv_sec = tv.tv_usec = 0;
554 
555  if (FD_ISSET(fd, readfds))
556  FD_SET(fd, &read_x);
557  else if (FD_ISSET(fd, writefds))
558  FD_SET(fd, &write_x);
559  else
560  continue;
561 
563  errno = 0;
564 
565  if (!fstat(fd, &sb)) {
566  debugs(5, 5, "FD " << fd << " is valid.");
567  continue;
568  }
569  int xerrno = errno;
570 
571  F = &fd_table[fd];
572  debugs(5, DBG_CRITICAL, "fstat(FD " << fd << "): " << xstrerr(xerrno));
573  debugs(5, DBG_CRITICAL, "WARNING: FD " << fd << " has handlers, but it's invalid.");
574  debugs(5, DBG_CRITICAL, "FD " << fd << " is a " << fdTypeStr[F->type] << " called '" << F->desc << "'");
575  debugs(5, DBG_CRITICAL, "tmout:" << F->timeoutHandler << " read:" << F->read_handler << " write:" << F->write_handler);
576 
577  for (ch = F->closeHandler; ch != nullptr; ch = ch->Next())
578  debugs(5, DBG_CRITICAL, " close handler: " << ch);
579 
580  if (F->closeHandler != nullptr) {
582  } else if (F->timeoutHandler != nullptr) {
583  debugs(5, DBG_CRITICAL, "examine_select: Calling Timeout Handler");
585  }
586 
587  F->closeHandler = nullptr;
588  F->timeoutHandler = nullptr;
589  F->read_handler = nullptr;
590  F->write_handler = nullptr;
591  FD_CLR(fd, readfds);
592  FD_CLR(fd, writefds);
593  }
594 
595  return 0;
596 }
597 
598 static void
600 {
601  storeAppendPrintf(sentry, "Current incoming_udp_interval: %d\n",
603  storeAppendPrintf(sentry, "Current incoming_dns_interval: %d\n",
605  storeAppendPrintf(sentry, "Current incoming_tcp_interval: %d\n",
607  storeAppendPrintf(sentry, "\n");
608  storeAppendPrintf(sentry, "Histogram of events per incoming socket type\n");
609  storeAppendPrintf(sentry, "ICP Messages handled per comm_select_udp_incoming() call:\n");
611  storeAppendPrintf(sentry, "DNS Messages handled per comm_select_dns_incoming() call:\n");
613  storeAppendPrintf(sentry, "HTTP Messages handled per comm_select_tcp_incoming() call:\n");
615 }
616 
617 void
618 commUpdateReadBits(int fd, PF * handler)
619 {
620  if (handler && !FD_ISSET(fd, &global_readfds)) {
621  FD_SET(fd, &global_readfds);
622  ++nreadfds;
623  } else if (!handler && FD_ISSET(fd, &global_readfds)) {
624  FD_CLR(fd, &global_readfds);
625  --nreadfds;
626  }
627 }
628 
629 void
630 commUpdateWriteBits(int fd, PF * handler)
631 {
632  if (handler && !FD_ISSET(fd, &global_writefds)) {
633  FD_SET(fd, &global_writefds);
634  ++nwritefds;
635  } else if (!handler && FD_ISSET(fd, &global_writefds)) {
636  FD_CLR(fd, &global_writefds);
637  --nwritefds;
638  }
639 }
640 
641 /* Called by async-io or diskd to speed up the polling */
642 void
644 {
645  MAX_POLL_TIME = 10;
646 }
647 
648 #endif /* USE_SELECT */
649 
#define EBIT_CLR(flag, bit)
Definition: defines.h:66
int DnsSocketB
const char * xstrerr(int error)
Definition: xstrerror.cc:83
double current_dtime
the current UNIX time in seconds (with microsecond precision)
Definition: stub_libtime.cc:19
bool check()
Definition: Incoming.h:86
static struct timeval zero_tv
Definition: ModSelect.cc:55
int incoming_sockets_accepted
#define DBG_CRITICAL
Definition: Stream.h:37
AnyP::PortCfgPointer HttpPortList
list of Squid http(s)_port configured
Definition: PortCfg.cc:22
int DnsSocketA
#define ScheduleCallHere(call)
Definition: AsyncCall.h:166
void storeAppendPrintf(StoreEntry *e, const char *fmt,...)
Definition: store.cc:855
void commCallCloseHandlers(int fd)
Definition: comm.cc:744
struct SquidConfig::CommIncoming comm_incoming
Comm::ConnectionPointer icpOutgoingConn
Definition: icp_v2.cc:101
Comm::Flag DoSelect(int)
Do poll and trigger callback functions as appropriate.
Definition: ModDevPoll.cc:308
static void comm_select_dns_incoming(void)
Definition: ModSelect.cc:491
static int fdIsTcpListener(int fd)
Definition: ModSelect.cc:112
Comm::Incoming comm_dns
Definition: StatCounters.h:125
@ TIMEOUT
Definition: Flag.h:18
static const int Factor
Definition: Incoming.h:50
static int fdIsUdpListener(int fd)
Definition: ModSelect.cc:88
bool IsConnOpen(const Comm::ConnectionPointer &conn)
Definition: Connection.cc:27
@ OK
Definition: Flag.h:16
struct SquidConfig::CommIncoming::Measure udp
bool startPolling(int n)
Definition: Incoming.h:68
static void comm_select_tcp_incoming(void)
Definition: ModSelect.cc:213
void * read_data
Definition: fde.h:150
Definition: fde.h:51
double select_time
Definition: StatCounters.h:120
static int nreadfds
Definition: ModSelect.cc:58
Comm::Incoming comm_udp
Definition: StatCounters.h:127
AsyncCall::Pointer closeHandler
Definition: fde.h:157
static int nwritefds
Definition: ModSelect.cc:59
void OBJH(StoreEntry *)
Definition: forward.h:44
PF * write_handler
Definition: fde.h:151
time_t timeout
Definition: fde.h:154
time_t getCurrentTime() STUB_RETVAL(0) int tvSubUsec(struct timeval
void dump(StoreEntry *sentry, StatHistBinDumper *bd) const
Definition: StatHist.cc:171
struct fde::_fde_flags flags
struct SquidConfig::CommIncoming::Measure dns
#define EBIT_TEST(flag, bit)
Definition: defines.h:67
void * write_data
Definition: fde.h:152
unsigned int type
Definition: fde.h:105
unsigned long fd_mask
Definition: types.h:133
bool open
Definition: fde.h:118
#define howmany(x, y)
Definition: ModSelect.cc:36
void count(double val)
Definition: StatHist.cc:55
#define assert(EX)
Definition: assert.h:17
static int examine_select(fd_set *, fd_set *)
Definition: ModSelect.cc:537
PF * read_handler
Definition: fde.h:149
@ COMM_ERROR
Definition: Flag.h:17
#define COMM_SELECT_READ
Definition: defines.h:24
time_t squid_curtime
Definition: stub_libtime.cc:20
static fd_set global_writefds
Definition: ModSelect.cc:57
int Squid_MaxFD
void SelectLoopInit(void)
Initialize the module on Squid startup.
Definition: ModDevPoll.cc:170
static OBJH commIncomingStats
Definition: ModSelect.cc:49
StatHist history
Definition: Incoming.h:104
static fd_set global_readfds
Definition: ModSelect.cc:56
Comm::ConnectionPointer icpIncomingConn
Definition: icp_v2.cc:99
Flag
Definition: Flag.h:15
int ignoreErrno(int ierrno)
Definition: comm.cc:1407
static void comm_select_udp_incoming(void)
Definition: ModSelect.cc:191
#define fd_table
Definition: fde.h:189
StatHist select_fds_hist
Definition: StatCounters.h:129
static void commUpdateWriteBits(int fd, PF *handler)
Definition: ModSelect.cc:630
struct SquidConfig::CommIncoming::Measure tcp
static void commUpdateReadBits(int fd, PF *handler)
Definition: ModSelect.cc:618
int xselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
POSIX select(2) equivalent.
Definition: select.h:22
unsigned long int select_loops
Definition: StatCounters.h:118
StatHistBinDumper statHistIntDumper
Definition: StatHist.h:119
void SetSelect(int, unsigned int, PF *, void *, time_t)
Mark an FD to be watched for its IO status.
Definition: ModDevPoll.cc:220
static int comm_check_incoming_select_handlers(int nfds, int *fds)
Definition: ModSelect.cc:123
void RegisterAction(char const *action, char const *desc, OBJH *handler, Protected, Atomic, Format)
Definition: Registration.cc:54
char desc[FD_DESC_SZ]
Definition: fde.h:115
#define FD_MASK_BITS
Definition: ModSelect.cc:42
void finishPolling(int, SquidConfig::CommIncoming::Measure &)
Definition: Incoming.cc:15
#define DBG_IMPORTANT
Definition: Stream.h:38
#define MYNAME
Definition: Stream.h:219
int shutting_down
static int MAX_POLL_TIME
Definition: ModSelect.cc:33
#define FD_MASK_BYTES
Definition: ModSelect.cc:41
AsyncCall::Pointer & Next()
Definition: AsyncCall.h:66
AsyncCall::Pointer timeoutHandler
Definition: fde.h:153
const char * fdTypeStr[]
Definition: fd.cc:34
Comm::Incoming comm_tcp
Definition: StatCounters.h:126
#define MAXTCPLISTENPORTS
Definition: PortCfg.h:86
static int fdIsDns(int fd)
Definition: ModSelect.cc:100
#define debugs(SECTION, LEVEL, CONTENT)
Definition: Stream.h:192
#define COMM_SELECT_WRITE
Definition: defines.h:25
void QuickPollRequired(void)
Definition: ModDevPoll.cc:414
int Biggest_FD
void PF(int, void *)
Definition: forward.h:18
class SquidConfig Config
Definition: SquidConfig.cc:12
struct StatCounters::@112 syscalls
StatCounters statCounter
Definition: StatCounters.cc:12
@ SHUTDOWN
Definition: Flag.h:19

 

Introduction

Documentation

Support

Miscellaneous