root/src/add-ons/kernel/bus_managers/scsi/scsi_io.cpp
/*
 * Copyright 2004-2007, Haiku, Inc. All RightsReserved.
 * Copyright 2002-2004, Thomas Kurschel. All rights reserved.
 *
 * Distributed under the terms of the MIT License.
 */

//!     Part of Open SCSI bus manager


#include "scsi_internal.h"
#include "queuing.h"

#include <string.h>

#include <algorithm>


/** put request back in queue because of device/bus overflow */

void
scsi_requeue_request(scsi_ccb *request, bool bus_overflow)
{
        scsi_bus_info *bus = request->bus;
        scsi_device_info *device = request->device;
        bool was_servicable, start_retry;

        SHOW_FLOW0(3, "");

        if (request->state != SCSI_STATE_SENT) {
                panic("Unsent ccb was request to requeue\n");
                return;
        }

        request->state = SCSI_STATE_QUEUED;

        mutex_lock(&bus->mutex);

        was_servicable = scsi_can_service_bus(bus);

        if (bus->left_slots++ == 0)
                scsi_unblock_bus_noresume(bus, false);

        if (device->left_slots++ == 0 || request->ordered)
                scsi_unblock_device_noresume(device, false);

        // make sure it's the next request for this device
        scsi_add_req_queue_first(request);

        if (bus_overflow) {
                // bus has overflown
                scsi_set_bus_overflow(bus);
                // add device to queue as last - other devices may be waiting already
                scsi_add_device_queue_last(device);
                // don't change device overflow condition as the device has never seen
                // this request
        } else {
                // device has overflown
                scsi_set_device_overflow(device);
                scsi_remove_device_queue(device);
                // either, the device has refused the request, i.e. it was transmitted
                // over the bus - in this case, the bus cannot be overloaded anymore;
                // or, the driver detected that the device can not be able to process
                // further requests, because the driver knows its maximum queue depth
                // or something - in this case, the bus state hasn't changed, but the
                // driver will tell us about any overflow when we submit the next
                // request, so the overflow state will be fixed automatically
                scsi_clear_bus_overflow(bus);
        }

        start_retry = !was_servicable && scsi_can_service_bus(bus);

        mutex_unlock(&bus->mutex);

        // submit requests to other devices in case bus was overloaded
        if (start_retry)
                release_sem_etc(bus->start_service, 1, 0/*B_DO_NOT_RESCHEDULE*/);
}


/** restart request ASAP because something went wrong */

void
scsi_resubmit_request(scsi_ccb *request)
{
        scsi_bus_info *bus = request->bus;
        scsi_device_info *device = request->device;
        bool was_servicable, start_retry;

        SHOW_FLOW0(3, "");

        if (request->state != SCSI_STATE_SENT) {
                panic("Unsent ccb was asked to get resubmitted\n");
                return;
        }

        request->state = SCSI_STATE_QUEUED;

        mutex_lock(&bus->mutex);

        was_servicable = scsi_can_service_bus(bus);

        if (bus->left_slots++ == 0)
                scsi_unblock_bus_noresume(bus, false);

        if (device->left_slots++ == 0 || request->ordered)
                scsi_unblock_device_noresume(device, false);

        // if SIM reported overflow of device/bus, this should (hopefully) be over now
        scsi_clear_device_overflow(device);
        scsi_clear_bus_overflow(bus);

        // we don't want to let anyone overtake this request
        request->ordered = true;

        // make it the next request submitted to SIM for this device
        scsi_add_req_queue_first(request);

        // if device is not blocked (anymore) add it to waiting list of bus
        if (device->lock_count == 0) {
                scsi_add_device_queue_first(device);
                // as previous line does nothing if already queued, we force device
                // to be the next one to get handled
                bus->waiting_devices = device;
        }

        start_retry = !was_servicable && scsi_can_service_bus(bus);

        mutex_unlock(&bus->mutex);

        // let the service thread do the resubmit
        if (start_retry)
                release_sem_etc(bus->start_service, 1, 0/*B_DO_NOT_RESCHEDULE*/);
}


/** submit autosense for request */

static void
submit_autosense(scsi_ccb *request)
{
        scsi_device_info *device = request->device;

        //snooze(1000000);

        SHOW_FLOW0(3, "sending autosense");
        // we cannot use scsi_scsi_io but must insert it brute-force

        // give SIM a well-defined first state
        // WARNING: this is a short version of scsi_async_io, so if
        // you change something there, do it here as well!

        // no DMA buffer (we made sure that the data buffer fulfills all
        // limitations)
        device->auto_sense_request->buffered = false;
        // don't let any request bypass us
        device->auto_sense_request->ordered = true;
        // request is not emulated
        device->auto_sense_request->emulated = false;

        device->auto_sense_originator = request;

        // make it next request to process
        scsi_add_queued_request_first(device->auto_sense_request);
}


/** finish special auto-sense request */

static void
finish_autosense(scsi_device_info *device)
{
        scsi_ccb *orig_request = device->auto_sense_originator;
        scsi_ccb *request = device->auto_sense_request;

        SHOW_FLOW0(3, "");

        if (request->subsys_status == SCSI_REQ_CMP) {
                int sense_len;

                // we got sense data -> copy it to sense buffer
                sense_len = std::min((uint32)SCSI_MAX_SENSE_SIZE,
                        request->data_length - request->data_resid);

                SHOW_FLOW(3, "Got sense: %d bytes", sense_len);

                memcpy(orig_request->sense, request->data, sense_len);

                orig_request->sense_resid = SCSI_MAX_SENSE_SIZE - sense_len;
                orig_request->subsys_status |= SCSI_AUTOSNS_VALID;
        } else {
                // failed to get sense
                orig_request->subsys_status = SCSI_AUTOSENSE_FAIL;
        }

        // inform peripheral driver
        orig_request->completion_cond.NotifyOne();
}


/** device refused request because command queue is full */

static void
scsi_device_queue_overflow(scsi_ccb *request, uint num_requests)
{
        scsi_bus_info *bus = request->bus;
        scsi_device_info *device = request->device;
        int diff_max_slots;

        // set maximum number of concurrent requests to number of
        // requests running when QUEUE FULL condition occurred - 1
        // (the "1" is the refused request)
        --num_requests;

        // at least one request at once must be possible
        if (num_requests < 1)
                num_requests = 1;

        SHOW_INFO(2, "Restricting device queue to %d requests", num_requests);

        // update slot count
        mutex_lock(&bus->mutex);

        diff_max_slots = device->total_slots - num_requests;
        device->total_slots = num_requests;
        device->left_slots -= diff_max_slots;

        mutex_unlock(&bus->mutex);

        // requeue request, blocking further device requests
        scsi_requeue_request(request, false);
}


/** finish scsi request */

void
scsi_request_finished(scsi_ccb *request, uint num_requests)
{
        scsi_device_info *device = request->device;
        scsi_bus_info *bus = request->bus;
        bool was_servicable, start_service, do_autosense;

        SHOW_FLOW(3, "%p", request);

        if (request->state != SCSI_STATE_SENT) {
                panic("Unsent ccb %p was reported as done\n", request);
                return;
        }

        if (request->subsys_status == SCSI_REQ_INPROG) {
                panic("ccb %p with status \"Request in Progress\" was reported as done\n",
                        request);
                return;
        }

        // check for queue overflow reported by device
        if (request->subsys_status == SCSI_REQ_CMP_ERR
                && request->device_status == SCSI_STATUS_QUEUE_FULL) {
                scsi_device_queue_overflow(request, num_requests);
                return;
        }

        request->state = SCSI_STATE_FINISHED;

        mutex_lock(&bus->mutex);

        was_servicable = scsi_can_service_bus(bus);

        // do pseudo-autosense if device doesn't support it and
        // device reported a check condition state and auto-sense haven't
        // been retrieved by SIM
        // (last test is implicit as SIM adds SCSI_AUTOSNS_VALID to subsys_status)
        do_autosense = device->manual_autosense
                && (request->flags & SCSI_DIS_AUTOSENSE) == 0
                && request->subsys_status == SCSI_REQ_CMP_ERR
                && request->device_status == SCSI_STATUS_CHECK_CONDITION;

        if (request->subsys_status != SCSI_REQ_CMP) {
                SHOW_FLOW(3, "subsys=%x, device=%x, flags=%" B_PRIx32
                        ", manual_auto_sense=%d", request->subsys_status,
                        request->device_status, request->flags, device->manual_autosense);
        }

        if (do_autosense) {
                // queue auto-sense request after checking was_servicable but before
                // releasing locks so no other request overtakes auto-sense
                submit_autosense(request);
        }

        if (bus->left_slots++ == 0)
                scsi_unblock_bus_noresume(bus, false);

        if (device->left_slots++ == 0 || request->ordered)
                scsi_unblock_device_noresume(device, false);

        // if SIM reported overflow of device/bus, this should (hopefully) be over now
        scsi_clear_device_overflow(device);
        scsi_clear_bus_overflow(bus);

        // if device is not blocked (anymore) and has pending requests,
        // add it to waiting list of bus
        if (device->lock_count == 0 && device->queued_reqs != NULL)
                scsi_add_device_queue_last(device);

        start_service = !was_servicable && scsi_can_service_bus(bus);

        mutex_unlock(&bus->mutex);

        // tell service thread to submit new requests to SIM
        // (do this ASAP to keep bus/device busy)
        if (start_service)
                release_sem_etc(bus->start_service, 1, 0/*B_DO_NOT_RESCHEDULE*/);

        if (request->emulated)
                scsi_finish_emulation(request);

        // copy data from buffer and release it
        if (request->buffered)
                scsi_release_dma_buffer(request);

        // special treatment for finished auto-sense
        if (request == device->auto_sense_request)
                finish_autosense(device);
        else {
                // tell peripheral driver about completion
                if (!do_autosense)
                        request->completion_cond.NotifyOne();
        }
}


/**     check whether request can be executed right now, enqueuing it if not,
 *      return: true if request can be executed
 *      side effect: updates device->last_sort
 */

static inline bool
scsi_check_enqueue_request(scsi_ccb *request)
{
        scsi_bus_info *bus = request->bus;
        scsi_device_info *device = request->device;
        bool execute;

        mutex_lock(&bus->mutex);

        // if device/bus is locked, or there are waiting requests
        // or waiting devices (last condition makes sure we don't overtake
        // requests that got queued because bus was full)
        if (device->lock_count > 0 || device->queued_reqs != NULL
                || bus->lock_count > 0 || bus->waiting_devices != NULL) {
                SHOW_FLOW0(3, "bus/device is currently locked");
                scsi_add_queued_request(request);
                execute = false;
        } else {
                // if bus is saturated, block it
                if (--bus->left_slots == 0) {
                        SHOW_FLOW0(3, "bus is saturated, blocking further requests");
                        scsi_block_bus_nolock(bus, false);
                }

                // if device saturated or blocking request, block device
                if (--device->left_slots == 0 || request->ordered) {
                        SHOW_FLOW0( 3, "device is saturated/blocked by requests, blocking further requests" );
                        scsi_block_device_nolock(device, false);
                }

                if (request->sort >= 0) {
                        device->last_sort = request->sort;
                        SHOW_FLOW(1, "%" B_PRId64, device->last_sort);
                }

                execute = true;
        }

        mutex_unlock(&bus->mutex);

        return execute;
}


// size of SCSI command according to function group
int func_group_len[8] = {
        6, 10, 10, 0, 16, 12, 0, 0
};


/** execute scsi command asynchronously */

void
scsi_async_io(scsi_ccb *request)
{
        scsi_bus_info *bus = request->bus;

        //SHOW_FLOW( 0, "path_id=%d", bus->path_id );

        //snooze( 1000000 );

        // do some sanity tests first
        if (request->state != SCSI_STATE_FINISHED) {
                panic("Passed ccb to scsi_action that isn't ready (state = %d)\n",
                        request->state);
        }

        if (request->cdb_length < func_group_len[request->cdb[0] >> 5]) {
                SHOW_ERROR(3, "invalid command len (%d instead of %d)",
                        request->cdb_length, func_group_len[request->cdb[0] >> 5]);

                request->subsys_status = SCSI_REQ_INVALID;
                goto err;
        }

        if (!request->device->valid) {
                SHOW_ERROR0( 3, "device got removed" );

                // device got removed meanwhile
                request->subsys_status = SCSI_DEV_NOT_THERE;
                goto err;
        }

        if ((request->flags & SCSI_DIR_MASK) != SCSI_DIR_NONE
                && request->sg_list == NULL && request->data_length > 0) {
                SHOW_ERROR( 3, "Asynchronous SCSI I/O requires S/G list (data is %"
                        B_PRIu32 " bytes)", request->data_length );
                request->subsys_status = SCSI_DATA_RUN_ERR;
                goto err;
        }

        request->buffered = request->emulated = 0;

        // make data DMA safe
        // (S/G list must be created first to be able to verify DMA restrictions)
        if ((request->flags & SCSI_DMA_SAFE) == 0 && request->data_length > 0) {
                request->buffered = true;
                if (!scsi_get_dma_buffer(request)) {
                        SHOW_ERROR0( 3, "cannot create DMA buffer for request - reduce data volume" );

                        request->subsys_status = SCSI_DATA_RUN_ERR;
                        goto err;
                }
        }

        // emulate command if not supported
        if ((request->device->emulation_map[request->cdb[0] >> 3]
                & (1 << (request->cdb[0] & 7))) != 0) {
                request->emulated = true;

                if (!scsi_start_emulation(request)) {
                        SHOW_ERROR(3, "cannot emulate SCSI command 0x%02x", request->cdb[0]);
                        goto err2;
                }
        }

        // SCSI-1 uses 3 bits of command packet for LUN
        // SCSI-2 uses identify message, but still needs LUN in command packet
        //        (though it won't fit, as LUNs can be 4 bits wide)
        // SCSI-3 doesn't use command packet for LUN anymore
        // ATAPI uses 3 bits of command packet for LUN

        // currently, we always copy LUN into command packet as a safe bet
        {
                // abuse TUR to find proper spot in command packet for LUN
                scsi_cmd_tur *cmd = (scsi_cmd_tur *)request->cdb;

                cmd->lun = request->device->target_lun;
        }

        request->ordered = (request->flags & SCSI_ORDERED_QTAG) != 0;

        SHOW_FLOW(3, "ordered=%d", request->ordered);

        // make sure device/bus is not blocked
        if (!scsi_check_enqueue_request(request))
                return;

        bus = request->bus;

        request->state = SCSI_STATE_SENT;
        bus->interface->scsi_io(bus->sim_cookie, request);
        return;

err2:
        if (request->buffered)
                scsi_release_dma_buffer(request);
err:
        request->completion_cond.NotifyOne(B_ERROR);
        return;
}


/** execute SCSI command synchronously */

void
scsi_sync_io(scsi_ccb *request)
{
        bool tmp_sg = false;

        // create scatter-gather list if required
        if ((request->flags & SCSI_DIR_MASK) != SCSI_DIR_NONE
                && request->sg_list == NULL && request->data_length > 0) {
                tmp_sg = true;
                if (!create_temp_sg(request)) {
                        SHOW_ERROR0( 3, "data is too much fragmented - you should use s/g list" );

                        // ToDo: this means too much (fragmented) data
                        request->subsys_status = SCSI_DATA_RUN_ERR;
                        return;
                }
        }

        ConditionVariableEntry entry;
        request->completion_cond.Add(&entry);

        scsi_async_io(request);

        entry.Wait();

        if (tmp_sg)
                cleanup_temp_sg(request);
}


uchar
scsi_term_io(scsi_ccb *ccb_to_terminate)
{
        scsi_bus_info *bus = ccb_to_terminate->bus;

        return bus->interface->term_io(bus->sim_cookie, ccb_to_terminate);
}


uchar
scsi_abort(scsi_ccb *req_to_abort)
{
        scsi_bus_info *bus = req_to_abort->bus;

        if (bus == NULL) {
                // checking the validity of the request to abort is a nightmare
                // this is just a beginning
                return SCSI_REQ_INVALID;
        }

        mutex_lock(&bus->mutex);

        switch (req_to_abort->state) {
                case SCSI_STATE_FINISHED:
                case SCSI_STATE_SENT:
                        mutex_unlock(&bus->mutex);
                        break;

                case SCSI_STATE_QUEUED: {
                        bool was_servicable, start_retry;

                        was_servicable = scsi_can_service_bus(bus);

                        // remove request from device queue
                        scsi_remove_queued_request(req_to_abort);

                        start_retry = scsi_can_service_bus(bus) && !was_servicable;

                        mutex_unlock(&bus->mutex);

                        req_to_abort->subsys_status = SCSI_REQ_ABORTED;

                        // finish emulation
                        if (req_to_abort->emulated)
                                scsi_finish_emulation(req_to_abort);

                        // release DMA buffer
                        if (req_to_abort->buffered)
                                scsi_release_dma_buffer(req_to_abort);

                        // tell peripheral driver about
                        req_to_abort->completion_cond.NotifyOne(B_CANCELED);

                        if (start_retry)
                                release_sem(bus->start_service);

                        break;
                }
        }

        return SCSI_REQ_CMP;
}


/** submit pending request (at most one!) */

bool
scsi_check_exec_service(scsi_bus_info *bus)
{
        SHOW_FLOW0(3, "");
        mutex_lock(&bus->mutex);

        if (scsi_can_service_bus(bus)) {
                scsi_ccb *request;
                scsi_device_info *device;

                SHOW_FLOW0(3, "servicing bus");

                //snooze( 1000000 );

                // handle devices in round-robin-style
                device = bus->waiting_devices;
                bus->waiting_devices = bus->waiting_devices->waiting_next;

                request = device->queued_reqs;
                scsi_remove_queued_request(request);

                // if bus is saturated, block it
                if (--bus->left_slots == 0) {
                        SHOW_FLOW0(3, "bus is saturated, blocking further requests");
                        scsi_block_bus_nolock(bus, false);
                }

                // if device saturated or blocking request, block device
                if (--device->left_slots == 0 || request->ordered) {
                        SHOW_FLOW0(3, "device is saturated/blocked by requests, blocking further requests");
                        scsi_block_device_nolock(device, false);
                }

                if (request->sort >= 0) {
                        device->last_sort = request->sort;
                        SHOW_FLOW(1, "%" B_PRId64, device->last_sort);
                }

                mutex_unlock(&bus->mutex);

                request->state = SCSI_STATE_SENT;
                bus->interface->scsi_io(bus->sim_cookie, request);

                return true;
        }

        mutex_unlock(&bus->mutex);

        return false;
}