1 /*        $NetBSD: progressmeter.c,v 1.16 2025/04/09 15:49:32 christos Exp $    */
2 /* $OpenBSD: progressmeter.c,v 1.54 2024/09/22 12:56:21 jsg 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.16 2025/04/09 15:49:32 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 int 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
can_output(void)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 *
format_rate(off_t bytes)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           if (i == 0) {
101                     i++;
102                     bytes = (bytes + 512) / 1024;
103           }
104           snprintf(buf, sizeof(buf), "%3lld.%1lld%c%s",
105               (long long) (bytes + 5) / 100,
106               (long long) (bytes + 5) / 10 % 10,
107               unit[i],
108               i ? "B" : " ");
109           return buf;
110 }
111 
112 static const char *
format_size(off_t bytes)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
refresh_progress_meter(int force_update)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           int cur_speed;
136           int hours, minutes, seconds;
137           int file_len, cols;
138           off_t delta_pos;
139 
140           if ((!force_update && !alarm_fired && !win_resized) || !can_output())
141                     return;
142           alarm_fired = 0;
143 
144           if (win_resized) {
145                     setscreensize();
146                     win_resized = 0;
147           }
148 
149           transferred = *counter - (cur_pos ? cur_pos : start_pos);
150           cur_pos = *counter;
151           now = monotime_double();
152           bytes_left = end_pos - cur_pos;
153 
154           delta_pos = cur_pos - last_pos;
155           if (delta_pos > max_delta_pos)
156                     max_delta_pos = delta_pos;
157 
158           if (bytes_left > 0)
159                     elapsed = now - last_update;
160           else {
161                     elapsed = now - start;
162                     /* Calculate true total speed when done */
163                     transferred = end_pos - start_pos;
164                     bytes_per_second = 0;
165           }
166 
167           /* calculate speed */
168           if (elapsed != 0)
169                     cur_speed = (transferred / elapsed);
170           else
171                     cur_speed = transferred;
172 
173 #define AGE_FACTOR 0.9
174           if (bytes_per_second != 0) {
175                     bytes_per_second = (bytes_per_second * AGE_FACTOR) +
176                         (cur_speed * (1.0 - AGE_FACTOR));
177           } else
178                     bytes_per_second = cur_speed;
179 
180           last_update = now;
181 
182           /* Don't bother if we can't even display the completion percentage */
183           if (win_size < 4)
184                     return;
185 
186           /* filename */
187           file_len = cols = win_size - 36;
188           if (file_len > 0) {
189                     asmprintf(&buf, INT_MAX, &cols, "%-*s", file_len, file);
190                     /* If we used fewer columns than expected then pad */
191                     if (cols < file_len)
192                               xextendf(&buf, NULL, "%*s", file_len - cols, "");
193           }
194           /* percent of transfer done */
195           if (end_pos == 0 || cur_pos == end_pos)
196                     percent = 100;
197           else
198                     percent = ((float)cur_pos / end_pos) * 100;
199 
200           /* percent / amount transferred / bandwidth usage */
201           xextendf(&buf, NULL, " %3d%% %s %s/s ", percent, format_size(cur_pos),
202               format_rate((off_t)bytes_per_second));
203 
204           /* ETA */
205           if (!transferred)
206                     stalled += elapsed;
207           else
208                     stalled = 0;
209 
210           if (stalled >= STALL_TIME)
211                     xextendf(&buf, NULL, "- stalled -");
212           else if (bytes_per_second == 0 && bytes_left)
213                     xextendf(&buf, NULL, "  --:-- ETA");
214           else {
215                     if (bytes_left > 0)
216                               seconds = bytes_left / bytes_per_second;
217                     else
218                               seconds = elapsed;
219 
220                     hours = seconds / 3600;
221                     seconds -= hours * 3600;
222                     minutes = seconds / 60;
223                     seconds -= minutes * 60;
224 
225                     if (hours != 0) {
226                               xextendf(&buf, NULL, "%d:%02d:%02d",
227                                   hours, minutes, seconds);
228                     } else
229                               xextendf(&buf, NULL, "  %02d:%02d", minutes, seconds);
230 
231                     if (bytes_left > 0)
232                               xextendf(&buf, NULL, " ETA");
233                     else
234                               xextendf(&buf, NULL, "    ");
235           }
236 
237           /* Finally, truncate string at window width */
238           cols = win_size - 1;
239           asmprintf(&obuf, INT_MAX, &cols, " %s", buf);
240           if (obuf != NULL) {
241                     *obuf = '\r'; /* must insert as asmprintf() would escape it */
242                     atomicio(vwrite, STDOUT_FILENO, obuf, strlen(obuf));
243           }
244           free(buf);
245           free(obuf);
246 }
247 
248 static void
sig_alarm(int ignore)249 sig_alarm(int ignore)
250 {
251           alarm_fired = 1;
252           alarm(UPDATE_INTERVAL);
253 }
254 
255 void
start_progress_meter(const char * f,off_t filesize,off_t * ctr)256 start_progress_meter(const char *f, off_t filesize, off_t *ctr)
257 {
258           start = last_update = monotime_double();
259           file = f;
260           start_pos = *ctr;
261           end_pos = filesize;
262           cur_pos = 0;
263           counter = ctr;
264           stalled = 0;
265           bytes_per_second = 0;
266 
267           setscreensize();
268           refresh_progress_meter(1);
269 
270           ssh_signal(SIGALRM, sig_alarm);
271           ssh_signal(SIGWINCH, sig_winch);
272           alarm(UPDATE_INTERVAL);
273 }
274 
275 void
stop_progress_meter(void)276 stop_progress_meter(void)
277 {
278           alarm(0);
279 
280           if (!can_output())
281                     return;
282 
283           /* Ensure we complete the progress */
284           if (cur_pos != end_pos)
285                     refresh_progress_meter(1);
286 
287           atomicio(vwrite, STDOUT_FILENO, __UNCONST("\n"), 1);
288 }
289 
290 static void
sig_winch(int sig)291 sig_winch(int sig)
292 {
293           win_resized = 1;
294 }
295 
296 static void
setscreensize(void)297 setscreensize(void)
298 {
299           struct winsize winsize;
300 
301           if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) != -1 &&
302               winsize.ws_col != 0) {
303                     if (winsize.ws_col > MAX_WINSIZE)
304                               win_size = MAX_WINSIZE;
305                     else
306                               win_size = winsize.ws_col;
307           } else
308                     win_size = DEFAULT_WINSIZE;
309           win_size += 1;                                              /* trailing \0 */
310 }
311