Bug 811646 - Split big base installer download into multiple HTTP range requests. r=rstrong

This commit is contained in:
Brian R. Bondy 2012-11-16 21:39:17 -05:00
parent 67543c33b9
commit d6a9337d42

View File

@ -30,6 +30,10 @@ CRITICAL_SECTION g_CritLock;
UINT g_N_CCH;
PTSTR g_N_Vars;
DWORD g_ConnectTimeout = 0;
DWORD g_ReceiveTimeout = 0;
#define NSISPI_INITGLOBALS(N_CCH, N_Vars) do { \
g_N_CCH = N_CCH; \
g_N_Vars = N_Vars; \
@ -121,10 +125,95 @@ UINT_PTR __cdecl NSISPluginCallback(UINT Event)
return NULL;
}
/* ParseURL is a quickly thrown together simple way to parse a URL into parts.
* It is only designed to support simple URLs and doesn't support things like
* authorization information in the URL.
* The format of the URL is assumed to be:
* <protocol>://<server>:<port>/<path>
*
* @param url The input URL to parse
* @param protocol Out variable which will store the protocol of the passed url
* @param server Out variable which will store the server of the passed url
* @param port Out variable which will store the port of the passed url
* @param path Out variable which will store the path of the passed url
* @return true if successful
*/
template<size_t A, size_t B, size_t C> static bool
BasicParseURL(LPCWSTR url, wchar_t (*protocol)[A], INTERNET_PORT *port,
wchar_t (*server)[B], wchar_t (*path)[C])
{
ZeroMemory(*protocol, A * sizeof(wchar_t));
ZeroMemory(*server, B * sizeof(wchar_t));
ZeroMemory(*path, C * sizeof(wchar_t));
const WCHAR *p = url;
// Skip past the protocol
int pos = 0;
while (*p != L'\0' && *p != L'/' && *p != L':')
{
if (pos + 1 >= A)
return false;
(*protocol)[pos++] = *p++;
}
// Skip past the ://
p += 3;
*port = INTERNET_DEFAULT_HTTP_PORT;
if (!wcsicmp(*protocol, L"https"))
{
*port = INTERNET_DEFAULT_HTTPS_PORT;
}
// Get the server
pos = 0;
while (*p != L'\0' && *p != L'/' && *p != L':')
{
if (pos + 1 >= B)
return false;
(*server)[pos++] = *p++;
}
// Get the port if specified
if (*p == L':')
{
WCHAR portStr[16];
p++;
pos = 0;
while (*p != L'\0' && *p != L'/')
{
if (pos + 1 >= 16)
return false;
portStr[pos++] = *p++;
}
portStr[pos] = '\0';
*port = (INTERNET_PORT)_wtoi(portStr);
}
else
{
// Skip the slash after the server
while (*p != L'\0' && *p != L'/')
{
p++;
}
}
// Get the rest as the path
pos = 0;
while (*p != L'\0')
{
if (pos + 1 >= C)
return false;
(*path)[pos++] = *p++;
}
return true;
}
DWORD CALLBACK TaskThreadProc(LPVOID ThreadParam)
{
NSIS::stack_t *pURL,*pFile;
HINTERNET hInetSes = NULL;
HINTERNET hInetSes = NULL, hInetCon = NULL;
HANDLE hLocalFile;
bool completedFile = false;
startnexttask:
@ -147,7 +236,8 @@ startnexttask:
#ifndef ONELOCKTORULETHEMALL
StatsLock_AcquireExclusive();
#endif
if (completedFile) {
if (completedFile)
{
++g_FilesCompleted;
}
completedFile = false;
@ -157,7 +247,8 @@ startnexttask:
{
if (g_FilesTotal)
{
if (g_FilesTotal == g_FilesCompleted) {
if (g_FilesTotal == g_FilesCompleted)
{
g_Status = STATUS_COMPLETEDALL;
}
}
@ -181,6 +272,10 @@ diegle:
{
InternetCloseHandle(hInetSes);
}
if (hInetCon)
{
InternetCloseHandle(hInetCon);
}
if (INVALID_HANDLE_VALUE != hLocalFile)
{
CloseHandle(hLocalFile);
@ -209,96 +304,244 @@ diegle:
InternetSetOption(hInetSes, INTERNET_OPTION_CONNECTED_STATE, &ci, sizeof(ci));
}
}
if(g_ConnectTimeout > 0)
{
InternetSetOption(hInetSes, INTERNET_OPTION_CONNECT_TIMEOUT,
&g_ConnectTimeout, sizeof(g_ConnectTimeout));
}
}
DWORD ec = ERROR_SUCCESS;
hLocalFile = CreateFile(pFile->text,GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_DELETE,NULL,CREATE_ALWAYS,0,NULL);
if (INVALID_HANDLE_VALUE == hLocalFile) {
if (INVALID_HANDLE_VALUE == hLocalFile)
{
goto diegle;
}
const DWORD IOUFTPFlags = INTERNET_FLAG_PASSIVE;
const DWORD IOURedirFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS;
const DWORD IOUCacheFlags = INTERNET_FLAG_RESYNCHRONIZE | INTERNET_FLAG_NO_CACHE_WRITE;
const DWORD IOUCookieFlags = INTERNET_FLAG_NO_COOKIES;
DWORD IOUFlags = IOUFTPFlags | IOURedirFlags | IOUCacheFlags | IOUCookieFlags | INTERNET_FLAG_NO_UI | INTERNET_FLAG_EXISTING_CONNECT;
HINTERNET hInetFile = InternetOpenUrl(hInetSes, pURL->text, NULL, 0, IOUFlags, NULL);
if (!hInetFile) {
DWORD IOUFlags = IOURedirFlags | IOUCacheFlags | IOUCookieFlags | INTERNET_FLAG_NO_UI | INTERNET_FLAG_EXISTING_CONNECT;
WCHAR protocol[16];
WCHAR server[128];
WCHAR path[1024];
INTERNET_PORT port;
// Good old VC6 cannot deduce the size of these params :'(
if (!BasicParseURL<16, 128, 1024>(pURL->text, &protocol, &port, &server, &path))
{
// Insufficient buffer or bad URL passed in
goto diegle;
}
DWORD context;
hInetCon = InternetConnect(hInetSes, server, port, NULL, NULL,
INTERNET_SERVICE_HTTP, 0, (unsigned long)&context);
if (!hInetCon)
{
goto diegle;
}
// Setup a buffer of size 256KiB to store the downloaded data.
// Get at most 2MiB at a time from the partial HTTP Range requests.
// Biffer buffers will be faster.
// cbRangeReadBufXF should be a multiple of cbBufXF.
const UINT cbBufXF = 262144;
const UINT cbRangeReadBufXF = 2097152;
BYTE bufXF[cbBufXF];
// Up the default internal buffer size from 4096 to internalReadBufferSize.
DWORD internalReadBufferSize = cbRangeReadBufXF;
if (!InternetSetOption(hInetCon, INTERNET_OPTION_READ_BUFFER_SIZE,
&internalReadBufferSize, sizeof(DWORD)))
{
// Maybe it's too big, try half of the optimal value. If that fails just
// use the default.
internalReadBufferSize /= 2;
InternetSetOption(hInetCon, INTERNET_OPTION_READ_BUFFER_SIZE,
&internalReadBufferSize, sizeof(DWORD));
}
// Change the default timeout of 30 seconds to the specified value.
// Is case a proxy in between caches the results, it could in theory
// take longer to get the first chunk, so it is good to set this high.
if (g_ReceiveTimeout)
{
InternetSetOption(hInetCon, INTERNET_OPTION_DATA_RECEIVE_TIMEOUT,
&g_ReceiveTimeout, sizeof(DWORD));
}
// Obtain the file size using a HEAD request.
// Some proxy servers will hold up an entire download for GET requests, so
// HEAD is important here.
HINTERNET hInetFile = HttpOpenRequest(hInetCon, L"HEAD",
path, NULL, NULL, NULL, 0, 0);
if (!hInetFile)
{
goto diegle;
}
if (!HttpSendRequest(hInetFile, NULL, 0, NULL, 0))
{
goto diegle;
}
// Get the file length via the Content-Length header
FILESIZE_T cbThisFile;
DWORD cbio = sizeof(cbThisFile);
if (!HttpQueryInfo(hInetFile, HTTP_QUERY_CONTENT_LENGTH|HTTP_QUERY_FLAG_NUMBER, &cbThisFile, &cbio, NULL))
if (!HttpQueryInfo(hInetFile,
HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER,
&cbThisFile, &cbio, NULL))
{
cbThisFile = FILESIZE_UNKNOWN;
}
// Determine if we should use byte range requests. We want to use it if:
// 1. Server accepts byte range requests
// 2. The size of the file is known and more than our Range buffer size.
bool useRangeRequest = true;
WCHAR rangeRequestAccepted[64] = { '\0' };
cbio = sizeof(rangeRequestAccepted);
if (cbThisFile != FILESIZE_UNKNOWN && cbThisFile <= cbRangeReadBufXF ||
!HttpQueryInfo(hInetFile,
HTTP_QUERY_ACCEPT_RANGES,
(LPDWORD)rangeRequestAccepted, &cbio, NULL))
{
useRangeRequest = false;
}
else
{
useRangeRequest = wcsstr(rangeRequestAccepted, L"bytes") != 0 &&
cbThisFile != FILESIZE_UNKNOWN;
}
// If the server doesn't have range request support or doesn't have a
// Content-Length header, then get everything all at once.
if (!useRangeRequest)
{
InternetCloseHandle(hInetFile);
InternetCloseHandle(hInetCon);
hInetFile = InternetOpenUrl(hInetSes, pURL->text,
NULL, 0, IOUFlags, NULL);
if (!hInetFile)
{
goto diegle;
}
// For some reason this also needs to be set on the hInetFile and
// not just the connection.
if (g_ReceiveTimeout > 0)
{
InternetSetOption(hInetCon, INTERNET_OPTION_DATA_RECEIVE_TIMEOUT,
&g_ReceiveTimeout, sizeof(DWORD));
}
}
for(;;)
{
#if PLUGIN_DEBUG
const UINT cbBufXF = 512;
BYTE bufXF[cbBufXF];
#else
const UINT cbBufXF = 4096;
BYTE*bufXF = (BYTE*) pURL->text;
#endif
DWORD cbio,cbXF;
BOOL retXF = InternetReadFile(hInetFile, bufXF, cbBufXF, &cbio);
if (!retXF)
DWORD cbio = 0,cbXF = 0;
// If we know the file size, download it in chunks
if (useRangeRequest && cbThisFile != g_cbCurrXF)
{
ec = GetLastError();
TRACE1(_T("InternetReadFile failed, gle=%u\n"), ec);
break;
}
if (0 == cbio)
{
ASSERT(ERROR_SUCCESS == ec);
// EOF or broken connection?
// TODO: Can InternetQueryDataAvailable detect this?
TRACE2(_T("InternetReadFile true with 0 cbio, cbThisFile=%d gle=%u\n"), cbThisFile, GetLastError());
// If we haven't transferred all of the file, and we know how big the file
// is, and we have no more data to read from the HTTP request, then set a
// broken pipe error. Reading without StatsLock is ok in this thread.
if (FILESIZE_UNKNOWN != cbThisFile && g_cbCurrXF != cbThisFile)
// Close the previous request, but not the connection
InternetCloseHandle(hInetFile);
hInetFile = HttpOpenRequest(hInetCon, L"GET", path,
NULL, NULL, NULL, 0, 0);
if (!hInetFile)
{
ec = ERROR_BROKEN_PIPE;
// TODO: we could add retry here to be more tolerant
goto diegle;
}
// For some reason this also needs to be set on the hInetFile and
// not just the connection.
if (g_ReceiveTimeout > 0)
{
InternetSetOption(hInetCon, INTERNET_OPTION_DATA_RECEIVE_TIMEOUT,
&g_ReceiveTimeout, sizeof(DWORD));
}
WCHAR range[32];
swprintf(range, L"Range: bytes=%d-%d", g_cbCurrXF,
min(g_cbCurrXF + cbRangeReadBufXF, cbThisFile));
if (!HttpSendRequest(hInetFile, range, wcslen(range), NULL, 0))
{
// TODO: we could add retry here to be more tolerant
goto diegle;
}
}
// Read the chunk (or full file if we don't know the size) we downloaded
BOOL retXF;
for (;;)
{
DWORD cbioThisIteration = 0;
retXF = InternetReadFile(hInetFile, bufXF, cbBufXF, &cbioThisIteration);
if (!retXF)
{
ec = GetLastError();
TRACE1(_T("InternetReadFile failed, gle=%u\n"), ec);
// TODO: we could add retry here to be more tolerant
goto diegle;
}
// Check if we're done reading
if (cbioThisIteration == 0)
{
break;
}
// Write what we found
cbXF = cbioThisIteration;
retXF = WriteFile(hLocalFile, bufXF, cbXF, &cbioThisIteration, NULL);
if (!retXF || cbXF != cbioThisIteration)
{
cbio += cbioThisIteration;
ec = GetLastError();
break;
}
cbio += cbioThisIteration;
StatsLock_AcquireExclusive();
if (FILESIZE_UNKNOWN != cbThisFile)
{
g_cbCurrTot = cbThisFile;
}
g_cbCurrXF += cbXF;
StatsLock_ReleaseExclusive();
// Avoid an extra call to InternetReadFile if we already read everything
// in the current request
if (cbio == cbRangeReadBufXF && useRangeRequest)
{
break;
}
}
// Check if we're done transferring the file successfully
if (0 == cbio &&
(cbThisFile == FILESIZE_UNKNOWN || cbThisFile == g_cbCurrXF))
{
ASSERT(ERROR_SUCCESS == ec);
TRACE2(_T("InternetReadFile true with 0 cbio, cbThisFile=%d gle=%u\n"), cbThisFile, GetLastError());
break;
}
if (0==g_FilesTotal)
// Check if we canceled the download
if (0 == g_FilesTotal)
{
TRACEA("0==g_FilesTotal, aborting transfer loop...\n");
ec = ERROR_CANCELLED;
break;
}
cbXF = cbio;
if (cbXF)
{
retXF = WriteFile(hLocalFile, bufXF, cbXF, &cbio, NULL);
if (!retXF || cbXF!=cbio)
{
ec = GetLastError();
break;
}
StatsLock_AcquireExclusive();
if (FILESIZE_UNKNOWN != cbThisFile) {
g_cbCurrTot = cbThisFile;
}
g_cbCurrXF += cbXF;
StatsLock_ReleaseExclusive();
}
}
TRACE2(_T("TaskThreadProc completed %s, ec=%u\n"), pURL->text, ec);
InternetCloseHandle(hInetFile);
if (ERROR_SUCCESS == ec) {
if (INVALID_HANDLE_VALUE != hLocalFile) {
if (ERROR_SUCCESS == ec)
{
if (INVALID_HANDLE_VALUE != hLocalFile)
{
CloseHandle(hLocalFile);
hLocalFile = INVALID_HANDLE_VALUE;
}
@ -320,25 +563,39 @@ NSISPIEXPORTFUNC Get(HWND hwndNSIS, UINT N_CCH, TCHAR*N_Vars, NSIS::stack_t**ppS
for (;;)
{
NSIS::stack_t*pURL = StackPopItem(ppST);
if (!pURL) {
if (!pURL)
{
break;
}
if (0==lstrcmp(pURL->text,_T("/END")))
if (lstrcmpi(pURL->text, _T("/connecttimeout")) == 0)
{
freeurlandexit:
StackFreeItem(pURL);
break;
NSIS::stack_t*pConnectTimeout = StackPopItem(ppST);
g_ConnectTimeout = _tcstol(pConnectTimeout->text, NULL, 10) * 1000;
continue;
}
if (0==lstrcmp(pURL->text,_T("/RESET")))
else if (lstrcmpi(pURL->text, _T("/receivetimeout")) == 0)
{
NSIS::stack_t*pReceiveTimeout = StackPopItem(ppST);
g_ReceiveTimeout = _tcstol(pReceiveTimeout->text, NULL, 10) * 1000;
continue;
}
else if (lstrcmpi(pURL->text, _T("/reset")) == 0)
{
StackFreeItem(pURL);
Reset();
continue;
}
else if (lstrcmpi(pURL->text, _T("/end")) == 0)
{
freeurlandexit:
StackFreeItem(pURL);
break;
}
NSIS::stack_t*pFile = StackPopItem(ppST);
if (!pFile) {
if (!pFile)
{
goto freeurlandexit;
}
@ -367,7 +624,8 @@ freeurlandexit:
g_hThread = CreateThread(NULL, 0, TaskThreadProc, NULL, 0, &tid);
}
if (!g_hThread) {
if (!g_hThread)
{
goto freeurlandexit;
}
@ -391,7 +649,8 @@ NSISPIEXPORTFUNC GetStats(HWND hwndNSIS, UINT N_CCH, TCHAR*N_Vars, NSIS::stack_t
NSIS_SetRegUINT(2, g_FilesTotal - g_FilesCompleted);
NSIS_SetRegUINT(3, g_cbCurrXF);
NSIS_SetRegStrEmpty(4);
if (FILESIZE_UNKNOWN != g_cbCurrTot) {
if (FILESIZE_UNKNOWN != g_cbCurrTot)
{
NSIS_SetRegUINT(4, g_cbCurrTot);
}
StatsLock_ReleaseShared();
@ -411,3 +670,11 @@ BOOL WINAPI DllMain(HINSTANCE hInst, ULONG Reason, LPVOID pCtx)
{
return _DllMainCRTStartup(hInst, Reason, pCtx);
}
// For some reason VC6++ doesn't like wcsicmp and swprintf.
// If you use them, you get a linking error about _main
// as an unresolved external.
int main(int argc, char**argv)
{
return 0;
}