partial autodownload rewrite

* own source file for autodownload
* use cleaned up zip functions
* use new http get function (no more curl)
* enforce strict file naming rules and file size limits
* ping all package servers on every game start to determine ping times
* support media file update lists (also kept on package servers)
* allow users to manually prioritise package servers
* automatically select server with newest media file revision

new or changed commands:

addpckserver hostname [priority]
  Adds a host name to the list of package servers. If a priority is given,
  it influences the sorting of servers. Servers with higher priority are
  queried first. If servers have the same priority, they are sorted by
  ping. Default priority is zero.

getpackageserver
  Returns a table with four columns of all configured package servers. The
  columns are: host name, priority, ping and resolved.
  "resolved" is a flag, indicating, if the server answered the ping during
  game start.

removed cubescript idents: sortpckservers gamestarts
DOCREF addpckserver getpckserver
This commit is contained in:
ac-stef
2015-09-07 14:53:56 +02:00
parent c8313a946e
commit 7988443b1d
16 changed files with 616 additions and 585 deletions
+1 -301
View File
@@ -796,8 +796,7 @@ void getmap(char *name, char *callback)
}
else
{
defformatstring(package)("packages/maps/%s.cgz", name);
requirepackage(PCK_MAP, package);
requirepackage(PCK_MAP, name);
if(downloadpackages())
{
if(callback && *callback) execute(callback);
@@ -883,302 +882,3 @@ COMMAND(listdemos, "");
COMMANDN(setmr, setminutesremaining, "i");
COMMANDN(rewind, rewinddemo, "i");
// packages auto - downloader
// arrays
vector<pckserver *> pckservers;
hashtable<const char *, package *> pendingpackages;
// cubescript
VARP(autodownload, 0, 1, 1);
void resetpckservers()
{
pckservers.deletecontents();
delfile(findfile("config" PATHDIVS "pcksources.cfg", "w"));
conoutf("\f3Restart AssaultCube to take effect after resetting the list of packages source servers");
}
void addpckserver(char *addr)
{
pckserver *srcserver = new pckserver();
srcserver->addr = newstring(addr);
srcserver->pending = true;
pckservers.add(srcserver);
}
COMMAND(resetpckservers, "");
COMMAND(addpckserver, "s");
// cURL / Network
bool havecurl = false, canceldownloads = false;
void setupcurl()
{
if(curl_global_init(CURL_GLOBAL_NOTHING)) conoutf("\f3could not init cURL, content downloads not available");
else
{
havecurl = true;
execfile("config/pcksources.cfg");
}
}
// detect the "nearest" server
int pckserversort(pckserver **a, pckserver **b)
{
if((*a)->ping < 0) return ((*b)->ping < 0) ? 0 : 1;
if((*b)->ping < 0) return -1;
return (*a)->ping == (*b)->ping ? 0 : ((*a)->ping < (*b)->ping ? -1 : 1);
}
SDL_Thread* pingpcksrvthread = NULL;
SDL_mutex *pingpcksrvlock = NULL;
int pingpckservers(void *data)
{
SDL_mutexP(pingpcksrvlock);
vector<pckserver> serverstoping;
loopv(pckservers) serverstoping.add(*pckservers[i]);
SDL_mutexV(pingpcksrvlock);
// measure the time it took to receive each's server response
// not very accurate but it should do the trick
loopv(serverstoping)
{
double ping = 0, namelookup = 0;
pckserver *serv = &serverstoping[i];
CURL *cu = curl_easy_init();
curl_easy_setopt(cu, CURLOPT_URL, serv->addr);
curl_easy_setopt(cu, CURLOPT_NOSIGNAL, 1); // Fixes crashbug for some buggy libcurl versions (Linux)
curl_easy_setopt(cu, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt(cu, CURLOPT_NOBODY, 1); // don't download response body (as its size may vary a lot from one server to another)
curl_easy_setopt(cu, CURLOPT_CONNECTTIMEOUT, 10); // the timeout should be large here
int result = curl_easy_perform(cu);
curl_easy_getinfo(cu, CURLINFO_TOTAL_TIME, &ping);
curl_easy_getinfo(cu, CURLINFO_NAMELOOKUP_TIME, &namelookup);
ping -= namelookup; // ignore DNS lookup time as it should be performed only once
curl_easy_cleanup(cu);
cu = NULL;
if(result == CURLE_OPERATION_TIMEDOUT || result == CURLE_COULDNT_RESOLVE_HOST)
serv->responsive = false;
else
serv->ping = (int)(ping*1000);
}
SDL_mutexP(pingpcksrvlock);
loopv(serverstoping) loopvj(pckservers) if(!strcmp(serverstoping[i].addr, pckservers[j]->addr)) *pckservers[j] = serverstoping[i];
pckservers.sort(pckserversort);
// print the results. we should make it silent once tested enough
loopv(pckservers)
{
pckserver *serv = pckservers[i];
if(serv->ping > 0) clientlogf("%d. %s (%d ms)", i+1, serv->addr, serv->ping);
else clientlogf("%d. %s (did not reply)", i+1, serv->addr);
}
SDL_mutexV(pingpcksrvlock);
SDL_DestroyMutex(pingpcksrvlock);
pingpcksrvthread = NULL;
pingpcksrvlock = NULL;
return 0;
}
void sortpckservers()
{
if(pingpcksrvthread || !havecurl) return;
pingpcksrvlock = SDL_CreateMutex();
pingpcksrvthread = SDL_CreateThread(pingpckservers, NULL);
}
COMMAND(sortpckservers, "");
size_t write_callback(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
return fwrite(ptr, size, nmemb, stream);
}
int progress_callback_dlpackage(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow)
{
package *pck = (package *)clientp;
loadingscreen("downloading package %d out of %d...\n%s %.0f/%.0f KB (%.1f%%)\n(ESC to cancel)", pck->number + 1, pck->number + pendingpackages.numelems,
pck->name, dlnow/double(1024.0), dltotal/double(1024.0), dltotal == 0 ? 0 : (dlnow/dltotal * double(100.0)));
if(interceptkey(SDLK_ESCAPE))
{
canceldownloads = true;
loadingscreen();
return 1;
}
return 0;
}
VAR(rereadtexturelists, 0, 1, 1); // flag to indicate additional texture files to trigger a menu rebuild (defaults to "1" to build the menus after startup)
VAR(rereadsoundlists, 0, 1, 1); // same for map sounds
int processdownload(package *pck)
{
string tmpname = "";
copystring(tmpname, findfile("tmp", "rb"));
if(!pck->pending)
{
switch(pck->type)
{
case PCK_TEXTURE: case PCK_AUDIO:
{
const char *pckname = findfile(path(pck->name, true), "w+");
// with textures/sounds, the image/audio file itself is sent. Just need to copy it from the temporary file
if(rename(tmpname, pckname)) conoutf("\f3failed to install %s", pckname);
if(pck->type == PCK_TEXTURE) rereadtexturelists = 1;
else rereadsoundlists = 1;
break;
}
case PCK_MAP: case PCK_MAPMODEL:
{
// addzip("tmp", pck->name, NULL, true, pck->type);
break;
}
case PCK_SKYBOX:
{
char *fname = newstring(pck->name), *ls = strrchr(fname, '/');
if(ls) *ls = '\0';
// addzip("tmp", fname, NULL, true, pck->type);
rereadtexturelists = 1;
break;
}
default:
conoutf("could not install package %s", pck->name);
break;
}
delfile(tmpname);
}
return 0;
}
// download a package
double dlpackage(package *pck)
{
if(!pck || !pck->source) return false;
const char *tmpname = findfile("tmp", "wb");
FILE *outfile = fopen(tmpname, "wb");
string req, pckname = "";
formatstring(req)("%s/%s%s", pck->source->addr, strreplace(pckname, pck->name, " ", "%20"), (pck->type==PCK_MAP || pck->type==PCK_MAPMODEL || pck->type==PCK_SKYBOX) ? ".zip" : "");
conoutf("downloading %s from %s ...", pck->name, pck->source->addr);
if(!outfile)
{
pck->pending = false;
conoutf("\f3failed to write temporary file \"%s\"", tmpname);
return 0;
}
int result, httpresult = 0;
double dlsize;
pck->curl = curl_easy_init();
curl_easy_setopt(pck->curl, CURLOPT_URL, req);
curl_easy_setopt(pck->curl, CURLOPT_NOSIGNAL, 1); // Fixes crashbug for some buggy libcurl versions (Linux)
curl_easy_setopt(pck->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(pck->curl, CURLOPT_WRITEDATA, outfile);
curl_easy_setopt(pck->curl, CURLOPT_NOPROGRESS, 0);
curl_easy_setopt(pck->curl, CURLOPT_PROGRESSFUNCTION, progress_callback_dlpackage);
curl_easy_setopt(pck->curl, CURLOPT_PROGRESSDATA, pck);
curl_easy_setopt(pck->curl, CURLOPT_CONNECTTIMEOUT, 10); // generous timeout for Bukz ;)
result = curl_easy_perform(pck->curl);
curl_easy_getinfo(pck->curl, CURLINFO_RESPONSE_CODE, &httpresult);
curl_easy_getinfo(pck->curl, CURLINFO_SIZE_DOWNLOAD, &dlsize);
curl_easy_cleanup(pck->curl);
pck->curl = NULL;
fclose(outfile);
pck->pending = false;
if(result == CURLE_OPERATION_TIMEDOUT || result == CURLE_COULDNT_RESOLVE_HOST)
{
// mark source unresponsive
pck->source->responsive = false;
conoutf("\f3could not connect to %s", pck->source->addr);
// try to find another source
pckserver *source = NULL;
loopv(pckservers) if(pckservers[i]->responsive) { source = pckservers[i]; break; }
if(!source)
{
conoutf("\f3no more servers to connect to");
canceldownloads = true;
return 0;
}
// update all packages
enumerate(pendingpackages, package *, pack,
{
pack->source = source;
});
pck->pending = true;
return dlpackage(pck); // retry current
}
if(!result && httpresult == 200) processdownload(pck);
else if(result == CURLE_ABORTED_BY_CALLBACK) conoutf("\f3download cancelled");
else conoutf("\f2request for %s failed (cURL %d, HTTP %d)", req, result, httpresult);
return (!result && httpresult == 200) ? dlsize : 0;
}
int lastpackagesdownload = 0;
int downloadpackages()
{
if(!pendingpackages.numelems) return 0;
else if(totalmillis - lastpackagesdownload < 2000)
{
conoutf("\f3quick download attempt aborted");
return 0;
}
double total = 0;
int downloaded = 0;
enumerate(pendingpackages, package *, pck,
{
if(!canceldownloads)
{
if(connected) c2skeepalive(); // try to avoid time out
pck->number = downloaded++;
total += dlpackage(pck);
}
pendingpackages.remove(pck->name);
delete pck;
});
canceldownloads = false;
lastpackagesdownload = lastmillis;
return (int)total;
}
bool requirepackage(int type, const char *path)
{
pckserver *source = NULL;
loopv(pckservers) if(pckservers[i]->responsive) { source = pckservers[i]; break; }
if(!source) { conoutf("\f3no responsive source server found, can't download"); return false; }
char *upath = unixpath(newstring(path)), *cleanpath = newstring(path);
filtertext(cleanpath, cleanpath, FTXT__MEDIAFILEPATH);
if(strcmp(upath, cleanpath)) { conoutf("refuse to download \"%s\": illegal characters", path); delstring(upath); delstring(cleanpath); return false; }
if(!havecurl || canceldownloads || type < 0 || type >= PCK_NUM || pendingpackages.access(cleanpath)) { delstring(cleanpath); return false; }
package *pck = new package;
pck->name = cleanpath;
pck->type = type;
pck->source = source;
pck->pending = true;
pendingpackages.access(pck->name, pck);
delstring(upath);
return true;
}
void writepcksourcecfg()
{
if(pckservers.length())
{
stream *f = openfile("config" PATHDIVS "pcksources.cfg", "w");
if(!f) return;
f->printf("// list of package source servers (only add servers you trust!)\n\n");
loopv(pckservers) f->printf("addpckserver %s\n", pckservers[i]->addr);
delete f;
}
}