Hi Daniel,
Sorry for top-posting, was not aware of how to turn off digest mode. Now I
have turned off digest mode by login into libcurl account.
I have observed that epoll_wait() function alway's waiting for given
timeout for the first iteration (no any event get ready) and in next
successive iteration events are available. My endpoint return response
within 50ms, so why epoll_wait() not return me any event within specified
timeout (currently its 200ms).
As we already given a call to *curl_multi_socket_action(multi,
CURL_SOCKET_TIMEOUT, 0, &running_handles), *why data transfer is not
started and why epoll_wait() is waiting for the complete timeout without
returning any event ready. I'm missing any call before epoll_wait() ?
I have attached my implementation.
On Mon, Jul 24, 2017 at 10:14 PM, Daniel Stenberg <[email protected]> wrote:
> On Mon, 24 Jul 2017, Tushar Pathare via curl-library wrote:
>
> Thank you for the reply.
>>
>
> You're still top-posting and quoting a series of unrelated posts in your
> email. Please don't.
>
> I have removed curl_multi_perform() usage in epoll() implementation and
>> used only curl_multi_socket_action(), but still i'm seeing request drop.
>>
>
> What exactly is a "request drop" according to you?
>
>
Let's say I have configured two thread and each thread making 50 parallel
requests using curl multi handle, so the total number of expected
request hit will be 100. But I'm getting only 85 to 90 request hit on my
request endpoint. So the delta between expected hits and actual hit I'm
considering as request drops. Here now it is between 10-15.
> Also I have reviewed my implementation for error logging, I have checked
>> the return value of each called libcurl library function and added error
>> logging. No any error observed in complete log.
>>
>
> So where do the "drops" happen if they're not errors?
>
>
> I have created epoll fd and multi-curl handle at thread level to make it
>> thread safe.
>>
>
> If I were you, I'd be careful to make it work perfectly fine in a single
> thread first before I try multi-threading...
>
> I have tried single thread implementation and it working fine with single
threaded. No any request drop is observed. 100% request hits.
> I have one more question, in epoll_wait(), when the actual data transfer
>> start?
>>
>
> libcurl tells you what to wait for, you wait for that and then tell
> libcurl what happened. You know when you start to receive data when the
> write callback gets called!
>
> curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, ev_bitmask,
>> &running_handles);
>>
>
> The bitmask argument has no particular meaning when the socket fd argument
> is a timeout, so you can just as well pass a zero instead of a bitmask.
>
>
Yes, I have set ev_bitmask to 0 itself.
Also, your code you showed us didn't include any socket or timeout
> callbacks, which should be two fundamental building blocks in an
> event-based libcurl using app.
>
> I have attached my complete code. I have implemented both
CURLMOPT_SOCKETFUNCTION and CURLMOPT_TIMERFUNCTION.
> while(running_handles > 0 && waiting_time > 0) {
>> struct epoll_event events[MAX_EVENTS];
>> event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, waiting_time);
>>
>
> What's MAX_EVENTS? That looks like a fixed value which seems very odd.
>
> For me, MAX_EVENTS is total number of easy_handle added into multi-handle.
> Are you setting the waiting_time with the timeout callback?
>
> No, I'm measuring spend time in epoll_wait by adding timmer function and
subtract the spend time from waiting time for next iteration.
> for(it = 0; it < event_count; it++) {
>> curl_multi_socket_action(multi, events[it].data.fd, ev_bitmask,
>> &running_handles);
>>
>
> The ev_bitmask looks weird here. It should tell libcurl what event that
> happened on each individual socket. It seems unlikely that numerous sockets
> all had the same event at the same time...
>
> Was using ev_bitmask = 0 for all socket fds. Now I have changed this to
respective bitmask (CURL_CSELECT_IN, CURL_CSELECT_OUT , CURL_CSELECT_ERR).
But still observing request drop.
> --
>
> / daniel.haxx.se
--
Thanks & Regards,
Tushar
#include <string.h>
#include <assert.h>
#include <curl/curl.h>
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <gperftools/profiler.h>
#define DEFAULT_EPOLL_WAIT_TIME 5
#define MAX_URLS 50
#define MAX_EVENTS MAX_URLS
#define MAX_THREADS 300
const int conn_timeout = 30;
struct thread_params {
int th_id; // thread id
int epoll_fd; // epoll fd
int waiting_time; // waiting time for epoll_wait()
int timeouts; // collect number of timeouts
int run_epoll;
int iteration;
};
char* urls[MAX_URLS] = {
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50",
"http://172.16.4.81/dsp?wt=50"
};
int socket_callback(CURL* easy, curl_socket_t fd, int run_epoll, void* u, void* s)
{
struct thread_params* params = (struct thread_params*)u;
int epollFd = params->epoll_fd;
struct epoll_event event;
event.events = 0;
event.data.fd = fd;
// fprintf(stderr, "socket_callback\n");
if (run_epoll == CURL_POLL_REMOVE) {
#ifdef DEBUG
fprintf(stderr, ">>> %s: removing fd=%d\n", __func__, fd);
#endif
int res = epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, &event);
if (res == -1 && errno != EBADF)
fprintf(stderr, "ERROR epoll_ctl(DEL)\n");
return 0;
}
if (run_epoll == CURL_POLL_IN || run_epoll == CURL_POLL_INOUT) {
event.events |= EPOLLIN;
}
if (run_epoll == CURL_POLL_OUT || run_epoll == CURL_POLL_INOUT) {
event.events |= EPOLLOUT;
}
#ifdef DEBUG
fprintf(stderr, ">>> %s: adding fd=%d run_epoll=%d\n", __func__, fd, run_epoll);
#endif
if (event.events != 0) {
int res = epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &event);
if (res == -1)
res = epoll_ctl(epollFd, EPOLL_CTL_MOD, fd, &event);
if (res == -1)
fprintf(stderr, "ERROR epoll_ctl(MOD)\n");
}
return 0;
}
// TODO: use provate timmer
int timer_callback(CURLM* multi, long timeout_ms, void* u)
{
struct thread_params* params = (struct thread_params*)u;
fprintf(stderr, "timer_callback:ThreadId:%d|timeout_ms:%ld\n", params->th_id, timeout_ms);
return 0;
}
int response_writter(void *ptr, size_t size, size_t nmemb, void* userdata) {
fprintf(stderr, "\nIn response_writter\n");
int i = 0;
int len = size * nmemb;
char* cptr = (char*) ptr;
//#if DEBUG
fprintf(stderr, "\n");
for (i = 0; i < len; i++) {
fprintf(stderr, "%c", cptr[i]);
}
fprintf(stderr, "\n");
//#endif
return len;
}
/*Note: POST request are not tested yet */
int set_curl_common_option(CURL *easy, char* resp_buff, int req_timeout) {
int rc_multi = 0;
rc_multi = curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
rc_multi = curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, response_writter);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
rc_multi = curl_easy_setopt(easy, CURLOPT_WRITEDATA, (void*)resp_buff);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
rc_multi = curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, conn_timeout);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
rc_multi = curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, req_timeout);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
rc_multi = curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
rc_multi = curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, NULL);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
rc_multi = curl_easy_setopt(easy, CURLOPT_HEADERDATA, NULL);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
#ifdef DEBUG
rc_multi = curl_easy_setopt(easy, CURLOPT_VERBOSE, 1);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
#endif
/*
rc_multi = curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1);
if (rc_multi != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(rc_multi), __FILE__, __LINE__);
}
*/
return rc_multi;
}
int execute_epoll(CURLM* multi, struct thread_params* params) {
int th_id = params->th_id;
int timeout_counter = 0;
CURLMcode retval;
fprintf(stderr, "INFO: running epoll() implementation\n");
int running_handles = 1;
int it = 0;
int ev_bitmask = 0;
int event_count;
struct timespec t0;
struct timespec t1;
double t0_ms;
double t1_ms;
double t_diff;
retval = curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, ev_bitmask, &running_handles);
if ( retval != CURLM_OK) {
fprintf(stderr, "%d:ERROR curl_multi_socket_action() return error = %d %s:%d\n", th_id, retval,__FILE__, __LINE__);
return 1;
}
while (running_handles > 0 && params->waiting_time > 0) {
struct epoll_event events[MAX_EVENTS];
clock_gettime(CLOCK_MONOTONIC_RAW, &t0);
event_count = epoll_wait(params->epoll_fd, events, MAX_EVENTS, params->waiting_time);
clock_gettime(CLOCK_MONOTONIC_RAW, &t1);
t0_ms = (t0.tv_sec * 1000.0 + (t0.tv_nsec / 1000000.0));
t1_ms = (t1.tv_sec * 1000.0 + (t1.tv_nsec / 1000000.0));
t_diff = t1_ms - t0_ms;
//params->waiting_time -= t_diff;
fprintf(stderr, "%d: epoll_wait: running_handles:%d event_counts:%d | time spend:%lf | params->waiting_time:%d\n", th_id, running_handles, event_count, t_diff, params->waiting_time);
if (event_count == -1) {
fprintf(stderr, "%d:ERROR epoll_wait:%d|%s\n", th_id, errno, strerror(errno));
}
else if (event_count == 0) {
retval = curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, ev_bitmask, &running_handles);
if ( retval != CURLM_OK) {
fprintf(stderr, "%d:ERROR curl_multi_socket_action() return error = %d %s:%d\n", th_id, retval,__FILE__, __LINE__);
break;
}
}
else {
for(it = 0; it < event_count; it++) {
switch(events[it].events) {
case EPOLLIN:
ev_bitmask = CURL_CSELECT_IN;
break;
case EPOLLOUT:
ev_bitmask = CURL_CSELECT_OUT;
break;
case EPOLLERR:
ev_bitmask = CURL_CSELECT_ERR;
break;
default:
ev_bitmask = 0;
}
retval = curl_multi_socket_action(multi, events[it].data.fd, ev_bitmask, &running_handles);
if ( retval != CURLM_OK) {
fprintf(stderr, "%d:ERROR curl_multi_socket_action() return error = %d %s:%d\n", th_id, retval,__FILE__, __LINE__);
break;
}
}
}
if (event_count == 0 || (running_handles > 0 && params->waiting_time <= 0)) {
timeout_counter++;
}
}
fprintf(stderr, "\nwhile end: running_handles:%d|params->waiting_time:%d\n", running_handles, params->waiting_time);
return timeout_counter;
}
int execute_select(CURLM* multi, struct thread_params* params) {
int th_id = params->th_id;
int timeout_counter = 0;
CURLMcode retval = CURLM_OK;
int still_running = 0;
int rc; /* select() return code */
struct timeval timeout_tv;
fd_set fdread;
fd_set fdwrite;
fd_set fdexcep;
int maxfd;
timeout_tv.tv_sec = params->waiting_time/1000;
timeout_tv.tv_usec = (params->waiting_time%1000)*1000;
while((retval = curl_multi_perform(multi, &still_running)) == CURLM_CALL_MULTI_PERFORM);
if ( retval != CURLM_OK) {
fprintf(stderr, "\nERROR curl_multi_perform() return error = %d %s:%d\n", retval,__FILE__, __LINE__);
return 1;
}
while(still_running) {
FD_ZERO(&fdread);
FD_ZERO(&fdwrite);
FD_ZERO(&fdexcep);
retval = curl_multi_fdset(multi, &fdread, &fdwrite, &fdexcep, &maxfd);
if(retval != CURLM_OK) {
fprintf(stderr, "\nERROR curl_multi_fdset() return error = %d %s:%d\n", retval, __FILE__, __LINE__);
return 1;
}
if(maxfd == -1) {
fprintf(stderr, "\nERROR curl_multi_perform maxfd = -1 %s:%d\n", __FILE__, __LINE__);
fprintf(stderr, "INFO: running select() implementation\n");
return 1;
}else {
if(timeout_tv.tv_sec == 0 && timeout_tv.tv_usec == 0) {
fprintf(stderr, "\nERROR tv_sec = 0 and tv_usec = 0 %s:%d\n", __FILE__, __LINE__);
still_running = 0;
rc = 0;
break;
}else {
errno = 0;
rc = select(maxfd+1, &fdread, &fdwrite, &fdexcep, &timeout_tv);
}
}
switch(rc) {
case -1:
fprintf(stderr,"\nERROR:RTB select returned error %s:%d\n",__FILE__,__LINE__);
perror("\nERROR SELECT RTB");
still_running = 0;
break;
case 0:
still_running = 0;
timeout_counter++;
break;
default:
while((retval=curl_multi_perform(multi, &still_running)) == CURLM_CALL_MULTI_PERFORM );
if (retval != CURLM_OK) {
fprintf(stderr, "\nERROR curl_multi_perform() return error = %d %s:%d\n", retval, __FILE__, __LINE__);
return 1;
}
break;
}
}
return timeout_counter;
}
void* do_task(void* ptr) {
struct thread_params* params = (struct thread_params*) ptr;
int it, idx;
int timeout_counter = 0;
CURLMcode retval = CURLM_OK;
CURL* easy[MAX_URLS];
char resp_buff[MAX_URLS][10240];
CURLM* multi = curl_multi_init();
retval = curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, 100);
if ( retval != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(retval), __FILE__, __LINE__);
}
int req_timeout = params->waiting_time;
for(idx = 0; idx < MAX_URLS; idx++) {
easy[idx] = curl_easy_init();
}
for(it = 0; it < params->iteration; it++) {
params->waiting_time = req_timeout;
if(1 == params->run_epoll) {
/* SET socket callback function */
retval = curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION, socket_callback);
if ( retval != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(retval), __FILE__, __LINE__);
return;
}
/* SET socket callback function parameters*/
retval = curl_multi_setopt(multi, CURLMOPT_SOCKETDATA, ptr);
if ( retval != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(retval), __FILE__, __LINE__);
return;
}
/* SET timmer function */
retval = curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, timer_callback);
if ( retval != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(retval), __FILE__, __LINE__);
return;
}
/* SET timmer function parameters */
retval = curl_multi_setopt(multi, CURLMOPT_TIMERDATA, ptr);
if ( retval != CURLM_OK) {
fprintf(stderr,"\nERROR curl_multi_setopt() failed : %s %s:%d\n", curl_multi_strerror(retval), __FILE__, __LINE__);
return;
}
}
for(idx = 0; idx < MAX_URLS; idx++) {
int url_idx = idx % MAX_URLS;
retval = curl_easy_setopt(easy[idx], CURLOPT_URL, urls[url_idx]);
if ( retval != CURLM_OK) {
fprintf(stderr, "\nERROR curl_multi_perform() return error = %d %s:%d\n", retval,__FILE__, __LINE__);
return;
}
// set curl options
//memset((void*)resp_buff[idx], 0, sizeof(resp_buff[idx]));
set_curl_common_option(easy[idx], resp_buff[idx], req_timeout);
retval = curl_multi_add_handle(multi, easy[idx]);
if ( retval != CURLM_OK) {
fprintf(stderr, "\nERROR curl_multi_perform() return error = %d %s:%d\n", retval,__FILE__, __LINE__);
return;
}
}
if(1 == params->run_epoll) { // running epoll() implementation
params->timeouts += execute_epoll(multi, params);
}else { // running select() implementation
params->timeouts += execute_select(multi, params);
}
for(idx = 0; idx < MAX_URLS; idx++) {
retval = curl_multi_remove_handle(multi, easy[idx]);
if (retval != CURLM_OK) {
fprintf(stderr, "\nERROR curl_multi_remove_handle RTB failed with rc = %d %s:%d\n", retval, __FILE__, __LINE__);
return;
}
}
}
// close the multi handle
curl_multi_cleanup(multi);
}
/*
* 1. argv[1] number of threads needs to be created
* 2. argv[2] number of iteration per thread
* 3. argv[3] run_epoll (0/1) (select/epoll)
* 4. argv[4] http request timeout
*/
int main(int argc, char** argv)
{
if(5 != argc) {
fprintf(stderr, "\nUsage: thread_count thread_iteration, run_poll(0/1) req_timeout_ms\n");
return 1;
}
struct timespec t0;
struct timespec t1;
double t0_ms;
double t1_ms;
double t_diff;
int idx = 0;
int ret = 0;
struct thread_params* params = NULL;
int nthreads = atoi(argv[1]);
int iteration = atoi(argv[2]);
int run_epoll = atoi(argv[3]);
int req_timeout = atoi(argv[4]);
if (run_epoll) {
ProfilerStart("/tmp/epoll.prof");
}else{
ProfilerStart("/tmp/select.prof");
}
curl_global_init(CURL_GLOBAL_ALL);
printf("Using %s\n", curl_version());
params = (struct thread_params *) malloc(sizeof(struct thread_params) * nthreads);
if (NULL == params) {
fprintf(stderr, "\nERROR malloc() failed %s:%d\n", __FILE__, __LINE__);
return 1;
}
clock_gettime(CLOCK_MONOTONIC_RAW, &t0);
// Thread start
pthread_t tid[MAX_THREADS];
for (idx = 0; idx < nthreads; idx++) {
params[idx].th_id = idx;
params[idx].waiting_time = req_timeout;
params[idx].timeouts = 0;
params[idx].run_epoll = run_epoll;
params[idx].iteration = iteration;
// create epoll_fd for thread;
params[idx].epoll_fd = epoll_create(50);
if (params[idx].epoll_fd == -1) {
fprintf(stderr, "ERROR epoll_create");
return 1;
}
ret = pthread_create(&tid[idx], NULL, do_task, (void*) (¶ms[idx]));
if (ret != 0) {
fprintf(stderr, "\nERROR pthread_create() ret = %d %s:%d\n", ret, __FILE__, __LINE__);
return 1;
}
}
// Thread wait
for (idx = 0; idx < nthreads; idx++) {
ret = pthread_join(tid[idx], NULL);
if (ret != 0) {
fprintf(stderr, "\nERROR pthread_join() ret = %d %s:%d\n", ret, __FILE__, __LINE__);
return 1;
}
}
curl_global_cleanup();
// cleanup all epoll_fds
for(idx = 0; idx < nthreads; idx++) {
close(params[idx].epoll_fd);
}
printf(">>> bye bye\n");
clock_gettime(CLOCK_MONOTONIC_RAW, &t1);
t0_ms = (t0.tv_sec * 1000.0 + (t0.tv_nsec / 1000000.0));
t1_ms = (t1.tv_sec * 1000.0 + (t1.tv_nsec / 1000000.0));
t_diff = t1_ms - t0_ms;
curl_global_cleanup();
int total_timeouts = 0;
for(idx = 0; idx < nthreads; idx++) {
total_timeouts += params[idx].timeouts;
}
fprintf(stderr, "\nMain thread returning after time %lf ms and total timeouts = %d\n", t_diff, total_timeouts);
ProfilerStop();
return 0;
}
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.haxx.se/mail/etiquette.html