/*
   BAREOS® - Backup Archiving REcovery Open Sourced

   Copyright (C) 2000-2013 Free Software Foundation Europe e.V.
   Copyright (C) 2015-2026 Bareos GmbH & Co. KG

   This program is Free Software; you can redistribute it and/or
   modify it under the terms of version three of the GNU Affero General Public
   License as published by the Free Software Foundation and included
   in the file LICENSE.

   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
   Affero General Public License for more details.

   You should have received a copy of the GNU Affero 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.
*/
/*
 * Kern Sibbald, MM
 * Split from reserve.c October 2008
 */
/**
 * @file
 * Volume management functions for Storage Daemon
 */

#include "include/bareos.h"
#include "stored/stored.h"
#include "stored/stored_globals.h"
#include "stored/device_control_record.h"
#include "stored/autochanger.h"
#include "include/jcr.h"
#include "lib/berrno.h"
#include "stored/reserve.h"

namespace storagedaemon {

const int debuglevel = 150;

static brwlock_t vol_list_lock;
static dlist<VolumeReservationItem>* vol_list = NULL;
static dlist<VolumeReservationItem>* read_vol_list = NULL;
static pthread_mutex_t read_vol_lock = PTHREAD_MUTEX_INITIALIZER;

/* Global static variables */
static int vol_list_lock_count = 0;
static int read_vol_list_lock_count = 0;

/* Forward referenced functions */
static void FreeVolItem(VolumeReservationItem* vol);
static VolumeReservationItem* new_vol_item(DeviceControlRecord* dcr,
                                           const char* VolumeName);
static void DebugListVolumes(const char* imsg);

// For append volumes the key is the VolumeName.
static int CompareByVolumename(VolumeReservationItem* item1,
                               VolumeReservationItem* item2)
{
  VolumeReservationItem* vol1 = item1;
  VolumeReservationItem* vol2 = item2;

  ASSERT(vol1->vol_name);
  ASSERT(vol2->vol_name);

  return strcmp(vol1->vol_name, vol2->vol_name);
}

// For read volumes the key is JobId, VolumeName.
static int ReadCompare(VolumeReservationItem* item1,
                       VolumeReservationItem* item2)
{
  VolumeReservationItem* vol1 = item1;
  VolumeReservationItem* vol2 = item2;

  ASSERT(vol1->vol_name);
  ASSERT(vol2->vol_name);

  if (vol1->GetJobid() == vol2->GetJobid()) {
    return strcmp(vol1->vol_name, vol2->vol_name);
  }

  if (vol1->GetJobid() < vol2->GetJobid()) { return -1; }

  return 1;
}

bool IsVolListEmpty() { return vol_list->empty(); }

//  Initialized the main volume list. Note, we are using a recursive lock.
void InitVolListLock()
{
  int errstat;

  if ((errstat = RwlInit(&vol_list_lock, PRIO_SD_VOL_LIST)) != 0) {
    BErrNo be;
    Emsg1(M_ABORT, 0, T_("Unable to initialize volume list lock. ERR=%s\n"),
          be.bstrerror(errstat));
  }
}

void TermVolListLock() { RwlDestroy(&vol_list_lock); }

namespace private_locks {
// This allows a given thread to recursively call to LockVolumes()
void LockVolumes(void)
{
  int errstat;

  vol_list_lock_count++;
  if ((errstat = RwlWritelock(&vol_list_lock)) != 0) {
    BErrNo be;
    Emsg2(M_ABORT, 0, "RwlWritelock failure. stat=%d: ERR=%s\n", errstat,
          be.bstrerror(errstat));
  }
}

void UnlockVolumes(void)
{
  int errstat;

  vol_list_lock_count--;
  if ((errstat = RwlWriteunlock(&vol_list_lock)) != 0) {
    BErrNo be;
    Emsg2(M_ABORT, 0, "RwlWriteunlock failure. stat=%d: ERR=%s\n", errstat,
          be.bstrerror(errstat));
  }
}

void LockReadVolumes()
{
  read_vol_list_lock_count++;
  pthread_mutex_lock(&read_vol_lock);
}

void UnlockReadVolumes()
{
  read_vol_list_lock_count--;
  pthread_mutex_unlock(&read_vol_lock);
}
};  // namespace private_locks

/**
 * Add a volume to the read list.
 *
 * Note, we use VolumeReservationItem because it simplifies the code
 * even though, the only part of VolumeReservationItem that we need is
 * the volume name.  The same volume may be in the list
 * multiple times, but each one is distinguished by the
 * JobId.  We use JobId, VolumeName as the key.
 *
 * We can get called multiple times for the same volume because
 * when parsing the bsr, the volume name appears multiple times.
 */
void AddReadVolume(JobControlRecord* jcr, const char* VolumeName)
{
  VolumeReservationItem *nvol, *vol;

  nvol = new_vol_item(NULL, VolumeName);
  nvol->SetJobid(jcr->JobId);
  nvol->SetReading();
  with_read_volume_lock([&] {
    vol = (VolumeReservationItem*)read_vol_list->binary_insert(nvol,
                                                               ReadCompare);
  });
  if (vol != nvol) {
    FreeVolItem(nvol);
    Dmsg2(debuglevel, "read_vol=%s JobId=%" PRIu32 " already in list.\n",
          VolumeName, jcr->JobId);
  } else {
    Dmsg2(debuglevel, "add_read_vol=%s JobId=%" PRIu32 "\n", VolumeName,
          jcr->JobId);
  }
}

// Remove a given volume name from the read list.
void RemoveReadVolume(JobControlRecord* jcr, const char* VolumeName)
{
  VolumeReservationItem vol, *fvol;

  with_read_volume_lock([&] {
    vol.vol_name = strdup(VolumeName);
    vol.SetJobid(jcr->JobId);

    fvol = (VolumeReservationItem*)read_vol_list->binary_search(&vol,
                                                                ReadCompare);
    free(vol.vol_name);

    if (fvol) {
      Dmsg3(debuglevel, "remove_read_vol=%s JobId=%" PRIu32 " found=%d\n",
            VolumeName, jcr->JobId, fvol != NULL);
    }
    if (fvol) {
      read_vol_list->remove(fvol);
      FreeVolItem(fvol);
    }
  });
  // pthread_cond_broadcast(&wait_next_vol);
}

/**
 * Search for a Volume name in the read Volume list.
 *
 * Returns: VolumeReservationItem entry on success
 *          NULL if the Volume is not in the list
 */
static VolumeReservationItem* find_read_volume(const char* VolumeName)
{
  VolumeReservationItem vol, *fvol = nullptr;

  // Do not lock reservations here
  with_read_volume_lock([&] {
    if (read_vol_list->empty()) {
      Dmsg0(debuglevel, "find_read_vol: read_vol_list empty.\n");
      return;
    }
    vol.vol_name = strdup(VolumeName);

    // Note, we do want a simple CompareByVolumename on volume name only here
    fvol = (VolumeReservationItem*)read_vol_list->binary_search(
        &vol, CompareByVolumename);
    free(vol.vol_name);

    Dmsg2(debuglevel, "find_read_vol=%s found=%d\n", VolumeName, fvol != NULL);
  });

  return fvol;
}

// List Volumes -- this should be moved to status.c
enum
{
  debug_lock = true,
  debug_nolock = false
};

static void DebugListVolumes(const char* imsg)
{
  PoolMem msg(PM_MESSAGE);

  foreach_vol ([&msg, &imsg](auto* vol) {
    if (vol->dev) {
      Mmsg(msg, "List %s: %s in_use=%d swap=%d on device %s\n", imsg,
           vol->vol_name, vol->IsInUse(), vol->IsSwapping(),
           vol->dev->print_name());
    } else {
      Mmsg(msg, "List %s: %s in_use=%d swap=%d no dev\n", imsg, vol->vol_name,
           vol->IsInUse(), vol->IsSwapping());
    }
    Dmsg1(debuglevel, "%s", msg.c_str());
  });
}

/**
 * Create a Volume item to put in the Volume list
 * Ensure that the device points to it.
 */
static VolumeReservationItem* new_vol_item(DeviceControlRecord* dcr,
                                           const char* VolumeName)
{
  VolumeReservationItem* vol;

  vol = (VolumeReservationItem*)malloc(sizeof(VolumeReservationItem));
  vol = new (vol) VolumeReservationItem();

  vol->vol_name = strdup(VolumeName);
  if (dcr) {
    vol->dev = dcr->dev;
    Dmsg3(debuglevel, "new Vol=%s at %p dev=%s\n", VolumeName, vol->vol_name,
          vol->dev->print_name());
  }
  vol->InitMutex();
  vol->IncUseCount();

  return vol;
}

static void FreeVolItem(VolumeReservationItem* vol)
{
  Device* dev = nullptr;

  vol->Lock();
  vol->DecUseCount();
  if (vol->UseCount() > 0) {
    vol->Unlock();
    return;
  }
  vol->Unlock();
  free(vol->vol_name);
  if (vol->dev) { dev = vol->dev; }
  vol->DestroyMutex();
  free(vol);
  if (dev) { dev->vol = nullptr; }
}

/**
 * Put a new Volume entry in the Volume list. This
 * effectively reserves the volume so that it will
 * not be mounted again.
 *
 * If the device has any current volume associated with it,
 * and it is a different Volume, and the device is not busy,
 * we release the old Volume item and insert the new one.
 *
 * It is assumed that the device is free and locked so that
 * we can change the device structure.
 *
 * Some details of the Volume list handling:
 *
 *  1. The Volume list entry is attached to the drive (rather than
 *     attached to a job as it was previously. I.e. the drive that "owns"
 *     the volume (in use, mounted)
 *     must point to the volume (still to be maintained in a list).
 *
 *  2. The Volume is entered in the list when a drive is reserved.
 *
 *  3. When a drive is in use, the device code must appropriately update the
 *     volume name as it changes.
 *
 *     This code keeps the same list entry as long as the drive
 *     has any volume associated with it but the volume name in the list
 *     must be updated when the drive has a different volume mounted.
 *
 *  4. A job that has reserved a volume, can un-reserve the volume, and if the
 *     volume is not mounted, and not reserved, and not in use, it will be
 *     removed from the list.
 *
 *  5. If a job wants to reserve a drive with a different Volume from the one on
 *     the drive, it can re-use the drive for the new Volume.
 *
 *  6. If a job wants a Volume that is in a different drive, it can either use
 * the other drive or take the volume, only if the other drive is not in use or
 *     not reserved.
 *
 *  One nice aspect of this is that the reserve use count and the writer use
 * count already exist and are correctly programmed and will need no changes --
 * use counts are always very tricky.
 *
 *  The old code had a concept of "reserving" a Volume, but was changed
 *  to reserving and using a drive.  A volume is must be attached to (owned by)
 * a drive and can move from drive to drive or be unused given certain specific
 *  conditions of the drive.  The key is that the drive must "own" the Volume.
 *
 *  Return: VolumeReservationItem entry on success
 *          NULL volume busy on another drive
 */
VolumeReservationItem* reserve_volume(DeviceControlRecord* dcr,
                                      const char* VolumeName)
{
  VolumeReservationItem *vol, *nvol;

  if (dcr->jcr->IsJobCanceled()) { return NULL; }
  ASSERT(dcr->dev != NULL);

  Dmsg2(debuglevel, "enter reserve_volume=%s drive=%s\n", VolumeName,
        dcr->dev->print_name());

  // If acquiring a volume for writing it may not be on the read volume list.
  if (me->filedevice_concurrent_read && dcr->IsWriting()
      && find_read_volume(VolumeName)) {
    Mmsg(dcr->jcr->errmsg,
         T_("Could not reserve volume \"%s\" for append, because it is read by "
            "another Job.\n"),
         dcr->dev->VolHdr.VolumeName);
    return NULL;
  }

  /* We lock the reservations system here to ensure when adding a new volume
   * that no newly scheduled job can reserve it. */
  with_volume_lock([&] {
    if (debug_level >= debuglevel) { DebugListVolumes("begin reserve_volume"); }

    /* First, remove any old volume attached to this device as it is no longer
     * used. */
    if (dcr->dev->vol) {
      vol = dcr->dev->vol;
      Dmsg4(debuglevel, "Vol attached=%s, newvol=%s volinuse=%d on %s\n",
            vol->vol_name, VolumeName, vol->IsInUse(), dcr->dev->print_name());
      /* Make sure we don't remove the current volume we are inserting
       * because it was probably inserted by another job, or it
       * is not being used and is marked as not reserved. */
      if (bstrcmp(vol->vol_name, VolumeName)) {
        Dmsg2(debuglevel, "=== set reserved vol=%s dev=%s\n", VolumeName,
              vol->dev->print_name());
        goto get_out; /* Volume already on this device */
      } else {
        // Don't release a volume if it was reserved by someone other than us
        if (vol->IsInUse() && !dcr->reserved_volume) {
          Dmsg1(debuglevel, "Cannot free vol=%s. It is reserved.\n",
                vol->vol_name);
          vol = NULL; /* vol in use */
          goto get_out;
        }
        Dmsg2(debuglevel, "reserve_vol free vol=%s at %p\n", vol->vol_name,
              vol->vol_name);

        // If old Volume is still mounted, must unload it
        if (bstrcmp(vol->vol_name, dcr->dev->VolHdr.VolumeName)) {
          Dmsg0(50, "SetUnload\n");
          dcr->dev->SetUnload(); /* have to unload current volume */
        }
        FreeVolume(dcr->dev); /* Release old volume entry */

        if (debug_level >= debuglevel) { DebugListVolumes("reserve_vol free"); }
      }
    }

    // Create a new Volume entry
    nvol = new_vol_item(dcr, VolumeName);

    /* See if this is a request for reading a file type device which can be
     * accesses by multiple readers at once without disturbing each other. */
    if (me->filedevice_concurrent_read && !dcr->IsWriting()
        && dcr->dev->CanReadConcurrently()) {
      nvol->SetJobid(dcr->jcr->JobId);
      nvol->SetReading();
      vol = nvol;
      dcr->dev->vol = vol;

      /* Read volumes on file based devices are not inserted into the write
       * volume list. */
      goto get_out;
    } else {
      // Now try to insert the new Volume
      vol = (VolumeReservationItem*)vol_list->binary_insert(
          nvol, CompareByVolumename);
    }

    if (vol != nvol) {
      Dmsg2(debuglevel, "Found vol=%s dev-same=%d\n", vol->vol_name,
            dcr->dev == vol->dev);

      /* At this point, a Volume with this name already is in the list,
       * so we simply release our new Volume entry. Note, this should
       * only happen if we are moving the volume from one drive to another. */
      Dmsg2(debuglevel, "reserve_vol free-tmp vol=%s at %p\n", vol->vol_name,
            vol->vol_name);

      // Clear dev pointer so that FreeVolItem() doesn't take away our volume.
      nvol->dev = NULL; /* don't zap dev entry */
      FreeVolItem(nvol);

      if (vol->dev) {
        Dmsg2(debuglevel, "dev=%s vol->dev=%s\n", dcr->dev->print_name(),
              vol->dev->print_name());
      }

      /* Check if we are trying to use the Volume on a different drive dev is
       * our device vol->dev is where the Volume we want is */
      if (dcr->dev != vol->dev) {
        // Caller wants to switch Volume to another device

        // should be different devices with different names
        if (bstrcmp(dcr->dev->print_name(), vol->dev->print_name())) {
          // names are same
          Dmsg1(100, "device pointers are different but have same name %s\n",
                dcr->dev->print_name());
        }

        if (!vol->dev->IsBusy() && !vol->IsSwapping()) {
          slot_number_t slot;

          Dmsg3(debuglevel, "==== Swap vol=%s from dev=%s to %s\n", VolumeName,
                vol->dev->print_name(), dcr->dev->print_name());
          FreeVolume(dcr->dev); /* free any volume attached to our drive */
          Dmsg1(50, "SetUnload dev=%s\n", dcr->dev->print_name());
          dcr->dev->SetUnload(); /* Unload any volume that is on our drive */
          {
            auto original_dcr_dev = dcr->dev;
            dcr->SetDev(vol->dev);                /* temp point to other dev */
            slot = GetAutochangerLoadedSlot(dcr); /* get slot on other drive */
            dcr->SetDev(original_dcr_dev);        /* restore dev */
          }
          vol->SetSlotNumber(slot);      /* save slot */
          vol->dev->SetUnload();         /* unload the other drive */
          vol->SetSwapping();            /* swap from other drive */
          dcr->dev->swap_dev = vol->dev; /* remember to get this vol */
          dcr->dev->SetLoad();           /* then reload on our drive */
          vol->dev->vol = NULL;          /* remove volume from other drive */
          vol->dev = dcr->dev;           /* point the Volume at our drive */
          dcr->dev->vol = vol;           /* point our drive at the Volume */
        } else {
          Jmsg7(dcr->jcr, M_WARNING, 0,
                "Need volume from other drive, but swap not possible. "
                "Status: read=%d num_writers=%d num_reserve=%d swap=%d "
                "vol=%s from dev=%s to %s\n",
                vol->dev->CanRead(), vol->dev->num_writers,
                vol->dev->NumReserved(), vol->IsSwapping(), VolumeName,
                vol->dev->print_name(), dcr->dev->print_name());
          if (vol->IsSwapping() && dcr->dev->swap_dev) {
            Dmsg3(debuglevel, "Swap failed vol=%s from=%s to dev=%s\n",
                  vol->vol_name, dcr->dev->swap_dev->print_name(),
                  dcr->dev->print_name());
          } else {
            Dmsg3(debuglevel, "Swap failed vol=%s from=%p to dev=%s\n",
                  vol->vol_name, dcr->dev->swap_dev, dcr->dev->print_name());
          }

          if (debug_level >= debuglevel) { DebugListVolumes("failed swap"); }

          vol = NULL; /* device busy */
          goto get_out;
        }
      } else {
        dcr->dev->vol = vol;
      }
    } else {
      dcr->dev->vol = vol; /* point to newly inserted volume */
    }

  get_out:
    if (vol) {
      Dmsg2(debuglevel, "=== set in_use. vol=%s dev=%s\n", vol->vol_name,
            vol->dev->print_name());
      vol->SetInUse();
      dcr->reserved_volume = true;
      bstrncpy(dcr->VolumeName, vol->vol_name, sizeof(dcr->VolumeName));
    }

    if (debug_level >= debuglevel) { DebugListVolumes("end new volume"); }
  });
  return vol;
}

namespace private_functions {
// Start walk of vol chain
VolumeReservationItem* vol_walk_start()
{
  VolumeReservationItem* vol;
  vol = (VolumeReservationItem*)vol_list->first();
  if (vol) {
    vol->IncUseCount();
    Dmsg2(debuglevel, "Inc walk_start UseCount=%d volname=%s\n",
          vol->UseCount(), vol->vol_name);
  }

  return vol;
}

// Get next vol from chain, and release current one
VolumeReservationItem* VolWalkNext(VolumeReservationItem* prev_vol)
{
  VolumeReservationItem* vol;

  vol = (VolumeReservationItem*)vol_list->next(prev_vol);
  if (vol) {
    vol->IncUseCount();
    Dmsg2(debuglevel, "Inc walk_next UseCount=%d volname=%s\n", vol->UseCount(),
          vol->vol_name);
  }
  if (prev_vol) { FreeVolItem(prev_vol); }

  return vol;
}

// Start walk of vol chain
VolumeReservationItem* read_vol_walk_start()
{
  VolumeReservationItem* vol;
  vol = (VolumeReservationItem*)read_vol_list->first();
  if (vol) {
    vol->IncUseCount();
    Dmsg2(debuglevel, "Inc walk_start UseCount=%d volname=%s\n",
          vol->UseCount(), vol->vol_name);
  }

  return vol;
}

// Get next vol from chain, and release current one
VolumeReservationItem* ReadVolWalkNext(VolumeReservationItem* prev_vol)
{
  VolumeReservationItem* vol;

  vol = (VolumeReservationItem*)read_vol_list->next(prev_vol);
  if (vol) {
    vol->IncUseCount();
    Dmsg2(debuglevel, "Inc walk_next UseCount=%d volname=%s\n", vol->UseCount(),
          vol->vol_name);
  }
  if (prev_vol) { FreeVolItem(prev_vol); }

  return vol;
}
};  // namespace private_functions

/**
 * Search for a Volume name in the Volume list.
 *
 * Returns: VolumeReservationItem entry on success
 *          NULL if the Volume is not in the list
 */
static VolumeReservationItem* find_volume(const char* VolumeName)
{
  VolumeReservationItem vol, *fvol = nullptr;

  /* Do not lock reservations here */
  with_volume_lock([&] {
    if (vol_list->empty()) { return; }
    vol.vol_name = strdup(VolumeName);
    fvol = (VolumeReservationItem*)vol_list->binary_search(&vol,
                                                           CompareByVolumename);
    free(vol.vol_name);
    Dmsg2(debuglevel, "find_vol=%s found=%d\n", VolumeName, fvol != NULL);

    if (debug_level >= debuglevel) { DebugListVolumes("find_volume"); }
  });
  return fvol;
}

/**
 * Free a Volume from the Volume list if it is no longer used
 * Note, for tape drives we want to remember where the Volume
 * was when last used, so rather than free the volume entry,
 * we simply mark it "not reserved" so when the drive is really
 * needed for another volume, we can reuse it.
 *
 * Returns: true if the Volume found and "removed" from the list
 *          false if the Volume is not in the list or is in use
 */
bool VolumeUnused(DeviceControlRecord* dcr)
{
  Device* dev = dcr->dev;

  if (!dev->vol) {
    Dmsg1(debuglevel, "vol_unused: no vol on %s\n", dev->print_name());
    if (debug_level >= debuglevel) {
      DebugListVolumes("null vol cannot unreserve_volume");
    }

    return false;
  }

  Dmsg1(debuglevel, "=== clear in_use vol=%s\n", dev->vol->vol_name);
  dev->vol->ClearInUse();

  if (dev->vol->IsSwapping()) {
    Dmsg1(debuglevel, "vol_unused: vol being swapped on %s\n",
          dev->print_name());

    if (debug_level >= debuglevel) {
      DebugListVolumes("swapping vol cannot FreeVolume");
    }
    return false;
  }

  /* If this is a tape, we do not free the volume, rather we wait
   * until the autoloader unloads it, or until another tape is
   * explicitly read in this drive. This allows the SD to remember
   * where the tapes are or last were. */
  Dmsg4(debuglevel,
        "=== set not reserved vol=%s num_writers=%d dev_reserved=%d dev=%s\n",
        dev->vol->vol_name, dev->num_writers, dev->NumReserved(),
        dev->print_name());
  if (dev->IsTape() || dev->AttachedToAutochanger()) {
    return true;
  } else {
    /* Note, this frees the volume reservation entry, but the file descriptor
     * remains open with the OS. */
    return FreeVolume(dev);
  }
}

// Unconditionally release the volume entry
bool FreeVolume(Device* dev)
{
  VolumeReservationItem* vol;

  bool result = true;

  with_volume_lock([&] {
    vol = dev->vol;
    if (vol == NULL) {
      Dmsg1(debuglevel, "No vol on dev %s\n", dev->print_name());
      result = false;
      return;
    }

    // Don't free a volume while it is being swapped
    if (!vol->IsSwapping()) {
      Dmsg1(debuglevel, "=== clear in_use vol=%s\n", vol->vol_name);
      dev->vol = NULL;

      bool is_on_list = false;

      // this is ok, since we locked the volume list
      for (VolumeReservationItem* other_vol = vol_list->first(); other_vol;
           other_vol = vol_list->next(other_vol)) {
        if (other_vol == vol) {
          vol_list->remove(vol);
          is_on_list = true;
          Dmsg2(debuglevel, "=== remove volume %s dev=%s\n", vol->vol_name,
                dev->print_name());
          break;
        }
      }

      /* Volume is on write volume list if one of the following is applicable:
       *  - The volume is written to.
       *  - Config option filedevice_concurrent_read is not on.
       *  - The device is not of type File. */
      bool should_be_on_list = vol->IsWriting()
                               || !me->filedevice_concurrent_read
                               || !dev->CanReadConcurrently();

      if (should_be_on_list != is_on_list) {
        if (is_on_list) {
          Emsg0(M_ERROR, 0,
                T_("logic error detected! trying to free item still on volume "
                   "list\n"));
        } else {
          Emsg0(M_ERROR, 0,
                T_("logic error detected! trying to remove item not in the "
                   "volume list\n"));
        }
        // this message is split up so we get at least the first message
        // if vol->vol_name points to unallocated memory ...
        Emsg1(M_ERROR, 0, T_("  volume = %s (dev = %s)\n"),
              vol->vol_name ? vol->vol_name : "unset", dev->print_name());
      }
      FreeVolItem(vol);

      if (debug_level >= debuglevel) { DebugListVolumes("FreeVolume"); }
    } else {
      Dmsg1(debuglevel, "=== cannot clear swapping vol=%s\n", vol->vol_name);
    }

    // pthread_cond_broadcast(&wait_next_vol);
  });

  return result;
}

// Create the Volume list
void CreateVolumeLists()
{
  if (vol_list == NULL) { vol_list = new dlist<VolumeReservationItem>(); }
  if (read_vol_list == NULL) {
    read_vol_list = new dlist<VolumeReservationItem>();
  }
}

// Free normal append volumes list
static inline void FreeVolumeList(const char* what,
                                  dlist<VolumeReservationItem>* vollist)
{
  VolumeReservationItem* vol;

  foreach_dlist (vol, vollist) {
    if (vol->dev) {
      Dmsg3(debuglevel, "free %s Volume=%s dev=%s\n", what, vol->vol_name,
            vol->dev->print_name());
    } else {
      Dmsg2(debuglevel, "free %s Volume=%s No dev\n", what, vol->vol_name);
    }
    free(vol->vol_name);
    vol->vol_name = NULL;
    vol->DestroyMutex();
  }
}

// Release all Volumes from the list
void FreeVolumeLists()
{
  if (vol_list) {
    with_volume_lock([&] {
      FreeVolumeList("vol_list", vol_list);
      delete vol_list;
      vol_list = NULL;
    });
  }

  if (read_vol_list) {
    with_read_volume_lock([&] {
      FreeVolumeList("read_vol_list", read_vol_list);
      delete read_vol_list;
      read_vol_list = NULL;
    });
  }
}

// Determine if caller can write on volume
bool DeviceControlRecord::Can_i_write_volume()
{
  VolumeReservationItem* vol;

  vol = find_read_volume(VolumeName);
  if (vol) {
    Dmsg1(100, "Found in read list; cannot write vol=%s\n", VolumeName);
    return false;
  }

  return Can_i_use_volume();
}

// Determine if caller can read or write volume
bool DeviceControlRecord::Can_i_use_volume()
{
  bool rtn = true;
  VolumeReservationItem* vol;

  if (jcr->IsJobCanceled()) { return false; }
  with_volume_lock([&] {
    vol = find_volume(VolumeName);
    if (!vol) {
      Dmsg1(debuglevel, "Vol=%s not in use.\n", VolumeName);
      return; /* vol not in list */
    }
    ASSERT(vol->dev != NULL);

    if (dev == vol->dev) { /* same device OK */
      Dmsg1(debuglevel, "Vol=%s on same dev.\n", VolumeName);
      return;
    } else {
      Dmsg3(debuglevel, "Vol=%s on %s we have %s\n", VolumeName,
            vol->dev->print_name(), dev->print_name());
    }
    /* ***FIXME*** check this ... */
    if (!vol->dev->IsBusy()) {
      Dmsg2(debuglevel, "Vol=%s dev=%s not busy.\n", VolumeName,
            vol->dev->print_name());
      return;
    } else {
      Dmsg2(debuglevel, "Vol=%s dev=%s busy.\n", VolumeName,
            vol->dev->print_name());
    }
    Dmsg2(debuglevel, "Vol=%s in use by %s.\n", VolumeName,
          vol->dev->print_name());
    rtn = false;
  });
  return rtn;
}

/**
 * Create a temporary copy of the volume list.  We do this,
 * to avoid having the volume list locked during the
 * call to ReserveDevice(), which would cause a deadlock.
 *
 * Note, we may want to add an update counter on the vol_list
 * so that if it is modified while we are traversing the copy
 * we can take note and act accordingly (probably redo the
 * search at least a few times).
 */
dlist<VolumeReservationItem>* dup_vol_list(JobControlRecord* jcr)
{
  dlist<VolumeReservationItem>* temp_vol_list;

  Dmsg0(debuglevel, "lock volumes\n");

  Dmsg0(debuglevel, "duplicate vol list\n");
  temp_vol_list = new dlist<VolumeReservationItem>();
  foreach_vol ([&](auto* vol) {
    VolumeReservationItem *nvol, *tvol;

    tvol = new_vol_item(NULL, vol->vol_name);
    tvol->dev = vol->dev;
    nvol = (VolumeReservationItem*)temp_vol_list->binary_insert(
        tvol, CompareByVolumename);
    if (tvol != nvol) {
      tvol->dev = NULL; /* don't zap dev entry */
      FreeVolItem(tvol);
      Pmsg0(000, "Logic error. Duplicating vol list hit duplicate.\n");
      Jmsg(jcr, M_WARNING, 0,
           "Logic error. Duplicating vol list hit duplicate.\n");
    }
  });
  Dmsg0(debuglevel, "unlock volumes\n");

  return temp_vol_list;
}

// Free the specified temp list.
void FreeTempVolList(dlist<VolumeReservationItem>* temp_vol_list)
{
  FreeVolumeList("temp_vol_list", temp_vol_list);
  delete temp_vol_list;
}

} /* namespace storagedaemon */
