591 lines
20 KiB
C
591 lines
20 KiB
C
/*
|
|
* static char *rcsid_metaserver_c =
|
|
* "$Id: metaserver.c 11578 2009-02-23 22:02:27Z lalo $";
|
|
*/
|
|
|
|
/*
|
|
CrossFire, A Multiplayer game for X-windows
|
|
|
|
Copyright (C) 2002,2007 Mark Wedel & Crossfire Development Team
|
|
Copyright (C) 1992 Frank Tore Johansen
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
The authors can be reached via e-mail at crossfire-devel@real-time.com
|
|
*/
|
|
|
|
/**
|
|
* \file
|
|
* \date 2003-12-02
|
|
* Meta-server related functions.
|
|
*/
|
|
|
|
#include <global.h>
|
|
|
|
#ifndef WIN32 /* ---win32 exclude unix header files */
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <netdb.h>
|
|
#include <arpa/inet.h>
|
|
|
|
#endif /* end win32 */
|
|
|
|
#include <pthread.h>
|
|
#include <metaserver2.h>
|
|
#include <version.h>
|
|
|
|
#ifdef HAVE_CURL_CURL_H
|
|
#include <curl/curl.h>
|
|
#include <curl/easy.h>
|
|
#endif
|
|
|
|
static int metafd = -1;
|
|
|
|
static struct sockaddr_in sock;
|
|
|
|
/**
|
|
* Connects to metaserver.
|
|
*
|
|
* Its only called once. If we are not
|
|
* trying to contact the metaserver of the connection attempt fails, metafd will be
|
|
* set to -1. We use this instead of messing with the settings.meta_on so that
|
|
* that can be examined to at least see what the user was trying to do.
|
|
*/
|
|
void metaserver_init(void) {
|
|
#ifdef WIN32 /* ***win32 metaserver_init(): init win32 socket */
|
|
struct hostent *hostbn;
|
|
int temp = 1;
|
|
#endif
|
|
|
|
if (!settings.meta_on) {
|
|
metafd = -1;
|
|
return;
|
|
}
|
|
|
|
if (isdigit(settings.meta_server[0]))
|
|
sock.sin_addr.s_addr = inet_addr(settings.meta_server);
|
|
else {
|
|
struct hostent *hostbn = gethostbyname(settings.meta_server);
|
|
|
|
if (hostbn == NULL) {
|
|
LOG(llevDebug, "metaserver_init: Unable to resolve hostname %s\n", settings.meta_server);
|
|
return;
|
|
}
|
|
memcpy(&sock.sin_addr, hostbn->h_addr, hostbn->h_length);
|
|
}
|
|
#ifdef WIN32 /* ***win32 metaserver_init(): init win32 socket */
|
|
ioctlsocket(metafd, FIONBIO , &temp);
|
|
#else
|
|
fcntl(metafd, F_SETFL, O_NONBLOCK);
|
|
#endif
|
|
if ((metafd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
|
|
LOG(llevDebug, "metaserver_init: Unable to create socket, err %d\n", errno);
|
|
return;
|
|
}
|
|
sock.sin_family = AF_INET;
|
|
sock.sin_port = htons(settings.meta_port);
|
|
|
|
/* No hostname specified, so lets try to figure one out */
|
|
if (settings.meta_host[0] == 0) {
|
|
char hostname[MAX_BUF], domain[MAX_BUF];
|
|
|
|
if (gethostname(hostname, MAX_BUF-1)) {
|
|
LOG(llevDebug, "metaserver_init: gethostname failed - will not report hostname\n");
|
|
return;
|
|
}
|
|
|
|
#ifdef WIN32 /* ***win32 metaserver_init(): gethostbyname! */
|
|
hostbn = gethostbyname(hostname);
|
|
if (hostbn != (struct hostent *)NULL) /* quick hack */
|
|
memcpy(domain, hostbn->h_addr, hostbn->h_length);
|
|
|
|
if (hostbn == (struct hostent *)NULL) {
|
|
#else
|
|
if (getdomainname(domain, MAX_BUF-1)) {
|
|
#endif /* win32 */
|
|
LOG(llevDebug, "metaserver_init: getdomainname failed - will not report hostname\n");
|
|
return;
|
|
}
|
|
/* Potential overrun here but unlikely to occur */
|
|
sprintf(settings.meta_host, "%s.%s", hostname, domain);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates our info in the metaserver
|
|
* Note that this is used for both metaserver1 and metaserver2 -
|
|
* for metaserver2, it just copies dynamic data into private
|
|
* data structure, doing locking in the process.
|
|
*/
|
|
void metaserver_update(void) {
|
|
char data[MAX_BUF], num_players = 0;
|
|
player *pl;
|
|
|
|
/* We could use socket_info.nconns, but that is not quite as accurate,
|
|
* as connections in the progress of being established, are listening
|
|
* but don't have a player, etc. The checks below are basically the
|
|
* same as for the who commands with the addition that WIZ, AFK, and BOT
|
|
* players are not counted.
|
|
*/
|
|
for (pl = first_player; pl != NULL; pl = pl->next) {
|
|
if (pl->ob->map == NULL)
|
|
continue;
|
|
if (pl->hidden)
|
|
continue;
|
|
if (QUERY_FLAG(pl->ob, FLAG_WIZ))
|
|
continue;
|
|
if (QUERY_FLAG(pl->ob, FLAG_AFK))
|
|
continue;
|
|
if (pl->state != ST_PLAYING && pl->state != ST_GET_PARTY_PASSWORD)
|
|
continue;
|
|
if (pl->socket.is_bot)
|
|
continue;
|
|
num_players++;
|
|
}
|
|
|
|
/* Only do this if we have a valid connection */
|
|
if (metafd != -1) {
|
|
snprintf(data, sizeof(data), "%s|%d|%s|%s|%d|%d|%ld", settings.meta_host, num_players,
|
|
FULL_VERSION,
|
|
settings.meta_comment, cst_tot.ibytes, cst_tot.obytes,
|
|
(long)time(NULL)-cst_tot.time_start);
|
|
if (sendto(metafd, data, strlen(data), 0, (struct sockaddr *)&sock, sizeof(sock)) < 0) {
|
|
LOG(llevDebug, "metaserver_update: sendto failed, err = %d\n", errno);
|
|
}
|
|
}
|
|
|
|
/* Everything inside the pthread lock/unlock is related
|
|
* to metaserver2 synchronization.
|
|
*/
|
|
pthread_mutex_lock(&ms2_info_mutex);
|
|
metaserver2_updateinfo.num_players = num_players;
|
|
metaserver2_updateinfo.in_bytes = cst_tot.ibytes;
|
|
metaserver2_updateinfo.out_bytes = cst_tot.obytes;
|
|
metaserver2_updateinfo.uptime = (long)time(NULL)-cst_tot.time_start;
|
|
pthread_mutex_unlock(&ms2_info_mutex);
|
|
|
|
}
|
|
|
|
/**
|
|
* Start of metaserver2 logic
|
|
* Note: All static structures in this file should be treated as strictly
|
|
* private. The metaserver2 update logic runs in its own thread,
|
|
* so if something needs to modify these structures, proper locking
|
|
* is needed.
|
|
*/
|
|
|
|
/**
|
|
* This is a linked list of all the metaservers -
|
|
* never really know how many we have.
|
|
*/
|
|
|
|
typedef struct _MetaServer2 {
|
|
char *hostname;
|
|
struct _MetaServer2 *next;
|
|
} MetaServer2;
|
|
|
|
static MetaServer2 *metaserver2;
|
|
|
|
/**
|
|
* LocalMeta2Info basically holds all the non metaserver2 information
|
|
* that we read from the metaserver2 file. Could just have
|
|
* individual variables, but doing it as a structure is cleaner
|
|
* I think, and might be more flexible if in the future, we
|
|
* want to pass this information to other functions (like plugin
|
|
* or something)
|
|
*/
|
|
typedef struct _LocalMeta2Info {
|
|
int notification; /* if true, do updates to metaservers */
|
|
char *hostname; /* Hostname of this server */
|
|
int portnumber; /* Portnumber of this server */
|
|
char *html_comment; /* html comment to send to metaservers */
|
|
char *text_comment; /* text comment to send to metaservers */
|
|
char *archbase; /* Different sources for arches, maps */
|
|
char *mapbase; /* and server */
|
|
char *codebase;
|
|
char *flags; /* Short flags to send to metaserver */
|
|
} LocalMeta2Info;
|
|
|
|
static LocalMeta2Info local_info;
|
|
|
|
/* These two are globals, but we declare them here. */
|
|
pthread_mutex_t ms2_info_mutex;
|
|
|
|
MetaServer2_UpdateInfo metaserver2_updateinfo;
|
|
|
|
/**
|
|
* This frees any data associated with the MetaServer2 info,
|
|
* including the pointer itself. Caller is responsible for updating
|
|
* pointers (ms->next) - really only used when wanting to free
|
|
* all data.
|
|
*/
|
|
static void free_metaserver2(MetaServer2 *ms) {
|
|
free(ms->hostname);
|
|
free(ms);
|
|
}
|
|
|
|
/**
|
|
* This initializes the metaserver2 logic - it reads
|
|
* the metaserver2 file, storing the values
|
|
* away. Note that it may be possible/desirable for the
|
|
* server to re-read the values and restart connections (for example,
|
|
* a new metaserver has been added and you want to start updates to
|
|
* it immediately and not restart the server). Because of that,
|
|
* there is some extra logic (has_init) to try to take that
|
|
* into account.
|
|
*
|
|
* @return
|
|
* 1 if we will be updating the metaserver, 0 if no
|
|
* metaserver updates
|
|
*/
|
|
int metaserver2_init(void) {
|
|
static int has_init = 0;
|
|
FILE *fp;
|
|
char buf[MAX_BUF], *cp;
|
|
MetaServer2 *ms2, *msnext;
|
|
int comp;
|
|
pthread_t thread_id;
|
|
|
|
#ifdef HAVE_CURL_CURL_H
|
|
if (!has_init) {
|
|
memset(&local_info, 0, sizeof(LocalMeta2Info));
|
|
memset(&metaserver2_updateinfo, 0, sizeof(MetaServer2_UpdateInfo));
|
|
|
|
local_info.portnumber = settings.csport;
|
|
metaserver2 = NULL;
|
|
pthread_mutex_init(&ms2_info_mutex, NULL);
|
|
curl_global_init(CURL_GLOBAL_ALL);
|
|
} else {
|
|
local_info.notification = 0;
|
|
if (local_info.hostname)
|
|
FREE_AND_CLEAR(local_info.hostname);
|
|
if (local_info.html_comment)
|
|
FREE_AND_CLEAR(local_info.html_comment);
|
|
if (local_info.text_comment)
|
|
FREE_AND_CLEAR(local_info.text_comment);
|
|
if (local_info.archbase)
|
|
FREE_AND_CLEAR(local_info.archbase);
|
|
if (local_info.mapbase)
|
|
FREE_AND_CLEAR(local_info.mapbase);
|
|
if (local_info.codebase)
|
|
FREE_AND_CLEAR(local_info.codebase);
|
|
if (local_info.flags)
|
|
FREE_AND_CLEAR(local_info.flags);
|
|
for (ms2 = metaserver2; ms2; ms2 = msnext) {
|
|
msnext = ms2->next;
|
|
free_metaserver2(ms2);
|
|
}
|
|
metaserver2 = NULL;
|
|
}
|
|
#endif
|
|
|
|
/* Now load up the values from the file */
|
|
snprintf(buf, sizeof(buf), "%s/metaserver2", settings.confdir);
|
|
|
|
if ((fp = open_and_uncompress(buf, 0, &comp)) == NULL) {
|
|
LOG(llevError, "Warning: No metaserver2 file found\n");
|
|
return 0;
|
|
}
|
|
while (fgets(buf, MAX_BUF-1, fp) != NULL) {
|
|
if (buf[0] == '#')
|
|
continue;
|
|
/* eliminate newline */
|
|
if ((cp = strrchr(buf, '\n')) != NULL)
|
|
*cp = '\0';
|
|
|
|
/* Skip over empty lines */
|
|
if (buf[0] == 0)
|
|
continue;
|
|
|
|
/* Find variable pairs */
|
|
|
|
if ((cp = strpbrk(buf, " \t")) != NULL) {
|
|
while (isspace(*cp))
|
|
*cp++ = 0;
|
|
} else {
|
|
/* This makes it so we don't have to do NULL checks against
|
|
* cp everyplace
|
|
*/
|
|
cp = "";
|
|
}
|
|
|
|
if (!strcasecmp(buf, "metaserver2_notification")) {
|
|
if (!strcasecmp(cp, "on") || !strcasecmp(cp, "true")) {
|
|
local_info.notification = TRUE;
|
|
} else if (!strcasecmp(cp, "off") || !strcasecmp(cp, "false")) {
|
|
local_info.notification = FALSE;
|
|
} else {
|
|
LOG(llevError, "metaserver2: Unknown value for metaserver2_notification: %s\n", cp);
|
|
}
|
|
} else if (!strcasecmp(buf, "metaserver2_server")) {
|
|
if (*cp != 0) {
|
|
ms2 = calloc(1, sizeof(MetaServer2));
|
|
ms2->hostname = strdup(cp);
|
|
ms2->next = metaserver2;
|
|
metaserver2 = ms2;
|
|
} else {
|
|
LOG(llevError, "metaserver2: metaserver2_server must have a value.\n");
|
|
}
|
|
} else if (!strcasecmp(buf, "localhostname")) {
|
|
if (*cp != 0) {
|
|
local_info.hostname = strdup_local(cp);
|
|
} else {
|
|
LOG(llevError, "metaserver2: localhostname must have a value.\n");
|
|
}
|
|
} else if (!strcasecmp(buf, "portnumber")) {
|
|
if (*cp != 0) {
|
|
local_info.portnumber = atoi(cp);
|
|
} else {
|
|
LOG(llevError, "metaserver2: portnumber must have a value.\n");
|
|
}
|
|
/* For the following values, it is easier to make sure
|
|
* the pointers are set to something, even if it is a blank
|
|
* string, so don't care if there is data in the string or not.
|
|
*/
|
|
} else if (!strcasecmp(buf, "html_comment")) {
|
|
local_info.html_comment = strdup(cp);
|
|
} else if (!strcasecmp(buf, "text_comment")) {
|
|
local_info.text_comment = strdup(cp);
|
|
} else if (!strcasecmp(buf, "archbase")) {
|
|
local_info.archbase = strdup(cp);
|
|
} else if (!strcasecmp(buf, "mapbase")) {
|
|
local_info.mapbase = strdup(cp);
|
|
} else if (!strcasecmp(buf, "codebase")) {
|
|
local_info.codebase = strdup(cp);
|
|
} else if (!strcasecmp(buf, "flags")) {
|
|
local_info.flags = strdup(cp);
|
|
} else {
|
|
LOG(llevError, "Unknown value in metaserver2 file: %s\n", buf);
|
|
}
|
|
}
|
|
close_and_delete(fp, comp);
|
|
|
|
/* If no hostname is set, can't do updates */
|
|
if (!local_info.hostname)
|
|
local_info.notification = 0;
|
|
|
|
#ifndef HAVE_CURL_CURL_H
|
|
if (local_info.notification) {
|
|
LOG(llevError, "metaserver2 file is set to do notification, but libcurl is not found.\n");
|
|
LOG(llevError, "Either fix your compilation, or turn of metaserver2 notification in \n");
|
|
LOG(llevError, "the %s/metaserver2 file.\n", settings.confdir);
|
|
LOG(llevError, "Exiting program.\n");
|
|
exit(1);
|
|
}
|
|
#endif
|
|
|
|
if (local_info.notification) {
|
|
/* As noted above, it is much easier for the rest of the code
|
|
* to not have to check for null pointers. So we do that
|
|
* here, and anything that is null, we just allocate
|
|
* an empty string.
|
|
*/
|
|
if (!local_info.html_comment)
|
|
local_info.html_comment = strdup("");
|
|
if (!local_info.text_comment)
|
|
local_info.text_comment = strdup("");
|
|
if (!local_info.archbase)
|
|
local_info.archbase = strdup("");
|
|
if (!local_info.mapbase)
|
|
local_info.mapbase = strdup("");
|
|
if (!local_info.codebase)
|
|
local_info.codebase = strdup("");
|
|
if (!local_info.flags)
|
|
local_info.flags = strdup("");
|
|
|
|
comp = pthread_create(&thread_id, NULL, metaserver2_thread, NULL);
|
|
if (comp) {
|
|
LOG(llevError, "metaserver2_init: return code from pthread_create() is %d\n", comp);
|
|
|
|
/* Effectively true - we're not going to update the metaserver */
|
|
local_info.notification = 0;
|
|
}
|
|
}
|
|
return local_info.notification;
|
|
}
|
|
|
|
/**
|
|
* Handles writing of HTTP request data from the metaserver2.
|
|
* We treat the data as a string. We should really pay attention to the
|
|
* header data, and do something clever if we get 404 codes
|
|
* or the like.
|
|
*/
|
|
static size_t metaserver2_writer(void *ptr, size_t size, size_t nmemb, void *data) {
|
|
size_t realsize = size*nmemb;
|
|
|
|
LOG(llevDebug, "metaserver2_writer- Start of text:\n%s\n", (const char*)ptr);
|
|
LOG(llevDebug, "metaserver2_writer- End of text:\n");
|
|
|
|
return realsize;
|
|
}
|
|
|
|
|
|
/**
|
|
* This sends an update to the various metaservers.
|
|
* It generates the form, and then sends it to the
|
|
* server
|
|
*/
|
|
static void metaserver2_updates(void) {
|
|
#ifdef HAVE_CURL_CURL_H
|
|
MetaServer2 *ms2;
|
|
struct curl_httppost *formpost = NULL;
|
|
struct curl_httppost *lastptr = NULL;
|
|
char buf[MAX_BUF];
|
|
|
|
/* First, fill in the form - note that everything has to be a string,
|
|
* so we convert as needed with snprintf.
|
|
* The order of fields here really isn't important.
|
|
* The string after CURLFORM_COPYNAME is the name of the POST variable
|
|
* as the
|
|
*/
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "hostname",
|
|
CURLFORM_COPYCONTENTS, local_info.hostname,
|
|
CURLFORM_END);
|
|
|
|
snprintf(buf, MAX_BUF-1, "%d", local_info.portnumber);
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "port",
|
|
CURLFORM_COPYCONTENTS, buf,
|
|
CURLFORM_END);
|
|
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "html_comment",
|
|
CURLFORM_COPYCONTENTS, local_info.html_comment,
|
|
CURLFORM_END);
|
|
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "text_comment",
|
|
CURLFORM_COPYCONTENTS, local_info.text_comment,
|
|
CURLFORM_END);
|
|
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "archbase",
|
|
CURLFORM_COPYCONTENTS, local_info.archbase,
|
|
CURLFORM_END);
|
|
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "mapbase",
|
|
CURLFORM_COPYCONTENTS, local_info.mapbase,
|
|
CURLFORM_END);
|
|
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "codebase",
|
|
CURLFORM_COPYCONTENTS, local_info.codebase,
|
|
CURLFORM_END);
|
|
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "flags",
|
|
CURLFORM_COPYCONTENTS, local_info.flags,
|
|
CURLFORM_END);
|
|
|
|
pthread_mutex_lock(&ms2_info_mutex);
|
|
|
|
snprintf(buf, MAX_BUF-1, "%d", metaserver2_updateinfo.num_players);
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "num_players",
|
|
CURLFORM_COPYCONTENTS, buf,
|
|
CURLFORM_END);
|
|
|
|
snprintf(buf, MAX_BUF-1, "%d", metaserver2_updateinfo.in_bytes);
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "in_bytes",
|
|
CURLFORM_COPYCONTENTS, buf,
|
|
CURLFORM_END);
|
|
|
|
snprintf(buf, MAX_BUF-1, "%d", metaserver2_updateinfo.out_bytes);
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "out_bytes",
|
|
CURLFORM_COPYCONTENTS, buf,
|
|
CURLFORM_END);
|
|
|
|
snprintf(buf, MAX_BUF-1, "%ld", metaserver2_updateinfo.uptime);
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "uptime",
|
|
CURLFORM_COPYCONTENTS, buf,
|
|
CURLFORM_END);
|
|
|
|
pthread_mutex_unlock(&ms2_info_mutex);
|
|
|
|
/* Following few fields are global variables,
|
|
* but are really defines, so won't ever change.
|
|
*/
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "version",
|
|
CURLFORM_COPYCONTENTS, FULL_VERSION,
|
|
CURLFORM_END);
|
|
|
|
snprintf(buf, MAX_BUF-1, "%d", VERSION_SC);
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "sc_version",
|
|
CURLFORM_COPYCONTENTS, buf,
|
|
CURLFORM_END);
|
|
|
|
snprintf(buf, MAX_BUF-1, "%d", VERSION_CS);
|
|
curl_formadd(&formpost, &lastptr,
|
|
CURLFORM_COPYNAME, "cs_version",
|
|
CURLFORM_COPYCONTENTS, buf,
|
|
CURLFORM_END);
|
|
|
|
for (ms2 = metaserver2; ms2; ms2 = ms2->next) {
|
|
CURL *curl;
|
|
CURLcode res;
|
|
|
|
curl = curl_easy_init();
|
|
if (curl) {
|
|
/* what URL that receives this POST */
|
|
curl_easy_setopt(curl, CURLOPT_URL, ms2->hostname);
|
|
curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
|
|
|
|
/* Almost always, we will get HTTP data returned
|
|
* to us - instead of it going to stderr,
|
|
* we want to take care of it ourselves.
|
|
*/
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, metaserver2_writer);
|
|
res = curl_easy_perform(curl);
|
|
|
|
if (res)
|
|
fprintf(stderr, "easy_perform got error %d\n", res);
|
|
|
|
/* always cleanup */
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
}
|
|
/* then cleanup the formpost chain */
|
|
curl_formfree(formpost);
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* metserver2_thread is the function called from pthread_create.
|
|
* it is a trivial function - it just sleeps and calls
|
|
* the update function. The sleep time here is really
|
|
* quite arbitrary, but once a minute is probably often
|
|
* enough. A better approach might be to
|
|
* do a time() call and see how long the update takes,
|
|
* and sleep according to that.
|
|
*
|
|
* @return
|
|
* This function should never return/exit.
|
|
*/
|
|
|
|
void *metaserver2_thread(void *junk) {
|
|
while (1) {
|
|
metaserver2_updates();
|
|
sleep(60);
|
|
}
|
|
}
|