diff options
-rw-r--r-- | lib/libalpm/be_sync.c | 1 | ||||
-rw-r--r-- | lib/libalpm/dload.c | 61 | ||||
-rw-r--r-- | lib/libalpm/dload.h | 2 | ||||
-rw-r--r-- | src/pacman/callback.c | 387 | ||||
-rw-r--r-- | src/pacman/callback.h | 4 | ||||
-rw-r--r-- | src/pacman/sync.c | 2 | ||||
-rw-r--r-- | src/pacman/util.c | 18 | ||||
-rw-r--r-- | src/pacman/util.h | 4 |
8 files changed, 320 insertions, 159 deletions
diff --git a/lib/libalpm/be_sync.c b/lib/libalpm/be_sync.c index add1a576..86015ab6 100644 --- a/lib/libalpm/be_sync.c +++ b/lib/libalpm/be_sync.c @@ -366,6 +366,7 @@ int SYMEXPORT alpm_dbs_update(alpm_handle_t *handle, alpm_list_t *dbs, int force if(siglevel & ALPM_SIG_DATABASE) { struct dload_payload *sig_payload; CALLOC(sig_payload, 1, sizeof(*sig_payload), GOTO_ERR(handle, ALPM_ERR_MEMORY, cleanup)); + sig_payload->signature = 1; /* print filename into a buffer (leave space for separator and .sig) */ len = strlen(db->treename) + strlen(dbext) + 5; diff --git a/lib/libalpm/dload.c b/lib/libalpm/dload.c index 2f360ab8..9ab0e5c4 100644 --- a/lib/libalpm/dload.c +++ b/lib/libalpm/dload.c @@ -90,6 +90,11 @@ static int dload_progress_cb(void *file, curl_off_t dltotal, curl_off_t dlnow, off_t current_size, total_size; alpm_download_event_progress_t cb_data = {0}; + /* do not print signature files progress bar */ + if(payload->signature) { + return 0; + } + /* avoid displaying progress bar for redirects with a body */ if(payload->respcode >= 300) { return 0; @@ -100,6 +105,11 @@ static int dload_progress_cb(void *file, curl_off_t dltotal, curl_off_t dlnow, return 1; } + if(dlnow < 0 || dltotal <= 0 || dlnow > dltotal) { + /* bogus values : stop here */ + return 0; + } + current_size = payload->initial_size + dlnow; /* is our filesize still under any set limit? */ @@ -115,34 +125,15 @@ static int dload_progress_cb(void *file, curl_off_t dltotal, curl_off_t dlnow, total_size = payload->initial_size + dltotal; - if(dltotal == 0 || payload->prevprogress == total_size) { + if(payload->prevprogress == total_size) { return 0; } - /* initialize the progress bar here to avoid displaying it when - * a repo is up to date and nothing gets downloaded. - * payload->handle->dlcb will receive the remote_name - * and the following arguments: - * 0, -1: download initialized - * 0, 0: non-download event - * x {x>0}, x: download complete - * x {x>0, x<y}, y {y > 0}: download progress, expected total is known */ - if(!payload->cb_initialized) { - cb_data.downloaded = 0; - cb_data.total = -1; - payload->cb_initialized = 1; - } - if(payload->prevprogress == current_size) { - cb_data.downloaded = 0; - cb_data.total = 0; - } else { /* do NOT include initial_size since it wasn't part of the package's * download_size (nor included in the total download size callback) */ - cb_data.downloaded = dlnow; - cb_data.total = dltotal; - } + cb_data.total = dltotal; + cb_data.downloaded = dlnow; payload->handle->dlcb(payload->remote_name, ALPM_DOWNLOAD_PROGRESS, &cb_data); - payload->prevprogress = current_size; return 0; @@ -375,6 +366,8 @@ static int curl_download_internal(struct dload_payload *payload, double remote_size, bytes_dl; struct sigaction orig_sig_pipe, orig_sig_int; CURLcode curlerr; + alpm_download_event_init_t init_cb_data = {0}; + alpm_download_event_completed_t completed_cb_data = {0}; /* shortcut to our handle within the payload */ alpm_handle_t *handle = payload->handle; CURL *curl = curl_easy_init(); @@ -444,6 +437,8 @@ static int curl_download_internal(struct dload_payload *payload, dload_interrupted = 0; mask_signal(SIGINT, &inthandler, &orig_sig_int); + handle->dlcb(payload->remote_name, ALPM_DOWNLOAD_INIT, &init_cb_data); + /* perform transfer */ curlerr = curl_easy_perform(curl); _alpm_log(handle, ALPM_LOG_DEBUG, "curl returned error %d from transfer\n", @@ -604,6 +599,10 @@ cleanup: raise(SIGINT); } + completed_cb_data.total = bytes_dl; + completed_cb_data.result = ret; + handle->dlcb(payload->remote_name, ALPM_DOWNLOAD_COMPLETED, &completed_cb_data); + return ret; } @@ -665,7 +664,6 @@ static int curl_multi_check_finished_download(CURLM *curlm, CURLMsg *msg, long remote_time = -1; struct stat st; char hostname[HOSTNAME_SIZE]; - alpm_download_event_completed_t cb_data = {0}; int ret = -1; curlerr = curl_easy_getinfo(curl, CURLINFO_PRIVATE, &payload); @@ -825,9 +823,12 @@ cleanup: unlink(payload->tempfile_name); } - cb_data.total = bytes_dl; - cb_data.result = ret; - handle->dlcb(payload->remote_name, ALPM_DOWNLOAD_COMPLETED, &cb_data); + if(!payload->signature) { + alpm_download_event_completed_t cb_data = {0}; + cb_data.total = bytes_dl; + cb_data.result = ret; + handle->dlcb(payload->remote_name, ALPM_DOWNLOAD_COMPLETED, &cb_data); + } curl_multi_remove_handle(curlm, curl); curl_easy_cleanup(curl); @@ -945,9 +946,11 @@ static int curl_multi_download_internal(alpm_handle_t *handle, struct dload_payload *payload = payloads->data; if(curl_multi_add_payload(handle, curlm, payload, localpath) == 0) { - alpm_download_event_init_t cb_data = {.optional = payload->errors_ok}; + if(!payload->signature) { + alpm_download_event_init_t cb_data = {.optional = payload->errors_ok}; + handle->dlcb(payload->remote_name, ALPM_DOWNLOAD_INIT, &cb_data); + } - handle->dlcb(payload->remote_name, ALPM_DOWNLOAD_INIT, &cb_data); payloads = payloads->next; // TODO: report that download has started } else { @@ -1129,6 +1132,7 @@ char SYMEXPORT *alpm_fetch_pkgurl(alpm_handle_t *handle, const char *url) sig_filepath = filecache_find_url(handle, payload.fileurl); if(sig_filepath == NULL) { + payload.signature = 1; payload.handle = handle; payload.trust_remote_name = 1; payload.force = 1; @@ -1184,5 +1188,4 @@ void _alpm_dload_payload_reset_for_retry(struct dload_payload *payload) payload->initial_size += payload->prevprogress; payload->prevprogress = 0; payload->unlink_on_fail = 0; - payload->cb_initialized = 0; } diff --git a/lib/libalpm/dload.h b/lib/libalpm/dload.h index a40b51b7..1e4af755 100644 --- a/lib/libalpm/dload.h +++ b/lib/libalpm/dload.h @@ -42,7 +42,7 @@ struct dload_payload { int errors_ok; int unlink_on_fail; int trust_remote_name; - int cb_initialized; + int signature; /* specifies if the payload is a signature file */ #ifdef HAVE_LIBCURL CURL *curl; char error_buffer[CURL_ERROR_SIZE]; diff --git a/src/pacman/callback.c b/src/pacman/callback.c index 613d59d4..c2e516ec 100644 --- a/src/pacman/callback.c +++ b/src/pacman/callback.c @@ -18,6 +18,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <assert.h> +#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -52,6 +54,50 @@ static alpm_list_t *output = NULL; #define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC #endif +struct pacman_progress_bar { + const char *filename; + off_t xfered; + off_t total_size; + uint64_t init_time; /* Time when this download started doing any progress */ + uint64_t sync_time; /* Last time we updated the bar info */ + double rate; + unsigned int eta; /* ETA in seconds */ + bool completed; /* transfer is completed */ +}; + +/* This datastruct represents the state of multiline progressbar UI */ +struct pacman_multibar_ui { + /* List of active downloads handled by multibar UI. + * Once the first download in the list is completed it is removed + * from this list and we never redraw it anymore. + * If the download is in this list, then the UI can redraw the progress bar or change + * the order of the bars (e.g. moving completed bars to the top of the list) + */ + alpm_list_t *active_downloads; /* List of type 'struct pacman_progress_bar' */ + + /* Number of active download bars that multibar UI handles. */ + size_t active_downloads_num; + + /* Specifies whether a completed progress bar need to be reordered and moved + * to the top of the list. + */ + bool move_completed_up; + + /* Cursor position relative to the first active progress bar, + * e.g. 0 means the first active progress bar, active_downloads_num-1 means the last bar, + * active_downloads_num - is the line below all progress bars. + */ + int cursor_lineno; +}; + +struct pacman_multibar_ui multibar_ui = {0}; + +static void cursor_goto_end(void); + +void multibar_move_completed_up(bool value) { + multibar_ui.move_completed_up = value; +} + static int64_t get_time_ms(void) { #if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0) && defined(CLOCK_MONOTONIC_COARSE) @@ -152,11 +198,7 @@ static void fill_progress(const int bar_percent, const int disp_percent, printf(" %3d%%", disp_percent); } - if(bar_percent == 100) { - putchar('\n'); - } else { - putchar('\r'); - } + putchar('\r'); fflush(stdout); } @@ -346,6 +388,7 @@ void cb_event(alpm_event_t *event) case ALPM_EVENT_DB_RETRIEVE_FAILED: case ALPM_EVENT_PKG_RETRIEVE_DONE: case ALPM_EVENT_PKG_RETRIEVE_FAILED: + cursor_goto_end(); flush_output_list(); on_progress = 0; break; @@ -629,6 +672,7 @@ void cb_progress(alpm_progress_t event, const char *pkgname, int percent, fill_progress(percent, percent, cols - infolen); if(percent == 100) { + putchar('\n'); flush_output_list(); on_progress = 0; } else { @@ -647,149 +691,87 @@ void cb_dl_total(off_t total) } } -/* callback to handle display of download progress */ -static void dload_progress_event(const char *filename, off_t file_xfered, off_t file_total) +static int dload_progressbar_enabled(void) +{ + return !config->noprogressbar && (getcols() != 0); +} + +/* Goto the line that corresponds to num-th active download */ +static void cursor_goto_bar(int num) +{ + if(num > multibar_ui.cursor_lineno) { + console_cursor_move_down(num - multibar_ui.cursor_lineno); + } else if(num < multibar_ui.cursor_lineno) { + console_cursor_move_up(multibar_ui.cursor_lineno - num); + } + multibar_ui.cursor_lineno = num; +} + +/* Goto the line *after* the last active progress bar */ +static void cursor_goto_end(void) +{ + cursor_goto_bar(multibar_ui.active_downloads_num); +} + +/* Returns true if element with the specified name is found, false otherwise */ +static bool find_bar_for_filename(const char *filename, int *index, struct pacman_progress_bar **bar) +{ + int i = 0; + alpm_list_t *listitem = multibar_ui.active_downloads; + for(; listitem; listitem = listitem->next, i++) { + struct pacman_progress_bar *b = listitem->data; + if (strcmp(b->filename, filename) == 0) { + /* we found a progress bar with the given name */ + *index = i; + *bar = b; + return true; + } + } + + return false; +} + +static void draw_pacman_progress_bar(struct pacman_progress_bar *bar) { - static double rate_last; - static off_t xfered_last; - static int64_t initial_time = 0; int infolen; int filenamelen; char *fname, *p; /* used for wide character width determination and printing */ int len, wclen, wcwid, padwid; wchar_t *wcfname; - - int totaldownload = 0; - off_t xfered, total; - double rate = 0.0; - unsigned int eta_h = 0, eta_m = 0, eta_s = 0; + unsigned int eta_h = 0, eta_m = 0, eta_s = bar->eta; double rate_human, xfered_human; const char *rate_label, *xfered_label; - int file_percent = 0, total_percent = 0; + int file_percent = 0; const unsigned short cols = getcols(); - /* Nothing has changed since last callback; stop here */ - if(file_xfered == 0 && file_total == 0) { - return; - } - - if(config->noprogressbar || cols == 0) { - if(file_xfered == 0 && file_total == -1) { - printf(_("downloading %s...\n"), filename); - fflush(stdout); - } - return; - } - - infolen = cols * 6 / 10; - if(infolen < 50) { - infolen = 50; - } - /* only use TotalDownload if enabled and we have a callback value */ - if(config->totaldownload && list_total) { - /* sanity check */ - if(list_xfered + file_total <= list_total) { - totaldownload = 1; - } else { - /* bogus values : don't enable totaldownload and reset */ - list_xfered = 0; - list_total = 0; - } - } - - if(totaldownload) { - xfered = list_xfered + file_xfered; - total = list_total; - } else { - xfered = file_xfered; - total = file_total; - } - - /* this is basically a switch on xfered: 0, total, and - * anything else */ - if(file_xfered == 0 && file_total == -1) { - /* set default starting values, ensure we only call this once - * if TotalDownload is enabled */ - if(!totaldownload || (totaldownload && list_xfered == 0)) { - initial_time = get_time_ms(); - xfered_last = (off_t)0; - rate_last = 0.0; - get_update_timediff(1); - } - } else if(xfered > total || xfered < 0) { - /* bogus values : stop here */ - return; - } else if(file_xfered == file_total) { - /* compute final values */ - int64_t timediff = get_time_ms() - initial_time; - if(timediff > 0) { - rate = (double)xfered / (timediff / 1000.0); - /* round elapsed time (in ms) to the nearest second */ - eta_s = (unsigned int)(timediff + 500) / 1000; - } else { - eta_s = 0; - } - } else { - /* compute current average values */ - int64_t timediff = get_update_timediff(0); - - if(timediff < UPDATE_SPEED_MS) { - /* return if the calling interval was too short */ - return; - } - rate = (double)(xfered - xfered_last) / (timediff / 1000.0); - /* average rate to reduce jumpiness */ - rate = (rate + 2 * rate_last) / 3; - if(rate > 0.0) { - eta_s = (total - xfered) / rate; - } else { - eta_s = UINT_MAX; - } - rate_last = rate; - xfered_last = xfered; - } - - if(file_total) { - file_percent = (file_xfered * 100) / file_total; + if(bar->total_size) { + file_percent = (bar->xfered * 100) / bar->total_size; } else { file_percent = 100; } - if(totaldownload) { - total_percent = ((list_xfered + file_xfered) * 100) / - list_total; - - /* if we are at the end, add the completed file to list_xfered */ - if(file_xfered == file_total) { - list_xfered += file_total; - } - } - /* fix up time for display */ eta_h = eta_s / 3600; eta_s -= eta_h * 3600; eta_m = eta_s / 60; eta_s -= eta_m * 60; - len = strlen(filename); + len = strlen(bar->filename); fname = malloc(len + 1); - memcpy(fname, filename, len + 1); + memcpy(fname, bar->filename, len + 1); /* strip package or DB extension for cleaner look */ if((p = strstr(fname, ".pkg")) || (p = strstr(fname, ".db")) || (p = strstr(fname, ".files"))) { - /* tack on a .sig suffix for signatures */ - if(memcmp(&filename[len - 4], ".sig", 4) == 0) { - memcpy(p, ".sig", 4); - - /* adjust length for later calculations */ - len = p - fname + 4; - } else { - len = p - fname; - } + len = p - fname; fname[len] = '\0'; } + infolen = cols * 6 / 10; + if(infolen < 50) { + infolen = 50; + } + /* 1 space + filenamelen + 1 space + 6 for size + 1 space + 3 for label + * + 2 spaces + 4 for rate + 1 space + 3 for label + 2 for /s + 1 space + * 8 for eta, gives us the magic 33 */ @@ -824,8 +806,8 @@ static void dload_progress_event(const char *filename, off_t file_xfered, off_t } - rate_human = humanize_size((off_t)rate, '\0', -1, &rate_label); - xfered_human = humanize_size(xfered, '\0', -1, &xfered_label); + rate_human = humanize_size((off_t)bar->rate, '\0', -1, &rate_label); + xfered_human = humanize_size(bar->xfered, '\0', -1, &xfered_label); printf(" %ls%-*s ", wcfname, padwid, ""); /* We will show 1.62 MiB/s, 11.6 MiB/s, but 116 KiB/s and 1116 KiB/s */ @@ -850,19 +832,166 @@ static void dload_progress_event(const char *filename, off_t file_xfered, off_t free(fname); free(wcfname); - if(totaldownload) { - fill_progress(file_percent, total_percent, cols - infolen); + fill_progress(file_percent, file_percent, cols - infolen); + return; +} + +static void dload_init_event(const char *filename, alpm_download_event_init_t *data) +{ + (void)data; + + if(!dload_progressbar_enabled()) { + printf(_(" %s downloading...\n"), filename); + return; + } + + struct pacman_progress_bar *bar = calloc(1, sizeof(struct pacman_progress_bar)); + assert(bar); + bar->filename = filename; + bar->init_time = get_time_ms(); + bar->rate = 0.0; + multibar_ui.active_downloads = alpm_list_add(multibar_ui.active_downloads, bar); + + cursor_goto_end(); + printf(_(" %s downloading...\n"), filename); + multibar_ui.cursor_lineno++; + multibar_ui.active_downloads_num++; +} + +/* Draws download progress */ +static void dload_progress_event(const char *filename, alpm_download_event_progress_t *data) +{ + int index; + struct pacman_progress_bar *bar; + int64_t curr_time = get_time_ms(); + double last_chunk_rate; + int64_t timediff; + + if(!dload_progressbar_enabled()) { + return; + } + + assert(find_bar_for_filename(filename, &index, &bar)); + + /* compute current average values */ + timediff = curr_time - bar->sync_time; + + if(timediff < UPDATE_SPEED_MS) { + /* return if the calling interval was too short */ + return; + } + bar->sync_time = curr_time; + + last_chunk_rate = (double)(data->downloaded - bar->xfered) / (timediff / 1000.0); + /* average rate to reduce jumpiness */ + bar->rate = (last_chunk_rate + 2 * bar->rate) / 3; + if(bar->rate > 0.0) { + bar->eta = (data->total - data->downloaded) / bar->rate; } else { - fill_progress(file_percent, file_percent, cols - infolen); + bar->eta = UINT_MAX; + } + + /* Total size is received after the download starts. */ + bar->total_size = data->total; + bar->xfered = data->downloaded; + + cursor_goto_bar(index); + draw_pacman_progress_bar(bar); + fflush(stdout); +} + +/* download completed */ +static void dload_complete_event(const char *filename, alpm_download_event_completed_t *data) +{ + int index; + struct pacman_progress_bar *bar; + int64_t timediff; + + if(!dload_progressbar_enabled()) { + return; + } + + assert(find_bar_for_filename(filename, &index, &bar)); + bar->completed = true; + + /* This may not have been initialized if the download finished before + * an alpm_download_event_progress_t event happened */ + bar->total_size = data->total; + + if(data->result == 1) { + cursor_goto_bar(index); + printf(_(" %s is up to date"), bar->filename); + /* The line contains text from previous status. Erase these leftovers. */ + console_erase_line(); + } else if(data->result == 0) { + /* compute final values */ + bar->xfered = bar->total_size; + timediff = get_time_ms() - bar->init_time; + + /* if transfer was too fast, treat it as a 1ms transfer, for the sake + * of the rate calculation */ + if(timediff < 1) + timediff = 1; + + bar->rate = (double)bar->xfered / (timediff / 1000.0); + /* round elapsed time (in ms) to the nearest second */ + bar->eta = (unsigned int)(timediff + 500) / 1000; + + if(multibar_ui.move_completed_up && index != 0) { + /* If this item completed then move it to the top. + * Swap 0-th bar data with `index`-th one + */ + struct pacman_progress_bar *former_topbar = multibar_ui.active_downloads->data; + alpm_list_t *baritem = alpm_list_nth(multibar_ui.active_downloads, index); + multibar_ui.active_downloads->data = bar; + baritem->data = former_topbar; + + cursor_goto_bar(index); + draw_pacman_progress_bar(former_topbar); + + index = 0; + } + + cursor_goto_bar(index); + draw_pacman_progress_bar(bar); + } else { + cursor_goto_bar(index); + printf(_(" %s failed to download"), bar->filename); + console_erase_line(); + } + fflush(stdout); + + /* If the first bar is completed then there is no reason to keep it + * in the list as we are not going to redraw it anymore. + */ + while(multibar_ui.active_downloads) { + alpm_list_t *head = multibar_ui.active_downloads; + struct pacman_progress_bar *j = head->data; + if(j->completed) { + multibar_ui.cursor_lineno--; + multibar_ui.active_downloads_num--; + multibar_ui.active_downloads = alpm_list_remove_item( + multibar_ui.active_downloads, head); + free(head); + free(j); + } else { + break; + } } - return; } +/* Callback to handle display of download progress */ void cb_download(const char *filename, alpm_download_event_type_t event, void *data) { - if(event == ALPM_DOWNLOAD_PROGRESS) { - alpm_download_event_progress_t *progress = data; - dload_progress_event(filename, progress->downloaded, progress->total); + if(event == ALPM_DOWNLOAD_INIT) { + dload_init_event(filename, data); + } else if(event == ALPM_DOWNLOAD_PROGRESS) { + dload_progress_event(filename, data); + } else if(event == ALPM_DOWNLOAD_COMPLETED) { + dload_complete_event(filename, data); + } else { + pm_printf(ALPM_LOG_ERROR, _("unknown callback event type %d for %s\n"), + event, filename); } } diff --git a/src/pacman/callback.h b/src/pacman/callback.h index 6d92e86b..09d544a6 100644 --- a/src/pacman/callback.h +++ b/src/pacman/callback.h @@ -20,6 +20,7 @@ #ifndef PM_CALLBACK_H #define PM_CALLBACK_H +#include <stdbool.h> #include <sys/types.h> /* off_t */ #include <alpm.h> @@ -44,4 +45,7 @@ void cb_download(const char *filename, alpm_download_event_type_t event, __attribute__((format(printf, 2, 0))) void cb_log(alpm_loglevel_t level, const char *fmt, va_list args); +/* specify if multibar UI should move completed bars to the top of the screen */ +void multibar_move_completed_up(bool value); + #endif /* PM_CALLBACK_H */ diff --git a/src/pacman/sync.c b/src/pacman/sync.c index f7dcb958..a05af5da 100644 --- a/src/pacman/sync.c +++ b/src/pacman/sync.c @@ -35,6 +35,7 @@ #include "pacman.h" #include "util.h" #include "package.h" +#include "callback.h" #include "conf.h" static int unlink_verbose(const char *pathname, int ignore_missing) @@ -824,6 +825,7 @@ int sync_prepare_execute(void) goto cleanup; } + multibar_move_completed_up(true); if(alpm_trans_commit(config->handle, &data) == -1) { alpm_errno_t err = alpm_errno(config->handle); pm_printf(ALPM_LOG_ERROR, _("failed to commit transaction (%s)\n"), diff --git a/src/pacman/util.c b/src/pacman/util.c index 97b8e06d..03035037 100644 --- a/src/pacman/util.c +++ b/src/pacman/util.c @@ -1837,3 +1837,21 @@ char *arg_to_string(int argc, char *argv[]) strcpy(p, argv[i]); return cl_text; } + +/* Moves console cursor `lines` up */ +void console_cursor_move_up(unsigned int lines) +{ + printf("\x1B[%dF", lines); +} + +/* Moves console cursor `lines` down */ +void console_cursor_move_down(unsigned int lines) +{ + printf("\x1B[%dE", lines); +} + +/* Erases line from the current cursor position till the end of the line */ +void console_erase_line(void) +{ + printf("\x1B[K"); +} diff --git a/src/pacman/util.h b/src/pacman/util.h index 2b21f3d5..c97048fb 100644 --- a/src/pacman/util.h +++ b/src/pacman/util.h @@ -83,6 +83,10 @@ char *arg_to_string(int argc, char *argv[]); char *safe_fgets_stdin(char *s, int size); void console_cursor_hide(void); void console_cursor_show(void); +void console_cursor_move_up(unsigned int lines); +void console_cursor_move_down(unsigned int lines); +/* Erases line from the current cursor position till the end of the line */ +void console_erase_line(void); int pm_printf(alpm_loglevel_t level, const char *format, ...) __attribute__((format(printf,2,3))); int pm_asprintf(char **string, const char *format, ...) __attribute__((format(printf,2,3))); |