| /* vi: set sw=4 ts=4: */ |
| /* |
| * Generic non-forking server infrastructure. |
| * Intended to make writing telnetd-type servers easier. |
| * |
| * Copyright (C) 2007 Denis Vlasenko |
| * |
| * Licensed under GPL version 2, see file LICENSE in this tarball for details. |
| */ |
| |
| #include "busybox.h" |
| #include "isrv.h" |
| |
| #define DEBUG 0 |
| |
| #if DEBUG |
| #define DPRINTF(args...) bb_error_msg(args) |
| #else |
| #define DPRINTF(args...) ((void)0) |
| #endif |
| |
| /* Helpers */ |
| |
| /* Even if _POSIX_MONOTONIC_CLOCK is defined, this |
| * may require librt */ |
| #if 0 /*def _POSIX_MONOTONIC_CLOCK*/ |
| static time_t monotonic_time(void) |
| { |
| struct timespec ts; |
| if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) |
| time(&ts.tv_sec); |
| return ts.tv_sec; |
| } |
| #else |
| #define monotonic_time() (time(NULL)) |
| #endif |
| |
| /* Opaque structure */ |
| |
| struct isrv_state_t { |
| short *fd2peer; /* one per registered fd */ |
| void **param_tbl; /* one per registered peer */ |
| /* one per registered peer; doesn't exist if !timeout */ |
| time_t *timeo_tbl; |
| int (*new_peer)(isrv_state_t *state, int fd); |
| time_t curtime; |
| int timeout; |
| int fd_count; |
| int peer_count; |
| int wr_count; |
| fd_set rd; |
| fd_set wr; |
| }; |
| #define FD2PEER (state->fd2peer) |
| #define PARAM_TBL (state->param_tbl) |
| #define TIMEO_TBL (state->timeo_tbl) |
| #define CURTIME (state->curtime) |
| #define TIMEOUT (state->timeout) |
| #define FD_COUNT (state->fd_count) |
| #define PEER_COUNT (state->peer_count) |
| #define WR_COUNT (state->wr_count) |
| |
| /* callback */ |
| void isrv_want_rd(isrv_state_t *state, int fd) |
| { |
| FD_SET(fd, &state->rd); |
| } |
| |
| /* callback */ |
| void isrv_want_wr(isrv_state_t *state, int fd) |
| { |
| if (!FD_ISSET(fd, &state->wr)) { |
| WR_COUNT++; |
| FD_SET(fd, &state->wr); |
| } |
| } |
| |
| /* callback */ |
| void isrv_dont_want_rd(isrv_state_t *state, int fd) |
| { |
| FD_CLR(fd, &state->rd); |
| } |
| |
| /* callback */ |
| void isrv_dont_want_wr(isrv_state_t *state, int fd) |
| { |
| if (FD_ISSET(fd, &state->wr)) { |
| WR_COUNT--; |
| FD_CLR(fd, &state->wr); |
| } |
| } |
| |
| /* callback */ |
| int isrv_register_fd(isrv_state_t *state, int peer, int fd) |
| { |
| int n; |
| |
| DPRINTF("register_fd(peer:%d,fd:%d)", peer, fd); |
| |
| if (FD_COUNT >= FD_SETSIZE) return -1; |
| if (FD_COUNT <= fd) { |
| n = FD_COUNT; |
| FD_COUNT = fd + 1; |
| |
| DPRINTF("register_fd: FD_COUNT %d", FD_COUNT); |
| |
| FD2PEER = xrealloc(FD2PEER, FD_COUNT * sizeof(FD2PEER[0])); |
| while (n < fd) FD2PEER[n++] = -1; |
| } |
| |
| DPRINTF("register_fd: FD2PEER[%d] = %d", fd, peer); |
| |
| FD2PEER[fd] = peer; |
| return 0; |
| } |
| |
| /* callback */ |
| void isrv_close_fd(isrv_state_t *state, int fd) |
| { |
| DPRINTF("close_fd(%d)", fd); |
| |
| close(fd); |
| isrv_dont_want_rd(state, fd); |
| if (WR_COUNT) isrv_dont_want_wr(state, fd); |
| |
| FD2PEER[fd] = -1; |
| if (fd == FD_COUNT-1) { |
| do fd--; while (fd >= 0 && FD2PEER[fd] == -1); |
| FD_COUNT = fd + 1; |
| |
| DPRINTF("close_fd: FD_COUNT %d", FD_COUNT); |
| |
| FD2PEER = xrealloc(FD2PEER, FD_COUNT * sizeof(FD2PEER[0])); |
| } |
| } |
| |
| /* callback */ |
| int isrv_register_peer(isrv_state_t *state, void *param) |
| { |
| int n; |
| |
| if (PEER_COUNT >= FD_SETSIZE) return -1; |
| n = PEER_COUNT++; |
| |
| DPRINTF("register_peer: PEER_COUNT %d", PEER_COUNT); |
| |
| PARAM_TBL = xrealloc(PARAM_TBL, PEER_COUNT * sizeof(PARAM_TBL[0])); |
| PARAM_TBL[n] = param; |
| if (TIMEOUT) { |
| TIMEO_TBL = xrealloc(TIMEO_TBL, PEER_COUNT * sizeof(TIMEO_TBL[0])); |
| TIMEO_TBL[n] = CURTIME; |
| } |
| return n; |
| } |
| |
| static void remove_peer(isrv_state_t *state, int peer) |
| { |
| int movesize; |
| int fd; |
| |
| DPRINTF("remove_peer(%d)", peer); |
| |
| fd = FD_COUNT - 1; |
| while (fd >= 0) { |
| if (FD2PEER[fd] == peer) { |
| isrv_close_fd(state, fd); |
| fd--; |
| continue; |
| } |
| if (FD2PEER[fd] > peer) |
| FD2PEER[fd]--; |
| fd--; |
| } |
| |
| PEER_COUNT--; |
| DPRINTF("remove_peer: PEER_COUNT %d", PEER_COUNT); |
| |
| movesize = (PEER_COUNT - peer) * sizeof(void*); |
| if (movesize > 0) { |
| memcpy(&PARAM_TBL[peer], &PARAM_TBL[peer+1], movesize); |
| if (TIMEOUT) |
| memcpy(&TIMEO_TBL[peer], &TIMEO_TBL[peer+1], movesize); |
| } |
| PARAM_TBL = xrealloc(PARAM_TBL, PEER_COUNT * sizeof(PARAM_TBL[0])); |
| if (TIMEOUT) |
| TIMEO_TBL = xrealloc(TIMEO_TBL, PEER_COUNT * sizeof(TIMEO_TBL[0])); |
| } |
| |
| static void handle_accept(isrv_state_t *state, int fd) |
| { |
| int n, newfd; |
| |
| /* suppress gcc warning "cast from ptr to int of different size" */ |
| fcntl(fd, F_SETFL, (int)(ptrdiff_t)(PARAM_TBL[0]) | O_NONBLOCK); |
| newfd = accept(fd, NULL, 0); |
| fcntl(fd, F_SETFL, (int)(ptrdiff_t)(PARAM_TBL[0])); |
| if (newfd < 0) { |
| if (errno == EAGAIN) return; |
| /* Most probably someone gave us wrong fd type |
| * (for example, non-socket). Don't want |
| * to loop forever. */ |
| bb_perror_msg_and_die("accept"); |
| } |
| |
| DPRINTF("new_peer(%d)", newfd); |
| n = state->new_peer(state, newfd); |
| if (n) |
| remove_peer(state, n); /* unsuccesful peer start */ |
| } |
| |
| void BUG_sizeof_fd_set_is_strange(void); |
| static void handle_fd_set(isrv_state_t *state, fd_set *fds, int (*h)(int, void **)) |
| { |
| enum { LONG_CNT = sizeof(fd_set) / sizeof(long) }; |
| int fds_pos; |
| int fd, peer; |
| /* need to know value at _the beginning_ of this routine */ |
| int fd_cnt = FD_COUNT; |
| |
| if (LONG_CNT * sizeof(long) != sizeof(fd_set)) |
| BUG_sizeof_fd_set_is_strange(); |
| |
| fds_pos = 0; |
| while (1) { |
| /* Find next nonzero bit */ |
| while (fds_pos < LONG_CNT) { |
| if (((long*)fds)[fds_pos] == 0) { |
| fds_pos++; |
| continue; |
| } |
| /* Found non-zero word */ |
| fd = fds_pos * sizeof(long)*8; /* word# -> bit# */ |
| while (1) { |
| if (FD_ISSET(fd, fds)) { |
| FD_CLR(fd, fds); |
| goto found_fd; |
| } |
| fd++; |
| } |
| } |
| break; /* all words are zero */ |
| found_fd: |
| if (fd >= fd_cnt) { /* paranoia */ |
| DPRINTF("handle_fd_set: fd > fd_cnt?? (%d > %d)", |
| fd, fd_cnt); |
| break; |
| } |
| DPRINTF("handle_fd_set: fd %d is active", fd); |
| peer = FD2PEER[fd]; |
| if (peer < 0) |
| continue; /* peer is already gone */ |
| if (peer == 0) { |
| handle_accept(state, fd); |
| continue; |
| } |
| DPRINTF("h(fd:%d)", fd); |
| if (h(fd, &PARAM_TBL[peer])) { |
| /* this peer is gone */ |
| remove_peer(state, peer); |
| } else if (TIMEOUT) { |
| TIMEO_TBL[peer] = monotonic_time(); |
| } |
| } |
| } |
| |
| static void handle_timeout(isrv_state_t *state, int (*do_timeout)(void **)) |
| { |
| int n, peer; |
| peer = PEER_COUNT-1; |
| /* peer 0 is not checked */ |
| while (peer > 0) { |
| DPRINTF("peer %d: time diff %d", peer, |
| (int)(CURTIME - TIMEO_TBL[peer])); |
| if ((CURTIME - TIMEO_TBL[peer]) >= TIMEOUT) { |
| DPRINTF("peer %d: do_timeout()", peer); |
| n = do_timeout(&PARAM_TBL[peer]); |
| if (n) |
| remove_peer(state, peer); |
| } |
| peer--; |
| } |
| } |
| |
| /* Driver */ |
| void isrv_run( |
| int listen_fd, |
| int (*new_peer)(isrv_state_t *state, int fd), |
| int (*do_rd)(int fd, void **), |
| int (*do_wr)(int fd, void **), |
| int (*do_timeout)(void **), |
| int timeout, |
| int linger_timeout) |
| { |
| isrv_state_t *state = xzalloc(sizeof(*state)); |
| state->new_peer = new_peer; |
| state->timeout = timeout; |
| |
| /* register "peer" #0 - it will accept new connections */ |
| isrv_register_peer(state, NULL); |
| isrv_register_fd(state, /*peer:*/ 0, listen_fd); |
| isrv_want_rd(state, listen_fd); |
| /* remember flags to make blocking<->nonblocking switch faster */ |
| /* (suppress gcc warning "cast from ptr to int of different size") */ |
| PARAM_TBL[0] = (void*)(ptrdiff_t)(fcntl(listen_fd, F_GETFL, 0)); |
| |
| while (1) { |
| struct timeval tv; |
| fd_set rd; |
| fd_set wr; |
| fd_set *wrp = NULL; |
| int n; |
| |
| tv.tv_sec = timeout; |
| if (PEER_COUNT <= 1) |
| tv.tv_sec = linger_timeout; |
| tv.tv_usec = 0; |
| rd = state->rd; |
| if (WR_COUNT) { |
| wr = state->wr; |
| wrp = ≀ |
| } |
| |
| DPRINTF("run: select(FD_COUNT:%d,timeout:%d)...", |
| FD_COUNT, (int)tv.tv_sec); |
| n = select(FD_COUNT, &rd, wrp, NULL, tv.tv_sec ? &tv : NULL); |
| DPRINTF("run: ...select:%d", n); |
| |
| if (n < 0) { |
| if (errno != EINTR) |
| bb_perror_msg("select"); |
| continue; |
| } |
| |
| if (n == 0 && linger_timeout && PEER_COUNT <= 1) |
| break; |
| |
| if (timeout) { |
| time_t t = monotonic_time(); |
| if (t != CURTIME) { |
| CURTIME = t; |
| handle_timeout(state, do_timeout); |
| } |
| } |
| if (n > 0) { |
| handle_fd_set(state, &rd, do_rd); |
| if (wrp) |
| handle_fd_set(state, wrp, do_wr); |
| } |
| } |
| DPRINTF("run: bailout"); |
| /* NB: accept socket is not closed. Caller is to decide what to do */ |
| } |