mirror of https://github.com/axmolengine/axmol.git
351 lines
12 KiB
C++
351 lines
12 KiB
C++
/****************************************************************************
|
|
Copyright (c) 2015 Chukong Technologies Inc.
|
|
|
|
http://www.cocos2d-x.org
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
****************************************************************************/
|
|
|
|
#include "network/CCDownloaderImpl.h"
|
|
|
|
#include <curl/curl.h>
|
|
|
|
#include "platform/CCFileUtils.h"
|
|
#include "deprecated/CCString.h"
|
|
|
|
USING_NS_CC;
|
|
using namespace cocos2d::network;
|
|
|
|
static const long LOW_SPEED_LIMIT = 1;
|
|
static const long LOW_SPEED_TIME = 5;
|
|
static const int DEFAULT_TIMEOUT = 5;
|
|
static const int MAX_REDIRS = 2;
|
|
static const int MAX_WAIT_MSECS = 30*1000; /* Wait max. 30 seconds */
|
|
|
|
static const int HTTP_CODE_SUPPORT_RESUME = 206;
|
|
static const char* TEMP_EXT = ".temp";
|
|
|
|
static size_t _fileWriteFunc(void *ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
DownloadUnit *unit = (DownloadUnit*)userdata;
|
|
DownloaderImpl* this_ = (DownloaderImpl*)unit->_reserved;
|
|
int ret = this_->getWriterCallback()(ptr, size, nmemb, unit);
|
|
return ret;
|
|
}
|
|
static size_t _fileWriteFuncForAdapter(void *ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
return nmemb;
|
|
}
|
|
static int _downloadProgressFunc(void* userdata, double totalToDownload, double nowDownloaded, double totalToUpLoad, double nowUpLoaded)
|
|
{
|
|
DownloadUnit *downloadUnit = (DownloadUnit*)userdata;
|
|
DownloaderImpl* this_ = (DownloaderImpl*)downloadUnit->_reserved;
|
|
|
|
// curl when downloading files, the progress function callback in first time, totalToDownload value is 0,
|
|
// then it do not need to do callback notification, so the exclusion of this situation, avoid subsequent judgment exception
|
|
if (totalToDownload != 0)
|
|
{
|
|
totalToDownload += downloadUnit->resumeDownloadedSize;
|
|
nowDownloaded += downloadUnit->resumeDownloadedSize;
|
|
}
|
|
this_->getProgressCallback()(downloadUnit, totalToDownload, nowDownloaded);
|
|
|
|
// must return 0, otherwise download will get cancelled
|
|
return 0;
|
|
}
|
|
|
|
DownloaderImpl::DownloaderImpl()
|
|
: IDownloaderImpl()
|
|
, _curlHandle(nullptr)
|
|
, _lastErrCode(CURLE_OK)
|
|
, _connectionTimeout(DEFAULT_TIMEOUT)
|
|
, _initialized(false)
|
|
{
|
|
}
|
|
DownloaderImpl::~DownloaderImpl()
|
|
{
|
|
if (_curlHandle)
|
|
curl_easy_cleanup(_curlHandle);
|
|
}
|
|
|
|
bool DownloaderImpl::init()
|
|
{
|
|
if (!_initialized) {
|
|
_curlHandle = curl_easy_init();
|
|
_initialized = true;
|
|
}
|
|
|
|
return _initialized;
|
|
}
|
|
|
|
std::string DownloaderImpl::getStrError() const
|
|
{
|
|
return curl_easy_strerror((CURLcode)_lastErrCode);
|
|
}
|
|
|
|
int DownloaderImpl::performDownload(DownloadUnit* unit,
|
|
const WriterCallback& writerCallback,
|
|
const ProgressCallback& progressCallback
|
|
)
|
|
{
|
|
CC_ASSERT(_initialized && "must be initialized");
|
|
|
|
// for callbacks
|
|
unit->_reserved = this;
|
|
|
|
curl_easy_setopt(_curlHandle, CURLOPT_URL, unit->srcUrl.c_str());
|
|
|
|
// Download pacakge
|
|
curl_easy_setopt(_curlHandle, CURLOPT_WRITEFUNCTION, _fileWriteFunc);
|
|
curl_easy_setopt(_curlHandle, CURLOPT_WRITEDATA, unit);
|
|
|
|
curl_easy_setopt(_curlHandle, CURLOPT_NOPROGRESS, false);
|
|
curl_easy_setopt(_curlHandle, CURLOPT_PROGRESSFUNCTION, _downloadProgressFunc);
|
|
curl_easy_setopt(_curlHandle, CURLOPT_PROGRESSDATA, unit);
|
|
|
|
curl_easy_setopt(_curlHandle, CURLOPT_FAILONERROR, true);
|
|
if (_connectionTimeout)
|
|
curl_easy_setopt(_curlHandle, CURLOPT_CONNECTTIMEOUT, _connectionTimeout);
|
|
curl_easy_setopt(_curlHandle, CURLOPT_NOSIGNAL, 1L);
|
|
curl_easy_setopt(_curlHandle, CURLOPT_LOW_SPEED_LIMIT, LOW_SPEED_LIMIT);
|
|
curl_easy_setopt(_curlHandle, CURLOPT_LOW_SPEED_TIME, LOW_SPEED_TIME);
|
|
|
|
_writerCallback = writerCallback;
|
|
_progressCallback = progressCallback;
|
|
_lastErrCode = curl_easy_perform(_curlHandle);
|
|
return _lastErrCode;
|
|
}
|
|
|
|
int DownloaderImpl::performBatchDownload(const DownloadUnits& units,
|
|
const WriterCallback& batchWriterCallback,
|
|
const ProgressCallback& batchProgressCallback,
|
|
const ErrorCallback& errorCallback)
|
|
{
|
|
CC_ASSERT(_initialized && "must be initialized");
|
|
|
|
if (units.size() == 0)
|
|
return -1;
|
|
|
|
CURLM* multi_handle = curl_multi_init();
|
|
int still_running = 0;
|
|
|
|
bool supportResume = supportsResume(units.cbegin()->second.srcUrl);
|
|
auto fileUtils = FileUtils::getInstance();
|
|
|
|
_writerCallback = batchWriterCallback;
|
|
_progressCallback = batchProgressCallback;
|
|
|
|
std::vector<CURL*> curls;
|
|
curls.reserve(units.size());
|
|
|
|
for (const auto& unitEntry: units)
|
|
{
|
|
const auto& unit = unitEntry.second;
|
|
|
|
// HACK: Needed for callbacks. "this" + "unit" are needed
|
|
unit._reserved = this;
|
|
|
|
if (unit.fp != NULL)
|
|
{
|
|
CURL* curl = curl_easy_init();
|
|
curl_easy_setopt(curl, CURLOPT_URL, unit.srcUrl.c_str());
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _fileWriteFunc);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &unit);
|
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
|
|
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, _downloadProgressFunc);
|
|
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &unit);
|
|
curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
|
|
if (_connectionTimeout)
|
|
curl_easy_setopt(_curlHandle, CURLOPT_CONNECTTIMEOUT, _connectionTimeout);
|
|
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, LOW_SPEED_LIMIT);
|
|
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, LOW_SPEED_TIME);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
|
|
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, MAX_REDIRS);
|
|
|
|
// Resuming download support
|
|
if (supportResume && unit.resumeDownload)
|
|
{
|
|
// Check already downloaded size for current download unit
|
|
long size = fileUtils->getFileSize(unit.storagePath + TEMP_EXT);
|
|
if (size != -1)
|
|
{
|
|
curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, size);
|
|
unit.resumeDownloadedSize = size;
|
|
}
|
|
}
|
|
|
|
CURLMcode code = curl_multi_add_handle(multi_handle, curl);
|
|
if (code != CURLM_OK)
|
|
{
|
|
errorCallback(StringUtils::format("Unable to add curl handler for %s: [curl error]%s", unit.customId.c_str(), curl_multi_strerror(code)),
|
|
code,
|
|
unit.customId);
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
else
|
|
{
|
|
curls.push_back(curl);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Query multi perform
|
|
CURLMcode curlm_code = CURLM_CALL_MULTI_PERFORM;
|
|
while(CURLM_CALL_MULTI_PERFORM == curlm_code) {
|
|
curlm_code = curl_multi_perform(multi_handle, &still_running);
|
|
}
|
|
if (curlm_code != CURLM_OK) {
|
|
errorCallback(StringUtils::format("Unable to continue the download process: [curl error]%s", curl_multi_strerror(curlm_code)),
|
|
curlm_code,
|
|
"");
|
|
}
|
|
else
|
|
{
|
|
bool failed = false;
|
|
while (still_running > 0 && !failed)
|
|
{
|
|
// set a suitable timeout to play around with
|
|
struct timeval select_tv;
|
|
long curl_timeo = -1;
|
|
select_tv.tv_sec = 1;
|
|
select_tv.tv_usec = 0;
|
|
|
|
curl_multi_timeout(multi_handle, &curl_timeo);
|
|
if(curl_timeo >= 0) {
|
|
select_tv.tv_sec = curl_timeo / 1000;
|
|
if(select_tv.tv_sec > 1)
|
|
select_tv.tv_sec = 1;
|
|
else
|
|
select_tv.tv_usec = (curl_timeo % 1000) * 1000;
|
|
}
|
|
|
|
int rc;
|
|
fd_set fdread;
|
|
fd_set fdwrite;
|
|
fd_set fdexcep;
|
|
int maxfd = -1;
|
|
FD_ZERO(&fdread);
|
|
FD_ZERO(&fdwrite);
|
|
FD_ZERO(&fdexcep);
|
|
// FIXME: when jenkins migrate to ubuntu, we should remove this hack code
|
|
#if (CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
|
|
curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd);
|
|
rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &select_tv);
|
|
#else
|
|
rc = curl_multi_wait(multi_handle,nullptr, 0, MAX_WAIT_MSECS, &maxfd);
|
|
#endif
|
|
|
|
switch(rc)
|
|
{
|
|
case -1:
|
|
failed = true;
|
|
break;
|
|
case 0:
|
|
default:
|
|
curlm_code = CURLM_CALL_MULTI_PERFORM;
|
|
while(CURLM_CALL_MULTI_PERFORM == curlm_code) {
|
|
curlm_code = curl_multi_perform(multi_handle, &still_running);
|
|
}
|
|
if (curlm_code != CURLM_OK) {
|
|
errorCallback(StringUtils::format("Unable to continue the download process: [curl error]%s", curl_multi_strerror(curlm_code)),
|
|
curlm_code,
|
|
"");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up and close files
|
|
for (auto& curl: curls)
|
|
{
|
|
curl_multi_remove_handle(multi_handle, curl);
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
curl_multi_cleanup(multi_handle);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int DownloaderImpl::getHeader(const std::string& url, HeaderInfo* headerInfo, bool resumeFlag)
|
|
{
|
|
|
|
void *curlHandle = curl_easy_init();
|
|
CC_ASSERT(headerInfo && "headerInfo must not be null");
|
|
|
|
curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str());
|
|
curl_easy_setopt(curlHandle, CURLOPT_HEADER, 1);
|
|
curl_easy_setopt(curlHandle, CURLOPT_NOBODY, 1);
|
|
curl_easy_setopt(curlHandle, CURLOPT_NOSIGNAL, 1);
|
|
// in win32 platform, if not set the writeFunction, it will return CURLE_WRITE_ERROR
|
|
curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, _fileWriteFuncForAdapter);
|
|
if (resumeFlag)
|
|
{
|
|
// use to judge the remote server support continuous transmission or not. the 1 is the test continue size, can't not use zero.
|
|
curl_easy_setopt(curlHandle, CURLOPT_RESUME_FROM_LARGE, 1);
|
|
}
|
|
|
|
if ((_lastErrCode=curl_easy_perform(curlHandle)) == CURLE_OK)
|
|
{
|
|
char *effectiveUrl;
|
|
char *contentType;
|
|
curl_easy_getinfo(curlHandle, CURLINFO_EFFECTIVE_URL, &effectiveUrl);
|
|
curl_easy_getinfo(curlHandle, CURLINFO_CONTENT_TYPE, &contentType);
|
|
curl_easy_getinfo(curlHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &headerInfo->contentSize);
|
|
curl_easy_getinfo(curlHandle, CURLINFO_RESPONSE_CODE, &headerInfo->responseCode);
|
|
|
|
if (contentType == nullptr || headerInfo->contentSize == -1 || headerInfo->responseCode >= 400)
|
|
{
|
|
headerInfo->valid = false;
|
|
}
|
|
else
|
|
{
|
|
headerInfo->url = effectiveUrl;
|
|
headerInfo->contentType = contentType;
|
|
headerInfo->valid = true;
|
|
}
|
|
|
|
curl_easy_cleanup(curlHandle);
|
|
return 0;
|
|
}
|
|
|
|
curl_easy_cleanup(curlHandle);
|
|
return -1;
|
|
}
|
|
|
|
bool DownloaderImpl::supportsResume(const std::string& url)
|
|
{
|
|
CC_ASSERT(_initialized && "must be initialized");
|
|
|
|
HeaderInfo headerInfo;
|
|
// Make a resume request
|
|
if (getHeader(url, &headerInfo, true) == 0 && headerInfo.valid)
|
|
{
|
|
return (headerInfo.responseCode == HTTP_CODE_SUPPORT_RESUME);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void DownloaderImpl::setConnectionTimeout(int connectionTimeout)
|
|
{
|
|
_connectionTimeout = connectionTimeout;
|
|
}
|