Files
GTASource/game/system/BacktraceDumpUpload.winrt.cpp
expvintl 419f2e4752 init
2025-02-23 17:40:52 +08:00

559 lines
15 KiB
C++

//
// system/BacktraceDumpUpload.winrt.cpp
//
// Copyright (C) 2022 Rockstar Games. All Rights Reserved.
//
#include "BacktraceDumpUpload.winrt.h"
#if RSG_DURANGO
#pragma warning(push)
#pragma warning(disable: 4668)
#pragma warning(disable: 4265)
#include <xdk.h>
#include <collection.h>
#include <ixmlhttprequest2.h>
#include <wrl.h>
#pragma warning(pop)
using namespace Windows::Foundation;
using namespace Microsoft::WRL;
using namespace Windows::Foundation::Collections;
extern __THREAD int RAGE_LOG_DISABLE;
#include "net/http.h"
PARAM(uploadDumpsWithRageNet, "Use RageNet to upload dumps on Durango");
// Borrowed from exception.cpp
class ScopedRageLogDisabler
{
public:
ScopedRageLogDisabler() { ++RAGE_LOG_DISABLE; }
~ScopedRageLogDisabler() { --RAGE_LOG_DISABLE; }
};
typedef atMap<atString, atString> AnnotationMap;
typedef AnnotationMap::ConstIterator AnnotationIterator;
// Helper class that provides the header, file data and footer for a file being uploaded as a multipart/form-data request
class MultipartFileHelper
{
public:
MultipartFileHelper() : m_boundary(nullptr), m_file(INVALID_HANDLE_VALUE), m_fileSize(0), m_inited(false), m_hasWrittenHeader(false) {}
~MultipartFileHelper()
{
if (m_file != INVALID_HANDLE_VALUE)
{
CloseHandle(m_file);
m_file = INVALID_HANDLE_VALUE;
}
}
void Init(const char* boundary, const wchar_t* fileFullPath, const char* fileName)
{
m_boundary = boundary;
m_filename = fileName;
m_file = CreateFileW(fileFullPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (m_file == INVALID_HANDLE_VALUE)
{
Errorf("Unable to open file");
return;
}
LARGE_INTEGER fileSize;
if (!GetFileSizeEx(m_file, &fileSize))
{
Errorf("Unable to get size of dump file");
return;
}
m_fileSize = fileSize.QuadPart;
m_inited = true;
}
bool IsInited() { return m_inited; }
// Header looks like this:
// --<boundary><CRLF>
// Content-Disposition: form-data; name="<filename>"; filename="<filename>"<CRLF>
// Content-Type: application/octet-stream<CRLF>
// <CRLF>
const char* GetHeaderFormat()
{
return "--%s\r\n"
"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n"
"Content-Type: application/octet-stream\r\n"
"\r\n";
}
u32 GetHeaderLength()
{
// Get format length without the format specifiers (3 x 2 characters, '%' and 's')
u32 fmtLen = strlen(GetHeaderFormat()) - 3 * 2;
// Add in the lengths of the substituted strings
return fmtLen + strlen(m_boundary) + strlen(m_filename) * 2;
}
u64 GetTotalLengthIncludingHeader()
{
return GetHeaderLength() + m_fileSize + 2; // +2 for the following CRLF
}
HRESULT Read(void* pv, ULONG cb, ULONG* pcbRead)
{
if (!m_inited)
{
*pcbRead = 0;
return S_FALSE;
}
if (!m_hasWrittenHeader)
{
u32 len = GetHeaderLength();
if (cb < len)
return S_FALSE;
sprintf_s((char*)pv, cb, GetHeaderFormat(), m_boundary, m_filename, m_filename);
*pcbRead = len;
m_hasWrittenHeader = true;
return S_OK;
}
else
{
DWORD bytesReadFromFile = 0;
ReadFile(m_file, pv, cb, &bytesReadFromFile, NULL);
*pcbRead = bytesReadFromFile;
if (*pcbRead == cb)
return S_OK;
else
{
// If there is space for the terminating newline, add it
if (*pcbRead <= cb - 2)
{
char* dst = (char*)pv;
dst[(*pcbRead)++] = '\r';
dst[(*pcbRead)++] = '\n';
return S_FALSE;
}
else
{
// Otherwise add it next time (after a zero-byte read from the file)
return S_OK;
}
}
}
}
private:
const char* m_filename;
HANDLE m_file;
u64 m_fileSize;
const char* m_boundary;
bool m_inited, m_hasWrittenHeader;
};
// Helper class that provides the header, value and footer for each annotation being set with a multipart/form-data request
class MultipartAnnotationsHelper
{
public:
MultipartAnnotationsHelper() : m_boundary(nullptr), m_boundaryLength(0), m_headerUnformattedLength(0), m_annotations(nullptr), m_iterator(sm_dummyMap.CreateIterator()) {}
void Init(const char* boundary, const AnnotationMap* annotations)
{
m_boundary = boundary;
m_boundaryLength = strlen(boundary);
m_headerUnformattedLength = strlen(GetHeaderFormat()) - 3 * 2; // Subtract 3x format specifiers, "%s"
if (annotations)
{
m_annotations = annotations;
}
else
{
m_annotations = &sm_dummyMap;
}
m_iterator = annotations->CreateIterator();
}
const char* GetHeaderFormat()
{
return "--%s\r\n"
"Content-Disposition: form-data; name=\"%s\"\r\n"
"\r\n"
"%s\r\n";
}
u64 GetHeaderAndDataLength(AnnotationIterator it)
{
return m_headerUnformattedLength + m_boundaryLength + it.GetKey().GetLength() + it.GetData().GetLength();
}
u64 GetTotalLengthIncludingHeaders()
{
if (!m_annotations)
return 0;
u64 total = 0;
for (AnnotationIterator it = m_annotations->CreateIterator(); !it.AtEnd(); it.Next())
{
total += GetHeaderAndDataLength(it);
}
return total;
}
HRESULT Read(void* pv, ULONG cb, ULONG* pcbRead)
{
if (m_iterator.AtEnd())
return S_FALSE;
u32 len = GetHeaderAndDataLength(m_iterator);
if (cb < len)
return S_FALSE;
sprintf_s((char*)pv, cb, GetHeaderFormat(), m_boundary, m_iterator.GetKey().c_str(), m_iterator.GetData().c_str());
*pcbRead = len;
m_iterator.Next();
return m_iterator.AtEnd() ? S_FALSE : S_OK;
}
private:
const char* m_boundary;
u32 m_boundaryLength, m_headerUnformattedLength;
const AnnotationMap* m_annotations;
AnnotationIterator m_iterator;
static const atMap<atString, atString> sm_dummyMap;
};
// Dummy (empty) map used when no annotations map is provided, because atMap::ConstIterator can't be used without a map
const atMap<atString, atString> MultipartAnnotationsHelper::sm_dummyMap;
// Class that represents the body of the multipart upload request for the dump, the log and the annotations
// The underlying HTTP library will call Read() multiple times (until it returns S_FALSE).
// Each iteration, we delegate the read to the current helper (dump, log or annotations), and when each is finished, we change to the next one until all are done.
// Finally, we read a terminating boundary ("--<boundary>--") which ends the content.
// Note that GetTotalLength() needs to return the exact length of the content (and is called *before* we start reading it) so any changes need to be reflected there.
// The methods for IDispatch and IUnknown are just required boilerplate, they do nothing of interest.
// (Adapted from netXblHttpRequestStream in xblhttp.cpp)
class DumpRequestStream : public RuntimeClass<RuntimeClassFlags<ClassicCom>, ISequentialStream, IDispatch>
{
public:
DumpRequestStream() : m_currentPart(PART_BEGIN)
{
}
~DumpRequestStream()
{
}
const char* GetBoundary() { return "multipart-boundary-multipart-boundary-multipart-boundary"; }
void Init(const wchar_t* dumpPath, const wchar_t* logPath, const AnnotationMap* annotations)
{
m_dumpFile.Init(GetBoundary(), dumpPath, "upload_file_minidump");
m_logFile.Init(GetBoundary(), logPath, "crashcontext.log");
m_annotations.Init(GetBoundary(), annotations);
}
HRESULT ReadEnding(void* pv, ULONG cb, ULONG* pcbRead)
{
ULONG len = strlen(GetBoundary()) + 4;
if (cb < len)
return S_FALSE;
sprintf_s((char*)pv, cb, "--%s--", GetBoundary());
*pcbRead = len;
return S_FALSE;
}
// ISequentialStream
STDMETHODIMP Read(void* pv, ULONG cb, ULONG* pcbRead)
{
HRESULT hr = S_FALSE;
*pcbRead = 0;
switch (m_currentPart)
{
case PART_DUMP: hr = m_dumpFile.Read(pv, cb, pcbRead); break;
case PART_LOG: hr = m_logFile.Read(pv, cb, pcbRead); break;
case PART_ANNOTATIONS: hr = m_annotations.Read(pv, cb, pcbRead); break;
case PART_LAST_BOUNDARY: hr = ReadEnding(pv, cb, pcbRead); break;
default: break;
}
// When one part has finished, start the next part
if (hr == S_FALSE && m_currentPart < PART_END)
{
hr = S_OK;
m_currentPart = (Part)((int)m_currentPart + 1);
}
return hr;
}
u64 GetTotalLength()
{
return
m_dumpFile.GetTotalLengthIncludingHeader() +
m_logFile.GetTotalLengthIncludingHeader() +
m_annotations.GetTotalLengthIncludingHeaders() +
strlen(GetBoundary()) + 4; // Final boundary with "--" before and after it
}
STDMETHODIMP Write(const void* UNUSED_PARAM(pv), ULONG UNUSED_PARAM(cb), ULONG* UNUSED_PARAM(pcbWritten)) { return S_FALSE; }
// IUnknown
STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject)
{
if (!ppvObject)
{
return E_INVALIDARG;
}
if (riid == IID_IUnknown)
{
*ppvObject = static_cast<IUnknown*>((IDispatch*)this);
}
else if (riid == IID_IDispatch)
{
*ppvObject = static_cast<IDispatch*>(this);
}
else if (riid == IID_ISequentialStream)
{
*ppvObject = static_cast<ISequentialStream*>(this);
}
else
{
*ppvObject = nullptr;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
// IDispatch
STDMETHODIMP GetTypeInfoCount(unsigned int FAR* pctinfo)
{
if (pctinfo)
{
*pctinfo = 0;
}
return E_NOTIMPL;
}
STDMETHODIMP GetTypeInfo(unsigned int UNUSED_PARAM(iTInfo), LCID UNUSED_PARAM(lcid), ITypeInfo FAR* FAR* ppTInfo)
{
if (ppTInfo)
{
*ppTInfo = nullptr;
}
return E_NOTIMPL;
}
STDMETHODIMP GetIDsOfNames(REFIID UNUSED_PARAM(riid), OLECHAR** UNUSED_PARAM(rgszNames), unsigned int UNUSED_PARAM(cNames), LCID UNUSED_PARAM(lcid), DISPID* UNUSED_PARAM(rgDispId))
{
return DISP_E_UNKNOWNNAME;
}
STDMETHODIMP Invoke(DISPID UNUSED_PARAM(dispIdMember), REFIID UNUSED_PARAM(riid), LCID UNUSED_PARAM(lcid), WORD UNUSED_PARAM(wFlags), DISPPARAMS* UNUSED_PARAM(pDispParams), VARIANT* UNUSED_PARAM(pVarResult), EXCEPINFO* UNUSED_PARAM(pExcepInfo), unsigned int* UNUSED_PARAM(puArgErr))
{
return S_OK;
}
private:
// Helpers that handle the dump, the log and the annotations
MultipartFileHelper m_dumpFile, m_logFile;
MultipartAnnotationsHelper m_annotations;
// Which part we are sending next
enum Part
{
PART_BEGIN,
PART_ANNOTATIONS = PART_BEGIN, // The annotations (note one Read iteration is needed per annotation)
PART_DUMP, // The minidump file itself
PART_LOG, // The crashcontext.log file
PART_LAST_BOUNDARY, // The terminating boundary ("--<boundary>--")
PART_END
};
Part m_currentPart;
};
// Provides callback functions that are called by IXHR2 in response to various events.
// Since we've crashed and are not making any decisions based on whether the upload succeeds or not, we basically ignore all of these except OnError,
// and even that one is only used for debugging purposes.
class DumpUploadCallbacks : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IXMLHTTPRequest2Callback>
{
public:
DumpUploadCallbacks() : m_finished(false), m_error(false) {}
STDMETHODIMP OnRedirect(IXMLHTTPRequest2 *UNUSED_PARAM(pXHR), const WCHAR *UNUSED_PARAM(pwszRedirectUrl)) { return S_OK; }
STDMETHODIMP OnHeadersAvailable(IXMLHTTPRequest2 *UNUSED_PARAM(pXHR), DWORD UNUSED_PARAM(dwStatus), const WCHAR *UNUSED_PARAM(pwszStatus)) { return S_OK; }
STDMETHODIMP OnDataAvailable(IXMLHTTPRequest2 *UNUSED_PARAM(pXHR), ISequentialStream *UNUSED_PARAM(pResponseStream)) { return S_OK; }
STDMETHODIMP OnResponseReceived(IXMLHTTPRequest2 *UNUSED_PARAM(pXHR), ISequentialStream *UNUSED_PARAM(pResponseStream)) { m_finished = true; return S_OK; }
STDMETHODIMP OnError(IXMLHTTPRequest2 *UNUSED_PARAM(pXHR), HRESULT OUTPUT_ONLY(hrError))
{
Errorf("OnError(XHR2) = 0x%08X", hrError);
m_finished = true;
m_error = true;
return S_OK;
}
bool HasFinished() { return m_finished; }
bool HadError() { return m_error; }
private:
bool m_finished, m_error;
};
#if !RSG_FINAL
bool DoRageNetDumpUpload(const wchar_t* dumpFullPath, const char* url)
{
HANDLE file = CreateFileW(dumpFullPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (file == INVALID_HANDLE_VALUE)
{
Errorf("Unable to open dump file");
return false;
}
LARGE_INTEGER fileSize;
if (!GetFileSizeEx(file, &fileSize))
{
Errorf("Unable to get size of dump file");
CloseHandle(file);
return false;
}
rage::netHttpRequest request;
netStatus status;
request.Init(nullptr, netTcp::GetInsecureSslCtx());
request.BeginPost(url, nullptr, 60, "Backtrace Dump Upload", nullptr, &status);
u8* fileBuffer = rage_new u8[fileSize.QuadPart];
const DWORD MAX_READ_BYTES = 4 * 1024 * 1024;
for (u64 fileOffset = 0; fileOffset < fileSize.QuadPart;)
{
u64 remaining = fileSize.QuadPart - fileOffset;
DWORD toRead = (remaining < MAX_READ_BYTES) ? remaining : MAX_READ_BYTES;
ReadFile(file, &fileBuffer[fileOffset], toRead, NULL, NULL);
fileOffset += toRead;
}
CloseHandle(file);
request.AppendContentZeroCopy(fileBuffer, fileSize.QuadPart);
request.Commit();
while (status.Pending())
{
request.Update();
Sleep(0);
}
return true;
}
#endif // !RSG_FINAL
bool BacktraceDumpUpload::UploadDump(const wchar_t* dumpFullPath, const wchar_t* logFullPath, const atMap<atString, atString>* annotations, const char* url)
{
#if !RSG_FINAL
// Test dump upload with RageNet. Note that the log and annotations are not included.
if (PARAM_uploadDumpsWithRageNet.Get())
{
return DoRageNetDumpUpload(dumpFullPath, url);
}
#endif
// COM objects that we'll use to upload the dump with:
ComPtr<IXMLHTTPRequest2> pXHR; // The IXHR2 object, wraps a HTTP request
ComPtr<DumpUploadCallbacks> pXHRCallbacks; // Callbacks object, defined above, which handles responses from IXHR2
ComPtr<DumpRequestStream> pRequestStream; // Request stream object, which provides the body of the request via calls made from IXHR2 to its Read() method
// WinRT calls new, so temporarily disable the error
ScopedRageLogDisabler disablerObject;
// Make and init the request stream
HRESULT hr = MakeAndInitialize<DumpRequestStream>(&pRequestStream);
if (FAILED(hr))
{
Errorf("MakeAndInitialize<DumpRequestStream> = 0x%08X", hr);
return false;
}
else
{
// There doesn't seem to be a way of initing a RuntimeClass subclass through its constructor, so do it here
pRequestStream->Init(dumpFullPath, logFullPath, annotations);
}
// Make the callbacks provider
hr = Microsoft::WRL::Details::MakeAndInitialize<DumpUploadCallbacks>(&pXHRCallbacks);
if (FAILED(hr))
{
Errorf("MakeAndInitialize<DumpUploadCallbacks> = 0x%08X", hr);
return false;
}
// Make the IXHR2 object
hr = CoCreateInstance(__uuidof(FreeThreadedXMLHTTP60), NULL, CLSCTX_SERVER, IID_PPV_ARGS(&pXHR));
if (FAILED(hr))
{
Errorf("CoCreateInstance(IXHR2) = 0x%08X", hr);
return false;
}
// Convert the URL to wide chars. It's used as UTF8 in other places (Crashpad, Breakpad).
USES_CONVERSION;
const char16* urlW = UTF8_TO_WIDE(url);
// Open the request
hr = pXHR->Open(L"POST", (const WCHAR*)urlW, pXHRCallbacks.Get(), nullptr, nullptr, nullptr, nullptr);
if (FAILED(hr))
{
Errorf("Error opening IXHR2");
return false;
}
// Set the content type
// Content-Type: multipart/form-data; boundary=<boundary>
char contentTypeBuffer[256];
sprintf_s(contentTypeBuffer, "multipart/form-data; boundary=%s", pRequestStream->GetBoundary());
const char16* contentTypeW = UTF8_TO_WIDE(contentTypeBuffer);
pXHR->SetRequestHeader(L"Content-Type", (const WCHAR*)contentTypeW);
// Set the connection timeout (this only affects the initial connection to the server, not the full upload)
hr = pXHR->SetProperty(XHR_PROP_TIMEOUT, 30 * 1000);
// Send the request
hr = pXHR->Send(pRequestStream.Get(), (ULONGLONG)pRequestStream->GetTotalLength());
if (FAILED(hr))
{
Errorf("Error sending XHR");
return false;
}
// Wait for the request to finish (successfully or otherwise)
while (!pXHRCallbacks->HasFinished())
{
Sleep(0);
}
return !pXHRCallbacks->HadError();
}
#endif // RSG_DURANGO