/*
 *  VBI proxy daemon
 *
 *  Copyright (C) 2002-2004 Tom Zoerner (and others)
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA 02110-1301, USA.
 *
 *
 *  Description:
 *
 *    This is the main module of the VBI proxy daemon.  Please refer to
 *    the manual page for information on the daemon's general purpose.
 *
 *    When started, the daemon will at first only create a named socket in
 *    /tmp for the devices given on the command line and wait for client
 *    connections.  When a client connects the VBI device is opened and
 *    configured for the requested services.  If no services are requested,
 *    the device is still opened, but acquisition not started.  When more
 *    clients connect, the daemon will reset service parameters and add them
 *    newly to the slicer in order of connection times, adjusting VBI device
 *    parameters as required and possible (e.g. enlarging VBI window.)
 *
 *    Client handling was originally derived from alevtd by Gerd Knorr, then
 *    adapted/extended for nxtvepg and again adapted/reduced for the VBI proxy
 *    by Tom Zoerner.
 *
 *
 *  $Log: not supported by cvs2svn $
 *  Revision 1.19  2008/07/26 06:22:24  mschimek
 *  Changed the license to GPLv2+ with Tom's permission.
 *
 *  Revision 1.18  2007/11/27 17:39:35  mschimek
 *  *** empty log message ***
 *
 *  Revision 1.17  2007/08/27 06:45:24  mschimek
 *  vbi_proxyd_take_service_req, vbi_proxyd_take_message,
 *  vbi_proxyd_take_message: Replaced strncpy() by the faster a safer
 *  strlcpy().
 *  vbi_proxyd_signal_handler, vbi_proxyd_parse_argv: Replaced sprintf() by
 *  the safer snprintf().
 *
 *  Revision 1.16  2006/05/22 09:01:53  mschimek
 *  s/vbi_asprintf/asprintf.
 *
 *  Revision 1.15  2006/02/10 06:25:36  mschimek
 *  *** empty log message ***
 *
 *  Revision 1.14  2005/01/20 01:39:15  mschimek
 *  gcc 4.0 char pointer signedness warnings.
 *
 *  Revision 1.13  2004/12/30 02:26:02  mschimek
 *  printf ptrdiff_t fixes.
 *
 *  Revision 1.12  2004/11/07 10:52:01  mschimek
 *  dprintf: s/proxyd/zvbid.
 *
 *  Revision 1.11  2004/10/25 16:56:26  mschimek
 *  *** empty log message ***
 *
 *  Revision 1.10  2004/10/24 18:15:33  tomzo
 *  - added handling of norm changes
 *  - adapted for interface change to proxy-msg.c (split socket read/write func)
 *  - improved debug level handling (command line option -debug)
 *
 */

static const char rcsid[] = "$Id: proxyd.c,v 1.20 2013-08-28 14:45:58 mschimek Exp $";

#include "config.h"

#ifdef ENABLE_PROXY

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <assert.h>
#include <pthread.h>

#include "src/vbi.h"
#include "src/inout.h"
#include "src/bcd.h"
#include "src/proxy-msg.h"

#ifdef ENABLE_V4L2
#include <asm/types.h>
#include "src/videodev2k.h"    /* for setting device priority */
#endif

#define DBG_MSG 1
#define DBG_QU 2
#define DBG_CLNT 4
#define DBG_SCHED 8
#define dprintf(flags, fmt, arg...)					\
do {									\
	if (opt_debug_level & (flags))					\
		fprintf (stderr, "zvbid: " fmt, ## arg);		\
} while (0)

/* Macro to cast (void *) to (int) and backwards without compiler warning
** (note: 64-bit compilers warn when casting a pointer to an int) */
#define  PVOID2INT(X)    ((int)((long)(X)))
#define  INT2PVOID(X)    ((void *)((long)(X)))

/* ----------------------------------------------------------------------------
** This struct is one element in the slicer data queue
*/
typedef struct PROXY_QUEUE_s
{
        struct PROXY_QUEUE_s  * p_next;
        unsigned int            ref_count;
        unsigned int            use_count;

        int                     max_lines;
        int                     line_count;
        double                  timestamp;
        void                  * p_raw_data;
        vbi_sliced              lines[1];
} PROXY_QUEUE;

#define QUEUE_ELEM_SIZE(Q,C)  (sizeof(PROXY_QUEUE) + (sizeof(vbi_sliced) * ((C) - 1)))

/* ----------------------------------------------------------------------------
** Declaration of types of internal state variables
*/

/* Note mutex conventions:
** - mutex are only required for v4l devices which do not support select(2),
**   because only then a separate thread is started which blocks in read(2)
** - when both the client chain and a slicer queue mutex is required, the
**   client mutex is acquired first; order is important to prevent deadlocks
** - the master thread locks the client chain mutex only for write access,
**   i.e. if a client is added or removed
*/

typedef enum
{
        REQ_TOKEN_NONE,         /* this client is not allowed to switch channels */
        REQ_TOKEN_RECLAIM,      /* return of token will be requested */
        REQ_TOKEN_RELEASE,      /* waiting for client to release token */
        REQ_TOKEN_GRANT,        /* this client will be sent the token a.s.a.p. */
        REQ_TOKEN_GRANTED,      /* this client currently holds the token */
        REQ_TOKEN_RETURNED      /* this client has returned the token, but still 'owns' the channel */
} REQ_TOKEN_STATE;

#define REQ_CONTROLS_CHN(X) ((X) >= REQ_TOKEN_GRANTED)

/* client channel control scheduler state */
typedef struct
{
        REQ_TOKEN_STATE         token_state;
        vbi_bool                is_completed;
        int                     cycle_count;
        time_t                  last_start;
        time_t                  last_duration;
} VBIPROXY_CHN_STATE;

/* client connection state */
typedef enum
{
        REQ_STATE_WAIT_CON_REQ,
        REQ_STATE_WAIT_CLOSE,
        REQ_STATE_FORWARD,
        REQ_STATE_CLOSED,
} REQ_STATE;

#define SRV_MAX_DEVICES                 4
#define VBI_MAX_BUFFER_COUNT           32
#define VBI_MIN_STRICT                 -1
#define VBI_MAX_STRICT                  2
#define VBI_GET_SERVICE_P(PREQ,STRICT)  ((PREQ)->services + (signed)(STRICT) - VBI_MIN_STRICT)
#define VBI_RAW_SERVICES(SRV)           (((SRV) & (VBI_SLICED_VBI_625 | VBI_SLICED_VBI_525)) != 0)

/* this struct holds client-specific state and parameters */
typedef struct PROXY_CLNT_s
{
        struct PROXY_CLNT_s   * p_next;

        REQ_STATE               state;
        VBIPROXY_MSG_STATE      io;
        vbi_bool                endianSwap;
        VBI_PROXY_CLIENT_FLAGS  client_flags;
        int                     dev_idx;

        VBIPROXY_MSG            msg_buf;

        unsigned int            services[VBI_MAX_STRICT - VBI_MIN_STRICT + 1];
        unsigned int            all_services;
        int                     vbi_start[2];
        int                     vbi_count[2];
        int                     buffer_count;
        vbi_bool                buffer_overflow;
        PROXY_QUEUE           * p_sliced;

        vbi_channel_profile     chn_profile;
        VBIPROXY_CHN_STATE      chn_state;
        VBI_CHN_PRIO            chn_prio;
        VBI_PROXY_CHN_FLAGS     chn_status_ind;

} PROXY_CLNT;

/* this struct holds the state of a device */
typedef struct
{
        const char            * p_dev_name;
        char                  * p_sock_path;
        int                     pipe_fd;

        vbi_capture           * p_capture;
        vbi_raw_decoder       * p_decoder;
        int                     vbi_fd;
        VBI_DRIVER_API_REV      vbi_api;

        unsigned int            all_services;
        unsigned int            scanning;
        int                     max_lines;
        PROXY_QUEUE           * p_sliced;
        PROXY_QUEUE           * p_free;
        PROXY_QUEUE           * p_tmp_buf;

        VBI_CHN_PRIO            chn_prio;

        vbi_bool                use_thread;
        int                     wr_fd;
        vbi_bool                wait_for_exit;
        vbi_bool                thread_active;
        pthread_t               thread_id;
        pthread_cond_t          start_cond;
        pthread_mutex_t         start_mutex;
        pthread_mutex_t         queue_mutex;

} PROXY_DEV;

/* this struct holds the global state of the module */
typedef struct
{
        char                  * listen_ip;
        char                  * listen_port;
        vbi_bool                do_tcp_ip;
        int                     tcp_ip_fd;
        int                     max_conn;
        vbi_bool                should_exit;
        vbi_bool                chn_sched_alarm;

        PROXY_CLNT            * p_clnts;
        int                     clnt_count;
        pthread_mutex_t         clnt_mutex;

        PROXY_DEV               dev[4];
        int                     dev_count;

} PROXY_SRV;

#define SRV_CONNECT_TIMEOUT     60
#define SRV_STALLED_STATS_INTV  15
#define SRV_QUEUE_BUFFER_COUNT  10

#define DEFAULT_MAX_CLIENTS     10
#define DEFAULT_VBI_DEV_PATH    "/dev/vbi"
#define DEFAULT_VBI_DEVFS_PATH  "/dev/v4l/vbi"
#define DEFAULT_CHN_PRIO        VBI_CHN_PRIO_INTERACTIVE
#define DEFAULT_BUFFER_COUNT     8

#define MAX_DEV_ERROR_COUNT     10

/* ----------------------------------------------------------------------------
** Local variables
*/
static PROXY_SRV      proxy;

static char         * p_opt_log_name = NULL;
static int            opt_log_level = -1;
static int            opt_syslog_level = -1;
static vbi_bool       opt_no_detach = FALSE;
static vbi_bool       opt_kill_daemon = FALSE;
static unsigned int   opt_max_clients = DEFAULT_MAX_CLIENTS;
static unsigned int   opt_debug_level = 0;
static unsigned int   opt_buffer_count = DEFAULT_BUFFER_COUNT;

/* ----------------------------------------------------------------------------
** Add one buffer to the tail of a queue
** - slicer queue is organized so that new data is appended to the tail,
**   forwarded data is taken from the head
** - note that a buffer is not released from the slicer queue until all
**   clients have processed it's data; client structs hold a pointer to
**   the first unprocessed (by the respective client) buffer in the queue
*/
static void vbi_proxy_queue_add_tail( PROXY_QUEUE ** q, PROXY_QUEUE * p_buf )
{
   PROXY_QUEUE * p_last;

   dprintf(DBG_QU, "queue_add_tail: buffer 0x%lX\n", (long)p_buf);
   p_buf->p_next = NULL;

   if (*q != NULL)
   {
      assert(*q != p_buf);
      p_last = *q;
      while (p_last->p_next != NULL)
         p_last = p_last->p_next;

      p_last->p_next = p_buf;
   }
   else
      *q = p_buf;

   assert((*q != NULL) && ((*q)->p_next != *q));
}

/* ----------------------------------------------------------------------------
** Retrieve one buffer from the queue of unused buffers
** - checks if the buffer size still matches the current VBI format
**   if not, the buffer is re-allocated
*/
static PROXY_QUEUE * vbi_proxy_queue_get_free( PROXY_DEV * p_proxy_dev )
{
   PROXY_QUEUE * p_buf;

   pthread_mutex_lock(&p_proxy_dev->queue_mutex);

   p_buf = p_proxy_dev->p_free;
   if (p_buf != NULL)
   {
      p_proxy_dev->p_tmp_buf = p_buf;
      p_proxy_dev->p_free    = p_buf->p_next;

      pthread_mutex_unlock(&p_proxy_dev->queue_mutex);

      if (p_buf->max_lines != p_proxy_dev->max_lines)
      {
         /* max line parameter changed -> re-alloc the buffer */
         p_proxy_dev->p_tmp_buf = NULL;
         if (p_buf->p_raw_data != NULL)
            free(p_buf->p_raw_data);
         free(p_buf);

         p_buf = malloc(QUEUE_ELEM_SIZE(p_buf, p_proxy_dev->max_lines));
         p_buf->p_raw_data = NULL;
         p_buf->max_lines = p_proxy_dev->max_lines;
         p_proxy_dev->p_tmp_buf = p_buf;
      }

      /* add/remove "sub-buffer" for raw data */
      if (VBI_RAW_SERVICES(p_proxy_dev->all_services))
      {
         if (p_buf->p_raw_data == NULL)
            p_buf->p_raw_data = malloc(p_proxy_dev->max_lines * VBIPROXY_RAW_LINE_SIZE);
      }
      else
      {
         if (p_buf->p_raw_data != NULL)
            free(p_buf->p_raw_data);
         p_buf->p_raw_data = NULL;
      }

      p_buf->p_next     = NULL;
      p_buf->ref_count  = 0;
      p_buf->use_count  = 0;
   }
   else
      pthread_mutex_unlock(&p_proxy_dev->queue_mutex);

   dprintf(DBG_QU, "queue_get_free: buffer 0x%lX\n", (long)p_buf);
   return p_buf;
}

/* ----------------------------------------------------------------------------
** Add a buffer to the queue of unused buffers
** - there's no ordering between buffers in the free queue, hence we don't
**   care if the buffer is inserted at head or tail of the queue
*/
static void vbi_proxy_queue_add_free( PROXY_DEV * p_proxy_dev, PROXY_QUEUE * p_buf )
{
   dprintf(DBG_QU, "queue_add_free: buffer 0x%lX\n", (long)p_buf);

   p_buf->p_next = p_proxy_dev->p_free;
   p_proxy_dev->p_free = p_buf;
}

/* ----------------------------------------------------------------------------
** Decrease reference counter on a buffer, add back to free queue upon zero
** - called when a buffer has been processed for one client
*/
static void vbi_proxy_queue_release_sliced( PROXY_CLNT * req )
{
   PROXY_QUEUE * p_buf;
   PROXY_DEV   * p_proxy_dev;

   p_proxy_dev = proxy.dev + req->dev_idx;

   p_buf = req->p_sliced;
   req->p_sliced = p_buf->p_next;

   if (p_buf->ref_count > 0)
      p_buf->ref_count -= 1;

   if (p_buf->ref_count == 0)
   {
      assert(p_proxy_dev->p_sliced == p_buf);
      p_proxy_dev->p_sliced = p_buf->p_next;

      /* add the buffer to the free queue */
      p_buf->p_next = p_proxy_dev->p_free;
      p_proxy_dev->p_free = p_buf;
   }
}

/* ----------------------------------------------------------------------------
** Free all resources of all buffers in a queue
** - called upon stop of acquisition for all queues
*/
static void vbi_proxy_queue_release_all( int dev_idx )
{
   PROXY_DEV   * p_proxy_dev;
   PROXY_CLNT  * req;
   PROXY_QUEUE * p_next;

   p_proxy_dev = proxy.dev + dev_idx;

   pthread_mutex_lock(&p_proxy_dev->queue_mutex);
   while (p_proxy_dev->p_sliced != NULL)
   {
      p_next = p_proxy_dev->p_sliced->p_next;

      vbi_proxy_queue_add_free(p_proxy_dev, p_proxy_dev->p_sliced);

      p_proxy_dev->p_sliced = p_next;
   }

   for (req = proxy.p_clnts; req != NULL; req = req->p_next)
   {
      if (req->dev_idx == dev_idx)
      {
         req->p_sliced    = NULL;
      }
   }
   pthread_mutex_unlock(&p_proxy_dev->queue_mutex);
}

/* ----------------------------------------------------------------------------
** Free all resources of all buffers in a queue
** - called upon stop of acquisition for all queues
*/
static void vbi_proxy_queue_free_all( PROXY_QUEUE ** q )
{
   PROXY_QUEUE * p_next;

   while (*q != NULL)
   {
      p_next = (*q)->p_next;

      if ((*q)->p_raw_data != NULL)
         free((*q)->p_raw_data);

      free(*q);

      *q = p_next;
   }
}

/* ----------------------------------------------------------------------------
** Allocate buffers
** - determines number of required buffers and adds or removes buffers from queue
** - buffer count depends on
**   (i) minimum which is always allocated (>= number of raw buffers)
**   (ii) max. requested buffer count of all connected clients
**   (iii) number of clients (one spare for each client)
*/
static vbi_bool vbi_proxy_queue_allocate( int dev_idx )
{
   PROXY_DEV    * p_proxy_dev;
   PROXY_CLNT   * p_walk;
   PROXY_QUEUE  * p_buf;
   int  client_count;
   int  buffer_count;
   int  buffer_free;
   int  buffer_used;

   p_proxy_dev = proxy.dev + dev_idx;

   buffer_count = opt_buffer_count;
   client_count = 0;
   for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
   {
      if (p_walk->dev_idx == dev_idx)
      {
         client_count += 1;
         if (buffer_count < p_walk->buffer_count)
            buffer_count = p_walk->buffer_count;
      }
   }
   buffer_count += client_count;

   pthread_mutex_lock(&p_proxy_dev->queue_mutex);

   /* count buffers in sliced data output queue */
   buffer_used = 0;
   for (p_buf = p_proxy_dev->p_sliced; p_buf != NULL; p_buf = p_buf->p_next)
   {
      buffer_used += 1;
   }
   /* count buffers in free queue */
   buffer_free = 0;
   for (p_buf = p_proxy_dev->p_free; p_buf != NULL; p_buf = p_buf->p_next)
   {
      buffer_free += 1;
   }

   dprintf(DBG_MSG, "queue_allocate: need %d buffers, have %d+%d (free+used)\n", buffer_count, buffer_free, buffer_used);

   if (buffer_free + buffer_used > buffer_count)
   {
      /* too many buffers: first reclaim from free queue (possibly too many) */
      vbi_proxy_queue_free_all(&p_proxy_dev->p_free);
      buffer_free = 0;
   }
   /* XXX we could also force-free more buffers in out queue, but that may be overkill */

   while (buffer_free + buffer_used < buffer_count)
   {
      p_buf = malloc(QUEUE_ELEM_SIZE(p_buf, p_proxy_dev->max_lines));
      if (p_buf != NULL)
      {
         p_buf->p_raw_data = NULL;
         p_buf->max_lines = p_proxy_dev->max_lines;

         vbi_proxy_queue_add_free(p_proxy_dev, p_buf);
         buffer_free += 1;
      }
      else
      {
         dprintf(DBG_MSG, "queue_allocate: failed to allocate buffer (errno %d)\n", errno);
         break;
      }
   }

   pthread_mutex_unlock(&p_proxy_dev->queue_mutex);

   return ((unsigned int)(buffer_free + buffer_used)
	   >= opt_buffer_count + client_count);
}

/* ----------------------------------------------------------------------------
** Free the first buffer in the output queue by force
** - required if one client is blocked but others still active
** - client(s) will lose this frame's data
*/
static PROXY_QUEUE * vbi_proxy_queue_force_free( PROXY_DEV * p_proxy_dev )
{
   PROXY_CLNT   * req;

   pthread_mutex_lock(&proxy.clnt_mutex);
   pthread_mutex_lock(&p_proxy_dev->queue_mutex);

   if ((p_proxy_dev->p_free == NULL) && (p_proxy_dev->p_sliced != NULL))
   {
      dprintf(DBG_MSG, "queue_force_free: buffer 0x%lX\n", (long)p_proxy_dev->p_sliced);

      for (req = proxy.p_clnts; req != NULL; req = req->p_next)
      {
         if (req->p_sliced == p_proxy_dev->p_sliced)
         {
            vbi_proxy_queue_release_sliced(req);
         }
      }
   }

   pthread_mutex_unlock(&p_proxy_dev->queue_mutex);
   pthread_mutex_unlock(&proxy.clnt_mutex);

   return vbi_proxy_queue_get_free(p_proxy_dev);
}

/* ----------------------------------------------------------------------------
** Read sliced data and forward it to all clients
*/
static void vbi_proxyd_forward_data( int dev_idx )
{
   PROXY_QUEUE    * p_buf;
   PROXY_CLNT     * req;
   PROXY_DEV      * p_proxy_dev;
   struct timeval timeout;
   int    res;

   p_proxy_dev = proxy.dev + dev_idx;

   /* unlink a buffer from the free queue */
   p_buf = vbi_proxy_queue_get_free(p_proxy_dev);

   if (p_buf == NULL)
      p_buf = vbi_proxy_queue_force_free(p_proxy_dev);

   if (p_buf != NULL)
   {
      timeout.tv_sec  = 0;
      timeout.tv_usec = 0;

      if (VBI_RAW_SERVICES(p_proxy_dev->all_services) == FALSE)
      {
         res = vbi_capture_read_sliced(p_proxy_dev->p_capture, p_buf->lines,
                                       &p_buf->line_count, &p_buf->timestamp, &timeout);
      }
      else
      {
         res = vbi_capture_read(p_proxy_dev->p_capture,
                                p_buf->p_raw_data, p_buf->lines,
                                &p_buf->line_count, &p_buf->timestamp, &timeout);
      }

      if (res > 0)
      {
         assert(p_buf->line_count < p_buf->max_lines);
         pthread_mutex_lock(&proxy.clnt_mutex);
         pthread_mutex_lock(&p_proxy_dev->queue_mutex);

         for (req = proxy.p_clnts; req != NULL; req = req->p_next)
         {
            if ( (req->dev_idx == dev_idx) &&
                 (req->state == REQ_STATE_FORWARD) &&
                 (req->all_services != 0) )
            {
               p_buf->ref_count += 1;

               if (req->p_sliced == NULL)
                  req->p_sliced = p_buf;
            }
         }

         pthread_mutex_unlock(&p_proxy_dev->queue_mutex);
         pthread_mutex_unlock(&proxy.clnt_mutex);
      }
      else if (res < 0)
      {
         /* XXX abort upon error (esp. EBUSY) */
         perror("VBI read");
      }

      pthread_mutex_lock(&p_proxy_dev->queue_mutex);

      if (p_buf->ref_count > 0)
         vbi_proxy_queue_add_tail(&p_proxy_dev->p_sliced, p_buf);
      else
         vbi_proxy_queue_add_free(p_proxy_dev, p_buf);

      p_proxy_dev->p_tmp_buf = NULL;
      pthread_mutex_unlock(&p_proxy_dev->queue_mutex);
   }
   else
      dprintf(DBG_MSG, "forward_data: queue overflow\n");
}

/* ----------------------------------------------------------------------------
** Process a norm change notification
** - query driver for new norm: if sucessful, this overrides information
**   provided by the client (client may also provide 0)
** - trigger sending of norm change indication to all clients if scanning changes:
**   -> clients must re-apply for their services; note norm changes which don't
**   affect the scanning (e.g. PAL<->SECAM) are ignored
*/
static void vbi_proxyd_update_scanning( int dev_idx, PROXY_CLNT * req, int scanning )
{
   PROXY_DEV    * p_proxy_dev;
   PROXY_CLNT   * p_walk;
   unsigned int new_scanning;

   p_proxy_dev = proxy.dev + dev_idx;

   if (p_proxy_dev->p_capture != NULL)
   {
      /* if the info is coming from a client verify it */
      if (req != NULL)
      {
         new_scanning = vbi_capture_get_scanning(p_proxy_dev->p_capture);
         if (new_scanning <= 0)
         {
            if ((scanning == 525) || (scanning == 625))
               new_scanning = scanning;
         }
      }
      else
         new_scanning = scanning;

      if (new_scanning != p_proxy_dev->scanning)
      {
         dprintf(DBG_MSG, "update_scanning: changed from %d to %d\n", p_proxy_dev->scanning, new_scanning);
         p_proxy_dev->scanning = new_scanning;

         /* trigger sending of change indication to all clients except the caller */
         for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
         {
            if ( (p_walk->dev_idx == dev_idx) &&
                 ((p_walk->client_flags & VBI_PROXY_CLIENT_NO_STATUS_IND) == 0) )
            {
               p_walk->chn_status_ind |= VBI_PROXY_CHN_NORM;
            }
         }
      }
   }
}

/* ----------------------------------------------------------------------------
** Helper function: calculate timespec for 50ms timeout
*/
static void vbi_proxyd_calc_timeout_ms( struct timespec * p_tsp, int msecs )
{
   struct timeval  tv;

   gettimeofday(&tv, NULL);
   tv.tv_usec += msecs * 1000L;
   if (tv.tv_usec > 1000 * 1000L)
   {
      tv.tv_sec  += 1;
      tv.tv_usec -= 1000 * 1000;
   }
   p_tsp->tv_sec  = tv.tv_sec;
   p_tsp->tv_nsec = tv.tv_usec * 1000;
}

/* ----------------------------------------------------------------------------
** Clean up after thread cancellation: signal waiting master thread
*/
static void vbi_proxyd_acq_thread_cleanup( void * pvoid_arg )
{
   PROXY_DEV * p_proxy_dev;
   int         dev_idx;

   dev_idx     = PVOID2INT(pvoid_arg);
   p_proxy_dev = proxy.dev + dev_idx;

   dprintf(DBG_QU, "acq thread cleanup: signaling master (%d)\n", p_proxy_dev->wait_for_exit);

   pthread_mutex_lock(&p_proxy_dev->start_mutex);
   if (p_proxy_dev->wait_for_exit)
   {
      pthread_cond_signal(&p_proxy_dev->start_cond);
   }
   if (p_proxy_dev->p_tmp_buf != NULL)
   {
      vbi_proxy_queue_add_free(p_proxy_dev, p_proxy_dev->p_tmp_buf);
      p_proxy_dev->p_tmp_buf = NULL;
   }
   p_proxy_dev->thread_active = FALSE;
   pthread_mutex_unlock(&p_proxy_dev->start_mutex);
}

/* ----------------------------------------------------------------------------
** Main loop for acquisition thread for devices that don't support select(2)
*/
static void * vbi_proxyd_acq_thread( void * pvoid_arg )
{
   PROXY_DEV * p_proxy_dev;
   int         dev_idx;
   int         ret;
   char        byte_buf[1];
   sigset_t    sigmask;

   dev_idx     = PVOID2INT(pvoid_arg);
   p_proxy_dev = proxy.dev + dev_idx;

   /* block signals which are handled by main thread */
   sigemptyset(&sigmask);
   sigaddset(&sigmask, SIGHUP);
   sigaddset(&sigmask, SIGINT);
   sigaddset(&sigmask, SIGTERM);
   pthread_sigmask(SIG_BLOCK, &sigmask, NULL);

   pthread_cleanup_push(vbi_proxyd_acq_thread_cleanup, pvoid_arg);
   pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
   pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

   p_proxy_dev->thread_active = TRUE;

   pthread_mutex_lock(&p_proxy_dev->start_mutex);
   pthread_cond_signal(&p_proxy_dev->start_cond);
   pthread_mutex_unlock(&p_proxy_dev->start_mutex);

   while (p_proxy_dev->wait_for_exit == FALSE)
   {
      /* read data from the VBI device and append the buffer to all client queues
      ** note: this function blocks in read(2) until data is available */
      vbi_proxyd_forward_data(dev_idx);

      /* wake up the master thread to process client queues */
      ret = write(p_proxy_dev->wr_fd, byte_buf, 1);

      if ((ret < 0) && (errno != EAGAIN))
      {
         dprintf(DBG_MSG, "acq_thread: write error to pipe: %d\n", errno);
         break;
      }
      else if (ret != 1)
         dprintf(DBG_MSG, "acq_thread: pipe overflow\n");
   }

   pthread_cleanup_pop(1);
   pthread_exit(0);

   return NULL;
}

/* ----------------------------------------------------------------------------
** Stop acquisition thread
*/
static void vbi_proxyd_stop_acq_thread( PROXY_DEV * p_proxy_dev )
{
   struct timespec tsp;
   int ret;
   int vbi_fd;

   assert(p_proxy_dev->use_thread);
   pthread_mutex_lock(&p_proxy_dev->start_mutex);

   if (p_proxy_dev->thread_active)
   {
      p_proxy_dev->wait_for_exit = TRUE;
      pthread_cancel(p_proxy_dev->thread_id);

      vbi_proxyd_calc_timeout_ms(&tsp, 50);
      ret = pthread_cond_timedwait(&p_proxy_dev->start_cond, &p_proxy_dev->start_mutex, &tsp);
      if (ret != 0)
      {  /* thread did not stop within 50ms: probably blocked in read with no incoming data */
         /* dirty hack: force to wake up by closing the file handle */
         vbi_fd = vbi_capture_fd(p_proxy_dev->p_capture);
         close(vbi_fd);
         dprintf(DBG_MSG, "stop_acq_thread: thread did not exit (%d): closed VBI filehandle %d\n", ret, vbi_fd);

         vbi_proxyd_calc_timeout_ms(&tsp, 50);
         ret = pthread_cond_timedwait(&p_proxy_dev->start_cond, &p_proxy_dev->start_mutex, &tsp);
      }
      if (ret == 0)
      {
         ret = pthread_join(p_proxy_dev->thread_id, NULL);
         if (ret == 0)
            dprintf(DBG_MSG, "stop_acq_thread: acq thread killed successfully\n");
         else
            dprintf(DBG_MSG, "stop_acq_thread: pthread_join failed: %d (%s)\n", errno, strerror(errno));
      }
   }

   close(p_proxy_dev->vbi_fd);
   close(p_proxy_dev->wr_fd);
   p_proxy_dev->vbi_fd = -1;
   p_proxy_dev->wr_fd = -1;
   p_proxy_dev->use_thread = FALSE;

   pthread_mutex_unlock(&p_proxy_dev->start_mutex);
}

/* ----------------------------------------------------------------------------
** Start a thread to block in read(2) for devices that don't support select(2)
*/
static vbi_bool vbi_proxyd_start_acq_thread( int dev_idx )
{
   PROXY_DEV * p_proxy_dev;
   int       pipe_fds[2];
   vbi_bool  result = FALSE;

   p_proxy_dev = proxy.dev + dev_idx;
   p_proxy_dev->use_thread    = TRUE;
   p_proxy_dev->wait_for_exit = FALSE;
   p_proxy_dev->thread_active = FALSE;

   if (pipe(pipe_fds) == 0)
   {
      p_proxy_dev->vbi_fd = pipe_fds[0];
      p_proxy_dev->wr_fd  = pipe_fds[1];

      fcntl(p_proxy_dev->vbi_fd, F_SETFL, O_NONBLOCK);
      fcntl(p_proxy_dev->wr_fd,  F_SETFL, O_NONBLOCK);

      /* start thread */
      pthread_mutex_lock(&p_proxy_dev->start_mutex);
      if (pthread_create(&p_proxy_dev->thread_id, NULL,
                         vbi_proxyd_acq_thread, INT2PVOID(dev_idx)) == 0)
      {
         dprintf(DBG_MSG, "acquisiton thread started: "
	         "id %ld, device %ld, pipe rd/wr %d/%d\n",
		 (long)p_proxy_dev->thread_id,
		 (long)(p_proxy_dev - proxy.dev),
		 p_proxy_dev->vbi_fd, p_proxy_dev->wr_fd);

         /* wait for the slave to report the initialization result */
         pthread_cond_wait(&p_proxy_dev->start_cond, &p_proxy_dev->start_mutex);
         pthread_mutex_unlock(&p_proxy_dev->start_mutex);

         result = p_proxy_dev->thread_active;
      }
      else
         dprintf(DBG_MSG, "start_acq_thread: pthread_create: %d (%s)\n", errno, strerror(errno));
   }
   else
      dprintf(DBG_MSG, "start_acq_thread: create pipe: %d (%s)\n", errno, strerror(errno));

   return result;
}

/* ----------------------------------------------------------------------------
** Stop VBI acquisition (after the last client quit)
*/
static void vbi_proxy_stop_acquisition( PROXY_DEV * p_proxy_dev )
{
   if (p_proxy_dev->p_capture != NULL)
   {
      dprintf(DBG_MSG, "stop_acquisition: stopping (prev. services 0x%X)\n", p_proxy_dev->all_services);

      if (p_proxy_dev->use_thread)
         vbi_proxyd_stop_acq_thread(p_proxy_dev);

      vbi_capture_delete(p_proxy_dev->p_capture);
      p_proxy_dev->p_capture = NULL;
      p_proxy_dev->p_decoder = NULL;
      p_proxy_dev->vbi_fd = -1;

      vbi_proxy_queue_free_all(&p_proxy_dev->p_free);
      vbi_proxy_queue_free_all(&p_proxy_dev->p_sliced);
   }
}

/* ----------------------------------------------------------------------------
** Open capture device (for the first client)
** - does not yet any services yet
*/
static vbi_bool vbi_proxy_start_acquisition( int dev_idx, char ** pp_errorstr )
{
   PROXY_DEV    * p_proxy_dev;
   char         * p_errorstr;
   vbi_bool       result;

   p_proxy_dev = proxy.dev + dev_idx;
   result      = FALSE;

   /* assign dummy error string if necessary */
   p_errorstr = NULL;
   if (pp_errorstr == NULL)
      pp_errorstr = &p_errorstr;

   p_proxy_dev->vbi_api = VBI_API_V4L2;
   p_proxy_dev->p_capture = vbi_capture_v4l2_new(p_proxy_dev->p_dev_name, opt_buffer_count,
                                                 NULL, -1, pp_errorstr, opt_debug_level);
   if (p_proxy_dev->p_capture == NULL)
   {
      p_proxy_dev->vbi_api = VBI_API_V4L1;
      p_proxy_dev->p_capture = vbi_capture_v4l_new(p_proxy_dev->p_dev_name, p_proxy_dev->scanning,
                                                   NULL, -1, pp_errorstr, opt_debug_level);
   }

   if (p_proxy_dev->p_capture != NULL)
   {
      p_proxy_dev->p_decoder = vbi_capture_parameters(p_proxy_dev->p_capture);
      if (p_proxy_dev->p_decoder != NULL)
      {
         /* allocate buffer queue for sliced output data */
         vbi_proxy_queue_allocate(dev_idx);

         p_proxy_dev->chn_prio = VBI_CHN_PRIO_INTERACTIVE;

         /* get file handle for select() to wait for VBI data */
         if ((vbi_capture_get_fd_flags(p_proxy_dev->p_capture) & VBI_FD_HAS_SELECT) != 0)
         {
            p_proxy_dev->vbi_fd = vbi_capture_fd(p_proxy_dev->p_capture);
            result = (p_proxy_dev->vbi_fd != -1);
         }
         else
            result = vbi_proxyd_start_acq_thread(dev_idx);
      }
      else
         dprintf(DBG_MSG, "start_acquisition: capture device has no slicer!?\n");
   }

   if (result == FALSE)
   {
      vbi_proxy_stop_acquisition(p_proxy_dev);
   }

   if ((pp_errorstr == &p_errorstr) && (p_errorstr != NULL))
      free(p_errorstr);

   return result;
}

/* ----------------------------------------------------------------------------
** Update service mask after a client was added or closed
** - TODO: update buffer_count
*/
static vbi_bool vbi_proxyd_update_services( int dev_idx, PROXY_CLNT * p_new_req,
                                            int new_req_strict, char ** pp_errorstr )
{
   PROXY_CLNT   * req;
   PROXY_CLNT   * p_walk;
   PROXY_DEV    * p_proxy_dev;
   unsigned int   dev_services;
   unsigned int   tmp_services;
   unsigned int   next_srv;
   int            strict;
   int            strict2;
   vbi_bool       is_first;
   vbi_bool       result;

   p_proxy_dev = proxy.dev + dev_idx;

   if (p_proxy_dev->p_capture == NULL)
   {
      /* cpture device not opened yet */
      /* check if other clients have any services enabled */
      next_srv = 0;
      for (req = proxy.p_clnts; req != NULL; req = req->p_next)
         for (strict = VBI_MIN_STRICT; strict <= VBI_MAX_STRICT; strict++)
            next_srv |= *VBI_GET_SERVICE_P(req, strict);

      if (next_srv != 0)
      {
         result = vbi_proxy_start_acquisition(dev_idx, pp_errorstr);
      }
      else
      {
         /* XXX FIXME must open device at least once to query API
         ** XXX must be change since device open may fail with EBUSY: better leave device open while users are connected */
         if (p_proxy_dev->vbi_api == VBI_API_UNKNOWN)
         {
            vbi_proxy_start_acquisition(dev_idx, NULL);
            vbi_proxy_stop_acquisition(p_proxy_dev);
         }
         result = TRUE;
      }
   }
   else
      result = FALSE;

   if (p_proxy_dev->p_capture != NULL)
   {
      /* terminate acq thread because we're about to suspend capturing */
      if (p_proxy_dev->use_thread)
         vbi_proxyd_stop_acq_thread(p_proxy_dev);

      /* XXX TODO: possible optimization: reduce number of update_service calls:
      **           (1) collect all services first; (2) add services at 3 strict levels; (3) update all_services for all clients */
      is_first = TRUE;
      dev_services = 0;
      for (req = proxy.p_clnts; req != NULL; req = req->p_next)
      {
         if ( (req->dev_idx == dev_idx) &&
              (req->state == REQ_STATE_FORWARD) )
         {
            req->all_services = 0;

            for (strict = VBI_MIN_STRICT; strict <= VBI_MAX_STRICT; strict++)
            {
               tmp_services = *VBI_GET_SERVICE_P(req, strict);
               if (tmp_services != 0)
               {
                  next_srv = 0;
                  for (strict2 = strict + 1; strict2 <= VBI_MAX_STRICT; strict2++)
                     if ((next_srv |= *VBI_GET_SERVICE_P(req, strict2)) != 0)
                        break;
                  /* search following clients if more services follow */
                  if (next_srv == 0)
                     for (p_walk = req->p_next; p_walk != NULL; p_walk = p_walk->p_next)
                        for (strict2 = VBI_MIN_STRICT; strict2 <= VBI_MAX_STRICT; strict2++)
                           if ((next_srv |= *VBI_GET_SERVICE_P(p_walk, strict2)) != 0)
                              goto next_srv_found;  // break^2

                  next_srv_found:
                  dprintf(DBG_MSG, "service_update: fd %d: add services=0x%X strict=%d final=%d\n", req->io.sock_fd, tmp_services, strict, (next_srv == 0));

                  tmp_services =
                     vbi_capture_update_services( p_proxy_dev->p_capture,
                                                  is_first, (next_srv == 0),
                                                  tmp_services, strict,
                                                  /* return error strings only for the new client */
                                                  (((req == p_new_req) &&
                                                    (strict == new_req_strict)) ? pp_errorstr : NULL) );

                  dev_services |= tmp_services;
                  req->all_services |= tmp_services;
                  is_first = FALSE;

                  /* must not mask out client service bits unless upon a new request; afterwards
                  ** services must be cached and re-applied, e.g. in case the norm changes back */
                  if (req == p_new_req)
                     *VBI_GET_SERVICE_P(req, strict) &= tmp_services;
               }
            }
         }
      }

      /* check if scanning changed
      ** (even if all clients suspended: some might be waiting for scanning change) */
      vbi_proxyd_update_scanning(dev_idx, NULL, p_proxy_dev->p_decoder->scanning);

      if (dev_services != 0)
      {
         p_proxy_dev->all_services = dev_services;
         p_proxy_dev->max_lines = p_proxy_dev->p_decoder->count[0]
                                + p_proxy_dev->p_decoder->count[1];

         /* grow/shrink buffer queue for sliced output data */
         vbi_proxy_queue_allocate(dev_idx);

         dprintf(DBG_MSG, "service_update: new service mask 0x%X, max.lines=%d, scanning=%d\n", dev_services, p_proxy_dev->max_lines, p_proxy_dev->scanning);

         if ((vbi_capture_get_fd_flags(p_proxy_dev->p_capture) & VBI_FD_HAS_SELECT) != 0)
         {
            result = TRUE;
         }
         else
            result = vbi_proxyd_start_acq_thread(dev_idx);
      }
      else
      {  /* no services set: not an error if clien't didn't request any */
         result = is_first;
      }

      if ((dev_services == 0) || (result == FALSE))
      {
         /* no clients remaining or acq start failed -> stop acquisition */
         vbi_proxy_stop_acquisition(p_proxy_dev);
      }
   }

   return result;
}

/* ----------------------------------------------------------------------------
** Process a client's service request
** - either during connect request or later upon service request
** - note if it's not the first request a different "strictness" may be given;
**   must remember strictness for each service to be able to re-apply for the
**   same services mask to the decoder later
*/
static vbi_bool vbi_proxyd_take_service_req( PROXY_CLNT * req,
                                             unsigned int new_services, int new_strict,
                                             char * errormsg )
{
   char        * p_errorstr;
   PROXY_DEV   * p_proxy_dev;
   int           strict;
   vbi_bool      result;

   p_proxy_dev = proxy.dev + req->dev_idx;
   p_errorstr  = NULL;

   /* remove new services from all strict levels */
   for (strict = VBI_MIN_STRICT; strict <= VBI_MAX_STRICT; strict++)
      *VBI_GET_SERVICE_P(req, strict) &= ~ new_services;

   /* add new services at the given level of strictness */
   *VBI_GET_SERVICE_P(req, new_strict) |= new_services;

   /* merge with other client's requests and pass to the device */
   result = vbi_proxyd_update_services(req->dev_idx, req, new_strict, &p_errorstr);

   if ( (result == FALSE) ||
        ( ((req->all_services & new_services) == 0) && (new_services != 0) ))
   {
      if (p_errorstr != NULL)
      {
         strlcpy(errormsg, p_errorstr, VBIPROXY_ERROR_STR_MAX_LENGTH);
         errormsg[VBIPROXY_ERROR_STR_MAX_LENGTH - 1] = 0;
      }
      else if ( ((*VBI_GET_SERVICE_P(req, new_strict) & new_services) == 0) &&
                (new_services != 0) )
      {
         strlcpy(errormsg, "Sorry, proxy cannot capture any of the requested data services.",
                 VBIPROXY_ERROR_STR_MAX_LENGTH);
         errormsg[VBIPROXY_ERROR_STR_MAX_LENGTH - 1] = 0;
      }
      else
      {
         strlcpy(errormsg, "Internal error in service update.", VBIPROXY_ERROR_STR_MAX_LENGTH);
         errormsg[VBIPROXY_ERROR_STR_MAX_LENGTH - 1] = 0;
      }
      result = FALSE;
   }

   if (p_proxy_dev->p_decoder != NULL)
   {
      /* keep a copy of the VBI line ranges: used as upper/lower boundaries in
      ** sliced data messages MUST NOT be changed later (at least not increased)
      ** even if services change, to avoid overflowing clients' buffers */
      req->vbi_start[0] = p_proxy_dev->p_decoder->start[0];
      req->vbi_count[0] = p_proxy_dev->p_decoder->count[0];
      req->vbi_start[1] = p_proxy_dev->p_decoder->start[1];
      req->vbi_count[1] = p_proxy_dev->p_decoder->count[1];
   }

   if (p_errorstr != NULL)
      free(p_errorstr);

   return result;
}

/* ----------------------------------------------------------------------------
** Search for client which owns the token
*/
static PROXY_CLNT * vbi_proxyd_get_token_owner( int dev_idx )
{
   PROXY_DEV   * p_proxy_dev;
   PROXY_CLNT  * p_walk;
   PROXY_CLNT  * p_owner;

   p_proxy_dev = proxy.dev + dev_idx;
   p_owner = NULL;

   for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
   {
      if (p_walk->dev_idx == dev_idx)
      {
         switch (p_walk->chn_state.token_state)
         {
            case REQ_TOKEN_NONE:
               break;
            case REQ_TOKEN_GRANT:
            case REQ_TOKEN_RETURNED:
            case REQ_TOKEN_RECLAIM:
            case REQ_TOKEN_RELEASE:
            case REQ_TOKEN_GRANTED:
               assert(p_owner == NULL);
               p_owner = p_walk;
               break;
            default:
               assert(FALSE);  /* invalid state */
               break;
         }
      }
   }
   return p_owner;
}

/* ----------------------------------------------------------------------------
** Grant token to a given client
** - basically implements a matrix of all possible token states in current and
**   future token owner: however only one may have non-"NONE" state
** - if the token is still in posession of a different client the request will
**   fail, but the token is reclaimed from the other client
*/
static vbi_bool vbi_proxyd_token_grant( PROXY_CLNT * req )
{
   PROXY_CLNT  * p_owner;
   vbi_bool token_free = TRUE;

   switch (req->chn_state.token_state)
   {
      case REQ_TOKEN_NONE:
         p_owner = vbi_proxyd_get_token_owner(req->dev_idx);
         if ( (p_owner == NULL) ||
              ( (p_owner->chn_state.token_state == REQ_TOKEN_GRANT) ||
                (p_owner->chn_state.token_state == REQ_TOKEN_RETURNED) ))
         {
            /* token is free or grant message not yet sent -> immediately grant to new client */
            req->chn_state.token_state = REQ_TOKEN_GRANT;
            if (p_owner != NULL)
               p_owner->chn_state.token_state = REQ_TOKEN_NONE;
         }
         else
         {  /* have to reclaim token from previous owner first */
            if (p_owner->chn_state.token_state != REQ_TOKEN_RELEASE)
               p_owner->chn_state.token_state = REQ_TOKEN_RECLAIM;

            token_free = FALSE;
         }
         break;

      case REQ_TOKEN_GRANT:
         /* client is already about to be granted the token -> nothing to do */
         break;
      case REQ_TOKEN_RECLAIM:
         /* reclaim message not yet sent -> just return to GRANTED state */
         req->chn_state.token_state = REQ_TOKEN_GRANTED;
         break;
      case REQ_TOKEN_RELEASE:
         /* reclaim already sent -> must re-assign token */
         req->chn_state.token_state = REQ_TOKEN_GRANT;
         break;
      case REQ_TOKEN_GRANTED:
      case REQ_TOKEN_RETURNED:
         /* client is still in control of the channel: nothing to do */
         break;
      default:
         assert(FALSE);  /* invalid state */
         break;
   }
   return token_free;
}

/* ----------------------------------------------------------------------------
** Adapt channel scheduler state when switching away from a channel
*/
static void vbi_proxyd_channel_completed( PROXY_CLNT * req, time_t whence )
{
   PROXY_CLNT  * p_walk;

   assert(REQ_CONTROLS_CHN(req->chn_state.token_state));

   req->chn_state.last_duration = whence - req->chn_state.last_start;
   req->chn_state.is_completed = TRUE;
   req->chn_state.cycle_count += 1;

   dprintf(DBG_MSG, "channel_schedule: fd %d terminated (duration %d, cycle #%d)\n", req->io.sock_fd, (int)req->chn_state.last_duration, req->chn_state.cycle_count);

   if (req->chn_state.cycle_count > 2)
   {
      /* cycle counter overflow: only values 1, 2 allowed (plus 0 for new requests)
      ** -> reduce all counters by one */
      dprintf(DBG_MSG, "channel_schedule: dev #%d: leveling cycle counters\n", req->dev_idx);

      for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
         if (p_walk->dev_idx == req->dev_idx)
            if (p_walk->chn_state.cycle_count > 0)
               p_walk->chn_state.cycle_count -= 1;
   }
   else if (req->chn_state.cycle_count == 1)
   {
      /* cycle counter hops always to maximum, i.e. from 0 to 2 so that a new request
      ** has immediately highest prio, but is only scheduled once before the others */
      for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
         if (p_walk->dev_idx == req->dev_idx)
            if (p_walk->chn_state.cycle_count >= 2)
               break;

      if (p_walk != NULL)
         req->chn_state.cycle_count = 2;
   }
}

/* ----------------------------------------------------------------------------
** Adapt channel scheduler state when switching away from a channel
*/
static void vbi_proxyd_channel_stopped( PROXY_CLNT * req )
{
   time_t  now = time(NULL);

   assert(REQ_CONTROLS_CHN(req->chn_state.token_state));

   if ( (req->chn_state.is_completed == FALSE) &&
        (now - req->chn_state.last_start >= req->chn_profile.min_duration) )
   {
      vbi_proxyd_channel_completed(req, now);
   }
   req->chn_state.is_completed  = FALSE;

   if (req->chn_state.token_state == REQ_TOKEN_GRANTED)
      req->chn_state.token_state = REQ_TOKEN_RECLAIM;
   else
      req->chn_state.token_state = REQ_TOKEN_NONE;
}

/* ----------------------------------------------------------------------------
** Calculate next timer for scheduler
** - since there's only one alarm signal, the nearest timeout on all devices is searched
*/
static void vbi_proxyd_channel_timer_update( void )
{
   PROXY_CLNT  * p_walk;
   PROXY_DEV   * p_proxy_dev;
   time_t        rest;
   time_t        next_sched;
   time_t        now;

   now        = time(NULL);
   next_sched = 0;

   for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
   {
      p_proxy_dev = proxy.dev + p_walk->dev_idx;
      if ( (p_proxy_dev->chn_prio == VBI_CHN_PRIO_BACKGROUND) &&
           REQ_CONTROLS_CHN(p_walk->chn_state.token_state) &&
           (p_walk->chn_state.is_completed == FALSE) )
      {
         rest = p_walk->chn_profile.min_duration - (now - p_walk->chn_state.last_start);
         if ((rest > 0) && ((rest < next_sched) || (next_sched == 0)))
            next_sched = rest;
         else if (rest < 0)
            next_sched = 1;
      }
      /* XXX TODO: set timer to supervise TOKEN RELEASE */
   }

   if (next_sched != 0)
      dprintf(DBG_MSG, "channel_timer_update: set alarm timer in %d secs\n", (int)next_sched);

   alarm(next_sched);
   proxy.chn_sched_alarm = FALSE;
}

/* ----------------------------------------------------------------------------
** Determine which client's channel request is granted
*/
static PROXY_CLNT *
vbi_proxyd_channel_schedule( int dev_idx )
{
   PROXY_CLNT  * p_walk;
   PROXY_CLNT  * p_sched;
   PROXY_CLNT  * p_active;
   PROXY_DEV   * p_proxy_dev;
   time_t        now;

   p_proxy_dev = proxy.dev + dev_idx;
   p_sched     = NULL;
   p_active    = NULL;
   now         = time(NULL);

   for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
   {
      if ( (p_walk->dev_idx == dev_idx) &&
           (p_walk->chn_profile.is_valid) &&
           (p_walk->chn_prio == VBI_CHN_PRIO_BACKGROUND) )
      {
         /* if this client's channel is currently active, check if the reservation has expired */
         if (REQ_CONTROLS_CHN(p_walk->chn_state.token_state))
         {
            if ( (now - p_walk->chn_state.last_start >= p_walk->chn_profile.min_duration) &&
                 (p_walk->chn_state.is_completed == FALSE) )
            {
               vbi_proxyd_channel_completed(p_walk, now);
            }
            p_active = p_walk;
         }
         dprintf(DBG_MSG, "channel_schedule: fd %d: active=%d compl=%d sub-prio=0x%02X cycles#%d min-dur=%d\n", p_walk->io.sock_fd, REQ_CONTROLS_CHN(p_walk->chn_state.token_state), p_walk->chn_state.is_completed, p_walk->chn_profile.sub_prio, p_walk->chn_state.cycle_count, (int)p_walk->chn_profile.min_duration);

         if (p_sched != NULL)
         {
            if ( p_walk->chn_state.cycle_count
                   + ((REQ_CONTROLS_CHN(p_walk->chn_state.token_state) && p_walk->chn_state.is_completed) ? 1 : 0)
                     < p_sched->chn_state.cycle_count
                       + ((REQ_CONTROLS_CHN(p_sched->chn_state.token_state) && p_sched->chn_state.is_completed) ? 1 : 0) )
            {  /* this one is already done (more often) */
               dprintf(DBG_SCHED, "channel_schedule: fd %d wins by cycle count\n", p_walk->io.sock_fd);
               p_sched = p_walk;
            }
            else if (p_walk->chn_profile.sub_prio > p_sched->chn_profile.sub_prio)
            {  /* higher priority found */
               dprintf(DBG_SCHED, "channel_schedule: fd %d wins by sub-prio\n", p_walk->io.sock_fd);
               p_sched = p_walk;
            }
            else if (p_walk->chn_profile.sub_prio == p_sched->chn_profile.sub_prio)
            {  /* same priority */
               if ( REQ_CONTROLS_CHN(p_walk->chn_state.token_state) &&
                    !p_walk->chn_state.is_completed )
               {  /* this one is still active */
                  dprintf(DBG_SCHED, "channel_schedule: fd %d wins by being already active and non-complete\n", p_walk->io.sock_fd);
                  p_sched = p_walk;
               }
               else if ( REQ_CONTROLS_CHN(p_sched->chn_state.token_state) &&
                         p_sched->chn_state.is_completed )
               {  /* prev. selected one was completed -> choose next */
                  dprintf(DBG_SCHED, "channel_schedule: fd %d wins because active one is completed\n", p_walk->io.sock_fd);
                  p_sched = p_walk;
               }
               else if ( !REQ_CONTROLS_CHN(p_walk->chn_state.token_state) &&
                         !REQ_CONTROLS_CHN(p_sched->chn_state.token_state) )
               {  /* none active -> first come first serve */
                  if ( (p_walk->chn_state.last_start < p_sched->chn_state.last_start) ||
                       ( (p_walk->chn_state.last_start == p_sched->chn_state.last_start) &&
                         (p_walk->chn_profile.min_duration < p_sched->chn_profile.min_duration) ))
                  {
                     dprintf(DBG_SCHED, "channel_schedule: fd %d wins because longer non-active\n", p_walk->io.sock_fd);
                     p_sched = p_walk;
                  }
               }
            }
         }
         else
            p_sched = p_walk;
      }
   }

   if ((p_sched != p_active) && (p_active != NULL))
   {
      vbi_proxyd_channel_stopped(p_active);
   }

   return p_sched;
}

/* ----------------------------------------------------------------------------
** Update channel, after channel change request or connection release
** - if only background-prio clients are connected, the scheduler decides;
**   else the channel is switched (if the client matches the daemon's max prio)
*/
static vbi_bool
vbi_proxyd_channel_update( int dev_idx, PROXY_CLNT * req, vbi_bool forced_switch )
{
   PROXY_CLNT  * p_walk;
   PROXY_CLNT  * p_sched;
   PROXY_DEV   * p_proxy_dev;
   VBI_CHN_PRIO  max_chn_prio;
   vbi_bool      result;

   p_proxy_dev = proxy.dev + dev_idx;
   result      = FALSE;

   /* determine new max. channel priority */
   max_chn_prio = VBI_CHN_PRIO_BACKGROUND;
   for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
      if ((p_walk->dev_idx == dev_idx) && (p_walk->chn_prio > max_chn_prio))
         max_chn_prio = p_walk->chn_prio;

   if (p_proxy_dev->chn_prio != max_chn_prio)
   {
#if defined(ENABLE_V4L2) && defined(VIDIOC_S_PRIORITY)
      if (p_proxy_dev->vbi_api == VBI_API_V4L2)
      {
         enum v4l2_priority v4l2_prio = max_chn_prio;
         int fd = vbi_capture_fd(p_proxy_dev->p_capture);
         if (fd != -1)
         {
            if (ioctl(fd, VIDIOC_S_PRIORITY, &v4l2_prio) != 0)
            {
               dprintf(DBG_MSG, "Failed to set register v4l2 channel prio to %d: %d (%s)\n", p_proxy_dev->chn_prio, errno, strerror(errno));
            }
            else
            {
               ioctl(fd, VIDIOC_G_PRIORITY, &v4l2_prio);
               dprintf(DBG_MSG, "channel_update: dev #%d: setting v4l2 channel prio to %d (was %d) (dev prio is %d)\n", dev_idx, max_chn_prio, p_proxy_dev->chn_prio, v4l2_prio);
            }
         }
      }
#endif
      /* save the priority registered with the device */
      p_proxy_dev->chn_prio = max_chn_prio;
   }

   /* non-bg prio OR channel has already been switched -> clear scheduler active flag */
   if ( (max_chn_prio > VBI_CHN_PRIO_BACKGROUND) || forced_switch )
   {
      for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
         if ((p_walk->dev_idx == dev_idx) && REQ_CONTROLS_CHN(p_walk->chn_state.token_state))
            vbi_proxyd_channel_stopped(p_walk);
   }

   if (max_chn_prio == VBI_CHN_PRIO_BACKGROUND)
   {  /* background -> let scheduler decide */
      p_sched = vbi_proxyd_channel_schedule(dev_idx);
   }
   else if ( (req != NULL) &&
             (req->chn_prio == max_chn_prio) )
   {  /* non-background prio -> latest request wins */
      p_sched = req;
   }
   else
   {  /* reject switch by priority */
      p_sched = NULL;
   }

   if ( (p_sched != NULL) &&
        (max_chn_prio == VBI_CHN_PRIO_BACKGROUND) &&
        (REQ_CONTROLS_CHN(p_sched->chn_state.token_state) == FALSE) )
   {
      if ( vbi_proxyd_token_grant(p_sched) )
      {
         p_sched->chn_state.is_completed  = FALSE;
         p_sched->chn_state.last_duration = 0;
         p_sched->chn_state.last_start    = time(NULL);

         /* return TRUE if requested channel control can be granted immediately */
         result = (p_sched == req);
      }
   }
   else
   {  /* no channel change is allowed or required */

      /* flush-only flag: assume client has already done the switch -> must flush VBI buffers */
      if (forced_switch)
      {
         vbi_capture_flush(p_proxy_dev->p_capture);
      }
   }

   if (max_chn_prio == VBI_CHN_PRIO_BACKGROUND)
      vbi_proxyd_channel_timer_update();

   return result;
}

/* ----------------------------------------------------------------------------
** Flush after channel change
*/
static void vbi_proxyd_channel_flush( int dev_idx, PROXY_CLNT * req )
{
   PROXY_CLNT  * p_walk;
   PROXY_DEV   * p_proxy_dev;

   req = req;

   p_proxy_dev = proxy.dev + dev_idx;

   if (p_proxy_dev->p_capture != NULL)
   {
      /* flush capture buffers */
      vbi_capture_flush(p_proxy_dev->p_capture);

      /* flush slicer output buffer queues */
      vbi_proxy_queue_release_all(dev_idx);
   }

   /* trigger sending of change indication to all clients except the caller */
   for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
   {
      if ( (p_walk->dev_idx == dev_idx) &&
           ((p_walk->client_flags & VBI_PROXY_CLIENT_NO_STATUS_IND) == 0) )
      {
         p_walk->chn_status_ind |= VBI_PROXY_CHN_FLUSH;
      }
   }
}

/* ----------------------------------------------------------------------------
** Check channel scheduling on all devices for expired timers
*/
static void vbi_proxyd_channel_timer( void )
{
   PROXY_CLNT  * p_walk;
   PROXY_DEV   * p_proxy_dev;
   int           dev_idx;
   time_t        now;
   vbi_bool      do_schedule;
   int           user_count;

   now = time(NULL);

   for (dev_idx = 0; dev_idx < proxy.dev_count; dev_idx++)
   {
      p_proxy_dev = proxy.dev + dev_idx;
      do_schedule = FALSE;
      user_count  = 0;

      if (p_proxy_dev->chn_prio == VBI_CHN_PRIO_BACKGROUND)
      {
         for (p_walk = proxy.p_clnts; p_walk != NULL; p_walk = p_walk->p_next)
         {
            if (p_walk->dev_idx == dev_idx)
            {
               if ( REQ_CONTROLS_CHN(p_walk->chn_state.token_state) &&
                    (p_walk->chn_state.is_completed == FALSE) &&
                    (now - p_walk->chn_state.last_start >= p_walk->chn_profile.min_duration) )
               {
                  do_schedule = TRUE;
               }
               user_count += 1;
            }
         }

         if (do_schedule && (user_count > 1))
         {
            dprintf(DBG_MSG, "schedule_timer: schedule device #%d\n", dev_idx);

            vbi_proxyd_channel_update(dev_idx, NULL, FALSE);
         }
      }
   }
}

/* ----------------------------------------------------------------------------
** Process client ioctl request
*/
static vbi_bool
vbi_proxyd_take_ioctl_req( PROXY_CLNT * req, int request, void * p_arg_data,
                           unsigned int arg_size, int * p_result, int * p_errcode )
{
   PROXY_DEV   * p_proxy_dev;
   vbi_bool      req_perm;
   int           size;
   int           vbi_fd;
   vbi_bool      opened = FALSE;
   vbi_bool      result = FALSE;

   p_proxy_dev = proxy.dev + req->dev_idx;

   if (p_proxy_dev->p_capture == NULL)
   {
      vbi_proxy_start_acquisition(req->dev_idx, NULL);
      opened = TRUE;
   }

   vbi_fd = vbi_capture_fd(p_proxy_dev->p_capture);
   if (vbi_fd != -1)
   {
      size = vbi_proxy_msg_check_ioctl(p_proxy_dev->vbi_api, request, p_arg_data, &req_perm);
      if ((size >= 0) && (size == (int) arg_size))
      {
         /* FIXME */
         if ( (req_perm == FALSE) ||
              (req->chn_prio >= p_proxy_dev->chn_prio) ||
              REQ_CONTROLS_CHN(req->chn_state.token_state) )
         {
            /* TODO: possibly update norm, flush channel */
            errno = 0;
            /* do the actual ioctl */
            *p_result  = ioctl(vbi_capture_fd(p_proxy_dev->p_capture), request, p_arg_data);
            *p_errcode = errno;

            result = TRUE;
         }
         else
            dprintf(DBG_MSG, "take_ioctl_req: no permission\n");
      }
      else
         dprintf(DBG_MSG, "take_ioctl_req: invalid ioctl 0x%X or size %d\n", request, arg_size);
   }

   if (opened)
   {
      vbi_proxy_stop_acquisition(p_proxy_dev);
   }

   return result;
}

/* ----------------------------------------------------------------------------
** Close the connection to the client
** - frees all allocated resources
*/
static void vbi_proxyd_close( PROXY_CLNT * req, vbi_bool close_all )
{
   close_all = close_all;

   if (req->state != REQ_STATE_CLOSED)
   {
      dprintf(DBG_MSG, "close: fd %d\n", req->io.sock_fd);
      vbi_proxy_msg_logger(LOG_INFO, req->io.sock_fd, 0, "closing connection", NULL);

      vbi_proxy_msg_close_io(&req->io);

      pthread_mutex_lock(&proxy.dev[req->dev_idx].queue_mutex);

      while (req->p_sliced != NULL)
      {
         vbi_proxy_queue_release_sliced(req);
      }

      pthread_mutex_unlock(&proxy.dev[req->dev_idx].queue_mutex);

      req->state = REQ_STATE_CLOSED;
   }
}

/* ----------------------------------------------------------------------------
** Initialize a request structure for a new client and add it to the list
*/
static void vbi_proxyd_add_connection( int listen_fd, int dev_idx, vbi_bool isLocal )
{
   PROXY_CLNT * req;
   PROXY_CLNT * p_walk;
   int sock_fd;

   isLocal = isLocal;

   sock_fd = vbi_proxy_msg_accept_connection(listen_fd);
   if (sock_fd != -1)
   {
      req = calloc(sizeof(*req), 1);
      if (req != NULL)
      {
         dprintf(DBG_MSG, "add_connection: fd %d\n", sock_fd);

         req->state         = REQ_STATE_WAIT_CON_REQ;
         req->io.lastIoTime = time(NULL);
         req->io.sock_fd    = sock_fd;
         req->dev_idx       = dev_idx;
         req->chn_prio      = DEFAULT_CHN_PRIO;

         pthread_mutex_lock(&proxy.clnt_mutex);

         /* append request to the end of the chain
         ** note: order is significant for priority in adding services */
         if (proxy.p_clnts != NULL)
         {
            p_walk = proxy.p_clnts;
            while (p_walk->p_next != NULL)
               p_walk = p_walk->p_next;

            p_walk->p_next = req;
         }
         else
            proxy.p_clnts = req;

         proxy.clnt_count  += 1;

         pthread_mutex_unlock(&proxy.clnt_mutex);
      }
      else
         dprintf(DBG_MSG, "add_connection: fd %d: virtual memory exhausted, abort\n", sock_fd);
   }
}

/* ----------------------------------------------------------------------------
** Initialize state for a new device
*/
static void vbi_proxyd_add_device( const char * p_dev_name )
{
   PROXY_DEV  * p_proxy_dev;

   if (proxy.dev_count < SRV_MAX_DEVICES)
   {
      p_proxy_dev = proxy.dev + proxy.dev_count;

      p_proxy_dev->p_dev_name  = p_dev_name;
      p_proxy_dev->p_sock_path = vbi_proxy_msg_get_socket_name(p_dev_name);
      p_proxy_dev->pipe_fd = -1;
      p_proxy_dev->vbi_fd  = -1;
      p_proxy_dev->wr_fd   = -1;

      /* initialize synchonization facilities */
      pthread_cond_init(&p_proxy_dev->start_cond, NULL);
      pthread_mutex_init(&p_proxy_dev->start_mutex, NULL);
      pthread_mutex_init(&p_proxy_dev->queue_mutex, NULL);

      proxy.dev_count += 1;
   }
}

/* ----------------------------------------------------------------------------
** Transmit one buffer of sliced data
** - returns FALSE upon I/O error
** - also returns a "blocked" flag which is TRUE if not all data could be written
**   can be used by the caller to "stuff" the pipe, i.e. write a series of messages
**   until the pipe is full
** - XXX optimization required: don't copy the block (required however if client
**   doesn't want all services)
*/
static vbi_bool vbi_proxyd_send_sliced( PROXY_CLNT * req, vbi_bool * p_blocked )
{
   VBIPROXY_MSG * p_msg;
   uint32_t  msg_size;
   vbi_bool  result = FALSE;
   int max_lines;
   int idx;

   if ((req != NULL) && (p_blocked != NULL) && (req->p_sliced != NULL))
   {
      if (VBI_RAW_SERVICES(req->all_services))
         msg_size = VBIPROXY_SLICED_IND_SIZE(0, req->p_sliced->max_lines);
      else
         msg_size = VBIPROXY_SLICED_IND_SIZE(req->p_sliced->line_count, 0);

      msg_size += sizeof(VBIPROXY_MSG_HEADER);
      p_msg = malloc(msg_size);

      /* filter for services requested by this client */
      max_lines = req->vbi_count[0] + req->vbi_count[1];
      p_msg->body.sliced_ind.timestamp = req->p_sliced->timestamp;
      p_msg->body.sliced_ind.sliced_lines = 0;
      p_msg->body.sliced_ind.raw_lines = 0;

      /* XXX TODO allow both raw and sliced in the same message */
      if (VBI_RAW_SERVICES(req->all_services) == FALSE)
      {
         for (idx = 0; (idx < req->p_sliced->line_count) && (idx < max_lines); idx++)
         {
            if ((req->p_sliced->lines[idx].id & req->all_services) != 0)
            {
               memcpy(p_msg->body.sliced_ind.u.sliced + p_msg->body.sliced_ind.sliced_lines,
                      req->p_sliced->lines + idx, sizeof(vbi_sliced));
               p_msg->body.sliced_ind.sliced_lines += 1;
            }
         }
      }
      else
      {
         if (req->p_sliced->p_raw_data != NULL)
         {
            memcpy(p_msg->body.sliced_ind.u.raw,
                   req->p_sliced->p_raw_data,
                   VBIPROXY_RAW_LINE_SIZE * req->p_sliced->max_lines);
            p_msg->body.sliced_ind.raw_lines = req->p_sliced->max_lines;
         }
      }
      msg_size = VBIPROXY_SLICED_IND_SIZE(p_msg->body.sliced_ind.sliced_lines,
                                          p_msg->body.sliced_ind.raw_lines);

      vbi_proxy_msg_write(&req->io, MSG_TYPE_SLICED_IND, msg_size, p_msg, TRUE);

      if (vbi_proxy_msg_handle_write(&req->io, p_blocked))
      {
         /* if the last block could not be transmitted fully, quit the loop */
         if (req->io.writeLen > 0)
         {
            dprintf(DBG_CLNT, "send_sliced: socket blocked\n");
            *p_blocked = TRUE;
         }
         result = TRUE;
      }
   }
   else
      dprintf(DBG_MSG, "send_sliced: illegal NULL ptr params\n");

   return result;
}

/* ----------------------------------------------------------------------------
** Checks the size of a message from client to server
*/
static vbi_bool vbi_proxyd_check_msg( VBIPROXY_MSG * pMsg, vbi_bool * pEndianSwap )
{
   VBIPROXY_MSG_HEADER * pHead = &pMsg->head;
   VBIPROXY_MSG_BODY   * pBody = &pMsg->body;
   unsigned int len = pMsg->head.len;
   vbi_bool result = FALSE;

   switch (pHead->type)
   {
      case MSG_TYPE_CONNECT_REQ:
         if ( (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->connect_req)) &&
              (memcmp(pBody->connect_req.magics.protocol_magic, VBIPROXY_MAGIC_STR, VBIPROXY_MAGIC_LEN) == 0) )
         {
            if (pBody->connect_req.magics.endian_magic == VBIPROXY_ENDIAN_MAGIC)
            {
               *pEndianSwap = FALSE;
               result       = TRUE;
            }
            else if (pBody->connect_req.magics.endian_magic == VBIPROXY_ENDIAN_MISMATCH)
            {
               *pEndianSwap = TRUE;
               result       = TRUE;
            }
         }
         break;

      case MSG_TYPE_SERVICE_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->service_req));
         break;

      case MSG_TYPE_CHN_TOKEN_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_token_req));
         break;

      case MSG_TYPE_CHN_NOTIFY_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_notify_req));
         break;

      case MSG_TYPE_CHN_SUSPEND_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_notify_req));
         break;

      case MSG_TYPE_CHN_IOCTL_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) +
                          VBIPROXY_CHN_IOCTL_REQ_SIZE(pBody->chn_ioctl_req.arg_size));
         break;

      case MSG_TYPE_CHN_RECLAIM_CNF:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_reclaim_cnf));
         break;

      case MSG_TYPE_CLOSE_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER));
         break;

      case MSG_TYPE_DAEMON_PID_REQ:
         result = ( (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->daemon_pid_req)) &&
                    (memcmp(pBody->daemon_pid_req.magics.protocol_magic, VBIPROXY_MAGIC_STR, VBIPROXY_MAGIC_LEN) == 0) &&
                    (pBody->daemon_pid_req.magics.endian_magic == VBIPROXY_ENDIAN_MAGIC) );
         break;

      case MSG_TYPE_DAEMON_PID_CNF:
         /* note: this is a daemon reply but accepted here since the daemon sends it to itself */
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->daemon_pid_cnf));
         break;

      case MSG_TYPE_CONNECT_CNF:
      case MSG_TYPE_CONNECT_REJ:
      case MSG_TYPE_SERVICE_CNF:
      case MSG_TYPE_SERVICE_REJ:
      case MSG_TYPE_SLICED_IND:
      case MSG_TYPE_CHN_TOKEN_CNF:
      case MSG_TYPE_CHN_TOKEN_IND:
      case MSG_TYPE_CHN_NOTIFY_CNF:
      case MSG_TYPE_CHN_SUSPEND_CNF:
      case MSG_TYPE_CHN_SUSPEND_REJ:
      case MSG_TYPE_CHN_IOCTL_CNF:
      case MSG_TYPE_CHN_IOCTL_REJ:
      case MSG_TYPE_CHN_RECLAIM_REQ:
      case MSG_TYPE_CHN_CHANGE_IND:
         dprintf(DBG_MSG, "check_msg: recv client msg %d (%s) at server side\n", pHead->type, vbi_proxy_msg_debug_get_type_str(pHead->type));
         result = FALSE;
         break;

      default:
         dprintf(DBG_MSG, "check_msg: unknown msg #%d\n", pHead->type);
         result = FALSE;
         break;
   }

   if (result == FALSE)
      dprintf(DBG_MSG, "check_msg: illegal msg: len=%d, type=%d (%s)\n", len, pHead->type, vbi_proxy_msg_debug_get_type_str(pHead->type));

   return result;
}

/* ----------------------------------------------------------------------------
** Handle message from client
** - note: consistancy checks were already done by the I/O handler
**   except for higher level messages (must be checked by acqctl module)
** - implemented as a matrix: "switch" over server state, and "if" cascades
**   over message type
** - XXX warning: inbound messages use the same buffer as outbound!
**   must have finished evaluating the message before assembling the reply
*/
static vbi_bool vbi_proxyd_take_message( PROXY_CLNT *req, VBIPROXY_MSG * pMsg )
{
   VBIPROXY_MSG_BODY * pBody = &pMsg->body;
   vbi_bool result = FALSE;

   dprintf(DBG_CLNT, "take_message: fd %d: recv msg type %d (%s)\n", req->io.sock_fd, pMsg->head.type, vbi_proxy_msg_debug_get_type_str(pMsg->head.type));

   switch (pMsg->head.type)
   {
      case MSG_TYPE_CONNECT_REQ:
         if (req->state == REQ_STATE_WAIT_CON_REQ)
         {
            if (pBody->connect_req.magics.protocol_compat_version == VBIPROXY_COMPAT_VERSION)
            {
               dprintf(DBG_MSG, "New client: fd %d: '%s' pid=%d services=0x%X\n", req->io.sock_fd, pBody->connect_req.client_name, pBody->connect_req.pid, pBody->connect_req.services);

               /* if provided, update norm hint (used for first client on ancient v4l1 drivers only) */
               if (pBody->connect_req.scanning != 0)
                  proxy.dev[req->dev_idx].scanning = pBody->connect_req.scanning;

               /* enable forwarding of captured data (must be set before processing request!) */
               req->state = REQ_STATE_FORWARD;

               req->buffer_count = pBody->connect_req.buffer_count;
               req->client_flags = pBody->connect_req.client_flags;  /* XXX TODO (timeout supression) */

               /* must make very sure strict is within bounds, because it's used as array index */
               if (pBody->connect_req.strict < VBI_MIN_STRICT)
                  pBody->connect_req.strict = VBI_MIN_STRICT;
               else if (pBody->connect_req.strict > VBI_MAX_STRICT)
                  pBody->connect_req.strict = VBI_MAX_STRICT;

               if ( vbi_proxyd_take_service_req(req, pBody->connect_req.services,
						pBody->connect_req.strict,
						(char *) req->msg_buf.body.connect_rej.errorstr) )
               { 
                  /* open & service initialization succeeded -> reply with confirm */
                  vbi_proxy_msg_fill_magics(&req->msg_buf.body.connect_cnf.magics);
                  strlcpy((char *) req->msg_buf.body.connect_cnf.dev_vbi_name,
                          proxy.dev[req->dev_idx].p_dev_name, VBIPROXY_DEV_NAME_MAX_LENGTH);
                  req->msg_buf.body.connect_cnf.dev_vbi_name[VBIPROXY_DEV_NAME_MAX_LENGTH - 1] = 0;
                  req->msg_buf.body.connect_cnf.pid = getpid();
                  req->msg_buf.body.connect_cnf.vbi_api_revision = proxy.dev[req->dev_idx].vbi_api;
                  req->msg_buf.body.connect_cnf.daemon_flags = ((opt_debug_level > 0) ? VBI_PROXY_DAEMON_NO_TIMEOUTS : 0);

                  req->msg_buf.body.connect_cnf.services = req->all_services;
                  if (proxy.dev[req->dev_idx].p_decoder != NULL)
                  {
                     req->msg_buf.body.connect_cnf.dec = *proxy.dev[req->dev_idx].p_decoder;
                     req->msg_buf.body.connect_cnf.dec.pattern = NULL;
                  }
                  else
                  {  /* acquisition not running: if the request is still considered sucessful
                     ** this is only possible if no services were requested */
                     memset(&req->msg_buf.body.connect_cnf.dec, 0,
                            sizeof(req->msg_buf.body.connect_cnf.dec));
                     req->msg_buf.body.connect_cnf.dec.start[0] = -1;
                     req->msg_buf.body.connect_cnf.dec.start[1] = -1;
                  }

                  vbi_proxy_msg_write(&req->io, MSG_TYPE_CONNECT_CNF,
                                      sizeof(req->msg_buf.body.connect_cnf),
                                      &req->msg_buf, FALSE);
               }
               else
               {
                  vbi_proxy_msg_fill_magics(&req->msg_buf.body.connect_rej.magics);

                  vbi_proxy_msg_write(&req->io, MSG_TYPE_CONNECT_REJ,
                                      sizeof(req->msg_buf.body.connect_rej),
                                      &req->msg_buf, FALSE);

                  /* drop the connection after sending the reject message */
                  req->state = REQ_STATE_WAIT_CLOSE;
               }
            }
            else
            {  /* client uses incompatible protocol version */
               vbi_proxy_msg_fill_magics(&req->msg_buf.body.connect_rej.magics);
               strlcpy((char *) req->msg_buf.body.connect_rej.errorstr,
                       "Incompatible proxy protocol version", VBIPROXY_ERROR_STR_MAX_LENGTH);
               req->msg_buf.body.connect_rej.errorstr[VBIPROXY_ERROR_STR_MAX_LENGTH - 1] = 0;
               vbi_proxy_msg_write(&req->io, MSG_TYPE_CONNECT_REJ,
                                   sizeof(req->msg_buf.body.connect_rej),
                                   &req->msg_buf, FALSE);
               /* drop the connection */
               req->state = REQ_STATE_WAIT_CLOSE;
            }
            result = TRUE;
         }
         break;

      case MSG_TYPE_DAEMON_PID_REQ:
         if (req->state == REQ_STATE_WAIT_CON_REQ)
         {  /* this message can be sent instead of a connect request */
            vbi_proxy_msg_fill_magics(&req->msg_buf.body.daemon_pid_cnf.magics);
            req->msg_buf.body.daemon_pid_cnf.pid = getpid();
            vbi_proxy_msg_write(&req->io, MSG_TYPE_DAEMON_PID_CNF,
                                sizeof(req->msg_buf.body.daemon_pid_cnf),
                                &req->msg_buf, FALSE);
            req->state = REQ_STATE_WAIT_CLOSE;
            result = TRUE;
         }
         break;

      case MSG_TYPE_SERVICE_REQ:
         if (req->state == REQ_STATE_FORWARD)
         {
            if (pBody->service_req.reset)
               memset(req->services, 0, sizeof(req->services));

            dprintf(DBG_MSG, "Update client: fd %d services: 0x%X (was %X)\n", req->io.sock_fd, pBody->service_req.services, req->all_services);

            /* flush all buffers in this client's queue */
            pthread_mutex_lock(&proxy.dev[req->dev_idx].queue_mutex);
            while (req->p_sliced != NULL)
            {
               vbi_proxy_queue_release_sliced(req);
            }
            pthread_mutex_unlock(&proxy.dev[req->dev_idx].queue_mutex);

            if ( vbi_proxyd_take_service_req(req, pBody->service_req.services,
					     pBody->service_req.strict,
					     (char *) req->msg_buf.body.service_rej.errorstr) )
            {
               if (proxy.dev[req->dev_idx].p_decoder != NULL)
               {
                  req->msg_buf.body.service_cnf.dec = *proxy.dev[req->dev_idx].p_decoder;
                  req->msg_buf.body.connect_cnf.dec.pattern = NULL;
               }
               else
               {  /* acquisition not running: if the request is still considered sucessful
                  ** this is only possible if no services were requested */
                  memset(&req->msg_buf.body.connect_cnf.dec, 0,
                         sizeof(req->msg_buf.body.connect_cnf.dec));
                  req->msg_buf.body.connect_cnf.dec.start[0] = -1;
                  req->msg_buf.body.connect_cnf.dec.start[1] = -1;
               }
               req->msg_buf.body.service_cnf.services = req->all_services;

               vbi_proxy_msg_write(&req->io, MSG_TYPE_SERVICE_CNF,
                                   sizeof(req->msg_buf.body.service_cnf),
                                   &req->msg_buf, FALSE);
            }
            else
            {
               vbi_proxy_msg_write(&req->io, MSG_TYPE_SERVICE_REJ,
                                   sizeof(req->msg_buf.body.service_rej),
                                   &req->msg_buf, FALSE);
            }
            result = TRUE;
         }
         break;

      case MSG_TYPE_CHN_TOKEN_REQ:
         if (req->state == REQ_STATE_FORWARD)
         {
            dprintf(DBG_MSG, "channel token request: fd %d: prio=%d sub-prio=0x%02X\n", req->io.sock_fd, pBody->chn_token_req.chn_prio, pBody->chn_token_req.chn_profile.sub_prio);

            /* update channel description and profile */
            req->chn_prio = pBody->chn_token_req.chn_prio;
            req->chn_profile = pBody->chn_token_req.chn_profile;
            memset(&req->chn_state, 0, sizeof(req->chn_state));

            /* XXX TODO: return elements: permitted, non_excl */
            memset(&req->msg_buf.body.chn_token_cnf, 0, sizeof(req->msg_buf.body.chn_token_cnf));
            vbi_proxyd_channel_update(req->dev_idx, req, FALSE);
            if (req->chn_state.token_state == REQ_TOKEN_GRANT)
            {
               req->chn_state.token_state = REQ_TOKEN_GRANTED;
               req->msg_buf.body.chn_token_cnf.token_ind = TRUE;
            }
            else
            {
               req->msg_buf.body.chn_token_cnf.token_ind = FALSE;
            }
            vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_TOKEN_CNF,
                                sizeof(req->msg_buf.body.chn_token_cnf),
                                &req->msg_buf, FALSE);
            result = TRUE;
         }
         break;

      case MSG_TYPE_CHN_NOTIFY_REQ:
         if (req->state == REQ_STATE_FORWARD)
         {
            vbi_bool chn_upd = FALSE;
            vbi_bool chn_forced = FALSE;
            dprintf(DBG_MSG, "channel notify: fd %d: flags=0x%X scanning=%d\n", req->io.sock_fd, pBody->chn_notify_req.notify_flags, pBody->chn_notify_req.scanning);

            if (pBody->chn_notify_req.notify_flags & VBI_PROXY_CHN_NORM)
            {
               /* query (verify) new scanning -> inform all clients (line count changes) */
               vbi_proxyd_update_scanning(req->dev_idx, req, pBody->chn_notify_req.scanning);
            }

            if (pBody->chn_notify_req.notify_flags & VBI_PROXY_CHN_FAIL)
            {
               // XXX TODO: ignore if client hasn't got the token
               //           else inform scheduler: 
            }
            if (pBody->chn_notify_req.notify_flags & VBI_PROXY_CHN_FLUSH)
            {
               vbi_proxyd_channel_flush(req->dev_idx, req);
               chn_upd = TRUE;
               chn_forced = ! REQ_CONTROLS_CHN(req->chn_state.token_state);
            }
            if (pBody->chn_notify_req.notify_flags & VBI_PROXY_CHN_RELEASE)
            {
               if (req->chn_state.token_state != REQ_TOKEN_NONE)
               {
                  req->chn_state.token_state = REQ_TOKEN_NONE;
                  chn_upd = TRUE;
               }
               req->chn_profile.is_valid = FALSE;
            }
            else if (pBody->chn_notify_req.notify_flags & VBI_PROXY_CHN_TOKEN)
            {
               req->chn_state.token_state = REQ_TOKEN_RETURNED;
               chn_upd = TRUE;
            }

            if (chn_upd)
               vbi_proxyd_channel_update(req->dev_idx, req, chn_forced);

            memset(&req->msg_buf.body.chn_notify_cnf, 0, sizeof(req->msg_buf.body.chn_notify_cnf));
            req->msg_buf.body.chn_notify_cnf.scanning = proxy.dev[req->dev_idx].scanning;

            vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_NOTIFY_CNF,
                                sizeof(req->msg_buf.body.chn_notify_cnf),
                                &req->msg_buf, FALSE);
            req->chn_status_ind = VBI_PROXY_CHN_NONE;
            result = TRUE;
         }
         break;

      case MSG_TYPE_CHN_SUSPEND_REQ:
         /* XXX TODO */
         vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_SUSPEND_REJ,
                             sizeof(req->msg_buf.body.chn_suspend_rej),
                             &req->msg_buf, FALSE);
         result = TRUE;
         break;

      case MSG_TYPE_CHN_IOCTL_REQ:
         if (req->state == REQ_STATE_FORWARD)
         {
            /* XXX TODO: message may be longer than pre-allocated message buffer */

            if ( vbi_proxyd_take_ioctl_req(req, req->msg_buf.body.chn_ioctl_req.request,
                                                req->msg_buf.body.chn_ioctl_req.arg_data,
                                                req->msg_buf.body.chn_ioctl_req.arg_size,
                                                &req->msg_buf.body.chn_ioctl_cnf.result,
                                                &req->msg_buf.body.chn_ioctl_cnf.errcode) )
            {
               /* note: argsize and arg_data unchanged from req. message */
               dprintf(DBG_MSG, "channel control ioctl: fd %d: request=0x%X result=%d errno=%d\n", req->io.sock_fd, req->msg_buf.body.chn_ioctl_req.request, req->msg_buf.body.chn_ioctl_cnf.result, req->msg_buf.body.chn_ioctl_cnf.errcode);

               vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_IOCTL_CNF,
                                   VBIPROXY_CHN_IOCTL_CNF_SIZE(req->msg_buf.body.chn_ioctl_req.arg_size),
                                   &req->msg_buf, FALSE);
            }
            else
            {
               vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_IOCTL_REJ,
                                   sizeof(req->msg_buf.body.chn_ioctl_rej),
                                   &req->msg_buf, FALSE);
            }
            result = TRUE;
         }
         break;

      case MSG_TYPE_CHN_RECLAIM_CNF:
         if (req->chn_state.token_state == REQ_TOKEN_RELEASE)
         {
            dprintf(DBG_MSG, "channel token reclain confirm: fd %d\n", req->io.sock_fd);
            req->chn_state.token_state = REQ_TOKEN_NONE;
            vbi_proxyd_channel_update(req->dev_idx, NULL, FALSE);
         }
         result = TRUE;
         break;

      case MSG_TYPE_CLOSE_REQ:
         /* close the connection */
         vbi_proxyd_close(req, FALSE);
         result = TRUE;
         break;

      default:
         /* unknown message or client-only message */
         dprintf(DBG_MSG, "take_message: protocol error: unexpected message type %d (%s)\n", pMsg->head.type, vbi_proxy_msg_debug_get_type_str(pMsg->head.type));
         break;
   }

   if (result == FALSE)
      dprintf(DBG_MSG, "take_message: message type %d (%s, len %d) not expected in state %d\n", pMsg->head.type, vbi_proxy_msg_debug_get_type_str(pMsg->head.type), pMsg->head.len, req->state);

   return result;
}

/* ----------------------------------------------------------------------------
** Set bits for all active sockets in fd_set for select syscall
*/
static int vbi_proxyd_get_fd_set( fd_set * rd, fd_set * wr )
{
   PROXY_CLNT   * req;
   PROXY_DEV    * p_proxy_dev;
   int            dev_idx;
   int            max_fd;

   max_fd = 0;

   /* add TCP/IP and UNIX-domain listening sockets */
   if ((proxy.max_conn == 0) || (proxy.clnt_count < proxy.max_conn))
   {
      if (proxy.tcp_ip_fd != -1)
      {
         FD_SET(proxy.tcp_ip_fd, rd);
         if (proxy.tcp_ip_fd > max_fd)
             max_fd = proxy.tcp_ip_fd;
      }
   }

   /* add listening sockets and VBI devices, if currently opened */
   p_proxy_dev = proxy.dev;
   for (dev_idx = 0; dev_idx < proxy.dev_count; dev_idx++, p_proxy_dev++)
   {
      if (p_proxy_dev->pipe_fd != -1)
      {
         FD_SET(p_proxy_dev->pipe_fd, rd);
         if (p_proxy_dev->pipe_fd > max_fd)
             max_fd = p_proxy_dev->pipe_fd;
      }

      if (p_proxy_dev->vbi_fd != -1)
      {
         FD_SET(p_proxy_dev->vbi_fd, rd);
         if (p_proxy_dev->vbi_fd > max_fd)
            max_fd = p_proxy_dev->vbi_fd;
      }
   }

   /* add client connection sockets */
   for (req = proxy.p_clnts; req != NULL; req = req->p_next)
   {
      /* read and write are exclusive and write takes precedence over read
      ** (i.e. read only if no write is pending or if a read operation has already been started)
      */
      if ( vbi_proxy_msg_read_idle(&req->io) == FALSE )
      {
         FD_SET(req->io.sock_fd, rd);
      }
      else
      if ( (vbi_proxy_msg_write_idle(&req->io) == FALSE) ||
           (req->p_sliced != NULL) || (req->chn_status_ind != VBI_PROXY_CHN_NONE) )
      {
         FD_SET(req->io.sock_fd, wr);
      }
      else
         FD_SET(req->io.sock_fd, rd);

      if (req->io.sock_fd > max_fd)
          max_fd = req->io.sock_fd;
   }

   return max_fd;
}

/* ----------------------------------------------------------------------------
** Proxy daemon central connection handling
*/
static void vbi_proxyd_handle_client_sockets( fd_set * rd, fd_set * wr )
{
   PROXY_CLNT    *req;
   PROXY_CLNT    *prev, *tmp;
   vbi_bool      io_blocked;
   time_t now = time(NULL);

   /* handle active connections */
   for (req = proxy.p_clnts, prev = NULL; req != NULL; )
   {
      io_blocked = FALSE;

      if ( FD_ISSET(req->io.sock_fd, rd) &&
           vbi_proxy_msg_write_idle(&req->io) )
      {
         /* incoming data -> start reading */
         dprintf(DBG_CLNT, "handle_client_sockets: fd %d: receiving data\n", req->io.sock_fd);

         if (vbi_proxy_msg_handle_read(&req->io, &io_blocked, TRUE, &req->msg_buf, sizeof(req->msg_buf)))
         {
            /* check for finished read -> process request */
            if ( (req->io.readOff != 0) && (req->io.readOff == req->io.readLen) )
            {
               if (vbi_proxyd_check_msg(&req->msg_buf, &req->endianSwap))
               {
                  vbi_proxy_msg_close_read(&req->io);

                  if (vbi_proxyd_take_message(req, &req->msg_buf) == FALSE)
                  {  /* message no accepted (e.g. wrong state) */
                     vbi_proxyd_close(req, FALSE);
                  }
               }
               else
               {  /* message has illegal size or content */
                  vbi_proxyd_close(req, FALSE);
               }
            }
         }
         else
            vbi_proxyd_close(req, FALSE);
      }
      else if ( FD_ISSET(req->io.sock_fd, wr) &&
                !vbi_proxy_msg_write_idle(&req->io) )
      {
         if (vbi_proxy_msg_handle_write(&req->io, &io_blocked) == FALSE)
         {
            vbi_proxyd_close(req, FALSE);
         }
      }

      if (req->state == REQ_STATE_WAIT_CLOSE)
      {  /* close was pending after last write */
         vbi_proxyd_close(req, FALSE);
      }
      else if (vbi_proxy_msg_is_idle(&req->io))
      {  /* currently no I/O in progress */

         if (req->chn_state.token_state == REQ_TOKEN_RECLAIM)
         {
            dprintf(DBG_MSG, "channel token reclaim: fd %d\n", req->io.sock_fd);
            /* XXX TODO: supervise return of token by timer */
            memset(&req->msg_buf, 0, sizeof(req->msg_buf));
            vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_RECLAIM_REQ,
                                sizeof(req->msg_buf.body.chn_reclaim_req), &req->msg_buf, FALSE);
            req->chn_state.token_state = REQ_TOKEN_RELEASE;
         }
         else if (req->chn_state.token_state == REQ_TOKEN_GRANT)
         {
            dprintf(DBG_MSG, "channel token grant: fd %d\n", req->io.sock_fd);
            memset(&req->msg_buf, 0, sizeof(req->msg_buf));
            vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_TOKEN_IND,
                                sizeof(req->msg_buf.body.chn_token_ind), &req->msg_buf, FALSE);
            req->chn_state.token_state = REQ_TOKEN_GRANTED;
         }
         else if (req->chn_status_ind)
         {  /* send channel change indication */
            memset(&req->msg_buf, 0, sizeof(req->msg_buf));
            req->msg_buf.body.chn_change_ind.notify_flags = req->chn_status_ind;
            req->msg_buf.body.chn_change_ind.scanning = proxy.dev[req->dev_idx].scanning;

            vbi_proxy_msg_write(&req->io, MSG_TYPE_CHN_CHANGE_IND,
                                sizeof(req->msg_buf.body.chn_change_ind), &req->msg_buf, FALSE);
            req->chn_status_ind = VBI_PROXY_CHN_NONE;
         }
         else
         {
            /* forward data from slicer out queue */
            while ((req->p_sliced != NULL) && (io_blocked == FALSE))
            {
               dprintf(DBG_QU, "handle_sockets: fd %d: forward sliced frame with %d lines (of max %d)\n", req->io.sock_fd, req->p_sliced->line_count, req->p_sliced->max_lines);
               if (vbi_proxyd_send_sliced(req, &io_blocked) )
               {  /* only in success case because close releases all buffers */
                  pthread_mutex_lock(&proxy.dev[req->dev_idx].queue_mutex);
                  vbi_proxy_queue_release_sliced(req);
                  pthread_mutex_unlock(&proxy.dev[req->dev_idx].queue_mutex);
               }
               else
               {  /* I/O error */
                  vbi_proxyd_close(req, FALSE);
                  io_blocked = TRUE;
               }
            }
         }
      }

      if (req->io.sock_fd == -1)
      {  /* free resources (should be redundant, but does no harm) */
         vbi_proxyd_close(req, FALSE);
      }
      else if ( (req->state == REQ_STATE_WAIT_CON_REQ) &&
                ((req->client_flags & VBI_PROXY_CLIENT_NO_TIMEOUTS) == 0) &&
                vbi_proxy_msg_check_timeout(&req->io, now) )
      {
         dprintf(DBG_MSG, "handle_sockets: fd %d: i/o timeout in state %d (writeLen=%d, readLen=%d, readOff=%d, read msg type=%d: %s)\n", req->io.sock_fd, req->state, req->io.writeLen, req->io.readLen, req->io.readOff, req->msg_buf.head.type, vbi_proxy_msg_debug_get_type_str(req->msg_buf.head.type));
         vbi_proxyd_close(req, FALSE);
      }
      else /* check for protocol or network I/O timeout */
      if ( (req->state == REQ_STATE_WAIT_CON_REQ) &&
           (now > req->io.lastIoTime + SRV_CONNECT_TIMEOUT) )
      {
         dprintf(DBG_MSG, "handle_sockets: fd %d: protocol timeout in state %d\n", req->io.sock_fd, req->state);
         vbi_proxyd_close(req, FALSE);
      }

      if (req->state == REQ_STATE_CLOSED)
      {  /* connection was closed after network error */
         unsigned int clnt_services = req->all_services;
         int dev_idx = req->dev_idx;
         if (proxy.clnt_count > 0)
            proxy.clnt_count -= 1;
         dprintf(DBG_MSG, "handle_sockets: closed conn, %d remain\n", proxy.clnt_count);

         pthread_mutex_lock(&proxy.clnt_mutex);
         /* unlink from list */
         tmp = req;
         if (prev == NULL)
         {
            proxy.p_clnts = req->p_next;
            req = proxy.p_clnts;
         }
         else
         {
            prev->p_next = req->p_next;
            req = req->p_next;
         }
         pthread_mutex_unlock(&proxy.clnt_mutex);

         if (clnt_services != 0)
            vbi_proxyd_update_services(dev_idx, NULL, 0, NULL);
         if (proxy.dev[dev_idx].p_capture != NULL)
            vbi_proxyd_channel_update(dev_idx, NULL, FALSE);
         free(tmp);
      }
      else
      {
         prev = req;
         req = req->p_next;
      }
   }
}

/* ----------------------------------------------------------------------------
** Set maximum number of open client connections
** - note: does not close connections if max count is already exceeded
*/
static void vbi_proxyd_set_max_conn( unsigned int max_conn )
{
   proxy.max_conn = max_conn;
}

/* ----------------------------------------------------------------------------
** Set server IP address
** - must be called before the listening sockets are created
*/
static void vbi_proxyd_set_address( vbi_bool do_tcp_ip, const char * pIpStr, const char * pPort )
{
   /* free the memory allocated for the old config strings */
   if (proxy.listen_ip != NULL)
   {
      free(proxy.listen_ip);
      proxy.listen_ip = NULL;
   }
   if (proxy.listen_port != NULL)
   {
      free(proxy.listen_port);
      proxy.listen_port = NULL;
   }

   /* make a copy of the new config strings */
   if (pIpStr != NULL)
   {
      proxy.listen_ip = malloc(strlen(pIpStr) + 1);
      strcpy(proxy.listen_ip, pIpStr);
   }
   if (pPort != NULL)
   {
      proxy.listen_port = malloc(strlen(pPort) + 1);
      strcpy(proxy.listen_port, pPort);
   }
   proxy.do_tcp_ip = do_tcp_ip;
}

/* ----------------------------------------------------------------------------
** Emulate device permissions on the socket file
*/
static void vbi_proxyd_set_socket_perm( PROXY_DEV * p_proxy_dev )
{
   struct stat st;

   if (stat(p_proxy_dev->p_dev_name, &st) != -1)
   {
      if ( (chown(p_proxy_dev->p_sock_path, st.st_uid, st.st_gid) != 0) &&
           (chown(p_proxy_dev->p_sock_path, geteuid(), st.st_gid) != 0) )
         dprintf(DBG_MSG, "set_perm: failed to set socket owner %d.%d: %s\n", st.st_uid, st.st_gid, strerror(errno));

      if (chmod(p_proxy_dev->p_sock_path, st.st_mode) != 0)
         dprintf(DBG_MSG, "set_perm: failed to set socket permission %o: %s\n", st.st_mode, strerror(errno));
   }
   else
      dprintf(DBG_MSG, "set_perm: failed to stat VBI device %s\n", p_proxy_dev->p_dev_name);
}

/* ----------------------------------------------------------------------------
** Stop the server, close all connections, free resources
*/
static void vbi_proxyd_destroy( void )
{
   PROXY_CLNT  *req, *p_next;
   int  dev_idx;

   /* close all devices */
   for (dev_idx = 0; dev_idx < proxy.dev_count; dev_idx++)
   {
      vbi_proxy_stop_acquisition(proxy.dev + dev_idx);
   }

   /* shutdown all client connections & free resources */
   req = proxy.p_clnts;
   while (req != NULL)
   {
      p_next = req->p_next;
      vbi_proxyd_close(req, TRUE);
      free(req);
      req = p_next;
   }
   proxy.p_clnts = NULL;
   proxy.clnt_count = 0;

   /* close listening sockets */
   for (dev_idx = 0; dev_idx < proxy.dev_count; dev_idx++)
   {
      if (proxy.dev[dev_idx].pipe_fd != -1)
      {
         vbi_proxy_msg_stop_listen(FALSE, proxy.dev[dev_idx].pipe_fd, proxy.dev[dev_idx].p_sock_path);
      }

      if (proxy.dev[dev_idx].p_sock_path != NULL)
         free(proxy.dev[dev_idx].p_sock_path);

      pthread_cond_destroy(&proxy.dev[dev_idx].start_cond);
      pthread_mutex_destroy(&proxy.dev[dev_idx].start_mutex);
      pthread_mutex_destroy(&proxy.dev[dev_idx].queue_mutex);
   }

   if (proxy.tcp_ip_fd != -1)
   {
      vbi_proxy_msg_stop_listen(TRUE, proxy.tcp_ip_fd, NULL);
   }

   vbi_proxy_msg_logger(LOG_NOTICE, -1, 0, "shutting down", NULL);

   /* free the memory allocated for the config strings */
   vbi_proxyd_set_address(FALSE, NULL, NULL);
   vbi_proxy_msg_set_logging(FALSE, 0, 0, NULL);
}

/* ---------------------------------------------------------------------------
** Signal handler to process alarm
*/
static void vbi_proxyd_alarm_handler( int sigval )
{
   sigval = sigval;

   proxy.chn_sched_alarm = TRUE;
}

/* ---------------------------------------------------------------------------
** Signal handler to catch deadly signals
*/
static void vbi_proxyd_signal_handler( int sigval )
{
   char str_buf[10];

   snprintf(str_buf, sizeof (str_buf), "%d", sigval);
   vbi_proxy_msg_logger(LOG_NOTICE, -1, 0, "terminated by signal", str_buf, NULL);

   proxy.should_exit = TRUE;
}

/* ----------------------------------------------------------------------------
** Initialize DB server
*/
static void vbi_proxyd_init( void )
{
   struct sigaction  act;

   if (opt_no_detach == FALSE)
   {
      if (fork() > 0)
         exit(0);
      close(0); 
      open("/dev/null", O_RDONLY, 0);

      if (opt_debug_level == 0)
      {
         close(1);
         open("/dev/null", O_WRONLY, 0);
         close(2);
         if (dup(1) == -1)
             exit(1);

         setsid();
      }
   }

   /* ignore broken pipes (handled by select/read) */
   memset(&act, 0, sizeof(act));
   act.sa_handler = SIG_IGN;
   sigaction(SIGPIPE, &act, NULL);

   /* handle alarm timers (for channel change scheduling) */
   memset(&act, 0, sizeof(act));
   act.sa_handler = vbi_proxyd_alarm_handler;
   sigaction(SIGALRM, &act, NULL);

   /* catch deadly signals for a clean shutdown (remove socket file) */
   memset(&act, 0, sizeof(act));
   sigemptyset(&act.sa_mask);
   sigaddset(&act.sa_mask, SIGINT);
   sigaddset(&act.sa_mask, SIGTERM);
   sigaddset(&act.sa_mask, SIGHUP);
   act.sa_handler = vbi_proxyd_signal_handler;
   act.sa_flags = SA_ONESHOT;
   sigaction(SIGINT, &act, NULL);
   sigaction(SIGTERM, &act, NULL);
   sigaction(SIGHUP, &act, NULL);
}

/* ----------------------------------------------------------------------------
** Set up sockets for listening to client requests
*/
static vbi_bool vbi_proxyd_listen( void )
{
   PROXY_DEV  * p_proxy_dev;
   int          dev_idx;
   vbi_bool     result;

   result      = TRUE;
   p_proxy_dev = proxy.dev;

   for (dev_idx = 0; (dev_idx < proxy.dev_count) && result; dev_idx++, p_proxy_dev++)
   {
      if (vbi_proxy_msg_check_connect(p_proxy_dev->p_sock_path) == FALSE)
      {
         /* create named socket in /tmp for listening to local clients */
         p_proxy_dev->pipe_fd = vbi_proxy_msg_listen_socket(FALSE, NULL, p_proxy_dev->p_sock_path);
         if (p_proxy_dev->pipe_fd != -1)
         {
            /* copy VBI device permissions to the listening socket */
            vbi_proxyd_set_socket_perm(p_proxy_dev);

            vbi_proxy_msg_logger(LOG_NOTICE, -1, 0, "started listening on local socket for ", p_proxy_dev->p_dev_name, NULL);
         }
         else
            result = FALSE;
      }
      else
      {
         vbi_proxy_msg_logger(LOG_ERR, -1, 0, "a proxy daemon is already running for ", p_proxy_dev->p_dev_name, NULL);
         result = FALSE;
      }
   }

   if (proxy.do_tcp_ip && result)
   {
      /* create TCP/IP socket */
      proxy.tcp_ip_fd = vbi_proxy_msg_listen_socket(TRUE, proxy.listen_ip, proxy.listen_port);
      if (proxy.tcp_ip_fd != -1)
      {
         vbi_proxy_msg_logger(LOG_NOTICE, -1, 0, "started listening on TCP/IP socket", NULL);
      }
      else
         result = FALSE;
   }

   return result;
}

/* ---------------------------------------------------------------------------
** Proxy daemon main loop
*/
static void vbi_proxyd_main_loop( void )
{
   fd_set  rd, wr;
   int     max_fd;
   int     sel_cnt;
   int     dev_idx;

   while (proxy.should_exit == FALSE)
   {
      FD_ZERO(&rd);
      FD_ZERO(&wr);
      max_fd = vbi_proxyd_get_fd_set(&rd, &wr);

      /* wait for new clients, client messages or VBI device data (indefinitly) */
      sel_cnt = select(((max_fd > 0) ? (max_fd + 1) : 0), &rd, &wr, NULL, NULL);

      if (sel_cnt != -1)
      {
         if (sel_cnt > 0)
            dprintf(DBG_CLNT, "main_loop: select: events on %d sockets\n", sel_cnt);

         for (dev_idx = 0; dev_idx < proxy.dev_count; dev_idx++)
         {
            /* accept new client connections on device socket */
            if ((proxy.dev[dev_idx].pipe_fd != -1) && (FD_ISSET(proxy.dev[dev_idx].pipe_fd, &rd)))
            {
               vbi_proxyd_add_connection(proxy.dev[dev_idx].pipe_fd, dev_idx, TRUE);
            }

            /* check for incoming data on VBI device */
            if ((proxy.dev[dev_idx].vbi_fd != -1) && (FD_ISSET(proxy.dev[dev_idx].vbi_fd, &rd)))
            {
               if (proxy.dev[dev_idx].use_thread == FALSE)
               {
                  vbi_proxyd_forward_data(dev_idx);
               }
               else
               {  /* message from acq thread slave:
                  ** sent data is only a trigger to wake up from select() above -> discard it */
                  char dummy_buf[100];
                  int  rd_count;
                  do {
                     rd_count = read(proxy.dev[dev_idx].vbi_fd, dummy_buf, sizeof(dummy_buf));
                     dprintf(DBG_QU, "main_loop: read from acq thread dev #%d pipe fd %d: %d errno=%d\n", dev_idx, proxy.dev[dev_idx].vbi_fd, sel_cnt, errno);
                  } while (rd_count == 100);
               }
            }
         }

         /* accept new TCP/IP connections */
         if ((proxy.tcp_ip_fd != -1) && (FD_ISSET(proxy.tcp_ip_fd, &rd)))
         {
            vbi_proxyd_add_connection(proxy.tcp_ip_fd, 0, FALSE);
         }

         /* send queued data or process incoming messages from clients */
         vbi_proxyd_handle_client_sockets(&rd, &wr);

         if (proxy.chn_sched_alarm)
         {
            proxy.chn_sched_alarm = FALSE;

            vbi_proxyd_channel_timer();
         }
      }
      else
      {
         if (errno != EINTR)
         {  /* select syscall failed */
            dprintf(DBG_MSG, "main_loop: select with max. fd %d: %s\n", max_fd, strerror(errno));
            sleep(1);
         }
      }
   }
}

/* ---------------------------------------------------------------------------
** Kill-daemon only: exit upon timeout in I/O to daemon
*/
static void vbi_proxyd_kill_timeout( int sigval )
{
   sigval = sigval;

   /* note: cannot use printf in signal handler without risking deadlock */
   exit(2);
}

/* ---------------------------------------------------------------------------
** Connect to running daemon, query its pid and kill it, exit.
*/
static void vbi_proxyd_kill_daemon( void )
{
   struct sigaction  act;
   char * p_errorstr;
   char * p_srv_port;
   vbi_bool     io_blocked;
   VBIPROXY_MSG msg_buf;
   VBIPROXY_MSG_STATE io;

   memset(&io, 0, sizeof(io));
   io.sock_fd  = -1;
   p_errorstr = NULL;
   p_srv_port = vbi_proxy_msg_get_socket_name(proxy.dev[0].p_dev_name);
   if (p_srv_port == NULL)
      goto failure;

   io.sock_fd = vbi_proxy_msg_connect_to_server(FALSE, NULL, p_srv_port, &p_errorstr);
   if (io.sock_fd == -1)
      goto failure;

   memset(&act, 0, sizeof(act));
   act.sa_handler = vbi_proxyd_kill_timeout;
   sigaction(SIGALRM, &act, NULL);

   /* use non-blocking I/O and alarm timer for timeout handling (simpler than select) */
   alarm(4);
   fcntl(io.sock_fd, F_SETFL, 0);

   /* wait for socket to reach connected state */
   if (vbi_proxy_msg_finish_connect(io.sock_fd, &p_errorstr) == FALSE)
      goto failure;

   /* write service request parameters */
   vbi_proxy_msg_fill_magics(&msg_buf.body.daemon_pid_req.magics);

   /* send the pid request message to the proxy server */
   vbi_proxy_msg_write(&io, MSG_TYPE_DAEMON_PID_REQ, sizeof(msg_buf.body.daemon_pid_req),
                       &msg_buf, FALSE);

   if (vbi_proxy_msg_handle_write(&io, &io_blocked) == FALSE)
      goto io_error;

   if (vbi_proxy_msg_handle_read(&io, &io_blocked, TRUE, &msg_buf, sizeof(msg_buf)) == FALSE)
      goto io_error;

   if ( (vbi_proxyd_check_msg(&msg_buf, FALSE) == FALSE) ||
        (msg_buf.head.type != MSG_TYPE_DAEMON_PID_CNF) )
   {
      asprintf(&p_errorstr, "%s", "Proxy protocol error");
      goto failure;
   }

   if (kill(msg_buf.body.daemon_pid_cnf.pid, SIGTERM) != 0)
   {
      asprintf(&p_errorstr, "Failed to kill the daemon process (pid %d): %s",
	       msg_buf.body.daemon_pid_cnf.pid, strerror(errno));
      goto failure;
   }

   dprintf(DBG_MSG, "Killed daemon process %d.\n", msg_buf.body.daemon_pid_cnf.pid);
   close(io.sock_fd);
   exit(0);

io_error:
   if (p_errorstr == NULL)
      asprintf(&p_errorstr, "Lost connection to proxy (I/O error)");

failure:
   /* failed to establish a connection to the server */
   if (io.sock_fd != -1)
      close(io.sock_fd);
   if (p_errorstr != NULL)
   {
      fprintf(stderr, "%s\n", p_errorstr);
      free(p_errorstr);
   }
   exit(1);
}

/* ---------------------------------------------------------------------------
** Print usage and exit
*/
static void proxy_usage_exit( const char *argv0, const char *argvn, const char * reason )
{
   fprintf(stderr, "%s: %s: %s\n"
                   "Options:\n"
                   "       -dev <path>         : VBI device path (allowed repeatedly)\n"
                   "       -buffers <count>    : number of raw capture buffers (v4l2 only)\n"
                   "       -nodetach           : process remains connected to tty\n"
                   "       -kill               : kill running daemon process, then exit\n"
                   "       -debug <level>      : enable debug output: 1=warnings, 2=all\n"
                   "       -syslog <level>     : enable syslog output\n"
                   "       -loglevel <level>   : log file level\n"
                   "       -logfile <path>     : log file name\n"
                   "       -maxclients <count> : max. number of clients\n"
                   "       -help               : this message\n",
                   argv0, reason, argvn);

   exit(1);
}

/* ---------------------------------------------------------------------------
** Parse numeric value in command line options
*/
static vbi_bool proxy_parse_argv_numeric( char * p_number, int * p_value )
{
   char * p_num_end;

   if (*p_number != 0)
   {
      *p_value = strtol(p_number, &p_num_end, 0);

      return (*p_num_end == 0);
   }
   else
      return FALSE;
}

/* ---------------------------------------------------------------------------
** Parse command line options
*/
static void vbi_proxyd_parse_argv( int argc, char * argv[] )
{
   struct stat stb;
   int arg_val;
   int arg_idx = 1;

   while (arg_idx < argc)
   {
      if (strcasecmp(argv[arg_idx], "-dev") == 0)
      {
         if (arg_idx + 1 < argc)
         {
            if (proxy.dev_count >= SRV_MAX_DEVICES)
               proxy_usage_exit(argv[0], argv[arg_idx], "too many device paths");
            if (stat(argv[arg_idx + 1], &stb) == -1)
               proxy_usage_exit(argv[0], argv[arg_idx +1], strerror(errno));
            if (!S_ISCHR(stb.st_mode))
               proxy_usage_exit(argv[0], argv[arg_idx +1], "not a character device");
            if (access(argv[arg_idx + 1], R_OK | W_OK) == -1)
               proxy_usage_exit(argv[0], argv[arg_idx +1], "failed to access device");

            vbi_proxyd_add_device(argv[arg_idx + 1]);
            arg_idx += 2;
         }
         else
            proxy_usage_exit(argv[0], argv[arg_idx], "missing mode keyword after");
      }
      else if (strcasecmp(argv[arg_idx], "-buffers") == 0)
      {
         if ((arg_idx + 1 < argc) && proxy_parse_argv_numeric(argv[arg_idx + 1], &arg_val))
         {
            opt_buffer_count = arg_val;
            if ((opt_buffer_count < 1) || (opt_buffer_count > VBI_MAX_BUFFER_COUNT))
               proxy_usage_exit(argv[0], argv[arg_idx], "buffer count unsupported");
            arg_idx += 2;
         }
         else
            proxy_usage_exit(argv[0], argv[arg_idx], "missing buffer count after");
      }
      else if (strcasecmp(argv[arg_idx], "-debug") == 0)
      {
         if ((arg_idx + 1 < argc) && proxy_parse_argv_numeric(argv[arg_idx + 1], &arg_val))
         {
            opt_debug_level = arg_val;
            if (opt_debug_level > 0)
               opt_debug_level |= DBG_MSG;
            arg_idx += 2;
         }
         else
            proxy_usage_exit(argv[0], argv[arg_idx], "missing debug level after");
      }
      else if (strcasecmp(argv[arg_idx], "-nodetach") == 0)
      {
         opt_no_detach = TRUE;
         arg_idx += 1;
      }
      else if (strcasecmp(argv[arg_idx], "-kill") == 0)
      {
         opt_kill_daemon = TRUE;
         arg_idx += 1;
      }
      else if (strcasecmp(argv[arg_idx], "-syslog") == 0)
      {
         if ((arg_idx + 1 < argc) && proxy_parse_argv_numeric(argv[arg_idx + 1], &arg_val))
         {
            opt_syslog_level = arg_val;
            arg_idx += 2;
         }
         else
            proxy_usage_exit(argv[0], argv[arg_idx], "missing log level after");
      }
      else if (strcasecmp(argv[arg_idx], "-loglevel") == 0)
      {
         if ((arg_idx + 1 < argc) && proxy_parse_argv_numeric(argv[arg_idx + 1], &arg_val))
         {
            opt_log_level = arg_val;
            arg_idx += 2;
         }
         else
            proxy_usage_exit(argv[0], argv[arg_idx], "missing log level after");
      }
      else if (strcasecmp(argv[arg_idx], "-logfile") == 0)
      {
         if (arg_idx + 1 < argc)
         {
            p_opt_log_name = argv[arg_idx + 1];
            arg_idx += 2;
         }
         else
            proxy_usage_exit(argv[0], argv[arg_idx], "missing mode keyword after");
      }
      else if (strcasecmp(argv[arg_idx], "-maxclients") == 0)
      {
         if ((arg_idx + 1 < argc) && proxy_parse_argv_numeric(argv[arg_idx + 1], &arg_val))
         {
            opt_max_clients = arg_val;
            arg_idx += 2;
         }
         else
            proxy_usage_exit(argv[0], argv[arg_idx], "missing log level after");
      }
      else if (strcasecmp(argv[arg_idx], "-help") == 0)
      {
         char versbuf[50];
         snprintf(versbuf, sizeof (versbuf), "(version %d.%d.%d)", VBIPROXY_VERSION>>16, (VBIPROXY_VERSION>>8)&0xff, VBIPROXY_VERSION&0xff);
         proxy_usage_exit(argv[0], versbuf, "the following options are available");
      }
      else
         proxy_usage_exit(argv[0], argv[arg_idx], "unknown option or argument");
   }

   /* if no device was given, use default path */
   if (proxy.dev_count == 0)
   {
      /* use devfs path if subdirectory exists */
      if (access(DEFAULT_VBI_DEVFS_PATH, R_OK | W_OK) == 0)
         vbi_proxyd_add_device(DEFAULT_VBI_DEVFS_PATH);
      else
         vbi_proxyd_add_device(DEFAULT_VBI_DEV_PATH);
   }
}

/* ----------------------------------------------------------------------------
** Proxy daemon entry point
*/
int main( int argc, char ** argv )
{
   /* initialize state struct */
   memset(&proxy, 0, sizeof(proxy));
   proxy.tcp_ip_fd = -1;
   pthread_mutex_init(&proxy.clnt_mutex, NULL);

   vbi_proxyd_parse_argv(argc, argv);
   vbi_proxy_msg_set_debug_level( (opt_debug_level == 0) ? 0 : ((opt_debug_level & DBG_CLNT) ? 2 : 1) );

   if (opt_kill_daemon)
   {
      vbi_proxyd_kill_daemon();
      exit(0);
   }

   dprintf(DBG_MSG, "proxy daemon starting, rev.\n%s\n", rcsid);

   vbi_proxyd_init();

   vbi_proxyd_set_max_conn(opt_max_clients);
   vbi_proxyd_set_address(FALSE, NULL, NULL);
   vbi_proxy_msg_set_logging(opt_debug_level > 0, opt_syslog_level, opt_log_level, p_opt_log_name);

   /* start listening for client connections */
   if (vbi_proxyd_listen())
   {
      vbi_proxyd_main_loop();
   }
   vbi_proxyd_destroy();
   pthread_mutex_destroy(&proxy.clnt_mutex);

   exit(0);
   return 0;
}

#endif  /* ENABLE_PROXY */
