diff options
Diffstat (limited to 'src/Duet/NetworkTransaction.cpp')
-rw-r--r-- | src/Duet/NetworkTransaction.cpp | 645 |
1 files changed, 645 insertions, 0 deletions
diff --git a/src/Duet/NetworkTransaction.cpp b/src/Duet/NetworkTransaction.cpp new file mode 100644 index 00000000..5c870f8a --- /dev/null +++ b/src/Duet/NetworkTransaction.cpp @@ -0,0 +1,645 @@ +/* + * NetworkTransaction.cpp + * + * Created on: 25 Dec 2016 + * Author: David + */ + +#include "NetworkTransaction.h" + +#include "ConnectionState.h" +#include "Network.h" +#include "OutputMemory.h" +#include "Platform.h" +#include "RepRap.h" +#include "Storage/FileStore.h" + +#include "lwip/src/include/lwip/tcp.h" +#include "lwip/src/include/lwip/tcp_impl.h" + +extern "C" { + +static err_t conn_poll(void *arg, tcp_pcb *pcb) +{ + ConnectionState *cs = (ConnectionState*)arg; + if (cs == sendingConnection) + { + // Data could not be sent last time, check if the connection has to be timed out + sendingRetries++; + if (sendingRetries == TCP_MAX_SEND_RETRIES) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: Could not transmit data after %.1f seconds\n", (float)TCP_WRITE_TIMEOUT / 1000.0); + tcp_abort(pcb); + return ERR_ABRT; + } + + // Try to write the remaining data once again (if required) + if (writeResult != ERR_OK) + { + writeResult = tcp_write(pcb, sendingWindow + (sendingWindowSize - sentDataOutstanding), sentDataOutstanding, 0); + if (ERR_IS_FATAL(writeResult)) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: Failed to write data in conn_poll (code %d)\n", writeResult); + tcp_abort(pcb); + return ERR_ABRT; + } + + if (writeResult != ERR_OK && reprap.Debug(moduleNetwork)) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: tcp_write resulted in error code %d\n", writeResult); + } + } + + // If that worked, try to output the remaining data (if required) + if (outputResult != ERR_OK) + { + outputResult = tcp_output(pcb); + if (ERR_IS_FATAL(outputResult)) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: Failed to output data in conn_poll (code %d)\n", outputResult); + tcp_abort(pcb); + return ERR_ABRT; + } + + if (outputResult != ERR_OK && reprap.Debug(moduleNetwork)) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: tcp_output resulted in error code %d\n", outputResult); + } + } + } + else + { + reprap.GetPlatform()->Message(HOST_MESSAGE, "Network: Mismatched pcb in conn_poll!\n"); + } + return ERR_OK; +} + +static err_t conn_sent(void *arg, tcp_pcb *pcb, u16_t len) +{ + ConnectionState *cs = (ConnectionState*)arg; + if (cs == sendingConnection) + { + if (sentDataOutstanding > len) + { + sentDataOutstanding -= len; + } + else + { + tcp_poll(pcb, nullptr, TCP_WRITE_TIMEOUT / TCP_SLOW_INTERVAL / TCP_MAX_SEND_RETRIES); + sendingConnection = nullptr; + } + } + else + { + reprap.GetPlatform()->Message(HOST_MESSAGE, "Network: Mismatched pcb in conn_sent!\n"); + } + return ERR_OK; +} + + +} + +//*************************************************************************************************** +// NetworkTransaction class + +NetworkTransaction::NetworkTransaction(NetworkTransaction *n) : next(n), status(released) +{ + sendStack = new OutputStack(); +} + +void NetworkTransaction::Set(pbuf *p, ConnectionState *c, TransactionStatus s) +{ + cs = c; + pb = readingPb = p; + status = s; + inputPointer = 0; + sendBuffer = nullptr; + fileBeingSent = nullptr; + closeRequested = false; + nextWrite = nullptr; + dataAcknowledged = false; +} + +// Read one char from the NetworkTransaction +bool NetworkTransaction::Read(char& b) +{ + if (readingPb == nullptr) + { + b = 0; + return false; + } + + b = ((const char*)readingPb->payload)[inputPointer++]; + if (inputPointer == readingPb->len) + { + readingPb = readingPb->next; + inputPointer = 0; + } + return true; +} + +// Read data from the NetworkTransaction and return true on success +bool NetworkTransaction::ReadBuffer(const char *&buffer, size_t &len) +{ + if (readingPb == nullptr) + { + return false; + } + + if (inputPointer >= readingPb->len) + { + readingPb = readingPb->next; + inputPointer = 0; + if (readingPb == nullptr) + { + return false; + } + } + + buffer = (const char*)readingPb->payload + inputPointer; + len = readingPb->len - inputPointer; + readingPb = readingPb->next; + inputPointer = 0; + return true; +} + +void NetworkTransaction::Write(char b) +{ + if (CanWrite()) + { + if (sendBuffer == nullptr && !OutputBuffer::Allocate(sendBuffer)) + { + // Should never get here + return; + } + sendBuffer->cat(b); + } +} + +void NetworkTransaction::Write(const char* s) +{ + if (CanWrite()) + { + if (sendBuffer == nullptr && !OutputBuffer::Allocate(sendBuffer)) + { + // Should never get here + return; + } + sendBuffer->cat(s); + } +} + +void NetworkTransaction::Write(StringRef ref) +{ + Write(ref.Pointer(), ref.strlen()); +} + +void NetworkTransaction::Write(const char* s, size_t len) +{ + if (CanWrite()) + { + if (sendBuffer == nullptr && !OutputBuffer::Allocate(sendBuffer)) + { + // Should never get here + return; + } + sendBuffer->cat(s, len); + } +} + +void NetworkTransaction::Write(OutputBuffer *buffer) +{ + if (CanWrite()) + { + // Note we use an individual stack here, because we don't want to link different + // OutputBuffers for different destinations together... + sendStack->Push(buffer); + } + else + { + // Don't keep buffers we can't send... + OutputBuffer::ReleaseAll(buffer); + } +} + +void NetworkTransaction::Write(OutputStack *stack) +{ + if (stack != nullptr) + { + if (CanWrite()) + { + sendStack->Append(stack); + } + else + { + stack->ReleaseAll(); + } + } +} + +void NetworkTransaction::Printf(const char* fmt, ...) +{ + if (CanWrite() && (sendBuffer != nullptr || OutputBuffer::Allocate(sendBuffer))) + { + va_list p; + va_start(p, fmt); + sendBuffer->vprintf(fmt, p); + va_end(p); + } +} + +void NetworkTransaction::SetFileToWrite(FileStore *file) +{ + if (CanWrite()) + { + fileBeingSent = file; + } + else if (file != nullptr) + { + file->Close(); + } +} + +// Send exactly one TCP window of data and return true when this transaction can be released +bool NetworkTransaction::Send() +{ + // Free up this transaction if the connection is supposed to be closed + if (closeRequested) + { + reprap.GetNetwork()->ConnectionClosed(cs, true); // This will release the transaction too + return false; + } + + // Fill up the TCP window with some data chunks from our OutputBuffer instances + size_t bytesBeingSent = 0, bytesLeftToSend = TCP_WND; + while (sendBuffer != nullptr && bytesLeftToSend > 0) + { + size_t copyLength = min<size_t>(bytesLeftToSend, sendBuffer->BytesLeft()); + memcpy(sendingWindow + bytesBeingSent, sendBuffer->Read(copyLength), copyLength); + bytesBeingSent += copyLength; + bytesLeftToSend -= copyLength; + + if (sendBuffer->BytesLeft() == 0) + { + sendBuffer = OutputBuffer::Release(sendBuffer); + if (sendBuffer == nullptr) + { + sendBuffer = sendStack->Pop(); + } + } + } + + // We also intend to send a file, so check if we can fill up the TCP window + if (sendBuffer == nullptr && bytesLeftToSend != 0 && fileBeingSent != nullptr) + { + // For HSMCI efficiency, read from the file in multiples of 4 bytes except at the end. + // This ensures that the second and subsequent chunks can be DMA'd directly into sendingWindow. + size_t bytesToRead = bytesLeftToSend & (~3); + if (bytesToRead != 0) + { + int bytesRead = fileBeingSent->Read(sendingWindow + bytesBeingSent, bytesToRead); + if (bytesRead > 0) + { + bytesBeingSent += bytesRead; + } + + if (bytesRead != (int)bytesToRead) + { + fileBeingSent->Close(); + fileBeingSent = nullptr; + } + } + } + + if (bytesBeingSent == 0) + { + // If we have no data to send, this connection can be closed next time + if (!cs->persistConnection && nextWrite == nullptr) + { + Close(); + return false; + } + + // We want to send data from another transaction as well, so only free up this one + cs->sendingTransaction = nextWrite; + return true; + } + + // The TCP window has been filled up as much as possible, so send it now. There is no need to check + // the available space in the SNDBUF queue, because we really write only one TCP window at once. + writeResult = tcp_write(cs->pcb, sendingWindow, bytesBeingSent, 0); + if (ERR_IS_FATAL(writeResult)) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: Failed to write data in Send (code %d)\n", writeResult); + tcp_abort(cs->pcb); + return false; + } + + outputResult = tcp_output(cs->pcb); + if (ERR_IS_FATAL(outputResult)) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: Failed to output data in Send (code %d)\n", outputResult); + tcp_abort(cs->pcb); + return false; + } + + if (outputResult != ERR_OK && reprap.Debug(moduleNetwork)) + { + reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: tcp_output resulted in error code %d\n", outputResult); + } + + // Set LwIP callbacks for ACK and retransmission handling + tcp_poll(cs->pcb, conn_poll, TCP_WRITE_TIMEOUT / TCP_SLOW_INTERVAL / TCP_MAX_SEND_RETRIES); + tcp_sent(cs->pcb, conn_sent); + + // Set all values for the send process + sendingConnection = cs; + sendingRetries = 0; + sendingWindowSize = sentDataOutstanding = bytesBeingSent; + return false; +} + +// This is called by the Webserver to send output data to a client. If keepConnectionAlive is set to false, +// the current connection will be terminated once everything has been sent. +void NetworkTransaction::Commit(bool keepConnectionAlive) +{ + // If the connection has been terminated (e.g. RST received while writing upload data), discard this transaction + if (!IsConnected() || status == released) + { + Discard(); + return; + } + + // Free buffer holding the incoming data and prepare some values for the sending process + FreePbuf(); + cs->persistConnection = keepConnectionAlive; + if (sendBuffer == nullptr) + { + sendBuffer = sendStack->Pop(); + } + status = sending; + + // Unlink the item(s) from the list of ready transactions + if (keepConnectionAlive) + { + // Our connection is still of interest, remove only this transaction from the list + NetworkTransaction *previous = nullptr; + for(NetworkTransaction *item = reprap.GetNetwork()->readyTransactions; item != nullptr; item = item->next) + { + if (item == this) + { + if (previous == nullptr) + { + reprap.GetNetwork()->readyTransactions = next; + } + else + { + previous->next = next; + } + break; + } + previous = item; + } + } + else + { + // We will close this connection soon, stop receiving data from this PCB + tcp_recv(cs->pcb, nullptr); + + // Also remove all ready transactions pointing to our ConnectionState + NetworkTransaction *previous = nullptr, *item = reprap.GetNetwork()->readyTransactions; + while (item != nullptr) + { + if (item->cs == cs) + { + if (item == this) + { + // Only unlink this item + if (previous == nullptr) + { + reprap.GetNetwork()->readyTransactions = next; + } + else + { + previous->next = next; + } + item = next; + } + else + { + // Remove all others + item->Discard(); + item = (previous == nullptr) ? reprap.GetNetwork()->readyTransactions : previous->next; + } + } + else + { + previous = item; + item = item->next; + } + } + } + + // Enqueue this transaction, so it's sent in the right order + NetworkTransaction *mySendingTransaction = cs->sendingTransaction; + if (mySendingTransaction == nullptr) + { + cs->sendingTransaction = this; + reprap.GetNetwork()->AppendTransaction(&reprap.GetNetwork()->writingTransactions, this); + } + else + { + while (mySendingTransaction->nextWrite != nullptr) + { + mySendingTransaction = mySendingTransaction->nextWrite; + } + mySendingTransaction->nextWrite = this; + } +} + +// Call this to perform some networking tasks while processing deferred requests, +// and to move this transaction and all transactions that are associated with its +// connection to the end of readyTransactions. There are three ways to do this: +// +// 1) DeferOnly: Do not modify any of the processed data and don't send an ACK. +// This will ensure that zero-window packets are sent back to the client +// 2) ResetData: Reset the read pointers and acknowledge that the data has been processed +// 3) DiscardData: Free the processed data, acknowledge it and append this transaction as +// an empty item again without payload (i.e. without pbufs) +// +void NetworkTransaction::Defer(DeferralMode mode) +{ + if (mode == DeferralMode::ResetData) + { + // Reset the reading pointers and send an ACK + inputPointer = 0; + readingPb = pb; + if (IsConnected() && pb != nullptr && !dataAcknowledged) + { + tcp_recved(cs->pcb, pb->tot_len); + dataAcknowledged = true; + } + } + else if (mode == DeferralMode::DiscardData) + { + // Discard the incoming data, because we don't need to process it any more + FreePbuf(); + } + + status = deferred; + + // Unlink this transaction from the list of ready transactions and append it again + Network *network = reprap.GetNetwork(); + NetworkTransaction *item, *previous = nullptr; + for(item = network->readyTransactions; item != nullptr; item = item->next) + { + if (item == this) + { + if (previous == nullptr) + { + network->readyTransactions = next; + } + else + { + previous->next = next; + } + break; + } + previous = item; + } + network->AppendTransaction(&network->readyTransactions, this); + + // Append all other transactions that are associated to this connection, so that the + // Webserver gets a chance to deal with all connected clients even while multiple + // deferred requests are present in the list. + item = network->readyTransactions; + previous = nullptr; + while (item != this) + { + if (item->cs == cs) + { + NetworkTransaction *nextItem = item->next; + if (previous == nullptr) + { + network->readyTransactions = item->next; + network->AppendTransaction(&network->readyTransactions, item); + } + else + { + previous->next = item->next; + network->AppendTransaction(&network->readyTransactions, item); + } + item = nextItem; + } + else + { + previous = item; + item = item->next; + } + } +} + + +// This method should be called if we don't want to send data to the client and if we +// don't want to interfere with the connection state. May also be called from ISR! +void NetworkTransaction::Discard() +{ + // Can we do anything? + if (status == released) + { + // No - don't free up released items multiple times + return; + } + + // Free up some resources + FreePbuf(); + + if (fileBeingSent != nullptr) + { + fileBeingSent->Close(); + fileBeingSent = nullptr; + } + + OutputBuffer::ReleaseAll(sendBuffer); + sendStack->ReleaseAll(); + + // Unlink this transactions from the list of ready transactions and free it. It is then appended to the list of + // free transactions because we don't want to risk reusing it when the ethernet ISR processes incoming data + NetworkTransaction *previous = nullptr; + for(NetworkTransaction *item = reprap.GetNetwork()->readyTransactions; item != nullptr; item = item->next) + { + if (item == this) + { + if (previous == nullptr) + { + reprap.GetNetwork()->readyTransactions = next; + } + else + { + previous->next = next; + } + break; + } + previous = item; + } + reprap.GetNetwork()->AppendTransaction(&reprap.GetNetwork()->freeTransactions, this); + bool callDisconnectHandler = (cs != nullptr && status == disconnected); + status = released; + + // Call disconnect event if this transaction indicates a graceful disconnect and if the connection + // still persists (may not be the case if a RST packet was received before) + if (callDisconnectHandler) + { + if (reprap.Debug(moduleNetwork)) + { + reprap.GetPlatform()->Message(HOST_MESSAGE, "Network: Discard() is handling a graceful disconnect\n"); + } + reprap.GetNetwork()->ConnectionClosed(cs, false); + } +} + +uint32_t NetworkTransaction::GetRemoteIP() const +{ + return (cs != nullptr) ? cs->GetRemoteIP() : 0; +} + +uint16_t NetworkTransaction::GetRemotePort() const +{ + return (cs != nullptr) ? cs->GetRemotePort() : 0; +} + +uint16_t NetworkTransaction::GetLocalPort() const +{ + return (cs != nullptr) ? cs->GetLocalPort() : 0; +} + +void NetworkTransaction::Close() +{ + tcp_pcb *pcb = cs->pcb; + tcp_recv(pcb, nullptr); + closeRequested = true; +} + +void NetworkTransaction::FreePbuf() +{ + // See if we have to send an ACK to the client + if (IsConnected() && pb != nullptr && !dataAcknowledged) + { + tcp_recved(cs->pcb, pb->tot_len); + dataAcknowledged = true; + } + + // Free all pbufs (pbufs are thread-safe) + if (pb != nullptr) + { + pbuf_free(pb); + pb = readingPb = nullptr; + } +} + +bool NetworkTransaction::IsConnected() const +{ + return (cs != nullptr && cs->IsConnected()); +} + +// End |