diff mbox series

[v5] channel_curl: Improve tracking of download progress

Message ID 20210121133707.111032-1-sava.jakovljev@teufel.de
State Accepted
Headers show
Series [v5] channel_curl: Improve tracking of download progress | expand

Commit Message

Sava Jakovljev Jan. 21, 2021, 1:37 p.m. UTC
* Compile xferinfo_legacy function only for version of libcurl
  not supporting newer XFERINFOFUNCTION option.
* Use either XFERINFOFUNCTION or PROGRESSFUNCTION libcurl option
  depending on libcurl version - use XFERINFOFUNCTION if possible.
* Introduce a structure containing information about file being
  downloaded.
* Move setting progress tracking options into separate functions.
* Improve callback for tracking download progress - correctly track
  percentage using valid variable and fix passing address of a
  temporary variable as callback argument.

Signed-off-by: Sava Jakovljev <sava.jakovljev@teufel.de>
---
When a server doesn't insert Content-Length header, there is no sense in
enabling libcurl option XFERINFOFUNCTION/PROGRESSFUNCTION.
This change also solves a bug that currently exists, where an address of
a local variable 'percent' is passed to libcurl, which in turn gives it
as an argument to a callback specified with
XFERINFOFUNCTION/PROGRESSFUNCTION.
Also, this change will use XFERINFOFUNCTION if libcurl supports it, and
PROGRESSFUNCTION otherwise, and not both of them will be set, as the
situation is right now. Legacy xferinfo callback function is now
compiled only when libcurl does not support XFERINFOFUNCTION.
Secondly, download progress tracking is now enabled only for get_file
HTTP request and if server has correctly reported total download file
size.

v2 of this patch deals with compiler warning and removes unused fields
of introduced download_callback_data_t structure. Also, code is cleaned
up.

v3 of this patch deals with grammar errors in the git commit message,
with no functional changes to the patch itself.

v4 of this patch addresses review comments: xferinfo callback has been
simplified, but download_callback_data_t structure has been kept, in
order to enable possible future extensions.

v5 of this patch addresses issued found by Coverty tool. 
Secondly, TRACE logs of download progress, in xferinfo function, are replaced
with DEBUG logs.

Best regards,
Sava Jakovljev

 corelib/channel_curl.c | 124 +++++++++++++++++++++++++++++++----------
 1 file changed, 95 insertions(+), 29 deletions(-)

Comments

Stefano Babic Jan. 23, 2021, 10:13 p.m. UTC | #1
Hi Sava,

On 21.01.21 14:37, Sava Jakovljev wrote:
> * Compile xferinfo_legacy function only for version of libcurl
>    not supporting newer XFERINFOFUNCTION option.
> * Use either XFERINFOFUNCTION or PROGRESSFUNCTION libcurl option
>    depending on libcurl version - use XFERINFOFUNCTION if possible.
> * Introduce a structure containing information about file being
>    downloaded.
> * Move setting progress tracking options into separate functions.
> * Improve callback for tracking download progress - correctly track
>    percentage using valid variable and fix passing address of a
>    temporary variable as callback argument.
> 
> Signed-off-by: Sava Jakovljev <sava.jakovljev@teufel.de>
> ---
> When a server doesn't insert Content-Length header, there is no sense in
> enabling libcurl option XFERINFOFUNCTION/PROGRESSFUNCTION.
> This change also solves a bug that currently exists, where an address of
> a local variable 'percent' is passed to libcurl, which in turn gives it
> as an argument to a callback specified with
> XFERINFOFUNCTION/PROGRESSFUNCTION.
> Also, this change will use XFERINFOFUNCTION if libcurl supports it, and
> PROGRESSFUNCTION otherwise, and not both of them will be set, as the
> situation is right now. Legacy xferinfo callback function is now
> compiled only when libcurl does not support XFERINFOFUNCTION.
> Secondly, download progress tracking is now enabled only for get_file
> HTTP request and if server has correctly reported total download file
> size.
> 
> v2 of this patch deals with compiler warning and removes unused fields
> of introduced download_callback_data_t structure. Also, code is cleaned
> up.
> 
> v3 of this patch deals with grammar errors in the git commit message,
> with no functional changes to the patch itself.
> 
> v4 of this patch addresses review comments: xferinfo callback has been
> simplified, but download_callback_data_t structure has been kept, in
> order to enable possible future extensions.
> 
> v5 of this patch addresses issued found by Coverty tool.
> Secondly, TRACE logs of download progress, in xferinfo function, are replaced
> with DEBUG logs.
> 
> Best regards,
> Sava Jakovljev
> 
>   corelib/channel_curl.c | 124 +++++++++++++++++++++++++++++++----------
>   1 file changed, 95 insertions(+), 29 deletions(-)
> 
> diff --git a/corelib/channel_curl.c b/corelib/channel_curl.c
> index a3e785f..f734be3 100644
> --- a/corelib/channel_curl.c
> +++ b/corelib/channel_curl.c
> @@ -55,6 +55,11 @@ typedef struct {
>   	output_data_t *outdata;
>   } write_callback_t;
>   
> +typedef struct {
> +	curl_off_t total_download_size;
> +	uint8_t percent;
> +} download_callback_data_t;
> +
>   
>   /* Prototypes for "internal" functions */
>   /* Note that they're not `static` so that they're callable from unit tests. */
> @@ -414,25 +419,31 @@ static int channel_callback_xferinfo(void *p, curl_off_t dltotal, curl_off_t dln
>   				     curl_off_t __attribute__((__unused__)) ultotal,
>   				     curl_off_t __attribute__((__unused__)) ulnow)
>   {
> -	if ((dltotal <= 0) || (dlnow > dltotal)) {
> +	if ((dltotal <= 0) || (dlnow > dltotal))
>   		return 0;
> -	}
> -	double percent = 100.0 * (dlnow/1024.0) / (dltotal/1024.0);
> -	double *last_percent = (double*)p;
> -	if ((int)*last_percent == (int)percent) {
> +
> +	uint8_t percent = 100.0 * ((double)dlnow / dltotal);
> +	download_callback_data_t *data = (download_callback_data_t*)p;
> +
> +	if (data->percent >= percent)
>   		return 0;
> -	}
> -	*last_percent = percent;
> +	else
> +		data->percent = percent;
> +
> +	DEBUG("Downloaded %d%% (%lu of %lu kB).", percent, dlnow / 1024, dltotal / 1024);
>   	swupdate_download_update(percent, dltotal);
> +
>   	return 0;
>   }
>   
> +#if LIBCURL_VERSION_NUM < 0x072000
>   static int channel_callback_xferinfo_legacy(void *p, double dltotal, double dlnow,
>   					    double ultotal, double ulnow)
>   {
>   	return channel_callback_xferinfo(p, (curl_off_t)dltotal, (curl_off_t)dlnow,
>   					 (curl_off_t)ultotal, (curl_off_t)ulnow);
>   }
> +#endif
>   
>   static size_t channel_callback_headers(char *buffer, size_t size, size_t nitems, void *userdata)
>   {
> @@ -566,28 +577,6 @@ channel_op_res_t channel_set_options(channel_t *this, channel_data_t *channel_da
>   		goto cleanup;
>   	}
>   
> -	double percent = -INFINITY;
> -	if ((curl_easy_setopt(channel_curl->handle, CURLOPT_PROGRESSFUNCTION,
> -			      channel_callback_xferinfo_legacy) != CURLE_OK) ||
> -	    (curl_easy_setopt(channel_curl->handle, CURLOPT_PROGRESSDATA,
> -				  &percent) != CURLE_OK)) {
> -		result = CHANNEL_EINIT;
> -		goto cleanup;
> -	}
> -#if LIBCURL_VERSION_NUM >= 0x072000
> -	if ((curl_easy_setopt(channel_curl->handle, CURLOPT_XFERINFOFUNCTION,
> -			      channel_callback_xferinfo) != CURLE_OK) ||
> -	    (curl_easy_setopt(channel_curl->handle, CURLOPT_XFERINFODATA,
> -				  &percent) != CURLE_OK)) {
> -		result = CHANNEL_EINIT;
> -		goto cleanup;
> -	}
> -#endif
> -	if (curl_easy_setopt(channel_curl->handle, CURLOPT_NOPROGRESS, 0L) != CURLE_OK) {
> -		result = CHANNEL_EINIT;
> -		goto cleanup;
> -	}
> -
>   	if (channel_data->headers) {
>   		/*
>   		 * Setup supply request and receive reply HTTP headers.
> @@ -731,6 +720,73 @@ cleanup:
>   	return result;
>   }
>   
> +static curl_off_t channel_get_total_download_size(channel_curl_t *this,
> +		const char *url)
> +{
> +	assert(this != NULL);
> +	assert(url  != NULL);
> +
> +	curl_off_t size = -1;
> +	if (curl_easy_setopt(this->handle, CURLOPT_URL, url) != CURLE_OK ||
> +		curl_easy_setopt(this->handle, CURLOPT_NOBODY, 1L) != CURLE_OK ||
> +		curl_easy_perform(this->handle) != CURLE_OK)
> +		goto cleanup;
> +
> +	if (curl_easy_getinfo(this->handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T,
> +			&size) != CURLE_OK)
> +		goto cleanup;
> +
> +cleanup:
> +	if (curl_easy_setopt(this->handle, CURLOPT_NOBODY, 0L) != CURLE_OK) {
> +		ERROR("Failed to properly clean-up handle.");
> +		size = -1;
> +	}
> +
> +	return size;
> +}
> +
> +static channel_op_res_t channel_enable_download_progress_tracking(
> +		channel_curl_t *this,
> +		const char *url,
> +		download_callback_data_t *download_data)
> +{
> +	assert(url != NULL);
> +	assert(download_data != NULL);
> +
> +	download_data->percent = 0;
> +
> +	channel_op_res_t result = CHANNEL_OK;
> +	if ((download_data->total_download_size = channel_get_total_download_size(
> +				this, url)) <= 0) {
> +		result = CHANNEL_EINIT;
> +		goto cleanup;
> +	}
> +
> +#if LIBCURL_VERSION_NUM >= 0x072000
> +	if ((curl_easy_setopt(this->handle, CURLOPT_XFERINFOFUNCTION,
> +			      channel_callback_xferinfo) != CURLE_OK) ||
> +	    (curl_easy_setopt(this->handle, CURLOPT_XFERINFODATA,
> +				  download_data) != CURLE_OK)) {
> +		result = CHANNEL_EINIT;
> +		goto cleanup;
> +	}
> +#else
> +	if ((curl_easy_setopt(this->handle, CURLOPT_PROGRESSFUNCTION,
> +			      channel_callback_xferinfo_legacy) != CURLE_OK) ||
> +	    (curl_easy_setopt(this->handle, CURLOPT_PROGRESSDATA,
> +				  download_data) != CURLE_OK)) {
> +		result = CHANNEL_EINIT;
> +		goto cleanup;
> +	}
> +#endif
> +	if (curl_easy_setopt(this->handle, CURLOPT_NOPROGRESS, 0L) != CURLE_OK) {
> +		result = CHANNEL_EINIT;
> +		goto cleanup;
> +	}
> +cleanup:
> +	return result;
> +}
> +
>   static size_t put_read_callback(void *ptr, size_t size, size_t nmemb, void *data)
>   {
>   	channel_data_t *channel_data = (channel_data_t *)data;
> @@ -1030,6 +1086,16 @@ channel_op_res_t channel_get_file(channel_t *this, void *data)
>   		goto cleanup_header;
>   	}
>   
> +	download_callback_data_t download_data;
> +	if (channel_enable_download_progress_tracking(channel_curl,
> +				channel_data->url,
> +				&download_data) == CHANNEL_EINIT) {
> +		WARN("Failed to get total download size for URL %s.",
> +				channel_data->url);
> +	} else
> +		INFO("Total download size is %lu kB.",
> +				download_data.total_download_size / 1024);
> +
>   	if (curl_easy_setopt(channel_curl->handle, CURLOPT_CUSTOMREQUEST, "GET") !=
>   	    CURLE_OK) {
>   		ERROR("Set GET channel method option failed.");
> 


Applied to -master, thanks !

Best regards,
Stefano Babic
diff mbox series

Patch

diff --git a/corelib/channel_curl.c b/corelib/channel_curl.c
index a3e785f..f734be3 100644
--- a/corelib/channel_curl.c
+++ b/corelib/channel_curl.c
@@ -55,6 +55,11 @@  typedef struct {
 	output_data_t *outdata;
 } write_callback_t;
 
+typedef struct {
+	curl_off_t total_download_size;
+	uint8_t percent;
+} download_callback_data_t;
+
 
 /* Prototypes for "internal" functions */
 /* Note that they're not `static` so that they're callable from unit tests. */
@@ -414,25 +419,31 @@  static int channel_callback_xferinfo(void *p, curl_off_t dltotal, curl_off_t dln
 				     curl_off_t __attribute__((__unused__)) ultotal,
 				     curl_off_t __attribute__((__unused__)) ulnow)
 {
-	if ((dltotal <= 0) || (dlnow > dltotal)) {
+	if ((dltotal <= 0) || (dlnow > dltotal))
 		return 0;
-	}
-	double percent = 100.0 * (dlnow/1024.0) / (dltotal/1024.0);
-	double *last_percent = (double*)p;
-	if ((int)*last_percent == (int)percent) {
+
+	uint8_t percent = 100.0 * ((double)dlnow / dltotal);
+	download_callback_data_t *data = (download_callback_data_t*)p;
+
+	if (data->percent >= percent)
 		return 0;
-	}
-	*last_percent = percent;
+	else
+		data->percent = percent;
+
+	DEBUG("Downloaded %d%% (%lu of %lu kB).", percent, dlnow / 1024, dltotal / 1024);
 	swupdate_download_update(percent, dltotal);
+
 	return 0;
 }
 
+#if LIBCURL_VERSION_NUM < 0x072000
 static int channel_callback_xferinfo_legacy(void *p, double dltotal, double dlnow,
 					    double ultotal, double ulnow)
 {
 	return channel_callback_xferinfo(p, (curl_off_t)dltotal, (curl_off_t)dlnow,
 					 (curl_off_t)ultotal, (curl_off_t)ulnow);
 }
+#endif
 
 static size_t channel_callback_headers(char *buffer, size_t size, size_t nitems, void *userdata)
 {
@@ -566,28 +577,6 @@  channel_op_res_t channel_set_options(channel_t *this, channel_data_t *channel_da
 		goto cleanup;
 	}
 
-	double percent = -INFINITY;
-	if ((curl_easy_setopt(channel_curl->handle, CURLOPT_PROGRESSFUNCTION,
-			      channel_callback_xferinfo_legacy) != CURLE_OK) ||
-	    (curl_easy_setopt(channel_curl->handle, CURLOPT_PROGRESSDATA,
-				  &percent) != CURLE_OK)) {
-		result = CHANNEL_EINIT;
-		goto cleanup;
-	}
-#if LIBCURL_VERSION_NUM >= 0x072000
-	if ((curl_easy_setopt(channel_curl->handle, CURLOPT_XFERINFOFUNCTION,
-			      channel_callback_xferinfo) != CURLE_OK) ||
-	    (curl_easy_setopt(channel_curl->handle, CURLOPT_XFERINFODATA,
-				  &percent) != CURLE_OK)) {
-		result = CHANNEL_EINIT;
-		goto cleanup;
-	}
-#endif
-	if (curl_easy_setopt(channel_curl->handle, CURLOPT_NOPROGRESS, 0L) != CURLE_OK) {
-		result = CHANNEL_EINIT;
-		goto cleanup;
-	}
-
 	if (channel_data->headers) {
 		/*
 		 * Setup supply request and receive reply HTTP headers.
@@ -731,6 +720,73 @@  cleanup:
 	return result;
 }
 
+static curl_off_t channel_get_total_download_size(channel_curl_t *this,
+		const char *url)
+{
+	assert(this != NULL);
+	assert(url  != NULL);
+
+	curl_off_t size = -1;
+	if (curl_easy_setopt(this->handle, CURLOPT_URL, url) != CURLE_OK ||
+		curl_easy_setopt(this->handle, CURLOPT_NOBODY, 1L) != CURLE_OK ||
+		curl_easy_perform(this->handle) != CURLE_OK)
+		goto cleanup;
+
+	if (curl_easy_getinfo(this->handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T,
+			&size) != CURLE_OK)
+		goto cleanup;
+
+cleanup:
+	if (curl_easy_setopt(this->handle, CURLOPT_NOBODY, 0L) != CURLE_OK) {
+		ERROR("Failed to properly clean-up handle.");
+		size = -1;
+	}
+
+	return size;
+}
+
+static channel_op_res_t channel_enable_download_progress_tracking(
+		channel_curl_t *this,
+		const char *url,
+		download_callback_data_t *download_data)
+{
+	assert(url != NULL);
+	assert(download_data != NULL);
+
+	download_data->percent = 0;
+
+	channel_op_res_t result = CHANNEL_OK;
+	if ((download_data->total_download_size = channel_get_total_download_size(
+				this, url)) <= 0) {
+		result = CHANNEL_EINIT;
+		goto cleanup;
+	}
+
+#if LIBCURL_VERSION_NUM >= 0x072000
+	if ((curl_easy_setopt(this->handle, CURLOPT_XFERINFOFUNCTION,
+			      channel_callback_xferinfo) != CURLE_OK) ||
+	    (curl_easy_setopt(this->handle, CURLOPT_XFERINFODATA,
+				  download_data) != CURLE_OK)) {
+		result = CHANNEL_EINIT;
+		goto cleanup;
+	}
+#else
+	if ((curl_easy_setopt(this->handle, CURLOPT_PROGRESSFUNCTION,
+			      channel_callback_xferinfo_legacy) != CURLE_OK) ||
+	    (curl_easy_setopt(this->handle, CURLOPT_PROGRESSDATA,
+				  download_data) != CURLE_OK)) {
+		result = CHANNEL_EINIT;
+		goto cleanup;
+	}
+#endif
+	if (curl_easy_setopt(this->handle, CURLOPT_NOPROGRESS, 0L) != CURLE_OK) {
+		result = CHANNEL_EINIT;
+		goto cleanup;
+	}
+cleanup:
+	return result;
+}
+
 static size_t put_read_callback(void *ptr, size_t size, size_t nmemb, void *data)
 {
 	channel_data_t *channel_data = (channel_data_t *)data;
@@ -1030,6 +1086,16 @@  channel_op_res_t channel_get_file(channel_t *this, void *data)
 		goto cleanup_header;
 	}
 
+	download_callback_data_t download_data;
+	if (channel_enable_download_progress_tracking(channel_curl,
+				channel_data->url,
+				&download_data) == CHANNEL_EINIT) {
+		WARN("Failed to get total download size for URL %s.",
+				channel_data->url);
+	} else
+		INFO("Total download size is %lu kB.",
+				download_data.total_download_size / 1024);
+
 	if (curl_easy_setopt(channel_curl->handle, CURLOPT_CUSTOMREQUEST, "GET") !=
 	    CURLE_OK) {
 		ERROR("Set GET channel method option failed.");