/*
   BAREOS® - Backup Archiving REcovery Open Sourced

   Copyright (C) 2002-2013 Free Software Foundation Europe e.V.
   Copyright (C) 2011-2012 Planets Communications B.V.
   Copyright (C) 2013-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, August MMII
/**
 * @file
 * Routines to acquire and release a device for read/write
 */

#include "include/bareos.h" /* pull in global headers */
#include "stored/stored.h"  /* pull in Storage Daemon headers */
#include "stored/acquire.h"
#include "stored/autochanger.h"
#include "stored/blocksize_boundaries.h"
#include "stored/bsr.h"
#include "stored/device_control_record.h"
#include "stored/job.h"
#include "stored/label.h"
#include "stored/sd_plugins.h"
#include "stored/sd_stats.h"
#include "stored/wait.h"
#include "lib/berrno.h"
#include "lib/edit.h"
#include "lib/berrno.h"
#include "include/jcr.h"
#include "stored/block.h"
#include "stored/stored_jcr_impl.h"
#include "lib/bpipe.h"

#include <algorithm>

namespace storagedaemon {

DeviceControlRecord::DeviceControlRecord()
{
  PoolMem errmsg(PM_MESSAGE);
  int errstat;

  tid = pthread_self();
  spool_fd = -1;
  if ((errstat = pthread_mutex_init(&mutex_, NULL)) != 0) {
    BErrNo be;

    Mmsg(errmsg, T_("Unable to init mutex: ERR=%s\n"), be.bstrerror(errstat));
    Jmsg0(NULL, M_ERROR_TERM, 0, "%s", errmsg.c_str());
  }

  if ((errstat = pthread_mutex_init(&r_mutex, NULL)) != 0) {
    BErrNo be;

    Mmsg(errmsg, T_("Unable to init r_mutex: ERR=%s\n"), be.bstrerror(errstat));
    Jmsg0(NULL, M_ERROR_TERM, 0, "%s", errmsg.c_str());
  }
}

static int const rdebuglevel = 100;

/* Forward referenced functions */
static void AttachDcrToDev(DeviceControlRecord* dcr);
static void DetachDcrFromDev(DeviceControlRecord* dcr);
static void SetDcrFromVol(DeviceControlRecord* dcr, VolumeList* vol);

/**
 * Acquire device for reading.
 *  The drive should have previously been reserved by calling
 *  ReserveDeviceForRead(). We read the Volume label from the block and
 *  leave the block pointers just after the label.
 *
 *  Returns: NULL if failed for any reason
 *           dcr  if successful
 */
bool AcquireDeviceForRead(DeviceControlRecord* dcr)
{
  Device* dev;
  JobControlRecord* jcr = dcr->jcr;
  bool retval = false;
  bool tape_previously_mounted;
  VolumeList* vol;
  bool try_autochanger = true;
  int i;
  int vol_label_status;
  int retry = 0;

  Enter(rdebuglevel);
  dev = dcr->dev;
  dev->Lock_read_acquire();
  Dmsg2(rdebuglevel, "dcr=%p dev=%p\n", dcr, dcr->dev);
  Dmsg2(rdebuglevel, "MediaType dcr=%s dev=%s\n", dcr->media_type,
        dev->device_resource->media_type);
  dev->dblock(BST_DOING_ACQUIRE);

  if (dev->num_writers > 0) {
    Jmsg2(jcr, M_FATAL, 0,
          T_("Acquire read: num_writers=%d not zero. Job %" PRIu32
             " canceled.\n"),
          dev->num_writers, jcr->JobId);
    goto get_out;
  }

  /* Find next Volume, if any */
  vol = jcr->sd_impl->VolList;
  if (!vol) {
    char ed1[50];
    Jmsg(jcr, M_FATAL, 0,
         T_("No volumes specified for reading. Job %s canceled.\n"),
         edit_int64(jcr->JobId, ed1));
    goto get_out;
  }
  jcr->sd_impl->CurReadVolume++;
  for (i = 1; i < jcr->sd_impl->CurReadVolume; i++) { vol = vol->next; }
  if (!vol) {
    Jmsg(jcr, M_FATAL, 0,
         T_("Logic error: no next volume to read. Numvol=%d Curvol=%d\n"),
         jcr->sd_impl->NumReadVolumes, jcr->sd_impl->CurReadVolume);
    goto get_out; /* should not happen */
  }
  SetDcrFromVol(dcr, vol);

  Dmsg2(rdebuglevel, "Want Vol=%s Slot=%d\n", vol->VolumeName, vol->Slot);

  /* If the MediaType requested for this volume is not the
   * same as the current drive, we attempt to find the same
   * device that was used to write the original volume.  If
   * found, we switch to using that device.
   *
   * N.B. A lot of routines rely on the dcr pointer not changing
   * ReadRecords.c even has multiple dcrs cached, so we take care
   * here to release all important parts of the dcr and re-acquire
   * them such as the block pointer (size may change), but we do
   * not release the dcr. */
  Dmsg2(rdebuglevel, "MediaType dcr=%s dev=%s\n", dcr->media_type,
        dev->device_resource->media_type);
  if (dcr->media_type[0]
      && !bstrcmp(dcr->media_type, dev->device_resource->media_type)) {
    ReserveContext rctx;
    int status;

    Jmsg3(jcr, M_INFO, 0,
          T_("Changing read device. Want Media Type=\"%s\" have=\"%s\"\n"
             "  device=%s\n"),
          dcr->media_type, dev->device_resource->media_type, dev->print_name());
    Dmsg3(rdebuglevel,
          "Changing read device. Want Media Type=\"%s\" have=\"%s\"\n"
          "  device=%s\n",
          dcr->media_type, dev->device_resource->media_type, dev->print_name());

    dev->dunblock(DEV_UNLOCKED);

    LockReservations();
    memset(&rctx, 0, sizeof(ReserveContext));
    jcr->sd_impl->read_dcr = dcr;
    rctx.any_drive = true;
    rctx.device_name = vol->device;
    director_storage store(false, "", vol->MediaType, dcr->pool_name,
                           dcr->pool_type);
    rctx.store = &store;
    CleanDevice(dcr); /* clean up the dcr */

    // Search for a new device
    status = SearchResForDevice(jcr, rctx);
    ClearReserveMessages(jcr); /* release queued messages */
    UnlockReservations();

    if (status == 1) { /* found new device to use */
      /* Switching devices, so acquire lock on new device, then release the old
       * one. */
      dcr->dev->Lock_read_acquire(); /* lock new one */
      dev->Unlock_read_acquire();    /* release old one */
      dev = dcr->dev;                /* get new device pointer */
      dev->dblock(BST_DOING_ACQUIRE);

      dcr->VolumeName[0] = 0;
      Jmsg(jcr, M_INFO, 0,
           T_("Media Type change.  New read device %s chosen.\n"),
           dev->print_name());
      Dmsg1(50, "Media Type change.  New read device %s chosen.\n",
            dev->print_name());

      bstrncpy(dcr->VolumeName, vol->VolumeName, sizeof(dcr->VolumeName));
      dcr->setVolCatName(vol->VolumeName);
      bstrncpy(dcr->media_type, vol->MediaType, sizeof(dcr->media_type));
      dcr->VolCatInfo.Slot = vol->Slot;
      dcr->VolCatInfo.InChanger = vol->Slot > 0;
      bstrncpy(dcr->pool_name, store.pool_name.c_str(), sizeof(dcr->pool_name));
      bstrncpy(dcr->pool_type, store.pool_type.c_str(), sizeof(dcr->pool_type));
    } else {
      /* error */
      Jmsg1(jcr, M_FATAL, 0,
            T_("No suitable device found to read Volume \"%s\"\n"),
            vol->VolumeName);
      Dmsg1(rdebuglevel, "No suitable device found to read Volume \"%s\"\n",
            vol->VolumeName);
      goto get_out;
    }
  }
  Dmsg2(rdebuglevel, "MediaType dcr=%s dev=%s\n", dcr->media_type,
        dev->device_resource->media_type);

  dev->ClearUnload();

  if (dev->vol && dev->vol->IsSwapping()) {
    dev->vol->SetSlotNumber(vol->Slot);
    Dmsg3(rdebuglevel, "swapping: slot=%d Vol=%s dev=%s\n", dev->vol->GetSlot(),
          dev->vol->vol_name, dev->print_name());
  }

  InitDeviceWaitTimers(dcr);

  tape_previously_mounted
      = dev->CanRead() || dev->CanAppend() || dev->IsLabeled();
  // tape_initially_mounted = tape_previously_mounted;

  // Volume info is always needed because of VolParts
  Dmsg1(rdebuglevel, "DirGetVolumeInfo vol=%s\n", dcr->VolumeName);
  if (!dcr->DirGetVolumeInfo(GET_VOL_INFO_FOR_READ)) {
    Dmsg2(rdebuglevel, "dir_get_vol_info failed for vol=%s: %s\n",
          dcr->VolumeName, jcr->errmsg);
    Jmsg1(jcr, M_WARNING, 0, "Read acquire: %s", jcr->errmsg);
  }
  dev->SetLoad(); /* set to load volume */

  while (1) {
    // If not polling limit retries
    if (!dev->poll && retry++ > 10) { break; }

    /* See if we are changing the volume in the device.
     * If so we need to force a reread of the tape label. */
    if (dev->device_resource->drive_crypto_enabled
        || (dev->HasCap(CAP_ALWAYSOPEN)
            && !bstrcmp(dev->VolHdr.VolumeName, dcr->VolumeName))) {
      dev->ClearLabeled();
    }

    if (jcr->IsJobCanceled()) {
      char ed1[50];
      Mmsg1(dev->errmsg, T_("Job %s canceled.\n"), edit_int64(jcr->JobId, ed1));
      Jmsg(jcr, M_INFO, 0, "%s", dev->errmsg);
      goto get_out; /* error return */
    }

    dcr->DoUnload();
    dcr->DoSwapping(false /*!IsWriting*/);
    dcr->DoLoad(false /*!IsWriting*/);
    SetDcrFromVol(dcr, vol); /* refresh dcr with desired volume info */

    /* This code ensures that the device is ready for reading. If it is a file,
     * it opens it. If it is a tape, it checks the volume name */
    Dmsg1(rdebuglevel, "stored: open vol=%s\n", dcr->VolumeName);
    if (!dev->open(dcr, DeviceMode::OPEN_READ_ONLY)) {
      if (!dev->poll) {
        Jmsg3(jcr, M_WARNING, 0,
              T_("Read open device %s Volume \"%s\" failed: ERR=%s\n"),
              dev->print_name(), dcr->VolumeName, dev->bstrerror());
      }
      goto default_path;
    }
    Dmsg1(rdebuglevel, "opened dev %s OK\n", dev->print_name());

    /* See if we are changing the volume in the device.
     * If so we need to force a reread of the tape label. */
    if (!dev->device_resource->drive_crypto_enabled
        && dev->HasCap(CAP_ALWAYSOPEN)
        && bstrcmp(dev->VolHdr.VolumeName, dcr->VolumeName)) {
      vol_label_status = VOL_OK;
    } else {
      // Read Volume Label
      Dmsg0(rdebuglevel, "calling read-vol-label\n");
      vol_label_status = ReadDevVolumeLabel(dcr);
    }

    switch (vol_label_status) {
      case VOL_OK:
        Dmsg0(rdebuglevel, "Got correct volume.\n");
        retval = true;
        dev->VolCatInfo = dcr->VolCatInfo; /* structure assignment */
        break;                             /* got it */
      case VOL_IO_ERROR:
        Dmsg0(rdebuglevel, "IO Error\n");
        /* Send error message generated by ReadDevVolumeLabel() only when we
         * really had a tape mounted. This suppresses superfluous error messages
         * when nothing is mounted. */
        if (tape_previously_mounted) {
          Jmsg(jcr, M_WARNING, 0, "Read acquire: %s", jcr->errmsg);
        }
        goto default_path;
      case VOL_NAME_ERROR:
        Dmsg3(rdebuglevel, "Vol name=%s want=%s drv=%s.\n",
              dev->VolHdr.VolumeName, dcr->VolumeName, dev->print_name());
        if (dev->IsVolumeToUnload()) { goto default_path; }
        dev->SetUnload(); /* force unload of unwanted tape */
        if (!UnloadAutochanger(dcr, -1)) {
          // At least free the device so we can re-open with correct volume
          dev->close(dcr);
          FreeVolume(dev);
        }
        dev->SetLoad();
        [[fallthrough]];
      default:
        Jmsg1(jcr, M_WARNING, 0, "Read acquire: %s", jcr->errmsg);
      default_path:
        Dmsg0(rdebuglevel, "default path\n");
        tape_previously_mounted = true;

        // If the device requires mount, close it, so the device can be ejected.
        if (dev->RequiresMount()) {
          dev->close(dcr);
          FreeVolume(dev);
        }

        // Call autochanger only once unless ask_sysop called
        if (try_autochanger) {
          int status;
          Dmsg2(rdebuglevel, "calling autoload Vol=%s Slot=%d\n",
                dcr->VolumeName, dcr->VolCatInfo.Slot);
          status = AutoloadDevice(dcr, 0, NULL);
          if (status > 0) {
            try_autochanger = false;
            continue; /* try reading volume mounted */
          }
        }

        // Mount a specific volume and no other
        Dmsg0(rdebuglevel, "calling dir_ask_sysop\n");
        if (!dcr->DirAskSysopToMountVolume(ST_READREADY)) {
          goto get_out; /* error return */
        }

        // Volume info is always needed because of VolParts
        Dmsg1(150, "DirGetVolumeInfo vol=%s\n", dcr->VolumeName);
        if (!dcr->DirGetVolumeInfo(GET_VOL_INFO_FOR_READ)) {
          Dmsg2(150, "dir_get_vol_info failed for vol=%s: %s\n",
                dcr->VolumeName, jcr->errmsg);
          Jmsg1(jcr, M_WARNING, 0, "Read acquire: %s", jcr->errmsg);
        }
        dev->SetLoad(); /* set to load volume */

        try_autochanger = true; /* permit trying the autochanger again */

        continue; /* try reading again */
    } /* end switch */
    break;
  } /* end while loop */

  if (!retval) {
    Jmsg1(jcr, M_FATAL, 0,
          T_("Too many errors trying to mount device %s for reading.\n"),
          dev->print_name());
    goto get_out;
  }

  dev->ClearAppend();
  dev->SetRead();
  jcr->sendJobStatus(JS_Running);
  Jmsg(jcr, M_INFO, 0, T_("Ready to read from volume \"%s\" on device %s.\n"),
       dcr->VolumeName, dev->print_name());

get_out:
  dev->Lock();
  dcr->ClearReserved();

  /* Normally we are blocked, but in at least one error case above we are not
   * blocked because we unsuccessfully tried changing devices. */
  if (dev->IsBlocked()) {
    dev->dunblock(DEV_LOCKED);
  } else {
    dev->Unlock(); /* dunblock() unlock the device too */
  }

  Dmsg2(rdebuglevel, "dcr=%p dev=%p\n", dcr, dcr->dev);
  Dmsg2(rdebuglevel, "MediaType dcr=%s dev=%s\n", dcr->media_type,
        dev->device_resource->media_type);

  dev->Unlock_read_acquire();

  Leave(rdebuglevel);

  return retval;
}

/**
 * Acquire device for writing. We permit multiple writers.
 *  If this is the first one, we read the label.
 *
 *  Returns: NULL if failed for any reason
 *           dcr if successful.
 *   Note, normally ReserveDeviceForAppend() is called
 *   before this routine.
 */
DeviceControlRecord* AcquireDeviceForAppend(DeviceControlRecord* dcr)
{
  Device* dev = dcr->dev;
  JobControlRecord* jcr = dcr->jcr;
  bool retval = false;

  Enter(200);
  InitDeviceWaitTimers(dcr);

  dev->Lock_acquire(); /* only one job at a time */
  dev->Lock();
  Dmsg1(100, "acquire_append device is %s\n", dev->type().c_str());

  // With the reservation system, this should not happen
  if (dev->CanRead()) {
    Jmsg1(jcr, M_FATAL, 0,
          T_("Want to append, but device %s is busy reading.\n"),
          dev->print_name());
    Dmsg1(200, "Want to append but device %s is busy reading.\n",
          dev->print_name());
    goto get_out;
  }

  dev->ClearUnload();


  {
    dev->rLock(true);
    BlockDevice(dev, BST_DOING_ACQUIRE);
    dev->Unlock();
    Dmsg1(190, "jid=%" PRIu32 " Do mount_next_write_vol\n", jcr->JobId);
    if (!dcr->MountNextWriteVolume()) {
      if (!jcr->IsJobCanceled()) {
        /* Reduce "noise" -- don't print if job canceled */
        Jmsg(jcr, M_FATAL, 0, T_("Could not ready device %s for append.\n"),
             dev->print_name());
        Dmsg1(200, "Could not ready device %s for append.\n",
              dev->print_name());
      }
      dev->Lock();
      UnblockDevice(dev);
      goto get_out;
    }
    Dmsg2(190, "Output pos=%u:%u\n", dcr->dev->file, dcr->dev->block_num);
    dev->Lock();
    UnblockDevice(dev);
  }

  dev->num_writers++; /* we are now a writer */
  if (jcr->sd_impl->NumWriteVolumes == 0) { jcr->sd_impl->NumWriteVolumes = 1; }
  dev->VolCatInfo.VolCatJobs++; /* increment number of jobs on vol */
  Dmsg4(100, "=== nwriters=%d nres=%d vcatjob=%" PRIu32 " dev=%s\n",
        dev->num_writers, dev->NumReserved(), dev->VolCatInfo.VolCatJobs,
        dev->print_name());
  dcr->DirUpdateVolumeInfo(
      is_labeloperation::False); /* send Volume info to Director */
  retval = true;

get_out:
  // Don't plugin close here, we might have multiple writers
  dcr->ClearReserved();
  dev->Unlock();
  dev->Unlock_acquire();
  Leave(200);

  return retval ? dcr : NULL;
}

/**
 * This job is done, so release the device. From a Unix standpoint, the device
 * remains open.
 *
 * Note, if we were spooling, we may enter with the device blocked.
 * We unblock at the end, only if it was us who blocked the device.
 */
bool ReleaseDevice(DeviceControlRecord* dcr)
{
  utime_t now;
  JobControlRecord* jcr = dcr->jcr;
  Device* dev = dcr->dev;
  bool retval = true;
  char tbuf[100];
  int was_blocked = BST_NOT_BLOCKED;

  Jmsg(jcr, M_INFO, 0, "Releasing device %s.\n", dev->print_name());

  // Capture job statistics now that we are done using this device.
  now = (utime_t)time(NULL);
  UpdateJobStatistics(jcr, now);

  /* Some devices do cache write operations (e.g. DropletDevice).
   * Therefore flushing the cache is required to determine
   * if a job have been written successfully.
   * As a flush operation can take quite a long time,
   * this must be done before acquiring locks.
   * A previous implementation did the flush inside dev->close(),
   * which resulted in various locking problems. */
  if (!jcr->IsJobCanceled()) {
    if (!dev->d_flush(dcr)) {
      Jmsg(jcr, M_FATAL, 0, "Failed to flush device %s.\n", dev->print_name());
    }
  }

  dev->Lock();
  if (!dev->IsBlocked()) {
    BlockDevice(dev, BST_RELEASING);
  } else {
    was_blocked = dev->blocked();
    dev->SetBlocked(BST_RELEASING);
  }

  with_volume_lock([&] {
    Dmsg1(100, "releasing device %s\n", dev->print_name());

    // If device is reserved, job never started, so release the reserve here
    dcr->ClearReserved();

    if (dev->CanRead()) {
      VolumeCatalogInfo* vol = &dev->VolCatInfo;

      dev->ClearRead(); /* clear read bit */
      Dmsg2(150, "dir_update_vol_info. label=%d Vol=%s\n", dev->IsLabeled(),
            vol->VolCatName);
      if (dev->IsLabeled() && vol->VolCatName[0] != 0) {
        dcr->DirUpdateVolumeInfo(
            is_labeloperation::False); /* send Volume info to Director */
        RemoveReadVolume(jcr, dcr->VolumeName);
        VolumeUnused(dcr);
      }
    } else if (dev->num_writers > 0) {
      /* Note if WEOT is set, we are at the end of the tape and may not be
       * positioned correctly, so the job_media_record and update_vol_info have
       * already been done, which means we skip them here. */
      dev->num_writers--;
      Dmsg1(100, "There are %d writers in ReleaseDevice\n", dev->num_writers);
      if (dcr->VolFirstIndex) {
        // Send a new jobmedia record if we have written to a volume
        // take note that the volume we wrote to is not necessarily the volume
        // that is currently inside the device.
        dcr->DirCreateJobmediaRecord(false);
      }
      if (dev->IsLabeled()) {
        Dmsg2(200, "dir_create_jobmedia. Release vol=%s dev=%s\n",
              dev->getVolCatName(), dev->print_name());
        if (!dev->AtWeot() && !dcr->DirCreateJobmediaRecord(false)) {
          Jmsg2(
              jcr, M_FATAL, 0,
              T_("Could not create JobMedia record for Volume=\"%s\" Job=%s\n"),
              dcr->getVolCatName(), jcr->Job);
        }

        // If no more writers, and no errors, and wrote something, write an EOF
        if (!dev->num_writers && dev->CanWrite() && dev->block_num > 0) {
          dev->weof(1);
          WriteAnsiIbmLabels(dcr, ANSI_EOF_LABEL, dev->VolHdr.VolumeName);
        }
        if (!dev->AtWeot()) {
          dev->VolCatInfo.VolCatFiles = dev->file; /* set number of files */

          // Note! do volume update before close, which zaps VolCatInfo
          dcr->DirUpdateVolumeInfo(
              is_labeloperation::False); /* send Volume info to Director */
          Dmsg2(200, "dir_update_vol_info. Release vol=%s dev=%s\n",
                dev->getVolCatName(), dev->print_name());
        }
        if (dev->num_writers == 0) { /* if not being used */
          VolumeUnused(dcr); /*  we obviously are not using the volume */
        }
      }

    } else {
      /* If we reach here, it is most likely because the job has failed,
       * since the device is not in read mode and there are no writers.
       * It was probably reserved. */
      VolumeUnused(dcr);
    }

    Dmsg3(100, "%d writers, %d reserve, dev=%s\n", dev->num_writers,
          dev->NumReserved(), dev->print_name());

    // If no writers, close if file or !CAP_ALWAYS_OPEN
    if (dev->num_writers == 0
        && (!dev->IsTape() || !dev->HasCap(CAP_ALWAYSOPEN))) {
      dev->close(dcr);
      FreeVolume(dev);
    }
  });

  // Fire off Alert command and include any output
  if (!jcr->IsJobCanceled()) {
    if (!dcr->device_resource->drive_tapealert_enabled
        && dcr->device_resource->alert_command) {
      int status = 1;
      POOLMEM *alert, *line;
      Bpipe* bpipe;

      alert = GetPoolMemory(PM_FNAME);
      line = GetPoolMemory(PM_FNAME);

      alert = edit_device_codes(dcr, alert, dcr->device_resource->alert_command,
                                "");

      // Wait maximum 5 minutes
      bpipe = OpenBpipe(alert, 60 * 5, "r");
      if (bpipe) {
        while (bfgets(line, bpipe->rfd)) {
          Jmsg(jcr, M_ALERT, 0, T_("Alert: %s\n"), line);
        }
        status = CloseBpipe(bpipe);
      } else {
        status = errno;
      }
      if (status != 0) {
        BErrNo be;
        Jmsg(jcr, M_ALERT, 0, T_("3997 Bad alert command: %s: ERR=%s.\n"),
             alert, be.bstrerror(status));
      }

      Dmsg1(400, "alert status=%d\n", status);
      FreePoolMemory(alert);
      FreePoolMemory(line);
    } else {
      /* If all reservations are cleared for this device raise an event that SD
       * plugins can register to. */
      if (dev->NumReserved() == 0) {
        GeneratePluginEvent(jcr, bSdEventDeviceRelease, dcr);
      }
    }
  }

  pthread_cond_broadcast(&dev->wait_next_vol);
  Dmsg2(100, "JobId=%" PRIu32 " broadcast wait_device_release at %s\n",
        jcr->JobId, bstrftimes(tbuf, sizeof(tbuf), (utime_t)time(NULL)));
  ReleaseDeviceCond();

  // If we are the thread that blocked the device, then unblock it
  if (pthread_equal(dev->no_wait_id, pthread_self())) {
    dev->dunblock(true);
  } else {
    // Otherwise, reset the prior block status and unlock
    dev->SetBlocked(was_blocked);
    dev->Unlock();
  }

  if (dcr->keep_dcr) {
    DetachDcrFromDev(dcr);
  } else {
    FreeDeviceControlRecord(dcr);
  }

  Dmsg2(100, "Device %s released by JobId=%" PRIu32 "\n", dev->print_name(),
        jcr->JobId);

  return retval;
}

// Clean up the device for reuse without freeing the memory
bool CleanDevice(DeviceControlRecord* dcr)
{
  bool retval;

  dcr->keep_dcr = true; /* do not free the dcr */
  retval = ReleaseDevice(dcr);
  dcr->keep_dcr = false;

  return retval;
}

// Setup DeviceControlRecord with a new device.
void SetupNewDcrDevice(JobControlRecord* jcr,
                       DeviceControlRecord* dcr,
                       Device* dev,
                       BlockSizeBoundaries* blocksizes)
{
  dcr->jcr = jcr; /* point back to jcr */

  // Set device information, possibly change device
  if (dev) {
    // Set wanted blocksizes
    if (blocksizes) {
      dev->min_block_size = blocksizes->min_block_size;
      dev->max_block_size = blocksizes->max_block_size;
    }

    if (dcr->block) { FreeBlock(dcr->block); }
    dcr->block = new_block(dev);

    if (dcr->rec) {
      FreeRecord(dcr->rec);
      dcr->rec = NULL;
    }
    dcr->rec = new_record();

    if (dcr->attached_to_dev) { DetachDcrFromDev(dcr); }

    // Use job spoolsize prior to device spoolsize
    if (jcr && jcr->sd_impl->spool_size) {
      dcr->max_job_spool_size = jcr->sd_impl->spool_size;
    } else {
      dcr->max_job_spool_size = dev->device_resource->max_job_spool_size;
    }

    dcr->device_resource = dev->device_resource;
    dcr->SetDev(dev);
    AttachDcrToDev(dcr);

    /* Initialize the auto deflation/inflation which can
     * be disabled per DeviceControlRecord when we want to. e.g. when we want to
     * send the data as part of a replication stream in which we
     * don't want to first inflate the data to then again
     * do deflation for sending it to the other storage daemon. */
    dcr->autodeflate = dcr->device_resource->autodeflate;
    dcr->autoinflate = dcr->device_resource->autoinflate;
  }
}

static void AttachDcrToDev(DeviceControlRecord* dcr)
{
  Device* dev;
  JobControlRecord* jcr;

  lock_mutex(dcr->mutex_);
  dev = dcr->dev;
  jcr = dcr->jcr;
  if (jcr) Dmsg1(500, "JobId=%" PRIu32 " enter AttachDcrToDev\n", jcr->JobId);
  /* ***FIXME*** return error if dev not initiated */
  if (!dcr->attached_to_dev && dev->initiated && jcr
      && jcr->getJobType() != JT_SYSTEM) {
    dev->Lock();
    Dmsg4(200, "Attach Jid=%" PRIu32 " dcr=%p size=%" PRIuz " dev=%s\n",
          jcr->JobId, dcr, dev->attached_dcrs.size(), dev->print_name());
    dev->attached_dcrs.push_back(dcr); /* attach dcr to device */
    dev->Unlock();
    dcr->attached_to_dev = true;
  }
  unlock_mutex(dcr->mutex_);
}

// DeviceControlRecord is locked before calling this routine
static void LockedDetachDcrFromDev(DeviceControlRecord* dcr)
{
  Device* dev = dcr->dev;
  Dmsg0(500, "Enter DetachDcrFromDev\n"); /* jcr is NULL in some cases */

  /* Detach this dcr only if attached */
  if (dcr->attached_to_dev && dev) {
    dcr->UnreserveDevice();
    dev->Lock();
    Dmsg4(200, "Detach Jid=%" PRIu32 " dcr=%p size=%" PRIuz " to dev=%s\n",
          dcr->jcr->JobId, dcr, dev->attached_dcrs.size(), dev->print_name());
    dcr->attached_to_dev = false;
    if (!dev->attached_dcrs.empty()) {
      // detach dcr from device
      auto it = std::remove(dev->attached_dcrs.begin(),
                            dev->attached_dcrs.end(), dcr);
      dev->attached_dcrs.erase(it, dev->attached_dcrs.end());
    }
    //    RemoveDcrFromDcrs(dcr);      /* remove dcr from jcr list */
    dev->Unlock();
  }
  dcr->attached_to_dev = false;
}


static void DetachDcrFromDev(DeviceControlRecord* dcr)
{
  lock_mutex(dcr->mutex_);
  LockedDetachDcrFromDev(dcr);
  unlock_mutex(dcr->mutex_);
}

/**
 * Free up all aspects of the given dcr -- i.e. dechain it,
 *  release allocated memory, zap pointers, ...
 */
void FreeDeviceControlRecord(DeviceControlRecord* dcr)
{
  JobControlRecord* jcr;

  lock_mutex(dcr->mutex_);
  jcr = dcr->jcr;

  LockedDetachDcrFromDev(dcr);

  if (dcr->block) { FreeBlock(dcr->block); }

  if (dcr->rec) { FreeRecord(dcr->rec); }

  if (jcr && jcr->sd_impl->dcr == dcr) { jcr->sd_impl->dcr = NULL; }

  if (jcr && jcr->sd_impl->read_dcr == dcr) { jcr->sd_impl->read_dcr = NULL; }

  unlock_mutex(dcr->mutex_);

  pthread_mutex_destroy(&dcr->mutex_);
  pthread_mutex_destroy(&dcr->r_mutex);

  delete dcr;
}

static void SetDcrFromVol(DeviceControlRecord* dcr, VolumeList* vol)
{
  /* Note, if we want to be able to work from a .bsr file only
   *  for disaster recovery, we must "simulate" reading the catalog */
  bstrncpy(dcr->VolumeName, vol->VolumeName, sizeof(dcr->VolumeName));
  dcr->setVolCatName(vol->VolumeName);
  bstrncpy(dcr->media_type, vol->MediaType, sizeof(dcr->media_type));
  dcr->VolCatInfo.Slot = vol->Slot;
  dcr->VolCatInfo.InChanger = vol->Slot > 0;
}

} /* namespace storagedaemon */
