/*************************************************************************** * _ _ ____ _ * Project ___| | | | _ \| | * / __| | | | |_) | | * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * * Copyright (C) Daniel Stenberg, , et al. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at https://curl.se/docs/copyright.html. * * You may opt to use, copy, modify, merge, publish, distribute and/or sell * copies of the Software, and permit persons to whom the Software is * furnished to do so, under the terms of the COPYING file. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * * SPDX-License-Identifier: curl * ***************************************************************************/ #include "curl_setup.h" #include "urldata.h" #include "curl_trc.h" #include "multiif.h" #include "progress.h" #include "transfer.h" #include "curlx/strcopy.h" /* check rate limits within this many recent milliseconds, at minimum. */ #define MIN_RATE_LIMIT_PERIOD 3000 #ifndef CURL_DISABLE_PROGRESS_METER /* Provide a string that is 7 letters long (plus the zero byte). Unit test 1636. */ UNITTEST void time2str(char *r, size_t rsize, curl_off_t seconds); UNITTEST void time2str(char *r, size_t rsize, curl_off_t seconds) { curl_off_t h; if(seconds <= 0) { curlx_strcopy(r, rsize, " ", 7); return; } h = seconds / 3600; if(h <= 99) { curl_off_t m = (seconds - (h * 3600)) / 60; if(h <= 9) { curl_off_t s = (seconds - (h * 3600)) - (m * 60); if(h) curl_msnprintf(r, rsize, "%" FMT_OFF_T ":%02" FMT_OFF_T ":" "%02" FMT_OFF_T, h, m, s); else curl_msnprintf(r, rsize, " %02" FMT_OFF_T ":%02" FMT_OFF_T, m, s); } else curl_msnprintf(r, rsize, "%" FMT_OFF_T "h %02" FMT_OFF_T "m", h, m); } else { curl_off_t d = seconds / 86400; h = (seconds - (d * 86400)) / 3600; if(d <= 99) curl_msnprintf(r, rsize, "%2" FMT_OFF_T "d %02" FMT_OFF_T "h", d, h); else if(d <= 999) curl_msnprintf(r, rsize, "%6" FMT_OFF_T "d", d); else { /* more than 999 days */ curl_off_t m = d / 30; if(m <= 999) curl_msnprintf(r, rsize, "%6" FMT_OFF_T "m", m); else { /* more than 999 months */ curl_off_t y = d / 365; if(y <= 99999) curl_msnprintf(r, rsize, "%6" FMT_OFF_T "y", y); else curlx_strcopy(r, rsize, ">99999y", 7); } } } } /* The point of this function would be to return a string of the input data, but never longer than 6 columns (+ one zero byte). Add suffix k, M, G when suitable... Unit test 1636 */ UNITTEST char *max6out(curl_off_t bytes, char *max6, size_t mlen); UNITTEST char *max6out(curl_off_t bytes, char *max6, size_t mlen) { /* a signed 64-bit value is 8192 petabytes maximum, shown as 8.0E (exabytes)*/ if(bytes < 100000) curl_msnprintf(max6, mlen, "%6" CURL_FORMAT_CURL_OFF_T, bytes); else { const char unit[] = { 'k', 'M', 'G', 'T', 'P', 'E', 0 }; int k = 0; curl_off_t nbytes; curl_off_t rest; do { nbytes = bytes / 1024; if(nbytes < 1000) break; bytes = nbytes; k++; DEBUGASSERT(unit[k]); } while(unit[k]); rest = bytes % 1024; if(nbytes <= 99) /* xx.yyU */ curl_msnprintf(max6, mlen, "%2" CURL_FORMAT_CURL_OFF_T ".%02" CURL_FORMAT_CURL_OFF_T "%c", nbytes, rest * 100 / 1024, unit[k]); else /* xxx.yU */ curl_msnprintf(max6, mlen, "%3" CURL_FORMAT_CURL_OFF_T ".%" CURL_FORMAT_CURL_OFF_T "%c", nbytes, rest * 10 / 1024, unit[k]); } return max6; } #endif static void pgrs_speedinit(struct Curl_easy *data) { memset(&data->state.keeps_speed, 0, sizeof(struct curltime)); } /* * @unittest: 1606 */ UNITTEST CURLcode pgrs_speedcheck(struct Curl_easy *data, const struct curltime *pnow) { if(!data->set.low_speed_time || !data->set.low_speed_limit || Curl_xfer_recv_is_paused(data) || Curl_xfer_send_is_paused(data)) /* A paused transfer is not qualified for speed checks */ return CURLE_OK; if(data->progress.current_speed >= 0) { if(data->progress.current_speed < data->set.low_speed_limit) { if(!data->state.keeps_speed.tv_sec) /* under the limit at this moment */ data->state.keeps_speed = *pnow; else { /* how long has it been under the limit */ timediff_t howlong = curlx_ptimediff_ms(pnow, &data->state.keeps_speed); if(howlong >= data->set.low_speed_time * 1000) { /* too long */ failf(data, "Operation too slow. Less than %" FMT_OFF_T " bytes/sec transferred the last %u seconds", data->set.low_speed_limit, data->set.low_speed_time); return CURLE_OPERATION_TIMEDOUT; } } } else /* faster right now */ data->state.keeps_speed.tv_sec = 0; } /* since low speed limit is enabled, set the expire timer to make this connection's speed get checked again in a second */ Curl_expire(data, 1000, EXPIRE_SPEEDCHECK); return CURLE_OK; } const struct curltime *Curl_pgrs_now(struct Curl_easy *data) { struct curltime *pnow = data->multi ? &data->multi->now : &data->progress.now; curlx_pnow(pnow); return pnow; } /* New proposed interface, 9th of February 2000: pgrsStartNow() - sets start time pgrsSetDownloadSize(x) - known expected download size pgrsSetUploadSize(x) - known expected upload size pgrsSetDownloadCounter() - amount of data currently downloaded pgrsSetUploadCounter() - amount of data currently uploaded pgrsUpdate() - show progress pgrsDone() - transfer complete */ int Curl_pgrsDone(struct Curl_easy *data) { int rc; data->progress.lastshow = 0; rc = Curl_pgrsUpdate(data); /* the final (forced) update */ if(rc) return rc; if(!data->progress.hide && !data->progress.callback) /* only output if we do not use a progress callback and we are not * hidden */ curl_mfprintf(data->set.err, "\n"); return 0; } void Curl_pgrsReset(struct Curl_easy *data) { Curl_pgrsSetUploadCounter(data, 0); data->progress.dl.cur_size = 0; Curl_pgrsSetUploadSize(data, -1); Curl_pgrsSetDownloadSize(data, -1); data->progress.speeder_c = 0; /* reset speed records */ pgrs_speedinit(data); } /* reset the known transfer sizes */ void Curl_pgrsResetTransferSizes(struct Curl_easy *data) { Curl_pgrsSetDownloadSize(data, -1); Curl_pgrsSetUploadSize(data, -1); } void Curl_pgrsRecvPause(struct Curl_easy *data, bool enable) { if(!enable) { data->progress.speeder_c = 0; /* reset speed records */ pgrs_speedinit(data); /* reset low speed measurements */ } } void Curl_pgrsSendPause(struct Curl_easy *data, bool enable) { if(!enable) { data->progress.speeder_c = 0; /* reset speed records */ pgrs_speedinit(data); /* reset low speed measurements */ } } /* * Curl_pgrsTimeWas(). Store the timestamp time at the given label. */ void Curl_pgrsTimeWas(struct Curl_easy *data, timerid timer, struct curltime timestamp) { timediff_t *delta = NULL; switch(timer) { default: case TIMER_NONE: /* mistake filter */ break; case TIMER_STARTOP: /* This is set at the start of a transfer */ data->progress.t_startop = timestamp; data->progress.t_startqueue = timestamp; data->progress.t_postqueue = 0; break; case TIMER_STARTSINGLE: /* This is set at the start of each single transfer */ data->progress.t_startsingle = timestamp; data->progress.is_t_startransfer_set = FALSE; break; case TIMER_POSTQUEUE: /* Queue time is accumulative from all involved redirects */ data->progress.t_postqueue += curlx_ptimediff_us(×tamp, &data->progress.t_startqueue); break; case TIMER_STARTACCEPT: data->progress.t_acceptdata = timestamp; break; case TIMER_NAMELOOKUP: delta = &data->progress.t_nslookup; break; case TIMER_CONNECT: delta = &data->progress.t_connect; break; case TIMER_APPCONNECT: delta = &data->progress.t_appconnect; break; case TIMER_PRETRANSFER: delta = &data->progress.t_pretransfer; break; case TIMER_STARTTRANSFER: delta = &data->progress.t_starttransfer; /* prevent updating t_starttransfer unless: * 1. this is the first time we are setting t_starttransfer * 2. a redirect has occurred since the last time t_starttransfer was set * This prevents repeated invocations of the function from incorrectly * changing the t_starttransfer time. */ if(data->progress.is_t_startransfer_set) { return; } else { data->progress.is_t_startransfer_set = TRUE; break; } case TIMER_POSTRANSFER: delta = &data->progress.t_posttransfer; break; case TIMER_REDIRECT: data->progress.t_redirect = curlx_ptimediff_us(×tamp, &data->progress.start); data->progress.t_startqueue = timestamp; break; } if(delta) { timediff_t us = curlx_ptimediff_us(×tamp, &data->progress.t_startsingle); if(us < 1) us = 1; /* make sure at least one microsecond passed */ *delta += us; } } /* * Curl_pgrsTime(). Store the current time at the given label. This fetches a * fresh "now" and returns it. * * @unittest: 1399 */ void Curl_pgrsTime(struct Curl_easy *data, timerid timer) { Curl_pgrsTimeWas(data, timer, *Curl_pgrs_now(data)); } void Curl_pgrsStartNow(struct Curl_easy *data) { struct Progress *p = &data->progress; p->speeder_c = 0; /* reset the progress meter display */ p->start = *Curl_pgrs_now(data); p->is_t_startransfer_set = FALSE; p->dl.cur_size = 0; p->ul.cur_size = 0; /* the sizes are unknown at start */ p->dl_size_known = FALSE; p->ul_size_known = FALSE; } void Curl_pgrs_download_inc(struct Curl_easy *data, size_t delta) { if(delta) { data->progress.dl.cur_size += delta; Curl_rlimit_drain(&data->progress.dl.rlimit, delta, Curl_pgrs_now(data)); } } void Curl_pgrs_upload_inc(struct Curl_easy *data, size_t delta) { if(delta) { data->progress.ul.cur_size += delta; Curl_rlimit_drain(&data->progress.ul.rlimit, delta, Curl_pgrs_now(data)); } } /* * Set the number of uploaded bytes so far. */ void Curl_pgrsSetUploadCounter(struct Curl_easy *data, curl_off_t size) { data->progress.ul.cur_size = size; } void Curl_pgrsSetDownloadSize(struct Curl_easy *data, curl_off_t size) { if(size >= 0) { data->progress.dl.total_size = size; data->progress.dl_size_known = TRUE; } else { data->progress.dl.total_size = 0; data->progress.dl_size_known = FALSE; } } void Curl_pgrsSetUploadSize(struct Curl_easy *data, curl_off_t size) { if(size >= 0) { data->progress.ul.total_size = size; data->progress.ul_size_known = TRUE; } else { data->progress.ul.total_size = 0; data->progress.ul_size_known = FALSE; } } void Curl_pgrsEarlyData(struct Curl_easy *data, curl_off_t sent) { data->progress.earlydata_sent = sent; } /* returns the average speed in bytes / second */ static curl_off_t trspeed(curl_off_t size, /* number of bytes */ curl_off_t us) /* microseconds */ { if(us < 1) return size * 1000000; else if(size < CURL_OFF_T_MAX / 1000000) return (size * 1000000) / us; else if(us >= 1000000) return size / (us / 1000000); else return CURL_OFF_T_MAX; } /* returns TRUE if it is time to show the progress meter */ static bool progress_calc(struct Curl_easy *data, const struct curltime *pnow) { struct Progress * const p = &data->progress; int i_next, i_oldest, i_latest; timediff_t duration_ms; curl_off_t amount; /* The time spent so far (from the start) in microseconds */ p->timespent = curlx_ptimediff_us(pnow, &p->start); p->dl.speed = trspeed(p->dl.cur_size, p->timespent); p->ul.speed = trspeed(p->ul.cur_size, p->timespent); if(!p->speeder_c) { /* no previous record exists */ p->speed_amount[0] = p->dl.cur_size + p->ul.cur_size; p->speed_time[0] = *pnow; p->speeder_c++; /* use the overall average at the start */ p->current_speed = p->ul.speed + p->dl.speed; p->lastshow = pnow->tv_sec; return TRUE; } /* We have at least one record now. Where to put the next and * where is the latest one? */ i_next = p->speeder_c % CURL_SPEED_RECORDS; i_latest = (i_next > 0) ? (i_next - 1) : (CURL_SPEED_RECORDS - 1); /* Make a new record only when some time has passed. * Too frequent calls otherwise ruin the history. */ if(curlx_ptimediff_ms(pnow, &p->speed_time[i_latest]) >= 1000) { p->speeder_c++; i_latest = i_next; p->speed_amount[i_latest] = p->dl.cur_size + p->ul.cur_size; p->speed_time[i_latest] = *pnow; } else if(data->req.done) { /* When a transfer is done, and we did not have a current speed * already, update the last record. Otherwise, stay at the speed * we have. The last chunk of data, when rate limiting, would increase * reported speed since it no longer measures a full second. */ if(!p->current_speed) { p->speed_amount[i_latest] = p->dl.cur_size + p->ul.cur_size; p->speed_time[i_latest] = *pnow; } } else { /* transfer ongoing, wait for more time to pass. */ return FALSE; } i_oldest = (p->speeder_c < CURL_SPEED_RECORDS) ? 0 : ((i_latest + 1) % CURL_SPEED_RECORDS); /* How much we transferred between oldest and current records */ amount = p->speed_amount[i_latest] - p->speed_amount[i_oldest]; /* How long this took */ duration_ms = curlx_ptimediff_ms(&p->speed_time[i_latest], &p->speed_time[i_oldest]); if(duration_ms <= 0) duration_ms = 1; if(amount > (CURL_OFF_T_MAX / 1000)) { /* the 'amount' value is bigger than would fit in 64 bits if multiplied with 1000, so we use the double math for this */ p->current_speed = (curl_off_t)(((double)amount * 1000.0) / (double)duration_ms); } else { /* the 'amount' value is small enough to fit within 32 bits even when multiplied with 1000 */ p->current_speed = amount * 1000 / duration_ms; } if((p->lastshow == pnow->tv_sec) && !data->req.done) return FALSE; p->lastshow = pnow->tv_sec; return TRUE; } #ifndef CURL_DISABLE_PROGRESS_METER struct pgrs_estimate { curl_off_t secs; curl_off_t percent; }; static curl_off_t pgrs_est_percent(curl_off_t total, curl_off_t cur) { if(total > 10000) return cur / (total / 100); else if(total > 0) return (cur * 100) / total; return 0; } static void pgrs_estimates(struct pgrs_dir *d, bool total_known, struct pgrs_estimate *est) { est->secs = 0; est->percent = 0; if(total_known && (d->speed > 0)) { est->secs = d->total_size / d->speed; est->percent = pgrs_est_percent(d->total_size, d->cur_size); } } static void progress_meter(struct Curl_easy *data) { struct Progress *p = &data->progress; char max6[6][7]; struct pgrs_estimate dl_estm; struct pgrs_estimate ul_estm; struct pgrs_estimate total_estm; curl_off_t total_cur_size; curl_off_t total_expected_size; curl_off_t dl_size; char time_left[8]; char time_total[8]; char time_spent[8]; curl_off_t cur_secs = (curl_off_t)p->timespent / 1000000; /* seconds */ if(!p->headers_out) { if(data->state.resume_from) { curl_mfprintf(data->set.err, "** Resuming transfer from byte position %" FMT_OFF_T "\n", data->state.resume_from); } curl_mfprintf(data->set.err, " %% Total %% Received %% Xferd Average Speed " "Time Time Time Current\n" " Dload Upload " "Total Spent Left Speed\n"); p->headers_out = TRUE; /* headers are shown */ } /* Figure out the estimated time of arrival for upload and download */ pgrs_estimates(&p->ul, (bool)p->ul_size_known, &ul_estm); pgrs_estimates(&p->dl, (bool)p->dl_size_known, &dl_estm); /* Since both happen at the same time, total expected duration is max. */ total_estm.secs = CURLMAX(ul_estm.secs, dl_estm.secs); /* create the three time strings */ time2str(time_left, sizeof(time_left), total_estm.secs > 0 ? (total_estm.secs - cur_secs) : 0); time2str(time_total, sizeof(time_total), total_estm.secs); time2str(time_spent, sizeof(time_spent), cur_secs); /* Get the total amount of data expected to get transferred */ total_expected_size = p->ul_size_known ? p->ul.total_size : p->ul.cur_size; dl_size = p->dl_size_known ? p->dl.total_size : p->dl.cur_size; /* integer overflow check */ if((CURL_OFF_T_MAX - total_expected_size) < dl_size) total_expected_size = CURL_OFF_T_MAX; /* capped */ else total_expected_size += dl_size; /* We have transferred this much so far */ total_cur_size = p->dl.cur_size + p->ul.cur_size; /* Get the percentage of data transferred so far */ total_estm.percent = pgrs_est_percent(total_expected_size, total_cur_size); curl_mfprintf(data->set.err, "\r" "%3" FMT_OFF_T " %s " "%3" FMT_OFF_T " %s " "%3" FMT_OFF_T " %s %s %s %s %s %s %s", total_estm.percent, /* 3 letters */ /* total % */ max6out(total_expected_size, max6[2], sizeof(max6[2])), /* total size */ dl_estm.percent, /* 3 letters */ /* rcvd % */ max6out(p->dl.cur_size, max6[0], sizeof(max6[0])), /* rcvd size */ ul_estm.percent, /* 3 letters */ /* xfer % */ max6out(p->ul.cur_size, max6[1], sizeof(max6[1])), /* xfer size */ max6out(p->dl.speed, max6[3], sizeof(max6[3])), /* avrg dl speed */ max6out(p->ul.speed, max6[4], sizeof(max6[4])), /* avrg ul speed */ time_total, /* 7 letters */ /* total time */ time_spent, /* 7 letters */ /* time spent */ time_left, /* 7 letters */ /* time left */ max6out(p->current_speed, max6[5], sizeof(max6[5])) /* current speed */ ); /* we flush the output stream to make it appear as soon as possible */ fflush(data->set.err); } #else /* CURL_DISABLE_PROGRESS_METER */ #define progress_meter(x) Curl_nop_stmt #endif /* * Curl_pgrsUpdate() returns 0 for success or the value returned by the * progress callback! */ static CURLcode pgrsupdate(struct Curl_easy *data, bool showprogress) { if(!data->progress.hide) { if(data->set.fxferinfo) { int result; /* There is a callback set, call that */ Curl_set_in_callback(data, TRUE); result = data->set.fxferinfo(data->set.progress_client, data->progress.dl.total_size, data->progress.dl.cur_size, data->progress.ul.total_size, data->progress.ul.cur_size); Curl_set_in_callback(data, FALSE); if(result != CURL_PROGRESSFUNC_CONTINUE) { if(result) { failf(data, "Callback aborted"); return CURLE_ABORTED_BY_CALLBACK; } return CURLE_OK; } } else if(data->set.fprogress) { int result; /* The older deprecated callback is set, call that */ Curl_set_in_callback(data, TRUE); result = data->set.fprogress(data->set.progress_client, (double)data->progress.dl.total_size, (double)data->progress.dl.cur_size, (double)data->progress.ul.total_size, (double)data->progress.ul.cur_size); Curl_set_in_callback(data, FALSE); if(result != CURL_PROGRESSFUNC_CONTINUE) { if(result) { failf(data, "Callback aborted"); return CURLE_ABORTED_BY_CALLBACK; } return CURLE_OK; } } if(showprogress) progress_meter(data); } return CURLE_OK; } static CURLcode pgrs_update(struct Curl_easy *data, const struct curltime *pnow) { bool showprogress = progress_calc(data, pnow); return pgrsupdate(data, showprogress); } CURLcode Curl_pgrsUpdate(struct Curl_easy *data) { return pgrs_update(data, Curl_pgrs_now(data)); } CURLcode Curl_pgrsCheck(struct Curl_easy *data) { CURLcode result; result = pgrs_update(data, Curl_pgrs_now(data)); if(!result && !data->req.done) result = pgrs_speedcheck(data, Curl_pgrs_now(data)); return result; } /* * Update all progress, do not do progress meter/callbacks. */ void Curl_pgrsUpdate_nometer(struct Curl_easy *data) { (void)progress_calc(data, Curl_pgrs_now(data)); }