/* workqueue.c - Maintain a queue of background tasks
 * Copyright (C) 2017 Werner Koch
 *
 * This file is part of GnuPG.
 *
 * GnuPG 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 3 of the License, or
 * (at your option) any later version.
 *
 * GnuPG 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, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0+
 */

#include <config.h>
#include <stdlib.h>
#include <string.h>

#include "dirmngr.h"


/* An object for one item in the workqueue.  */
struct wqitem_s
{
  struct wqitem_s *next;

  /* This flag is set if the task requires network access.  */
  unsigned int need_network:1;

  /* The id of the session which created this task.  If this is 0 the
   * task is not associated with a specific session.  */
  unsigned int session_id;

  /* The function to perform the background task.  */
  wqtask_t func;

  /* A string with the string argument for that task.  */
  char args[1];
};
typedef struct wqitem_s *wqitem_t;


/* The workque is a simple linked list.  */
static wqitem_t workqueue;


/* Dump the queue using Assuan status comments.  */
void
workqueue_dump_queue (ctrl_t ctrl)
{
  wqitem_t saved_workqueue;
  wqitem_t item;
  unsigned int count;

  /* Temporarily detach the entire workqueue so that other threads don't
   * get into our way.  */
  saved_workqueue = workqueue;
  workqueue = NULL;

  for (count=0, item = saved_workqueue; item; item = item->next)
    count++;

  dirmngr_status_helpf (ctrl, "wq: number of entries: %u", count);
  for (item = saved_workqueue; item; item = item->next)
    dirmngr_status_helpf (ctrl, "wq: sess=%u net=%d %s(\"%.100s%s\")",
                          item->session_id, item->need_network,
                          item->func? item->func (NULL, NULL): "nop",
                          item->args, strlen (item->args) > 100? "[...]":"");

  /* Restore the workqueue.  Actually we append the saved queue to
   * handle a possibly updated workqueue.  */
  if (!(item=workqueue))
    workqueue = saved_workqueue;
  else
    {
      while (item->next)
        item = item->next;
      item->next = saved_workqueue;
    }
}


/* Append the task (FUNC,ARGS) to the work queue.  FUNC shall return
 * its name when called with (NULL, NULL).  */
gpg_error_t
workqueue_add_task (wqtask_t func, const char *args, unsigned int session_id,
                    int need_network)
{
  wqitem_t item, wi;

  item = xtrycalloc (1, sizeof *item + strlen (args));
  if (!item)
    return gpg_error_from_syserror ();
  strcpy (item->args, args);
  item->func = func;
  item->session_id = session_id;
  item->need_network = !!need_network;

  if (!(wi=workqueue))
    workqueue = item;
  else
    {
      while (wi->next)
        wi = wi->next;
      wi->next = item;
    }
  return 0;
}


/* Run the task described by ITEM.  ITEM must have been detached from
 * the workqueue; its ownership is transferred to this function.  */
static void
run_a_task (ctrl_t ctrl, wqitem_t item)
{
  log_assert (!item->next);

  if (opt.verbose)
    log_info ("session %u: running %s(\"%s%s\")\n",
              item->session_id,
              item->func? item->func (NULL, NULL): "nop",
              item->args, strlen (item->args) > 100? "[...]":"");
  if (item->func)
    item->func (ctrl, item->args);

  xfree (item);
}


/* Run tasks not associated with a session.  This is called from the
 * ticker every few minutes.  If WITH_NETWORK is not set tasks which
 * require the network are not run.  */
void
workqueue_run_global_tasks (ctrl_t ctrl, int with_network)
{
  wqitem_t item, prev;

  with_network = !!with_network;

  if (opt.verbose)
    log_info ("running scheduled tasks%s\n", with_network?" (with network)":"");

  for (;;)
    {
      prev = NULL;
      for (item = workqueue; item; prev = item, item = item->next)
        if (!item->session_id
            && (!item->need_network || (item->need_network && with_network)))
          break;
      if (!item)
        break;  /* No more tasks to run.  */

      /* Detach that item from the workqueue.  */
      if (!prev)
        workqueue = item->next;
      else
        prev->next = item->next;
      item->next = NULL;

      /* Run the task.  */
      run_a_task (ctrl, item);
    }
}


/* Run tasks scheduled for running after a session.  Those tasks are
 * identified by the SESSION_ID.  */
void
workqueue_run_post_session_tasks (unsigned int session_id)
{
  struct server_control_s ctrlbuf;
  ctrl_t ctrl = NULL;
  wqitem_t item, prev;

  if (!session_id)
    return;

  for (;;)
    {
      prev = NULL;
      for (item = workqueue; item; prev = item, item = item->next)
        if (item->session_id == session_id)
          break;
      if (!item)
        break;  /* No more tasks for this session.  */

      /* Detach that item from the workqueue.  */
      if (!prev)
        workqueue = item->next;
      else
        prev->next = item->next;
      item->next = NULL;

      /* Create a CTRL object the first time we need it.  */
      if (!ctrl)
        {
          memset (&ctrlbuf, 0, sizeof ctrlbuf);
          ctrl = &ctrlbuf;
          dirmngr_init_default_ctrl (ctrl);
        }

      /* Run the task.  */
      run_a_task (ctrl, item);
    }

  dirmngr_deinit_default_ctrl (ctrl);
}
