Home | History | Annotate | Line # | Download | only in dist
      1 /*	$NetBSD: progressmeter.c,v 1.18 2026/04/08 18:58:41 christos Exp $	*/
      2 /* $OpenBSD: progressmeter.c,v 1.57 2026/03/29 01:08:13 djm Exp $ */
      3 
      4 /*
      5  * Copyright (c) 2003 Nils Nordman.  All rights reserved.
      6  *
      7  * Redistribution and use in source and binary forms, with or without
      8  * modification, are permitted provided that the following conditions
      9  * are met:
     10  * 1. Redistributions of source code must retain the above copyright
     11  *    notice, this list of conditions and the following disclaimer.
     12  * 2. Redistributions in binary form must reproduce the above copyright
     13  *    notice, this list of conditions and the following disclaimer in the
     14  *    documentation and/or other materials provided with the distribution.
     15  *
     16  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
     17  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
     18  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
     19  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
     20  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
     21  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     22  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     23  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     24  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     25  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     26  */
     27 
     28 #include "includes.h"
     29 __RCSID("$NetBSD: progressmeter.c,v 1.18 2026/04/08 18:58:41 christos Exp $");
     30 #include <sys/types.h>
     31 #include <sys/ioctl.h>
     32 #include <sys/uio.h>
     33 
     34 #include <errno.h>
     35 #include <limits.h>
     36 #include <signal.h>
     37 #include <stdarg.h>
     38 #include <stdlib.h>
     39 #include <stdio.h>
     40 #include <string.h>
     41 #include <time.h>
     42 #include <unistd.h>
     43 
     44 #include "progressmeter.h"
     45 #include "atomicio.h"
     46 #include "misc.h"
     47 #include "utf8.h"
     48 
     49 #define DEFAULT_WINSIZE 80
     50 #define MAX_WINSIZE 512
     51 #define UPDATE_INTERVAL 1	/* update the progress meter every second */
     52 #define STALL_TIME 5		/* we're stalled after this many seconds */
     53 
     54 /* determines whether we can output to the terminal */
     55 static int can_output(void);
     56 
     57 /* window resizing */
     58 static void sig_winch(int);
     59 static void setscreensize(void);
     60 
     61 /* signal handler for updating the progress meter */
     62 static void sig_alarm(int);
     63 
     64 static double start;		/* start progress */
     65 static double last_update;	/* last progress update */
     66 static const char *file;	/* name of the file being transferred */
     67 static off_t start_pos;		/* initial position of transfer */
     68 static off_t end_pos;		/* ending position of transfer */
     69 static off_t cur_pos;		/* transfer position as of last refresh */
     70 static off_t last_pos;
     71 static off_t max_delta_pos = 0;
     72 static volatile off_t *counter;	/* progress counter */
     73 static long stalled;		/* how long we have been stalled */
     74 static long long bytes_per_second; /* current speed in bytes per second */
     75 static int win_size;		/* terminal window size */
     76 static volatile sig_atomic_t win_resized; /* for window resizing */
     77 static volatile sig_atomic_t alarm_fired;
     78 
     79 /* units for format_size */
     80 static const char unit[] = " KMGT";
     81 
     82 static int
     83 can_output(void)
     84 {
     85 	return (getpgrp() == tcgetpgrp(STDOUT_FILENO));
     86 }
     87 
     88 /* size needed to format integer type v, using (nbits(v) * log2(10) / 10) */
     89 #define STRING_SIZE(v) (((sizeof(v) * 8 * 4) / 10) + 1)
     90 
     91 static const char *
     92 format_rate(off_t bytes)
     93 {
     94 	int i;
     95 	static char buf[STRING_SIZE(bytes) * 2 + 16];
     96 
     97 	bytes *= 100;
     98 	for (i = 0; bytes >= 100*1000 && unit[i] != 'T'; i++)
     99 		bytes = (bytes + 512) / 1024;
    100 	/* Display at least KB, even when rate is low or zero. */
    101 	if (i == 0) {
    102 		i++;
    103 		bytes = (bytes + 512) / 1024;
    104 	}
    105 	snprintf(buf, sizeof(buf), "%3lld.%1lld%cB",
    106 	    (long long) (bytes + 5) / 100,
    107 	    (long long) (bytes + 5) / 10 % 10,
    108 	    unit[i]);
    109 	return buf;
    110 }
    111 
    112 static const char *
    113 format_size(off_t bytes)
    114 {
    115 	int i;
    116 	static char buf[STRING_SIZE(bytes) + 16];
    117 
    118 	for (i = 0; bytes >= 10000 && unit[i] != 'T'; i++)
    119 		bytes = (bytes + 512) / 1024;
    120 	snprintf(buf, sizeof(buf), "%4lld%c%s",
    121 	    (long long) bytes,
    122 	    unit[i],
    123 	    i ? "B" : " ");
    124 	return buf;
    125 }
    126 
    127 void
    128 refresh_progress_meter(int force_update)
    129 {
    130 	char *buf = NULL, *obuf = NULL;
    131 	off_t transferred;
    132 	double elapsed, now;
    133 	int percent;
    134 	off_t bytes_left;
    135 	long long cur_speed;
    136 	int hours, minutes, seconds;
    137 	int file_len, cols;
    138 	off_t delta_pos;
    139 
    140 	if (file == NULL || (!force_update && !alarm_fired && !win_resized) ||
    141 	    !can_output())
    142 		return;
    143 	alarm_fired = 0;
    144 
    145 	if (win_resized) {
    146 		setscreensize();
    147 		win_resized = 0;
    148 	}
    149 
    150 	transferred = *counter - (cur_pos ? cur_pos : start_pos);
    151 	cur_pos = *counter;
    152 	now = monotime_double();
    153 	bytes_left = end_pos - cur_pos;
    154 
    155 	delta_pos = cur_pos - last_pos;
    156 	if (delta_pos > max_delta_pos)
    157 		max_delta_pos = delta_pos;
    158 
    159 	if (bytes_left > 0)
    160 		elapsed = now - last_update;
    161 	else {
    162 		elapsed = now - start;
    163 		/* Calculate true total speed when done */
    164 		transferred = end_pos - start_pos;
    165 		bytes_per_second = 0;
    166 	}
    167 
    168 	/* calculate speed */
    169 	if (elapsed != 0)
    170 		cur_speed = (transferred / elapsed);
    171 	else
    172 		cur_speed = transferred;
    173 
    174 #define AGE_FACTOR 0.9
    175 	if (bytes_per_second != 0) {
    176 		bytes_per_second = (bytes_per_second * AGE_FACTOR) +
    177 		    (cur_speed * (1.0 - AGE_FACTOR));
    178 	} else
    179 		bytes_per_second = cur_speed;
    180 
    181 	last_update = now;
    182 
    183 	/* Don't bother if we can't even display the completion percentage */
    184 	if (win_size < 4)
    185 		return;
    186 
    187 	/* filename */
    188 	file_len = cols = win_size - 36;
    189 	if (file_len > 0) {
    190 		asmprintf(&buf, INT_MAX, &cols, "%-*s", file_len, file);
    191 		/* If we used fewer columns than expected then pad */
    192 		if (cols < file_len)
    193 			xextendf(&buf, NULL, "%*s", file_len - cols, "");
    194 	}
    195 	/* percent of transfer done */
    196 	if (end_pos == 0 || cur_pos == end_pos)
    197 		percent = 100;
    198 	else
    199 		percent = ((float)cur_pos / end_pos) * 100;
    200 
    201 	/* percent / amount transferred / bandwidth usage */
    202 	xextendf(&buf, NULL, " %3d%% %s %s/s ", percent, format_size(cur_pos),
    203 	    format_rate((off_t)bytes_per_second));
    204 
    205 	/* ETA */
    206 	if (!transferred)
    207 		stalled += elapsed;
    208 	else
    209 		stalled = 0;
    210 
    211 	if (stalled >= STALL_TIME)
    212 		xextendf(&buf, NULL, "- stalled -");
    213 	else if (bytes_per_second == 0 && bytes_left)
    214 		xextendf(&buf, NULL, "  --:-- ETA");
    215 	else {
    216 		if (bytes_left > 0)
    217 			seconds = bytes_left / bytes_per_second;
    218 		else
    219 			seconds = elapsed;
    220 
    221 		hours = seconds / 3600;
    222 		seconds -= hours * 3600;
    223 		minutes = seconds / 60;
    224 		seconds -= minutes * 60;
    225 
    226 		if (hours != 0) {
    227 			xextendf(&buf, NULL, "%d:%02d:%02d",
    228 			    hours, minutes, seconds);
    229 		} else
    230 			xextendf(&buf, NULL, "  %02d:%02d", minutes, seconds);
    231 
    232 		if (bytes_left > 0)
    233 			xextendf(&buf, NULL, " ETA");
    234 		else
    235 			xextendf(&buf, NULL, "    ");
    236 	}
    237 
    238 	/* Finally, truncate string at window width */
    239 	cols = win_size - 1;
    240 	asmprintf(&obuf, INT_MAX, &cols, " %s", buf);
    241 	if (obuf != NULL) {
    242 		*obuf = '\r'; /* must insert as asmprintf() would escape it */
    243 		atomicio(vwrite, STDOUT_FILENO, obuf, strlen(obuf));
    244 	}
    245 	free(buf);
    246 	free(obuf);
    247 }
    248 
    249 static void
    250 sig_alarm(int ignore)
    251 {
    252 	alarm_fired = 1;
    253 	alarm(UPDATE_INTERVAL);
    254 }
    255 
    256 void
    257 start_progress_meter(const char *f, off_t filesize, off_t *ctr)
    258 {
    259 	start = last_update = monotime_double();
    260 	file = f;
    261 	start_pos = *ctr;
    262 	end_pos = filesize;
    263 	cur_pos = 0;
    264 	counter = ctr;
    265 	stalled = 0;
    266 	bytes_per_second = 0;
    267 
    268 	setscreensize();
    269 	refresh_progress_meter(1);
    270 
    271 	ssh_signal(SIGALRM, sig_alarm);
    272 	ssh_signal(SIGWINCH, sig_winch);
    273 	alarm(UPDATE_INTERVAL);
    274 }
    275 
    276 void
    277 stop_progress_meter(void)
    278 {
    279 	alarm(0);
    280 
    281 	if (!can_output())
    282 		return;
    283 
    284 	/* Ensure we complete the progress */
    285 	if (cur_pos != end_pos)
    286 		refresh_progress_meter(1);
    287 
    288 	atomicio(vwrite, STDOUT_FILENO, __UNCONST("\n"), 1);
    289 	file = NULL;
    290 }
    291 
    292 static void
    293 sig_winch(int sig)
    294 {
    295 	win_resized = 1;
    296 }
    297 
    298 static void
    299 setscreensize(void)
    300 {
    301 	struct winsize winsize;
    302 
    303 	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) != -1 &&
    304 	    winsize.ws_col != 0) {
    305 		if (winsize.ws_col > MAX_WINSIZE)
    306 			win_size = MAX_WINSIZE;
    307 		else
    308 			win_size = winsize.ws_col;
    309 	} else
    310 		win_size = DEFAULT_WINSIZE;
    311 	win_size += 1;					/* trailing \0 */
    312 }
    313