Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Svc/Ccsds/CfdpManager/Events.fppi
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,17 @@ event TxFileOpenFailed(
severity warning low \
format "TX class {} transaction {}:{} failed to open file {}, error={}"

event TxFileShortRead(
cfdpClass: Cfdp.Class @< CFDP class
srcEid: U32 @< Source entity ID
seqNum: U32 @< Transaction sequence number
filename: string size MaxFilePathSize @< Source file being sent
offset: U32 @< File offset where the read returned no data
fileSize: U32 @< File size when the transfer started
) \
severity warning low \
format "TX class {} transaction {}:{} source file {} ended at offset {}, before its starting size {}; transfer failed"

event TxFileSeekFailed(
cfdpClass: Cfdp.Class @< CFDP class
srcEid: U32 @< Source entity ID
Expand Down
8 changes: 8 additions & 0 deletions Svc/Ccsds/CfdpManager/TransactionTx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,14 @@ Status::T Transaction::sSendFileData(FileSize foffs, FileSize bytes_to_read, U8
Os::File::Status fileStatus = this->m_fd.read(fileDataBuffer, actual_bytes, Os::File::WaitType::WAIT);
if (fileStatus != Os::File::OP_OK) {
status = Cfdp::Status::ERROR;
} else if ((actual_bytes == 0) && (max_data_bytes > 0)) {
// The read succeeded but returned nothing before the size captured when the transfer
// started: the source file was truncated while it was being sent. Retrying would read
// 0 bytes forever and never advance, so fail instead of sending an empty File Data PDU.
this->m_cfdpManager->log_WARNING_LO_TxFileShortRead(
this->getClass(), this->m_history->src_eid, this->m_history->seq_num,
this->m_history->fnames.src_filename, foffs, this->m_fsize);
status = Cfdp::Status::ERROR;
}
}

Expand Down
1 change: 1 addition & 0 deletions Svc/Ccsds/CfdpManager/docs/sdd.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ The CFDP Manager provides comprehensive event reporting covering all aspects of
| TxZeroLengthFile | warning low | TX transaction cannot transfer zero-length file |
| TxFileOpenFailed | warning low | TX transaction failed to open file |
| TxFileSeekFailed | warning low | TX transaction failed to seek to beginning of file |
| TxFileShortRead | warning low | TX source file ended before its starting size (truncated while being sent); transfer failed |
| TxSendMetadataFailed | warning low | TX transaction failed to send metadata PDU |
| TxEarlyFinReceived | warning low | TX transaction received early FIN, cancelling transfer |
| TxInvalidNakPdu | warning low | TX transaction received invalid NAK PDU |
Expand Down
10 changes: 10 additions & 0 deletions Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTestMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ TEST(Transaction, Class2RxFileDataOffsetOverflow) {
tester.testClass2RxFileDataOffsetOverflow();
}

TEST(Transaction, Class1TxSourceTruncated) {
Svc::Ccsds::Cfdp::CfdpManagerTester tester;
tester.testClass1TxSourceTruncated();
}

TEST(Transaction, Class2TxSourceTruncated) {
Svc::Ccsds::Cfdp::CfdpManagerTester tester;
tester.testClass2TxSourceTruncated();
}

TEST(Transaction, Class2RxTruncatedFileDataCrcSpin) {
Svc::Ccsds::Cfdp::CfdpManagerTester tester;
tester.testClass2RxTruncatedFileDataCrcSpin();
Expand Down
66 changes: 66 additions & 0 deletions Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,72 @@ void CfdpManagerTester::testClass2RxFileDataOffsetOverflow() {
cleanupTestFile(txn->m_history->fnames.dst_filename.toChar());
}

void CfdpManagerTester::runTxSourceTruncatedCase(Cfdp::Class::T cfdpClass, const char* srcFile) {
const U8 channelId = TEST_CHANNEL_ID_0;
// Large enough that the downlink spans several cycles at the default throttle
const FwSizeType fileSize = 200000;

FwSizeType actualSize;
createAndVerifyTestFile(srcFile, fileSize, actualSize);
TransactionSetup setup;
const TxnState expectedState =
(cfdpClass == Cfdp::Class::CLASS_1) ? TxnState::TXN_STATE_S1 : TxnState::TXN_STATE_S2;
setupTxTransaction(srcFile, "/dest/truncated.bin", channelId, TEST_GROUND_EID, cfdpClass, TEST_PRIORITY,
expectedState, setup);

// First cycle opens the source and sends part of it
this->invoke_to_run1Hz(0, 0);
this->component.doDispatch();
ASSERT_GT(setup.txn->m_foffs, 0U);
ASSERT_LT(setup.txn->m_foffs, setup.txn->m_fsize) << "Downlink must still be in progress";
const FileSize truncatedAt = setup.txn->m_foffs;

// Something onboard truncates the source while it is being sent; the transaction keeps its
// open handle, so later reads at the current offset return 0 bytes
Os::File truncator;
ASSERT_EQ(Os::File::OP_OK, truncator.open(srcFile, Os::File::OPEN_CREATE, Os::File::OVERWRITE));
truncator.close();

this->clearHistory();
const FwSizeType pdusBefore = this->fromPortHistory_dataOut->size();
for (U32 cycle = 0; cycle < 10; cycle++) {
this->invoke_to_run1Hz(0, 0);
this->component.doDispatch();
}

ASSERT_EVENTS_TxFileShortRead_SIZE(1);
ASSERT_EVENTS_TxFileShortRead(0, cfdpClass, component.getLocalEidParam(), setup.expectedSeqNum, srcFile,
truncatedAt, static_cast<FileSize>(fileSize));
ASSERT_EVENTS_TxFileTransferCompleted_SIZE(0);
if (cfdpClass == Cfdp::Class::CLASS_1) {
// Class 1 finishes right after its EOF, and the transaction may already be recycled, so
// check the failure event rather than transaction state
ASSERT_EVENTS_TxFileTransferFailed_SIZE(1);
ASSERT_EVENTS_TxFileTransferFailed(0, cfdpClass, setup.expectedSeqNum, component.getLocalEidParam(), srcFile,
TEST_GROUND_EID, "/dest/truncated.bin",
static_cast<U8>(TxnStatus::TXN_STATUS_FILESTORE_REJECTION));
} else {
// Class 2 has sent its EOF with the error and is waiting for the EOF-ACK
ASSERT_NE(nullptr, setup.txn->m_history);
EXPECT_EQ(TxnStatus::TXN_STATUS_FILESTORE_REJECTION, setup.txn->m_history->txn_stat);
EXPECT_GE(setup.txn->m_state_data.send.sub_state, TxSubState::TX_SUB_STATE_EOF)
<< "Downlink must stop sending file data and move to EOF";
}
// Without the check, every cycle sends max_outgoing_pdus_per_cycle empty File Data PDUs
EXPECT_LE(this->fromPortHistory_dataOut->size() - pdusBefore, 4U)
<< "Only the EOF (and its retries) should be sent after the source was truncated";

cleanupTestFile(srcFile);
}

void CfdpManagerTester::testClass1TxSourceTruncated() {
runTxSourceTruncatedCase(Cfdp::Class::CLASS_1, "test/ut/output/tx_truncated_c1.bin");
}

void CfdpManagerTester::testClass2TxSourceTruncated() {
runTxSourceTruncatedCase(Cfdp::Class::CLASS_2, "test/ut/output/tx_truncated_c2.bin");
}

void CfdpManagerTester::testClass2RxTruncatedFileDataCrcSpin() {
const char* groundSrcFile = "/ground/test_class2_rx_crcspin_source.bin";
const char* dstFile = "test/ut/output/test_class2_rx_crcspin_received.bin";
Expand Down
5 changes: 5 additions & 0 deletions Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,11 @@ class CfdpManagerTester final : public CfdpManagerGTestBase {
//! Test Class 2 RX rejection of a FileData PDU whose offset and length overflow the offset space
void testClass2RxFileDataOffsetOverflow();

//! Test a downlink whose source file is truncated mid-transfer fails instead of stalling
void testClass1TxSourceTruncated();
void testClass2TxSourceTruncated();
void runTxSourceTruncatedCase(Cfdp::Class::T cfdpClass, const char* srcFile);

//! Test Class 2 RX handling of a truncated FileData PDU followed by a scheduler tick
void testClass2RxTruncatedFileDataCrcSpin();

Expand Down
Loading