diff --git a/CMakeLists.txt b/CMakeLists.txt index e2b305e..7b852e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,7 +149,8 @@ set(SRC_FILES device/devicefinder.cpp device/device.cpp disk/diskreaddriver.cpp - disk/nulldiskreaddriver.cpp + disk/diskwritedriver.cpp + disk/nulldiskdriver.cpp disk/neomemorydiskreaddriver.cpp ${PLATFORM_SRC} ) diff --git a/api/icsneocpp/event.cpp b/api/icsneocpp/event.cpp index 49aca7b..4f7d961 100644 --- a/api/icsneocpp/event.cpp +++ b/api/icsneocpp/event.cpp @@ -107,6 +107,8 @@ static constexpr const char* ETH_PHY_REGISTER_CONTROL_NOT_AVAILABLE = "Ethernet static constexpr const char* DISK_NOT_SUPPORTED = "This device does not support accessing the specified disk."; static constexpr const char* EOF_REACHED = "The requested length exceeds the available data from this disk."; static constexpr const char* SETTINGS_DEFAULTS_USED = "The device settings could not be loaded, the default settings have been applied."; +static constexpr const char* ATOMIC_OPERATION_RETRIED = "An operation failed to be atomically completed, but will be retried."; +static constexpr const char* ATOMIC_OPERATION_COMPLETED_NONATOMICALLY = "An ideally-atomic operation was completed nonatomically."; // Transport Errors static constexpr const char* FAILED_TO_READ = "A read operation failed."; @@ -226,6 +228,10 @@ const char* APIEvent::DescriptionForType(Type type) { return EOF_REACHED; case Type::SettingsDefaultsUsed: return SETTINGS_DEFAULTS_USED; + case Type::AtomicOperationRetried: + return ATOMIC_OPERATION_RETRIED; + case Type::AtomicOperationCompletedNonatomically: + return ATOMIC_OPERATION_COMPLETED_NONATOMICALLY; // Transport Errors case Type::FailedToRead: diff --git a/device/device.cpp b/device/device.cpp index 2f0ae94..ecde903 100644 --- a/device/device.cpp +++ b/device/device.cpp @@ -488,6 +488,20 @@ optional Device::readLogicalDisk(uint64_t pos, uint8_t* into, uint64_t return diskReadDriver->readLogicalDisk(*com, report, pos, into, amount, timeout); } +optional Device::writeLogicalDisk(uint64_t pos, const uint8_t* from, uint64_t amount, std::chrono::milliseconds timeout) { + if(!from || timeout <= std::chrono::milliseconds(0)) { + report(APIEvent::Type::RequiredParameterNull, APIEvent::Severity::Error); + return nullopt; + } + + if(!isOpen()) { + report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); + return nullopt; + } + + return diskWriteDriver->writeLogicalDisk(*com, report, *diskReadDriver, pos, from, amount, timeout); +} + optional Device::getDigitalIO(IO type, size_t number /* = 1 */) { if(number == 0) { // Start counting from 1 report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); diff --git a/disk/diskreaddriver.cpp b/disk/diskreaddriver.cpp index 5948b5d..10efa00 100644 --- a/disk/diskreaddriver.cpp +++ b/disk/diskreaddriver.cpp @@ -2,8 +2,9 @@ #include using namespace icsneo; +using namespace icsneo::Disk; -optional DiskReadDriver::readLogicalDisk(Communication& com, device_eventhandler_t report, +optional ReadDriver::readLogicalDisk(Communication& com, device_eventhandler_t report, uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout) { optional ret; diff --git a/disk/diskwritedriver.cpp b/disk/diskwritedriver.cpp new file mode 100644 index 0000000..1cfc182 --- /dev/null +++ b/disk/diskwritedriver.cpp @@ -0,0 +1,88 @@ +#include "icsneo/disk/diskwritedriver.h" +#include + +using namespace icsneo; +using namespace icsneo::Disk; + +const uint64_t WriteDriver::RetryAtomic = std::numeric_limits::max(); +const APIEvent::Severity WriteDriver::NonatomicSeverity = APIEvent::Severity::EventInfo; + +optional WriteDriver::writeLogicalDisk(Communication& com, device_eventhandler_t report, ReadDriver& readDriver, + uint64_t pos, const uint8_t* from, uint64_t amount, std::chrono::milliseconds timeout) { + optional ret; + + const uint32_t idealBlockSize = getBlockSizeBounds().second; + + // Write from here if we need to read-modify-write a block + // That would be the case either if we don't want some at the + // beginning or end of the block. + std::vector alignedWriteBuffer; + + // Read to here, ideally this can be sent back to the device to + // ensure an operation is atomic + std::vector atomicBuffer(idealBlockSize); + + const uint64_t startBlock = pos / idealBlockSize; + const uint32_t posWithinFirstBlock = static_cast(pos % idealBlockSize); + uint64_t blocks = amount / idealBlockSize + (amount % idealBlockSize ? 1 : 0); + if(blocks * idealBlockSize - posWithinFirstBlock < amount) + blocks++; // We need one more block to get the last partial block's worth + uint64_t blocksProcessed = 0; + + while(blocksProcessed < blocks && timeout >= std::chrono::milliseconds::zero()) { + const uint64_t currentBlock = startBlock + blocksProcessed; + + const uint64_t fromOffset = std::max((blocksProcessed * idealBlockSize) - posWithinFirstBlock, 0); + const uint32_t posWithinCurrentBlock = (blocksProcessed ? 0 : posWithinFirstBlock); + uint32_t curAmt = idealBlockSize - posWithinCurrentBlock; + const auto amountLeft = amount - ret.value_or(0); + if(curAmt > amountLeft) + curAmt = static_cast(amountLeft); + + auto start = std::chrono::high_resolution_clock::now(); + auto amount = readDriver.readLogicalDisk(com, report, currentBlock * idealBlockSize, atomicBuffer.data(), + idealBlockSize, timeout); + timeout -= std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start); + + if(amount != idealBlockSize) { + if(timeout < std::chrono::milliseconds::zero()) + report(APIEvent::Type::Timeout, APIEvent::Severity::Error); + else + report(blocksProcessed ? APIEvent::Type::EOFReached : APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); + break; + } + + const bool useAlignedWriteBuffer = (posWithinCurrentBlock != 0 || curAmt != idealBlockSize); + if(useAlignedWriteBuffer) { + if(alignedWriteBuffer.size() < idealBlockSize) + alignedWriteBuffer.resize(idealBlockSize); + memcpy(alignedWriteBuffer.data() + posWithinCurrentBlock, from + fromOffset, curAmt); + } + + start = std::chrono::high_resolution_clock::now(); + amount = writeLogicalDiskAligned(com, report, currentBlock * idealBlockSize, atomicBuffer.data(), + useAlignedWriteBuffer ? alignedWriteBuffer.data() : (from + fromOffset), idealBlockSize, timeout); + timeout -= std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start); + + if(amount == RetryAtomic) { + // The user may want to log these events in order to see how many atomic misses they are getting + report(APIEvent::Type::AtomicOperationRetried, APIEvent::Severity::EventInfo); + continue; + } + + if(!amount.has_value() || *amount == 0) { + if(timeout < std::chrono::milliseconds::zero()) + report(APIEvent::Type::Timeout, APIEvent::Severity::Error); + else + report(blocksProcessed ? APIEvent::Type::EOFReached : APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); + break; + } + + if(!ret) + ret.emplace(); + *ret += std::min(*amount, curAmt); + blocksProcessed++; + } + + return ret; +} \ No newline at end of file diff --git a/disk/neomemorydiskreaddriver.cpp b/disk/neomemorydiskreaddriver.cpp index ab41e43..22bb748 100644 --- a/disk/neomemorydiskreaddriver.cpp +++ b/disk/neomemorydiskreaddriver.cpp @@ -3,6 +3,7 @@ #include using namespace icsneo; +using namespace icsneo::Disk; optional NeoMemoryDiskReadDriver::readLogicalDiskAligned(Communication& com, device_eventhandler_t report, uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout) { diff --git a/disk/nulldiskdriver.cpp b/disk/nulldiskdriver.cpp new file mode 100644 index 0000000..bb44845 --- /dev/null +++ b/disk/nulldiskdriver.cpp @@ -0,0 +1,28 @@ +#include "icsneo/disk/nulldiskdriver.h" + +using namespace icsneo; +using namespace icsneo::Disk; + +optional NullDriver::readLogicalDisk(Communication&, device_eventhandler_t report, + uint64_t, uint8_t*, uint64_t, std::chrono::milliseconds) { + report(APIEvent::Type::DiskNotSupported, APIEvent::Severity::Error); + return nullopt; +} + +optional NullDriver::readLogicalDiskAligned(Communication&, device_eventhandler_t report, + uint64_t, uint8_t*, uint64_t, std::chrono::milliseconds) { + report(APIEvent::Type::DiskNotSupported, APIEvent::Severity::Error); + return nullopt; +} + +optional NullDriver::writeLogicalDisk(Communication&, device_eventhandler_t report, ReadDriver&, + uint64_t, const uint8_t*, uint64_t, std::chrono::milliseconds) { + report(APIEvent::Type::DiskNotSupported, APIEvent::Severity::Error); + return nullopt; +} + +optional NullDriver::writeLogicalDiskAligned(Communication&, device_eventhandler_t report, + uint64_t, const uint8_t*, const uint8_t*, uint64_t, std::chrono::milliseconds) { + report(APIEvent::Type::DiskNotSupported, APIEvent::Severity::Error); + return nullopt; +} \ No newline at end of file diff --git a/disk/nulldiskreaddriver.cpp b/disk/nulldiskreaddriver.cpp deleted file mode 100644 index 682d9c9..0000000 --- a/disk/nulldiskreaddriver.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "icsneo/disk/nulldiskreaddriver.h" - -using namespace icsneo; - -optional NullDiskReadDriver::readLogicalDisk(Communication&, device_eventhandler_t report, - uint64_t, uint8_t*, uint64_t, std::chrono::milliseconds) { - report(APIEvent::Type::DiskNotSupported, APIEvent::Severity::Error); - return nullopt; -} - -optional NullDiskReadDriver::readLogicalDiskAligned(Communication&, device_eventhandler_t report, - uint64_t, uint8_t*, uint64_t, std::chrono::milliseconds) { - report(APIEvent::Type::DiskNotSupported, APIEvent::Severity::Error); - return nullopt; -} \ No newline at end of file diff --git a/include/icsneo/api/event.h b/include/icsneo/api/event.h index 80901fb..9730b9e 100644 --- a/include/icsneo/api/event.h +++ b/include/icsneo/api/event.h @@ -84,6 +84,8 @@ public: DiskNotSupported = 0x2031, EOFReached = 0x2032, SettingsDefaultsUsed = 0x2033, + AtomicOperationRetried = 0x2034, + AtomicOperationCompletedNonatomically = 0x2035, // Transport Events FailedToRead = 0x3000, diff --git a/include/icsneo/device/device.h b/include/icsneo/device/device.h index 4fbc2f6..8952ddb 100644 --- a/include/icsneo/device/device.h +++ b/include/icsneo/device/device.h @@ -17,7 +17,8 @@ #include "icsneo/device/devicetype.h" #include "icsneo/device/deviceversion.h" #include "icsneo/disk/diskreaddriver.h" -#include "icsneo/disk/nulldiskreaddriver.h" +#include "icsneo/disk/diskwritedriver.h" +#include "icsneo/disk/nulldiskdriver.h" #include "icsneo/communication/communication.h" #include "icsneo/communication/packetizer.h" #include "icsneo/communication/encoder.h" @@ -156,7 +157,24 @@ public: * set in icsneo::GetLastError(). */ optional readLogicalDisk(uint64_t pos, uint8_t* into, uint64_t amount, - std::chrono::milliseconds timeout = DiskReadDriver::DefaultTimeout); + std::chrono::milliseconds timeout = Disk::DefaultTimeout); + + /** + * Write to the logical disk in this device, starting from byte `pos` + * and writing up to `amount` bytes. + * + * The number of bytes written will be returned in case of success. + * + * If the number of bytes written is less than the amount requested, + * an error will be set in icsneo::GetLastError() explaining why. + * Likely, either the end of the logical disk has been reached, or + * the timeout was reached while the write had only partially completed. + * + * Upon failure, icsneo::nullopt will be returned and an error will be + * set in icsneo::GetLastError(). + */ + optional writeLogicalDisk(uint64_t pos, const uint8_t* from, uint64_t amount, + std::chrono::milliseconds timeout = Disk::DefaultTimeout); /** * Retrieve the number of Ethernet (DoIP) Activation lines present @@ -276,7 +294,7 @@ protected: data.device = this; } - template + template void initialize() { report = makeEventHandler(); auto driver = makeDriver(); @@ -289,7 +307,8 @@ protected: setupCommunication(*com); settings = makeSettings(com); setupSettings(*settings); - diskReadDriver = std::make_unique(); + diskReadDriver = std::unique_ptr(new DiskRead()); + diskWriteDriver = std::unique_ptr(new DiskWrite()); setupSupportedRXNetworks(supportedRXNetworks); setupSupportedTXNetworks(supportedTXNetworks); setupExtensions(); @@ -365,7 +384,8 @@ private: neodevice_t data; std::shared_ptr latestResetStatus; std::vector> versions; - std::unique_ptr diskReadDriver; + std::unique_ptr diskReadDriver; + std::unique_ptr diskWriteDriver; mutable std::mutex extensionsLock; std::vector> extensions; diff --git a/include/icsneo/disk/diskdriver.h b/include/icsneo/disk/diskdriver.h new file mode 100644 index 0000000..309e6e8 --- /dev/null +++ b/include/icsneo/disk/diskdriver.h @@ -0,0 +1,38 @@ +#ifndef __DISKDRIVER_H__ +#define __DISKDRIVER_H__ + +#ifdef __cplusplus + +#include +#include +#include + +namespace icsneo { + +namespace Disk { + +constexpr const std::chrono::milliseconds DefaultTimeout{2000}; +constexpr const size_t SectorSize = 512; +enum class Access { + None, + EntireCard, + VSA +}; + +/** + * Interface for drivers which work with block data on devices + */ +class Driver { +public: + virtual ~Driver() = default; + virtual Access getAccess() const = 0; + virtual std::pair getBlockSizeBounds() const = 0; +}; + +} // namespace Disk + +} // namespace icsneo + +#endif // __cplusplus + +#endif // __DISKDRIVER_H__ \ No newline at end of file diff --git a/include/icsneo/disk/diskreaddriver.h b/include/icsneo/disk/diskreaddriver.h index 0db498b..5b52a9b 100644 --- a/include/icsneo/disk/diskreaddriver.h +++ b/include/icsneo/disk/diskreaddriver.h @@ -6,29 +6,21 @@ #include "icsneo/platform/optional.h" #include "icsneo/communication/communication.h" #include "icsneo/api/eventmanager.h" +#include "icsneo/disk/diskdriver.h" #include #include namespace icsneo { +namespace Disk { + /** * Interface for drivers which read block data from devices */ -class DiskReadDriver { +class ReadDriver : public virtual Driver { public: - static constexpr const std::chrono::milliseconds DefaultTimeout{2000}; - static constexpr const size_t SectorSize = 512; - enum class Access { - None, - EntireCard, - VSA - }; - - virtual ~DiskReadDriver() = default; virtual optional readLogicalDisk(Communication& com, device_eventhandler_t report, uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout = DefaultTimeout); - virtual Access getAccess() const = 0; - virtual std::pair getBlockSizeBounds() const = 0; protected: /** @@ -41,7 +33,9 @@ protected: uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout) = 0; }; -} +} // namespace Disk + +} // namespace icsneo #endif // __cplusplus #endif // __DISKREADDRIVER_H__ \ No newline at end of file diff --git a/include/icsneo/disk/diskwritedriver.h b/include/icsneo/disk/diskwritedriver.h new file mode 100644 index 0000000..2c5f112 --- /dev/null +++ b/include/icsneo/disk/diskwritedriver.h @@ -0,0 +1,65 @@ +#ifndef __DISKWRITEDRIVER_H__ +#define __DISKWRITEDRIVER_H__ + +#ifdef __cplusplus + +#include "icsneo/platform/optional.h" +#include "icsneo/communication/communication.h" +#include "icsneo/api/eventmanager.h" +#include "icsneo/disk/diskreaddriver.h" +#include +#include + +namespace icsneo { + +namespace Disk { + +/** + * Interface for drivers which write block data from devices + */ +class WriteDriver : public virtual Driver { +public: + virtual optional writeLogicalDisk(Communication& com, device_eventhandler_t report, ReadDriver& readDriver, + uint64_t pos, const uint8_t* from, uint64_t amount, std::chrono::milliseconds timeout = DefaultTimeout); + +protected: + /** + * Flag returned from writeLogicalDiskAligned when the + * operation failed to be performed atomically and can + * be retried after rereading. + */ + static const uint64_t RetryAtomic; + + /** + * The severity to report with when an atomic operation + * is requested that the driver is unable to attempt. + */ + static const APIEvent::Severity NonatomicSeverity; + + /** + * Perform a write which the driver can do in one shot. + * + * The `pos` requested must be sector-aligned, and the `amount` must be + * within the block size bounds provided by the driver. + * + * If `atomicBuf` is provided, it will be used to ensure that the disk + * data changes from `atomicBuf` to `from` without trampling any reads + * that may have happened while modifying the data. + * + * The flag `RetryAtomic` is returned if the operation was attempted + * atomically but failed. + * + * If the driver does not support atomic operations, but `atomicBuf` + * is non-null, an APIEvent::AtomicOperationCompletedNonatomically + * should be reported with `NonatomicSeverity`. + */ + virtual optional writeLogicalDiskAligned(Communication& com, device_eventhandler_t report, + uint64_t pos, const uint8_t* atomicBuf, const uint8_t* from, uint64_t amount, std::chrono::milliseconds timeout) = 0; +}; + +} // namespace Disk + +} // namespace icsneo + +#endif // __cplusplus +#endif // __DISKWRITEDRIVER_H__ \ No newline at end of file diff --git a/include/icsneo/disk/neomemorydiskreaddriver.h b/include/icsneo/disk/neomemorydiskreaddriver.h index 820c295..208b69d 100644 --- a/include/icsneo/disk/neomemorydiskreaddriver.h +++ b/include/icsneo/disk/neomemorydiskreaddriver.h @@ -8,12 +8,14 @@ namespace icsneo { +namespace Disk { + /** * A disk read driver which uses the neoMemory command to read from the disk * * This can only request reads by sector, so it will be very slow, but is likely supported by any device with a disk */ -class NeoMemoryDiskReadDriver : public DiskReadDriver { +class NeoMemoryDiskReadDriver : public ReadDriver { public: Access getAccess() const override { return Access::VSA; } std::pair getBlockSizeBounds() const override { @@ -29,7 +31,9 @@ private: uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout) override; }; -} +} // namespace Disk + +} // namespace icsneo #endif // __cplusplus #endif // __NEOMEMORYDISKREADDRIVER_H__ \ No newline at end of file diff --git a/include/icsneo/disk/nulldiskreaddriver.h b/include/icsneo/disk/nulldiskdriver.h similarity index 64% rename from include/icsneo/disk/nulldiskreaddriver.h rename to include/icsneo/disk/nulldiskdriver.h index 2edc6e6..dd97c9a 100644 --- a/include/icsneo/disk/nulldiskreaddriver.h +++ b/include/icsneo/disk/nulldiskdriver.h @@ -4,19 +4,24 @@ #ifdef __cplusplus #include "icsneo/disk/diskreaddriver.h" +#include "icsneo/disk/diskwritedriver.h" #include namespace icsneo { +namespace Disk { + /** * A disk driver which always returns the requested disk as unsupported * * Used for devices which do not have a disk, or do not provide any means for accessing it */ -class NullDiskReadDriver : public DiskReadDriver { +class NullDriver : public ReadDriver, public WriteDriver { public: optional readLogicalDisk(Communication& com, device_eventhandler_t report, uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout = DefaultTimeout) override; + optional writeLogicalDisk(Communication& com, device_eventhandler_t report, ReadDriver& readDriver, + uint64_t pos, const uint8_t* from, uint64_t amount, std::chrono::milliseconds timeout = DefaultTimeout) override; Access getAccess() const override { return Access::None; } std::pair getBlockSizeBounds() const override { static_assert(SectorSize <= std::numeric_limits::max(), "Incorrect sector size"); @@ -27,9 +32,13 @@ public: private: optional readLogicalDiskAligned(Communication& com, device_eventhandler_t report, uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout) override; + optional writeLogicalDiskAligned(Communication& com, device_eventhandler_t report, + uint64_t pos, const uint8_t* atomicBuf, const uint8_t* from, uint64_t amount, std::chrono::milliseconds timeout) override; }; -} +} // namespace Disk + +} // namespace icsneo #endif // __cplusplus #endif // __NULLDISKREADDRIVER_H__ \ No newline at end of file