Skip to main content

iscsi_client_rs/state_machine/
tur_states.rs

1//! This module defines the state machine for the iSCSI Test Unit Ready (TUR)
2//! command. It includes the states, context, and transitions for handling the
3//! TUR operation.
4
5// SPDX-License-Identifier: AGPL-3.0-or-later
6// Copyright (C) 2012-2025 Andrei Maltsev
7
8use std::{
9    marker::PhantomData,
10    pin::Pin,
11    sync::{
12        Arc,
13        atomic::{AtomicU32, Ordering},
14    },
15};
16
17use anyhow::{Context, Result, anyhow, bail};
18use tokio_util::sync::CancellationToken;
19use tracing::debug;
20
21use crate::{
22    client::client::ClientConnection,
23    control_block::test_unit_ready::build_test_unit_ready,
24    models::{
25        command::{
26            common::{ScsiStatus, TaskAttribute},
27            request::{ScsiCommandRequest, ScsiCommandRequestBuilder},
28            response::ScsiCommandResponse,
29        },
30        common::HEADER_LEN,
31        data_fromat::{PduRequest, PduResponse},
32    },
33    state_machine::common::{StateMachine, StateMachineCtx, Transition},
34};
35
36/// This structure represents the context for a SCSI Test Unit Ready (TUR)
37/// command.
38#[derive(Debug)]
39pub struct TurCtx<'a> {
40    _lt: PhantomData<&'a ()>,
41
42    /// The client connection.
43    pub conn: Arc<ClientConnection>,
44    /// The Initiator Task Tag.
45    pub itt: u32,
46    /// The Command Sequence Number.
47    pub cmd_sn: Arc<AtomicU32>,
48    /// The Expected Status Sequence Number.
49    pub exp_stat_sn: Arc<AtomicU32>,
50    /// The Logical Unit Number.
51    pub lun: u64,
52    /// A buffer for the BHS.
53    pub buf: [u8; HEADER_LEN],
54    /// The SCSI Command Descriptor Block.
55    pub cbd: [u8; 16],
56
57    /// The last received command response.
58    pub last_response: Option<PduResponse<ScsiCommandResponse>>,
59    state: Option<TurStates>,
60}
61
62impl<'a> TurCtx<'a> {
63    /// Creates a new `TurCtx` for a TUR operation.
64    pub fn new(
65        conn: Arc<ClientConnection>,
66        itt: Arc<AtomicU32>,
67        cmd_sn: Arc<AtomicU32>,
68        exp_stat_sn: Arc<AtomicU32>,
69        lun: u64,
70    ) -> Self {
71        Self {
72            conn,
73            itt: itt.fetch_add(1, Ordering::SeqCst),
74            cmd_sn,
75            exp_stat_sn,
76            lun,
77            buf: [0u8; HEADER_LEN],
78            cbd: [0u8; 16],
79            last_response: None,
80            state: Some(TurStates::Idle(Idle)),
81            _lt: PhantomData,
82        }
83    }
84
85    async fn send_tur(&mut self) -> Result<()> {
86        build_test_unit_ready(&mut self.cbd, 0);
87
88        let cmd_sn = self.cmd_sn.fetch_add(1, Ordering::SeqCst);
89        let esn = self.exp_stat_sn.load(Ordering::SeqCst);
90
91        let header = ScsiCommandRequestBuilder::new()
92            .initiator_task_tag(self.itt)
93            .lun(self.lun)
94            .cmd_sn(cmd_sn)
95            .exp_stat_sn(esn)
96            .task_attribute(TaskAttribute::Simple)
97            .expected_data_transfer_length(0)
98            .scsi_descriptor_block(&self.cbd);
99
100        header.header.to_bhs_bytes(&mut self.buf)?;
101        let pdu = PduRequest::<ScsiCommandRequest>::new_request(self.buf, &self.conn.cfg);
102
103        self.conn.send_request(self.itt, pdu).await?;
104        Ok(())
105    }
106
107    async fn recv_tur_resp(&mut self) -> Result<()> {
108        let rsp = self
109            .conn
110            .read_response::<ScsiCommandResponse>(self.itt)
111            .await?;
112        self.last_response = Some(rsp);
113
114        let lr = self.last_response.as_ref().expect("saved above");
115        let hv = lr.header_view()?;
116
117        self.exp_stat_sn
118            .store(hv.stat_sn.get().wrapping_add(1), Ordering::SeqCst);
119
120        let scsi_status = hv.status.decode()?;
121        if scsi_status != ScsiStatus::Good {
122            let data = lr.data()?;
123            if !data.is_empty() {
124                bail!(
125                    "TEST UNIT READY failed: status={:?}, sense ({} bytes)={:02X?}",
126                    scsi_status,
127                    data.len(),
128                    data
129                );
130            }
131            bail!("TEST UNIT READY failed: status={:?}", scsi_status);
132        }
133        Ok(())
134    }
135}
136
137/// Represents the initial state of a TUR operation.
138#[derive(Debug)]
139pub struct Idle;
140
141/// Represents the state of waiting for a response to a TUR command.
142#[derive(Debug)]
143pub struct Wait;
144
145/// Defines the possible states for a SCSI Test Unit Ready (TUR) operation state
146/// machine.
147#[derive(Debug)]
148pub enum TurStates {
149    Idle(Idle),
150    Wait(Wait),
151}
152
153type TurStepOut = Transition<TurStates, Result<()>>;
154
155impl<'ctx> StateMachine<TurCtx<'ctx>, TurStepOut> for Idle {
156    type StepResult<'a>
157        = Pin<Box<dyn Future<Output = TurStepOut> + Send + 'a>>
158    where
159        Self: 'a,
160        TurCtx<'ctx>: 'a;
161
162    fn step<'a>(&'a self, ctx: &'a mut TurCtx<'ctx>) -> Self::StepResult<'a> {
163        Box::pin(async move {
164            match ctx.send_tur().await {
165                Ok(()) => Transition::Next(TurStates::Wait(Wait), Ok(())),
166                Err(e) => Transition::Done(Err(e)),
167            }
168        })
169    }
170}
171
172impl<'ctx> StateMachine<TurCtx<'ctx>, TurStepOut> for Wait {
173    type StepResult<'a>
174        = Pin<Box<dyn std::future::Future<Output = TurStepOut> + Send + 'a>>
175    where
176        Self: 'a,
177        TurCtx<'ctx>: 'a;
178
179    fn step<'a>(&'a self, ctx: &'a mut TurCtx<'ctx>) -> Self::StepResult<'a> {
180        Box::pin(async move {
181            match ctx.recv_tur_resp().await {
182                Ok(()) => Transition::Done(Ok(())),
183                Err(e) => Transition::Done(Err(e)),
184            }
185        })
186    }
187}
188
189impl<'ctx> StateMachineCtx<TurCtx<'ctx>, PduResponse<ScsiCommandResponse>>
190    for TurCtx<'ctx>
191{
192    async fn execute(
193        &mut self,
194        _cancel: &CancellationToken,
195    ) -> Result<PduResponse<ScsiCommandResponse>> {
196        debug!("Loop TUR");
197
198        loop {
199            let state = self.state.take().context("state must be set TurCtx")?;
200            let tr = match state {
201                TurStates::Idle(s) => s.step(self).await,
202                TurStates::Wait(s) => s.step(self).await,
203            };
204
205            match tr {
206                Transition::Next(next, r) => {
207                    r?;
208                    self.state = Some(next);
209                },
210                Transition::Stay(Ok(_)) => {},
211                Transition::Stay(Err(e)) => return Err(e),
212                Transition::Done(r) => {
213                    r?;
214                    return self
215                        .last_response
216                        .take()
217                        .ok_or_else(|| anyhow!("no last response in ctx"));
218                },
219            }
220        }
221    }
222}