1 // SPDX-License-Identifier: BSD-2-Clause-FreeBSD
2 //
3 // Copyright (c) 2021 Tobias Kortkamp <tobik@FreeBSD.org>
4 // All rights reserved.
5 //
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions
8 // are met:
9 // 1. Redistributions of source code must retain the above copyright
10 //    notice, this list of conditions and the following disclaimer.
11 // 2. Redistributions in binary form must reproduce the above copyright
12 //    notice, this list of conditions and the following disclaimer in the
13 //    documentation and/or other materials provided with the distribution.
14 //
15 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 // ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25 // SUCH DAMAGE.
26 
27 #include "config.h"
28 
29 #include <sys/ioctl.h>
30 #include <sys/param.h>
31 #include <inttypes.h>
32 #include <signal.h>
33 #include <stdbool.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <unistd.h>
38 
39 #include <event2/event.h>
40 
41 #include <libias/array.h>
42 #include <libias/flow.h>
43 #include <libias/mem.h>
44 #include <libias/mempool.h>
45 #include <libias/str.h>
46 
47 #include "progress.h"
48 
49 struct Progress {
50 	struct event_base *base;
51 	struct event *timeout;
52 	struct event *sigint_event;
53 	struct event *sigwinch_event;
54 	FILE *out;
55 	char *current_file;
56 	off_t current_bytes;
57 	off_t total_bytes;
58 	struct winsize winsize;
59 	bool initialized;
60 	bool progressbar;
61 };
62 
63 // Prototypes
64 static void on_sigint(evutil_socket_t, short, void *);
65 static void on_sigwinch(evutil_socket_t, short, void *);
66 static void on_timeout(evutil_socket_t, short, void *);
67 static void progress_go_to_bar_row(struct Progress *);
68 static void progress_set_winsize(struct Progress *, unsigned short);
69 static void progress_step(struct Progress *);
70 
71 static const size_t PROGRESS_BAR_WIDTH = 15;
72 
73 static const char *cursor_up = "\e[1A";
74 static const char *cursor_save = "\e7";
75 static const char *cursor_restore = "\e8";
76 static const char *erase_below = "\e[0J";
77 static const char *erase_line_all = "\e[2K\r";
78 
79 void
on_sigint(evutil_socket_t fd,short events,void * userdata)80 on_sigint(evutil_socket_t fd, short events, void *userdata)
81 {
82 	struct Progress *this = userdata;
83 	progress_set_winsize(this, this->winsize.ws_row);
84 	fprintf(this->out, "interrupted by user\n");
85 	exit(1);
86 }
87 
88 void
on_sigwinch(evutil_socket_t fd,short events,void * userdata)89 on_sigwinch(evutil_socket_t fd, short events, void *userdata)
90 {
91 	struct Progress *this = userdata;
92 	ioctl(fileno(this->out), TIOCGWINSZ, &this->winsize);
93 	progress_set_winsize(this, this->winsize.ws_row - 1);
94 }
95 
96 void
on_timeout(evutil_socket_t fd,short events,void * userdata)97 on_timeout(evutil_socket_t fd, short events, void *userdata)
98 {
99 	struct Progress *this = userdata;
100 	progress_step(this);
101 }
102 
103 struct Progress *
progress_new(struct event_base * base,FILE * out)104 progress_new(struct event_base *base, FILE *out)
105 {
106 	struct Progress *this = xmalloc(sizeof(struct Progress));
107 	this->base = base;
108 	this->current_file = str_dup(NULL, "");
109 	this->out = out;
110 	if (isatty(fileno(this->out))) {
111 		this->progressbar = true;
112 	}
113 	this->timeout = event_new(base, 0, EV_PERSIST, on_timeout, this);
114 	struct timeval time;
115 	time.tv_sec = 1;
116 	time.tv_usec = 0;
117 	evtimer_add(this->timeout, &time);
118 
119 	this->sigint_event = evsignal_new(base, SIGINT, on_sigint, this);
120 	evsignal_add(this->sigint_event, NULL);
121 	this->sigwinch_event = evsignal_new(base, SIGWINCH, on_sigwinch, this);
122 	evsignal_add(this->sigwinch_event, NULL);
123 
124 	return this;
125 }
126 
127 void
progress_free(struct Progress * this)128 progress_free(struct Progress *this)
129 {
130 	progress_stop(this);
131 	progress_set_winsize(this, this->winsize.ws_row);
132 	free(this->current_file);
133 	event_free(this->timeout);
134 	event_free(this->sigint_event);
135 	event_free(this->sigwinch_event);
136 	free(this);
137 }
138 
139 void
progress_update(struct Progress * this,off_t delta,const char * current_file)140 progress_update(struct Progress *this, off_t delta, const char *current_file)
141 {
142 	this->current_bytes += delta;
143 	if (this->current_bytes < 0) {
144 		this->current_bytes = 0;
145 	}
146 	if (current_file) {
147 		free(this->current_file);
148 		this->current_file = str_dup(NULL, current_file);
149 	}
150 }
151 
152 void
progress_update_total(struct Progress * this,off_t delta)153 progress_update_total(struct Progress *this, off_t delta)
154 {
155 	this->total_bytes += delta;
156 	if (this->total_bytes < 0) {
157 		this->total_bytes = 0;
158 	}
159 }
160 
161 void
progress_go_to_bar_row(struct Progress * this)162 progress_go_to_bar_row(struct Progress *this)
163 {
164 	fprintf(this->out, "\e[%d;0H", this->winsize.ws_row + 1);
165 }
166 
167 void
progress_set_winsize(struct Progress * this,unsigned short row)168 progress_set_winsize(struct Progress *this, unsigned short row)
169 {
170 	fputs("\n", this->out);
171 	fputs(cursor_save, this->out);
172 	// set Scrolling Region
173 	fprintf(this->out, "\e[0;%dr", row);
174 	fputs(cursor_restore, this->out);
175 	fputs(cursor_up, this->out);
176 	fputs(erase_below, this->out);
177 	fflush(this->out);
178 }
179 
180 void
progress_step(struct Progress * this)181 progress_step(struct Progress *this)
182 {
183 	int progress = 0;
184 	if (this->total_bytes > 0) {
185 		// in makesum mode total_bytes is an estimation
186 		// and CURLOPT_MAXFILESIZE_LARGE is not set
187 		// either, so this might go over 100%
188 		progress = MIN(100, 100.0 * this->current_bytes / this->total_bytes);
189 	}
190 
191 	unless (this->initialized) {
192 		if (this->progressbar) {
193 			ioctl(fileno(this->out), TIOCGWINSZ, &this->winsize);
194 			progress_set_winsize(this, this->winsize.ws_row - 1);
195 		}
196 		this->initialized = true;
197 	}
198 
199 	if (this->progressbar && this->winsize.ws_col <= (PROGRESS_BAR_WIDTH + strlen("[100%] [] "))) {
200 		if (this->winsize.ws_col >= 4) {
201 			fputs(cursor_save, this->out);
202 			progress_go_to_bar_row(this);
203 			fputs(erase_line_all, this->out);
204 			fprintf(this->out, "%3d%%", progress);
205 			fputs(cursor_restore, this->out);
206 			fflush(this->out);
207 		}
208 		return;
209 	}
210 
211 	SCOPE_MEMPOOL(pool);
212 	int full = PROGRESS_BAR_WIDTH * progress / 100;
213 	char *bar = mempool_alloc(pool, PROGRESS_BAR_WIDTH + 1);
214 	memset(bar, ' ', PROGRESS_BAR_WIDTH);
215 	memset(bar, '=', full);
216 	if (full > 0) {
217 		bar[full - 1] = '>';
218 	}
219 
220 	ssize_t filename_width = this->winsize.ws_col - strlen("[100%] [] ") - PROGRESS_BAR_WIDTH;
221 	const char *filename = this->current_file;
222 	if (filename_width > 0) {
223 		filename = str_slice(pool, this->current_file, 0, filename_width);
224 	}
225 	if (this->progressbar) {
226 		fputs(cursor_save, this->out);
227 		progress_go_to_bar_row(this);
228 		fputs(erase_line_all, this->out);
229 		fprintf(this->out, "%3d%% [%s] ", progress, bar);
230 		fputs(filename, this->out);
231 		fputs(cursor_restore, this->out);
232 	} else {
233 		fprintf(this->out, "%3d%% [%s] %s\n", progress, bar, filename);
234 	}
235 	fflush(this->out);
236 }
237 
238 void
progress_stop(struct Progress * this)239 progress_stop(struct Progress *this)
240 {
241 	event_del(this->timeout);
242 	event_del(this->sigint_event);
243 	event_del(this->sigwinch_event);
244 }
245