Files
ppsspp/Common/Net/HTTPNaettRequest.cpp
T
Henrik Rydgård 1041c976c6 http: Say what the sink's ownership actually is
unique_ptr, not shared_ptr. Nothing ever shares it: naett holds a raw pointer
and takes no part in the counting, and the abandonment path hands the sink over
and lets go in the same breath. What's really going on is a single owner that
moves, which is what unique_ptr says and shared_ptr left you to work out from
reading all the uses.

naett never frees the pointer it's given for the writer - it only reads
bodyWriterData and hands it back to the callback - so deleting the sink is
ours to do. The things naett does allocate, the request and response, stay raw
pointers with explicit naettFree/naettClose.

The length field was just buffer.size() written down twice.
2026-09-05 13:57:31 -06:00

222 lines
7.4 KiB
C++

#ifndef HTTPS_NOT_AVAILABLE
#include <atomic>
#include <cstring>
#include <memory>
#include <vector>
#include "Common/Net/HTTPRequest.h"
#include "Common/Net/HTTPNaettRequest.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/StringUtils.h"
#include "Common/Log.h"
#include "ext/naett-lib/naett.h"
namespace http {
HTTPSRequest::HTTPSRequest(RequestMethod method, std::string_view url, std::string_view postData, std::string_view postMime, const Path &outfile, RequestFlags flags, std::string_view name)
: Request(method, url, name, outfile, &cancelled_, flags), postData_(postData), postMime_(postMime) {
}
HTTPSRequest::~HTTPSRequest() {
HTTPSRequest::Join();
}
// The response body, and the flag that stops it arriving. naett hands us chunks on its own
// transfer thread, and there's no way to make it stop and be sure it has: naettClose only really
// cancels on Android, and waiting for the others would mean blocking shutdown. So the buffer it
// writes into is owned separately from the request, and that ownership can move - if we go away
// first, the sink is handed off rather than destroyed, and a late chunk lands somewhere that
// still exists instead of in a destroyed object.
struct NaettBodySink {
Buffer buffer;
// Written by us, read by the transfer thread.
std::atomic<bool> cancelled{false};
};
// Sinks belonging to requests that hadn't finished when they were torn down, which only happens
// at shutdown - RequestManager cancels from its destructor. The naett objects can't be freed
// while a callback might still be in flight, and neither can these, so both are deliberately
// leaked. Allocated with new and never deleted, so it can't be destroyed out from under a late
// callback during static destruction either.
static std::vector<std::unique_ptr<NaettBodySink>> *g_abandonedSinks = new std::vector<std::unique_ptr<NaettBodySink>>();
int HTTPSRequest::WriteBodyThunk(const void *source, int bytes, void *userData) {
NaettBodySink *sink = (NaettBodySink *)userData;
if (sink->cancelled) {
// Taking less than we were given fails the request, which is how naett lets us stop a
// transfer. Without this, cancelling only relabelled the result once it finished anyway.
return 0;
}
if (bytes <= 0) {
return 0;
}
char *dest = sink->buffer.Append((size_t)bytes);
memcpy(dest, source, bytes);
return bytes;
}
void HTTPSRequest::Cancel() {
Request::Cancel();
if (sink_) {
sink_->cancelled = true;
}
}
void HTTPSRequest::Start() {
_dbg_assert_(!req_);
_dbg_assert_(!res_);
std::vector<naettOption *> options;
options.push_back(naettMethod(method_ == RequestMethod::GET ? "GET" : "POST"));
options.push_back(naettHeader("Accept", acceptMime_));
options.push_back(naettUserAgent(userAgent_.c_str()));
if (!postMime_.empty()) {
options.push_back(naettHeader("Content-Type", postMime_.c_str()));
}
if (method_ == RequestMethod::POST) {
if (!postData_.empty()) {
// Note: Naett does not take ownership over the body.
options.push_back(naettBody(postData_.data(), (int)postData_.size()));
}
} else {
_dbg_assert_(postData_.empty());
}
// 30 s timeout - not sure what's reasonable?
options.push_back(naettTimeout(30 * 1000)); // milliseconds
// Our own writer, so that Cancel() can actually stop a transfer rather than just relabelling
// it once it finishes.
sink_ = std::make_unique<NaettBodySink>();
// In case someone managed to cancel us between construction and here.
sink_->cancelled = cancelled_;
options.push_back(naettBodyWriter(&HTTPSRequest::WriteBodyThunk, sink_.get()));
const naettOption **opts = (const naettOption **)options.data();
req_ = naettRequestWithOptions(url_.c_str(), (int)options.size(), opts);
if (!req_) {
// naett couldn't set the request up - a URL it can't parse, most likely. Fail it here
// rather than handing a null to naettMake.
ERROR_LOG(Log::HTTP, "Couldn't create a request for '%s'", url_.c_str());
resultCode_ = naettGenericError;
failed_ = true;
completed_ = true;
sink_.reset();
progress_.Update(0, 0, true);
return;
}
res_ = naettMake(req_);
if (!res_) {
ERROR_LOG(Log::HTTP, "Couldn't start a request for '%s'", url_.c_str());
naettFree(req_);
req_ = nullptr;
resultCode_ = naettGenericError;
failed_ = true;
completed_ = true;
sink_.reset();
progress_.Update(0, 0, true);
return;
}
progress_.Update(0, 0, false);
}
void HTTPSRequest::Join() {
if (!res_ || !req_)
return; // No pending operation.
// Tear down. A request that finished while nobody was polling Done() can still be closed
// properly - it's only one that's genuinely still running that can't be.
if (completed_ || naettComplete(res_)) {
naettClose(res_);
naettFree(req_);
res_ = nullptr;
req_ = nullptr;
sink_.reset();
} else {
// Only reachable at shutdown, since RequestManager cancels from its destructor and
// otherwise waits for Done(). Closing a response naett is still working on isn't safe on
// three of the four backends, and there's nothing of ours to wait on, so let the request
// go and keep its sink alive - a chunk arriving after this point then writes somewhere
// that still exists. The process is on its way out; this is the last word on it.
WARN_LOG(Log::HTTP, "Abandoning an unfinished request to '%s' - shutting down", url_.c_str());
if (sink_) {
sink_->cancelled = true;
g_abandonedSinks->push_back(std::move(sink_));
}
res_ = nullptr;
req_ = nullptr;
}
}
bool HTTPSRequest::Done() {
if (completed_)
return true;
if (!res_) {
// Never started, or already let go of. Nothing left to wait for.
return true;
}
if (!naettComplete(res_)) {
int total = 0;
int size = naettGetTotalBytesRead(res_, &total);
progress_.Update(size, total, false);
return false;
}
// -1000 is a code specified by us to represent cancellation, that is unlikely to ever collide with naett error codes.
resultCode_ = IsCancelled() ? -1000 : naettGetStatus(res_);
// The body arrived in the sink as it was read; take it over now that nothing else will touch it.
const int bodyLength = sink_ ? (int)sink_->buffer.size() : 0;
if (bodyLength > 0) {
buffer_.Append(sink_->buffer);
}
if (resultCode_ < 0) {
// It's a naett error. Translate and handle.
switch (resultCode_) {
case naettConnectionError: // -1
ERROR_LOG(Log::HTTP, "Connection error");
break;
case naettProtocolError: // -2
ERROR_LOG(Log::HTTP, "Protocol error");
break;
case naettReadError: // -3
ERROR_LOG(Log::HTTP, "Read error");
break;
case naettWriteError: // -4
ERROR_LOG(Log::HTTP, "Write error");
break;
case naettGenericError: // -5
ERROR_LOG(Log::HTTP, "Generic error");
break;
case -1000:
// Ours, not naett's - see above. Cancelling is a normal thing to do, not an error.
INFO_LOG(Log::HTTP, "Request to '%s' cancelled", url_.c_str());
break;
default:
ERROR_LOG(Log::HTTP, "Unhandled naett error %d", resultCode_);
break;
}
failed_ = true;
progress_.Update(bodyLength, bodyLength, true);
} else if (resultCode_ == 200) {
bool clear = !(flags_ & RequestFlags::KeepInMemory);
if (!outfile_.empty() && !buffer_.FlushToFile(outfile_, clear)) {
ERROR_LOG(Log::HTTP, "Failed writing download to '%s'", outfile_.c_str());
}
progress_.Update(bodyLength, bodyLength, true);
} else {
WARN_LOG(Log::HTTP, "Naett request failed: %d", resultCode_);
failed_ = true;
progress_.Update(0, 0, true);
}
completed_ = true;
// The callback will be called later.
return true;
}
} // namespace http
#endif // HTTPS_NOT_AVAILABLE