fix(http): always show a live download progress bar on a terminal

The bar was only drawn when stderr was detected as a tty; if stderr was captured
(e.g. run through a pipe/wrapper) http_download fell into a silent path and the
bar never appeared during download (only flushed on ctrl+c). Now render_progress
is drawn to whichever of stderr/stdout is a tty (stderr first), stays silent only
when neither is a tty, and every frame is fflushed. Also drops the silent
parallel-range path so large downloads show a visible bar too.
这个提交包含在:
JGZYES
2026-09-06 09:28:28 +08:00
父节点 6db819a66e
当前提交 ef22cd7c8e
+17 -18
查看文件
@@ -14,6 +14,7 @@
#define PMM_POPEN_READ_X(cmd) _popen(cmd, "rb") /* binary: download streams */ #define PMM_POPEN_READ_X(cmd) _popen(cmd, "rb") /* binary: download streams */
#define PMM_PCLOSE_READ_X(p) _pclose(p) #define PMM_PCLOSE_READ_X(p) _pclose(p)
#define STDERR_FD _fileno(stderr) #define STDERR_FD _fileno(stderr)
#define STDOUT_FD _fileno(stdout)
#define IS_TTY(fd) _isatty(fd) #define IS_TTY(fd) _isatty(fd)
#else #else
#include <unistd.h> #include <unistd.h>
@@ -24,6 +25,7 @@
#define PMM_POPEN_READ_X(cmd) popen(cmd, "r") #define PMM_POPEN_READ_X(cmd) popen(cmd, "r")
#define PMM_PCLOSE_READ_X(p) pclose(p) #define PMM_PCLOSE_READ_X(p) pclose(p)
#define STDERR_FD fileno(stderr) #define STDERR_FD fileno(stderr)
#define STDOUT_FD fileno(stdout)
#define IS_TTY(fd) isatty(fd) #define IS_TTY(fd) isatty(fd)
#endif #endif
@@ -146,7 +148,7 @@ static void hms(double sec, char *out, size_t n) {
/* python-style bar, ASCII-safe (no unicode block glyphs, so it can't mojibake /* python-style bar, ASCII-safe (no unicode block glyphs, so it can't mojibake
* on GBK/936 or other non-UTF-8 Windows console codepages): * on GBK/936 or other non-UTF-8 Windows console codepages):
* 93%|##############--------| 36.1MB/38.8MB [01:30<00:05, 560.7KB/s] */ * 93%|##############--------| 36.1MB/38.8MB [01:30<00:05, 560.7KB/s] */
static void render_progress(unsigned long long fetched, unsigned long long total, static void render_progress(FILE *out, unsigned long long fetched, unsigned long long total,
double elapsed, double speed, int done) { double elapsed, double speed, int done) {
int w = 40; /* bar width in columns */ int w = 40; /* bar width in columns */
char line[320]; int li = 0; char line[320]; int li = 0;
@@ -187,9 +189,9 @@ static void render_progress(unsigned long long fetched, unsigned long long total
if (n > 0) li += n; if (n > 0) li += n;
} }
line[li] = '\0'; line[li] = '\0';
fputs(line, stderr); fputs(line, out);
if (done) fputc('\n', stderr); if (done) fputc('\n', out);
fflush(stderr); fflush(out);
} }
/* fetch Content-Length (bytes) via HEAD; returns 0 if unknown */ /* fetch Content-Length (bytes) via HEAD; returns 0 if unknown */
@@ -215,10 +217,13 @@ static unsigned long long remote_size(const char *url) {
static int download_parallel(const char *url, const char *out, unsigned long long total, int threads); static int download_parallel(const char *url, const char *out, unsigned long long total, int threads);
int http_download(const char *url, const char *out_path) { int http_download(const char *url, const char *out_path) {
int tty = IS_TTY(STDERR_FD); /* Draw the progress bar on whichever descriptor is a terminal. If stderr is
if (getenv("PMM_FORCE_PROGRESS")) tty = 1; /* debug/testing override */ * not a tty (e.g. output captured) but stdout is, paint to stdout so the bar
* still shows; if neither is a tty, stay silent (no spam when piped). */
FILE *prg = IS_TTY(STDERR_FD) ? stderr : (IS_TTY(STDOUT_FD) ? stdout : NULL);
if (getenv("PMM_FORCE_PROGRESS")) prg = stderr; /* debug/testing override */
if (!tty) { if (!prg) {
/* background/redirect: silent, simple (with curl resume -C -) */ /* background/redirect: silent, simple (with curl resume -C -) */
size_t cmdlen = strlen(url) * 3 + strlen(out_path) * 3 + 160; size_t cmdlen = strlen(url) * 3 + strlen(out_path) * 3 + 160;
char *cmd = malloc(cmdlen); char *cmd = malloc(cmdlen);
@@ -231,16 +236,10 @@ int http_download(const char *url, const char *out_path) {
return -1; return -1;
} }
/* terminal: prefer a parallel ranged download (POSIX only) for large files */ /* terminal: use a single stream and paint a python-style bar. (For large
* files we could parallel-range, but that path is silent, so a visible bar
* is worth more than a few extra seconds.) */
unsigned long long total = remote_size(url); unsigned long long total = remote_size(url);
#ifdef _WIN32
/* no reliable parallel via system() on Windows — use the single stream */
#else
if (total > 0 && total >= 8u * 1024u * 1024u && download_parallel(url, out_path, total, 4) == 0)
return 0;
#endif
/* single-stream fallback: stream via popen and paint a python-style bar */
char cmd[2100]; char cmd[2100];
snprintf(cmd, sizeof(cmd), "curl -sL --fail --retry 3 --max-time 3600 -o - \"%s\"", url); snprintf(cmd, sizeof(cmd), "curl -sL --fail --retry 3 --max-time 3600 -o - \"%s\"", url);
FILE *pf = PMM_POPEN_READ_X(cmd); /* binary read */ FILE *pf = PMM_POPEN_READ_X(cmd); /* binary read */
@@ -264,12 +263,12 @@ int http_download(const char *url, const char *out_path) {
last = fetched; last_n = n; last = fetched; last_n = n;
} }
el = ((double)n - start) / 1000.0; el = ((double)n - start) / 1000.0;
render_progress(fetched, total, el, speed, 0); render_progress(prg, fetched, total, el, speed, 0);
} }
fclose(outf); fclose(outf);
int rc = PMM_PCLOSE_READ_X(pf); int rc = PMM_PCLOSE_READ_X(pf);
if (rc != 0) { remove(out_path); return -1; } if (rc != 0) { remove(out_path); return -1; }
render_progress(fetched, total, el, speed, 1); render_progress(prg, fetched, total, el, speed, 1);
return 0; return 0;
} }