From 5f1893f094fd5b4cf7f5a4e7059fcc98590d2218 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Wed, 11 Mar 2026 23:37:09 +0000 Subject: [PATCH 01/33] Draft documentation and option parsing for a "--monitor" option to run a command and monitor its input and output (#67). --- docs/pv.1 | 19 +++++++++++++++++++ src/include/options.h | 3 ++- src/main/help.c | 3 +++ src/main/main.c | 5 +++++ src/main/options.c | 18 +++++++++++++++++- 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/pv.1 b/docs/pv.1 index 81980ed..6c5d97d 100644 --- a/docs/pv.1 +++ b/docs/pv.1 @@ -13,6 +13,8 @@ pv \- monitor and manage the progress of data through a pipe \fBpv\fR \fB\-R\fR|\fB\-\-remote\fR \fIPID\fR [\fIOPTION\fR]... .PP \fBpv\fR \fB\-Q\fR|\fB\-\-query\fR \fIPID\fR [\fIOPTION\fR]... +.PP +\fBpv\fR [\fIOPTION\fR]... \fB\-M\fR|\fB\-\-monitor\fR \-\- \fICOMMAND\fR [\fIARGS\fR]... .\" .SH DESCRIPTION Show the progress of data through a pipeline by giving information such as @@ -27,6 +29,9 @@ read. .PP In \*(lq\fB\-\-watchfd\fR\*(rq mode, inspect another process and show its progress through the files it has open. +.PP +In \*(lq\fB\-\-monitor\fR\*(rq mode, run a command and display the progress +of data through both its standard input and its standard output. .\" .SH OPTIONS .\" @@ -409,6 +414,20 @@ they match those of that process - such as \*(lq\fB\-\-null\fR\*(rq, and \*(lq\fB\-\-average\-rate\-window\fR\*(rq. Data transfer modifiers will have no effect. +.TP +.B \-M, \-\-monitor +Run the command specified by the remaining arguments, and monitor its +standard input and standard output. +Use \*(lq\fB\-\-\fR\*(rq after all of the \fBpv\fR options to keep the +command's options separate. +.IP +For example, \*(lq\fBpv\~\-\-cursor\~\-\-monitor\~\-\-\~gzip\~\-9\fR\*(rq is +equivalent to +\*(lq\fBpv\~\-\-cursor\~|\~gzip\~\-9\~|\~pv\~\-\-cursor\fR\*(rq, except that +the latter can't show the input:output ratio. +.\"TODO: choice of displaying two progress bars (in + out) or only one +.\"TODO: formatting option for input:output ratio +.\"TODO: a way to specify the name of each bar, if there are two .\" .SS "Other options" .TP diff --git a/src/include/options.h b/src/include/options.h index 2b339d8..366cc50 100644 --- a/src/include/options.h +++ b/src/include/options.h @@ -32,7 +32,8 @@ typedef enum { PV_ACTION_STORE_AND_FORWARD, /* store to file, then output from it */ PV_ACTION_WATCHFD, /* watch process file descriptors */ PV_ACTION_REMOTE_CONTROL, /* remotely control another pv */ - PV_ACTION_QUERY /* watch the state of another pv */ + PV_ACTION_QUERY, /* watch the state of another pv */ + PV_ACTION_MONITOR /* run a process, watch its stdin/out */ } pvaction_t; /* diff --git a/src/main/help.c b/src/main/help.c index 8c67489..07837e9 100644 --- a/src/main/help.c +++ b/src/main/help.c @@ -372,6 +372,9 @@ void display_help(void) N_("show progress of process PID"), { 0, 0, 0, 0} }, #endif /* PV_REMOTE_CONTROL */ + { "-M", "--monitor", NULL, + N_("monitor the input and output of a command"), + { 0, 0, 0, 0} }, { "", NULL, NULL, NULL, { 0, 0, 0, 0} }, { "-P", "--pidfile", N_("FILE"), N_("save process ID in FILE"), diff --git a/src/main/main.c b/src/main/main.c index 816287e..c71c26b 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -560,6 +560,11 @@ int main(int argc, char **argv) /* Query the progress of another running pv. */ retcode = pv_query_loop(state, opts->query); break; + case PV_ACTION_MONITOR: + /* Run a process and monitor its input and output. */ + /* TODO: run the monitor action. */ + retcode = 1; + break; } /* Clear up the PID file, if one was written. */ diff --git a/src/main/options.c b/src/main/options.c index f49a676..ce3b192 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -680,6 +680,7 @@ opts_t opts_parse(unsigned int argc, char **argv) { "watchfd", 1, NULL, (int) 'd' }, { "output", 1, NULL, (int) 'o' }, { "average-rate-window", 1, NULL, (int) 'm' }, + { "monitor", 0, NULL, (int) 'M' }, #ifdef ENABLE_DEBUGGING { "debug", 1, NULL, (int) '!' }, #endif /* ENABLE_DEBUGGING */ @@ -688,7 +689,7 @@ opts_t opts_parse(unsigned int argc, char **argv) /*@+nullassign@ */ int option_index = 0; #endif /* HAVE_GETOPT_LONG */ - char *short_options = "hVpteIrab8kTA:fvnqcWD:s:gl0i:w:H:N:u:F:x:L:B:CEZ:SYKOXU:R:Q:P:d:m:o:" + char *short_options = "hVpteIrab8kTA:fvnqcWD:s:gl0i:w:H:N:u:F:x:L:B:CEZ:SYKOXU:R:Q:P:d:m:o:M" #ifdef ENABLE_DEBUGGING "!:" #endif @@ -1102,6 +1103,9 @@ opts_t opts_parse(unsigned int argc, char **argv) case 'm': opts->average_rate_window = pv_getnum_count(optarg, opts->decimal_units); break; + case 'M': + opts->action = PV_ACTION_MONITOR; + break; #ifdef ENABLE_DEBUGGING case '!': debugging_output_destination(optarg); @@ -1250,6 +1254,18 @@ opts_t opts_parse(unsigned int argc, char **argv) /*@+mustfreefresh@ */ } + /* + * At least one non-option argument is required with -M. + */ + if ((PV_ACTION_MONITOR == opts->action) && (optind >= (int) argc)) { + /*@-mustfreefresh@ *//* see above */ + fprintf(stderr, "%s: -M: %s\n", opts->program_name, + _("a command to run must be specified")); + opts_free(opts); + return NULL; + /*@+mustfreefresh@ */ + } + /* * Store remaining command-line arguments. */ From 547b52a313a6d09bbd5bef0e4460cf51b3d5e757 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Thu, 12 Mar 2026 21:59:01 +0000 Subject: [PATCH 02/33] Specify which command since pv is also a command. --- docs/pv.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pv.1 b/docs/pv.1 index 6c5d97d..b0acc1f 100644 --- a/docs/pv.1 +++ b/docs/pv.1 @@ -419,7 +419,7 @@ Data transfer modifiers will have no effect. Run the command specified by the remaining arguments, and monitor its standard input and standard output. Use \*(lq\fB\-\-\fR\*(rq after all of the \fBpv\fR options to keep the -command's options separate. +monitored command's options separate. .IP For example, \*(lq\fBpv\~\-\-cursor\~\-\-monitor\~\-\-\~gzip\~\-9\fR\*(rq is equivalent to From 111b68744921f55ad05770c6fff35e69f44e0908 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 13 Mar 2026 23:06:43 +0000 Subject: [PATCH 03/33] Give "--monitor" a parameter to set which side(s) of the command to monitor (#67). --- docs/pv.1 | 24 ++++++++++++++++++------ src/main/help.c | 4 ++-- src/main/options.c | 10 ++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/pv.1 b/docs/pv.1 index b0acc1f..37d98c0 100644 --- a/docs/pv.1 +++ b/docs/pv.1 @@ -14,7 +14,7 @@ pv \- monitor and manage the progress of data through a pipe .PP \fBpv\fR \fB\-Q\fR|\fB\-\-query\fR \fIPID\fR [\fIOPTION\fR]... .PP -\fBpv\fR [\fIOPTION\fR]... \fB\-M\fR|\fB\-\-monitor\fR \-\- \fICOMMAND\fR [\fIARGS\fR]... +\fBpv\fR [\fIOPTION\fR]... \fB\-M\fR|\fB\-\-monitor\fR \fISIDE\fR \-\- \fICOMMAND\fR [\fIARGS\fR]... .\" .SH DESCRIPTION Show the progress of data through a pipeline by giving information such as @@ -415,19 +415,31 @@ they match those of that process - such as \*(lq\fB\-\-average\-rate\-window\fR\*(rq. Data transfer modifiers will have no effect. .TP -.B \-M, \-\-monitor +.BR \-M " \fISIDE\fR, " \-\-monitor " \fISIDE\fR" Run the command specified by the remaining arguments, and monitor its -standard input and standard output. +standard input, its standard output, or both, depending on whether +\fISIDE\fR is \*(lq\fBin\fR\*(rq, \*(lq\fBout\fR\*(rq, or +\*(lq\fBboth\fR\*(rq. Use \*(lq\fB\-\-\fR\*(rq after all of the \fBpv\fR options to keep the monitored command's options separate. .IP -For example, \*(lq\fBpv\~\-\-cursor\~\-\-monitor\~\-\-\~gzip\~\-9\fR\*(rq is +With a \fISIDE\fR of \*(lq\fBboth\fR\*(rq, two progress bars will be shown. +All of \fBpv\fR's display switches, output modifiers, and data transfer +modifiers will apply to both sides. +The \*(lq\fB\-\-format\fR\*(rq and \*(lq\fB\-\-name\fR\*(rq options may be +specified twice, in which case the first one applies to the input side and +the second applies to the output side. +.IP +For example, +\*(lq\fBpv\~\-\-cursor\~\-\-monitor\~both\~\-\-\~gzip\~\-9\fR\*(rq is equivalent to \*(lq\fBpv\~\-\-cursor\~|\~gzip\~\-9\~|\~pv\~\-\-cursor\fR\*(rq, except that the latter can't show the input:output ratio. -.\"TODO: choice of displaying two progress bars (in + out) or only one +.IP +The values \*(lq\fB0\fR\*(rq, \*(lq\fB1\fR\*(rq, and \*(lq\fB2\fR\*(rq may +be used as synonyms for \*(lq\fBin\fR\*(rq, \*(lq\fBout\fR\*(rq, and +\*(lq\fBboth\fR\*(rq. .\"TODO: formatting option for input:output ratio -.\"TODO: a way to specify the name of each bar, if there are two .\" .SS "Other options" .TP diff --git a/src/main/help.c b/src/main/help.c index 07837e9..5664bee 100644 --- a/src/main/help.c +++ b/src/main/help.c @@ -372,8 +372,8 @@ void display_help(void) N_("show progress of process PID"), { 0, 0, 0, 0} }, #endif /* PV_REMOTE_CONTROL */ - { "-M", "--monitor", NULL, - N_("monitor the input and output of a command"), + { "-M", "--monitor", N_("SIDE"), + N_("run a command and monitor its standard input, output, or both"), { 0, 0, 0, 0} }, { "", NULL, NULL, NULL, { 0, 0, 0, 0} }, { "-P", "--pidfile", N_("FILE"), diff --git a/src/main/options.c b/src/main/options.c index ce3b192..dc77b4f 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -680,7 +680,7 @@ opts_t opts_parse(unsigned int argc, char **argv) { "watchfd", 1, NULL, (int) 'd' }, { "output", 1, NULL, (int) 'o' }, { "average-rate-window", 1, NULL, (int) 'm' }, - { "monitor", 0, NULL, (int) 'M' }, + { "monitor", 1, NULL, (int) 'M' }, #ifdef ENABLE_DEBUGGING { "debug", 1, NULL, (int) '!' }, #endif /* ENABLE_DEBUGGING */ @@ -689,7 +689,7 @@ opts_t opts_parse(unsigned int argc, char **argv) /*@+nullassign@ */ int option_index = 0; #endif /* HAVE_GETOPT_LONG */ - char *short_options = "hVpteIrab8kTA:fvnqcWD:s:gl0i:w:H:N:u:F:x:L:B:CEZ:SYKOXU:R:Q:P:d:m:o:M" + char *short_options = "hVpteIrab8kTA:fvnqcWD:s:gl0i:w:H:N:u:F:x:L:B:CEZ:SYKOXU:R:Q:P:d:m:o:M:" #ifdef ENABLE_DEBUGGING "!:" #endif @@ -996,6 +996,7 @@ opts_t opts_parse(unsigned int argc, char **argv) opts->height_set_manually = opts->height == 0 ? false : true; break; case 'N': + /* TODO: preserve previous name for use with "-M both" */ opts->name = pv_strdup(optarg); if (NULL == opts->name) { fprintf(stderr, "%s: -N: %s\n", opts->program_name, strerror(errno)); @@ -1070,6 +1071,7 @@ opts_t opts_parse(unsigned int argc, char **argv) } break; case 'F': + /* TODO: preserve previous format for use with "-M both" */ opts->format = pv_strdup(optarg); if (NULL == opts->format) { fprintf(stderr, "%s: -F: %s\n", opts->program_name, strerror(errno)); @@ -1105,6 +1107,7 @@ opts_t opts_parse(unsigned int argc, char **argv) break; case 'M': opts->action = PV_ACTION_MONITOR; + /* TODO: parse optarg to set the side */ break; #ifdef ENABLE_DEBUGGING case '!': @@ -1259,8 +1262,7 @@ opts_t opts_parse(unsigned int argc, char **argv) */ if ((PV_ACTION_MONITOR == opts->action) && (optind >= (int) argc)) { /*@-mustfreefresh@ *//* see above */ - fprintf(stderr, "%s: -M: %s\n", opts->program_name, - _("a command to run must be specified")); + fprintf(stderr, "%s: -M: %s\n", opts->program_name, _("a command to run must be specified")); opts_free(opts); return NULL; /*@+mustfreefresh@ */ From c2bbb8eef1cc6009b0fa2770ee6788f95f37fb7a Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 14 Mar 2026 14:28:30 +0000 Subject: [PATCH 04/33] Parse the monitoring side setting from "--monitor" (#67). --- src/include/options.h | 10 ++++++++++ src/main/options.c | 46 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/include/options.h b/src/include/options.h index 366cc50..b1f0f3e 100644 --- a/src/include/options.h +++ b/src/include/options.h @@ -36,6 +36,15 @@ typedef enum { PV_ACTION_MONITOR /* run a process, watch its stdin/out */ } pvaction_t; +/* + * Sides of a monitored command to monitor with PV_ACTION_MONITOR. + */ +typedef enum { + PV_SIDE_IN, /* monitor only the input side */ + PV_SIDE_OUT, /* monitor only the output side */ + PV_SIDE_BOTH /* monitor both sides */ +} pvside_t; + /* * Structure describing run-time options. * @@ -71,6 +80,7 @@ struct opts_s { unsigned int watchfd_count; /* number of watchfd items */ unsigned int watchfd_length; /* allocated array size */ pvaction_t action; /* the program action to perform */ + pvside_t side; /* which side of the monitored command to monitor */ bool progress; /* progress bar flag */ bool timer; /* timer flag */ bool eta; /* ETA flag */ diff --git a/src/main/options.c b/src/main/options.c index dc77b4f..1f52d51 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -416,6 +416,27 @@ static bool opts_watchfd_parse(opts_t opts, const char *argument, /*@null@ */ co } +/* + * Return true if the first string is at least as long as the second, and + * its start matches the entirety of the second. Both strings must be + * null-terminated. + */ +static bool string_starts_with(const char *string, const char *match) +{ + size_t string_length, match_length; + + string_length = strlen(string); /* flawfinder: ignore */ + match_length = strlen(match); /* flawfinder: ignore */ + /* flawfinder - these are null-terminated strings. */ + + if (string_length < match_length) + return false; + if (0 == strncmp(string, match, match_length)) + return true; + return false; +} + + #ifdef HAVE_NFTW /* * Callback function for nftw() to add the size of the given file to the @@ -1107,7 +1128,30 @@ opts_t opts_parse(unsigned int argc, char **argv) break; case 'M': opts->action = PV_ACTION_MONITOR; - /* TODO: parse optarg to set the side */ + if (string_starts_with(optarg, "0")) { + opts->side = PV_SIDE_IN; + } else if (string_starts_with(optarg, "in")) { + opts->side = PV_SIDE_IN; + } else if (string_starts_with(optarg, "stdin")) { + opts->side = PV_SIDE_IN; + } else if (string_starts_with(optarg, "1")) { + opts->side = PV_SIDE_OUT; + } else if (string_starts_with(optarg, "out")) { + opts->side = PV_SIDE_OUT; + } else if (string_starts_with(optarg, "stdout")) { + opts->side = PV_SIDE_OUT; + } else if (string_starts_with(optarg, "2")) { + opts->side = PV_SIDE_BOTH; + } else if (string_starts_with(optarg, "both")) { + opts->side = PV_SIDE_BOTH; + } else { + /*@-mustfreefresh@ *//* see above */ + fprintf(stderr, "%s: -M: %s: %s\n", + opts->program_name, optarg, _("invalid side specification")); + opts_free(opts); + return NULL; + /*@+mustfreefresh@ */ + } break; #ifdef ENABLE_DEBUGGING case '!': From 882428883a2d4a6cd8583811c4e3e4bf0f0b52ee Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 14 Mar 2026 14:54:53 +0000 Subject: [PATCH 05/33] Remember both -N and -F values if two were given (#67). --- src/include/options.h | 2 ++ src/main/options.c | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/include/options.h b/src/include/options.h index b1f0f3e..491851f 100644 --- a/src/include/options.h +++ b/src/include/options.h @@ -56,8 +56,10 @@ struct opts_s { /*@keep@*/ const char *program_name; /* name the program is running as */ /*@keep@*/ /*@null@*/ char *output; /* fd to write output to */ /*@keep@*/ /*@null@*/ char *name; /* display name, if any */ + /*@keep@*/ /*@null@*/ char *name1; /* first name, if two were given */ /*@keep@*/ /*@null@*/ char *default_bar_style; /* default bar style */ /*@keep@*/ /*@null@*/ char *format; /* output format, if any */ + /*@keep@*/ /*@null@*/ char *format1; /* first format, if two were given */ /*@keep@*/ /*@null@*/ char *pidfile; /* PID file, if any */ /*@keep@*/ /*@null@*/ char *store_and_forward_file; /* store and forward file, if any */ /*@keep@*/ /*@null@*/ char *extra_display; /* extra display specifier, if any */ diff --git a/src/main/options.c b/src/main/options.c index 1f52d51..43177a6 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -64,8 +64,12 @@ void opts_free( /*@only@ */ opts_t opts) */ if (NULL != opts->name) free(opts->name); + if (NULL != opts->name1) + free(opts->name1); if (NULL != opts->format) free(opts->format); + if (NULL != opts->format1) + free(opts->format1); if (NULL != opts->pidfile) free(opts->pidfile); if (NULL != opts->output) @@ -1017,7 +1021,7 @@ opts_t opts_parse(unsigned int argc, char **argv) opts->height_set_manually = opts->height == 0 ? false : true; break; case 'N': - /* TODO: preserve previous name for use with "-M both" */ + opts->name1 = opts->name; opts->name = pv_strdup(optarg); if (NULL == opts->name) { fprintf(stderr, "%s: -N: %s\n", opts->program_name, strerror(errno)); @@ -1092,7 +1096,7 @@ opts_t opts_parse(unsigned int argc, char **argv) } break; case 'F': - /* TODO: preserve previous format for use with "-M both" */ + opts->format1 = opts->format; opts->format = pv_strdup(optarg); if (NULL == opts->format) { fprintf(stderr, "%s: -F: %s\n", opts->program_name, strerror(errno)); From 46066385674ae65d016979c84f4ef0a5e523dbc3 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 14 Mar 2026 17:20:30 +0000 Subject: [PATCH 06/33] Constrain the options so that -M cannot be used with -U (as it is a different action) and -M will report the right error message if mixed with -R or -Q (#67). --- docs/pv.1 | 2 ++ src/include/options.h | 1 + src/main/options.c | 26 ++++++++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/docs/pv.1 b/docs/pv.1 index 37d98c0..c2df1a1 100644 --- a/docs/pv.1 +++ b/docs/pv.1 @@ -439,6 +439,8 @@ the latter can't show the input:output ratio. The values \*(lq\fB0\fR\*(rq, \*(lq\fB1\fR\*(rq, and \*(lq\fB2\fR\*(rq may be used as synonyms for \*(lq\fBin\fR\*(rq, \*(lq\fBout\fR\*(rq, and \*(lq\fBboth\fR\*(rq. +.IP +This option cannot be used with \*(lq\fB\-\-store\-and\-forward\*(rq. .\"TODO: formatting option for input:output ratio .\" .SS "Other options" diff --git a/src/include/options.h b/src/include/options.h index 491851f..bf07871 100644 --- a/src/include/options.h +++ b/src/include/options.h @@ -40,6 +40,7 @@ typedef enum { * Sides of a monitored command to monitor with PV_ACTION_MONITOR. */ typedef enum { + PV_SIDE_NONE, /* don't monitor a command */ PV_SIDE_IN, /* monitor only the input side */ PV_SIDE_OUT, /* monitor only the output side */ PV_SIDE_BOTH /* monitor both sides */ diff --git a/src/main/options.c b/src/main/options.c index 43177a6..c6e691c 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -762,6 +762,7 @@ opts_t opts_parse(unsigned int argc, char **argv) numopts = 0; opts->action = PV_ACTION_TRANSFER; + opts->side = PV_SIDE_NONE; opts->interval = 1; opts->delay_start = 0; opts->average_rate_window = 30; @@ -1293,6 +1294,31 @@ opts_t opts_parse(unsigned int argc, char **argv) if (opts->error_skip_block > 0 && 0 == opts->skip_errors) opts->skip_errors = 1; + /* + * Don't allow -R or -Q with -M. + */ + if ((PV_ACTION_MONITOR == opts->action) && ((0 != opts->remote) || (0 != opts->query))) { + /*@-mustfreefresh@ *//* see above */ + fprintf(stderr, "%s: %s: %s\n", opts->program_name, 0 != opts->remote ? "-R" : "-Q", + _("monitor mode cannot be specified with this option")); + opts_free(opts); + return NULL; + /*@+mustfreefresh@ */ + } + + /* + * Don't allow -U with -M. + */ + if ((PV_ACTION_STORE_AND_FORWARD == opts->action && PV_SIDE_NONE != opts->side) + || (PV_ACTION_MONITOR == opts->action && NULL != opts->store_and_forward_file)) { + /*@-mustfreefresh@ *//* see above */ + fprintf(stderr, "%s: %s\n", opts->program_name, + _("monitor mode cannot be used with store-and-forward")); + opts_free(opts); + return NULL; + /*@+mustfreefresh@ */ + } + /* * Don't allow any non-option arguments with -R or -Q. */ From 220625f370839c7ea5ca699ec0267cd179aff4a1 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 14 Mar 2026 21:50:50 +0000 Subject: [PATCH 07/33] Add TODO markers for the next steps of monitor mode (#67). --- src/main/main.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/main.c b/src/main/main.c index c71c26b..dd35750 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -267,6 +267,25 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta } +/* + * Run in monitor mode: run a process and run the main transfer loop on its + * input, output, or both. Returns the appropriate exit status. + */ +static int pv__monitor(pvstate_t state, opts_t opts) +{ + int retcode; + + retcode = 0; + + /* TODO: make appropriate pipes. */ + /* TODO: fork and run the command to be monitored, with in/out fds set. */ + /* TODO: fork for out monitor if monitoring both. */ + /* TODO: monitor the appropriate side; if both, use name1/format1 for the in side. */ + + return retcode; +} + + /* * Process command-line arguments and set option flags, then call functions * to initialise, and finally enter the main loop. @@ -361,6 +380,8 @@ int main(int argc, char **argv) /* * Put our list of input files into the PV internal state. + * + * TODO: don't do this in monitor mode. */ if (NULL != opts->argv) { pv_state_inputfiles(state, opts->argc, (const char **) (opts->argv)); @@ -469,6 +490,8 @@ int main(int argc, char **argv) } } + /* TODO: size calculation for monitor mode. */ + /* Initialise the signal handling. */ pv_sig_init(state); @@ -562,8 +585,7 @@ int main(int argc, char **argv) break; case PV_ACTION_MONITOR: /* Run a process and monitor its input and output. */ - /* TODO: run the monitor action. */ - retcode = 1; + retcode = pv__monitor(state, opts); break; } From bd70b0c6cb35847f832e6bedfa0ce27e397779bd Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 14 Mar 2026 22:33:52 +0000 Subject: [PATCH 08/33] Set exit status for --monitor errors (#67). --- docs/pv.1 | 3 ++- src/include/pv.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/pv.1 b/docs/pv.1 index c2df1a1..8e5a57a 100644 --- a/docs/pv.1 +++ b/docs/pv.1 @@ -754,7 +754,8 @@ pv \-\-numeric \-\-format '{"elapsed":%t,"bytes":%b,"rate":%r,"percentage":%{pro .\" .SH EXIT STATUS An exit status of 1 indicates a problem with the \*(lq\fB\-\-remote\fR\*(rq, -\*(lq\fB\-\-query\fR\*(rq, or \*(lq\fB\-\-pidfile\fR\*(rq options. +\*(lq\fB\-\-query\fR\*(rq, \*(lq\fB\-\-pidfile\fR\*(rq, or +\*(lq\fB\-\-monitor\fR\*(rq options. .PP Any other exit status is a bitmask of the following: .TP 5 diff --git a/src/include/pv.h b/src/include/pv.h index fe9fc8c..c7690c0 100644 --- a/src/include/pv.h +++ b/src/include/pv.h @@ -29,6 +29,7 @@ extern "C" { */ #define PV_ERROREXIT_REMOTE_OR_PID 1 #define PV_ERROREXIT_SAF 1 /* store and forward error */ +#define PV_ERROREXIT_MONITOR 1 /* monitor mode error */ #define PV_ERROREXIT_ACCESS 2 #define PV_ERROREXIT_OUROBOROS 4 #define PV_ERROREXIT_TRANSITION 8 From 06721c7be1ecd0c16bb0ebf78e06b1c647a986cf Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 14 Mar 2026 22:34:17 +0000 Subject: [PATCH 09/33] Create pipes for monitoring (#67). --- src/main/main.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/main.c b/src/main/main.c index dd35750..7f6fd63 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -273,11 +273,41 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta */ static int pv__monitor(pvstate_t state, opts_t opts) { + int pipefd_in[2]; + int pipefd_out[2]; int retcode; retcode = 0; - /* TODO: make appropriate pipes. */ + /* + * Create the pipes for communicating with the command. + */ + + pipefd_in[0] = -1; + pipefd_in[1] = -1; + pipefd_out[0] = -1; + pipefd_out[1] = -1; + + /* Pipe for the input side of the command, if we're monitoring it. */ + if ((opts->side == PV_SIDE_IN) || (opts->side == PV_SIDE_BOTH)) { + if (0 != pipe(pipefd_in)) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + return PV_ERROREXIT_MONITOR; + } + } + + /* Pipe for the output side of the command, if we're monitoring it. */ + if ((opts->side == PV_SIDE_OUT) || (opts->side == PV_SIDE_BOTH)) { + if (0 != pipe(pipefd_out)) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + if (-1 != pipefd_in[0]) + (void) close(pipefd_in[0]); + if (-1 != pipefd_in[1]) + (void) close(pipefd_in[1]); + return PV_ERROREXIT_MONITOR; + } + } + /* TODO: fork and run the command to be monitored, with in/out fds set. */ /* TODO: fork for out monitor if monitoring both. */ /* TODO: monitor the appropriate side; if both, use name1/format1 for the in side. */ From f84a668098495ada9797e9f85558f15e5a437b27 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Tue, 17 Mar 2026 23:41:23 +0000 Subject: [PATCH 10/33] Fork for the command to monitor (#67). --- src/main/main.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/main.c b/src/main/main.c index 7f6fd63..bc051e2 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -276,6 +276,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) int pipefd_in[2]; int pipefd_out[2]; int retcode; + pid_t command_pid; retcode = 0; @@ -308,7 +309,24 @@ static int pv__monitor(pvstate_t state, opts_t opts) } } - /* TODO: fork and run the command to be monitored, with in/out fds set. */ + /* Create a process to run the command. */ + command_pid = fork(); + if (command_pid < 0) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + if (-1 != pipefd_in[0]) + (void) close(pipefd_in[0]); + if (-1 != pipefd_in[1]) + (void) close(pipefd_in[1]); + if (-1 != pipefd_out[0]) + (void) close(pipefd_out[0]); + if (-1 != pipefd_out[1]) + (void) close(pipefd_out[1]); + return PV_ERROREXIT_MONITOR; + } else if (0 == command_pid) { + /* TODO: set up file descriptors for stdin/out */ + /* TODO: execute the command */ + } + /* TODO: fork for out monitor if monitoring both. */ /* TODO: monitor the appropriate side; if both, use name1/format1 for the in side. */ From 29c65ad9e18cc7bb25335849d7579f9ca0d077f6 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Wed, 18 Mar 2026 22:28:51 +0000 Subject: [PATCH 11/33] Set up stdin/stdout for the monitored command (#67). --- src/main/main.c | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/main.c b/src/main/main.c index bc051e2..79cee66 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -323,10 +323,45 @@ static int pv__monitor(pvstate_t state, opts_t opts) (void) close(pipefd_out[1]); return PV_ERROREXIT_MONITOR; } else if (0 == command_pid) { - /* TODO: set up file descriptors for stdin/out */ + /* Command process. */ + + /* Close the write end of the "in" pipe. */ + if (-1 != pipefd_in[1]) + (void) close(pipefd_in[1]); + /* Close the read end of the "out" pipe. */ + if (-1 != pipefd_out[0]) + (void) close(pipefd_out[0]); + + /* Put the read end of the "in" pipe on stdin. */ + if (-1 != pipefd_in[0]) { + if (dup2(pipefd_in[0], STDIN_FILENO) < 0) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + exit(1); + } + (void) close(pipefd_in[0]); + } + + /* Put the write end of the "out" pipe on stdout. */ + if (-1 != pipefd_out[1]) { + if (dup2(pipefd_out[1], STDOUT_FILENO) < 0) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + exit(1); + } + (void) close(pipefd_out[1]); + } + /* TODO: execute the command */ } + /* Main process, not the command process. */ + + /* Close the read end of the "in" pipe. */ + if (-1 != pipefd_in[0]) + (void) close(pipefd_in[0]); + /* Close the write end of the "out" pipe. */ + if (-1 != pipefd_out[1]) + (void) close(pipefd_out[1]); + /* TODO: fork for out monitor if monitoring both. */ /* TODO: monitor the appropriate side; if both, use name1/format1 for the in side. */ From 04f99c49513427cf8b61a3b5551b4e36a78ccd45 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Thu, 19 Mar 2026 22:57:41 +0000 Subject: [PATCH 12/33] Use a special type for opts->argv[] so that the whole array, and array entries, can both be NULL, and the array entries pointers to constant strings; and ensure that argv[] always has a NULL pointer on the end - all so argv can be passed to execvp() (#67). --- src/include/options.h | 4 +++- src/main/options.c | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/include/options.h b/src/include/options.h index bf07871..ecf0b23 100644 --- a/src/include/options.h +++ b/src/include/options.h @@ -46,6 +46,8 @@ typedef enum { PV_SIDE_BOTH /* monitor both sides */ } pvside_t; +typedef /*@null@*/ const char * argv_string; + /* * Structure describing run-time options. * @@ -66,7 +68,7 @@ struct opts_s { /*@keep@*/ /*@null@*/ char *extra_display; /* extra display specifier, if any */ /*@keep@*/ /*@null@*/ pid_t *watchfd_pid; /* array of processes to watch fds of */ /*@keep@*/ /*@null@*/ int *watchfd_fd; /* array of fds to watch in each one (0=all) */ - /*@keep@*/ /*@null@*/ const char **argv; /* array of non-option arguments */ + /*@keep@*/ /*@null@*/ argv_string *argv; /* array of non-option arguments */ size_t lastwritten; /* show N bytes last written */ off_t rate_limit; /* rate limit, in bytes per second */ size_t buffer_size; /* buffer size, in bytes (0=default) */ diff --git a/src/main/options.c b/src/main/options.c index c6e691c..879f3e3 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -92,12 +92,13 @@ void opts_free( /*@only@ */ opts_t opts) /* * Add a filename to the list of non-option arguments, returning false on - * error. The filename is not copied - the pointer is stored. + * error. The filename is not copied - the pointer is stored. The list is + * guaranteed to have a NULL after the last item. */ bool opts_add_file(opts_t opts, const char *filename) { /*@-branchstate@ */ - if ((opts->argc >= opts->argv_length) || (NULL == opts->argv)) { + if (((1+opts->argc) >= opts->argv_length) || (NULL == opts->argv)) { opts->argv_length = opts->argc + 10; /*@-keeptrans@ */ opts->argv = realloc(opts->argv, opts->argv_length * sizeof(char *)); @@ -121,6 +122,7 @@ bool opts_add_file(opts_t opts, const char *filename) */ opts->argv[opts->argc++] = filename; + opts->argv[opts->argc] = NULL; return true; } From ff8e396500ce48ca9005c4e6d404525c0d5b75b2 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Thu, 19 Mar 2026 22:58:47 +0000 Subject: [PATCH 13/33] Do not add the monitor command arguments as PV input files; and execute the monitor command in the monitor process (#67). --- src/main/main.c | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/main.c b/src/main/main.c index 79cee66..7b6b94b 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -278,6 +278,14 @@ static int pv__monitor(pvstate_t state, opts_t opts) int retcode; pid_t command_pid; + /* Arguments check. */ + if ((NULL == opts->argv) || (opts->argc < 1) || (NULL == opts->argv[0])) { + /*@-mustfreefresh@ */ + fprintf(stderr, "%s: -M: %s\n", opts->program_name, _("a command to run must be specified")); + return PV_ERROREXIT_MONITOR; + /*@+mustfreefresh@ *//* see below about gettext _() calls. */ + } + retcode = 0; /* @@ -310,7 +318,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) } /* Create a process to run the command. */ - command_pid = fork(); + command_pid = (pid_t) fork(); if (command_pid < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); if (-1 != pipefd_in[0]) @@ -336,7 +344,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) if (-1 != pipefd_in[0]) { if (dup2(pipefd_in[0], STDIN_FILENO) < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); - exit(1); + exit(EXIT_FAILURE); } (void) close(pipefd_in[0]); } @@ -345,12 +353,15 @@ static int pv__monitor(pvstate_t state, opts_t opts) if (-1 != pipefd_out[1]) { if (dup2(pipefd_out[1], STDOUT_FILENO) < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); - exit(1); + exit(EXIT_FAILURE); } (void) close(pipefd_out[1]); } - /* TODO: execute the command */ + /* Execute the command. */ + (void) execvp(opts->argv[0], (char * const *) (opts->argv)); + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + exit(EXIT_FAILURE); } /* Main process, not the command process. */ @@ -464,9 +475,10 @@ int main(int argc, char **argv) /* * Put our list of input files into the PV internal state. * - * TODO: don't do this in monitor mode. + * Don't do this in monitor mode, since the rest of PV won't be + * using the list in that case. */ - if (NULL != opts->argv) { + if ((NULL != opts->argv) && (PV_ACTION_MONITOR != opts->action)) { pv_state_inputfiles(state, opts->argc, (const char **) (opts->argv)); } From f647b12a2d169619bd48c605ec476612ca3c0a72 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 20 Mar 2026 22:11:19 +0000 Subject: [PATCH 14/33] Pass through "make indent" for consistency (#67). --- src/main/options.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/options.c b/src/main/options.c index 879f3e3..ead226e 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -98,7 +98,7 @@ void opts_free( /*@only@ */ opts_t opts) bool opts_add_file(opts_t opts, const char *filename) { /*@-branchstate@ */ - if (((1+opts->argc) >= opts->argv_length) || (NULL == opts->argv)) { + if (((1 + opts->argc) >= opts->argv_length) || (NULL == opts->argv)) { opts->argv_length = opts->argc + 10; /*@-keeptrans@ */ opts->argv = realloc(opts->argv, opts->argv_length * sizeof(char *)); From 149be3f9ab36ee49b6d8e1914a5afcc0abe78598 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 20 Mar 2026 22:12:14 +0000 Subject: [PATCH 15/33] Run the transfer main loop for the in and out monitoring sides, using another process for "out" if monitoring both (#67). --- src/main/main.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 4 deletions(-) diff --git a/src/main/main.c b/src/main/main.c index 7b6b94b..0fd10db 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -267,6 +267,50 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta } +/* + * Run the main transfer loop for the given side of monitor mode, using the + * "command_fd" file descriptor as the transfer loop's output (on the "in" + * side, it's a pipe to the command) or as its input (on the "out" side, + * it's a pipe from the command). If both sides are active, + * "other_side_pid" is the PID of the other side. + * + * Returns the appropriate exit status. + * + * As side effect, "command_fd" is closed. + */ +static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t side, int command_fd, /*@unused@ */ + __attribute__((unused)) pid_t other_side_pid) +{ + switch (side) { + case PV_SIDE_NONE: /* fall through */ + case PV_SIDE_BOTH: + return PV_ERROREXIT_MONITOR; + case PV_SIDE_IN: + /* Replace stdout with the pipe to the command. */ + if (dup2(command_fd, STDOUT_FILENO) < 0) { + fprintf(stderr, "%s: %s\n", program_name, strerror(errno)); + return PV_ERROREXIT_MONITOR; + } + break; + case PV_SIDE_OUT: + /* Replace stdin with the pipe from the command. */ + if (dup2(command_fd, STDIN_FILENO) < 0) { + fprintf(stderr, "%s: %s\n", program_name, strerror(errno)); + return PV_ERROREXIT_MONITOR; + } + break; + } + + if (close(command_fd) < 0) { + fprintf(stderr, "%s: %s\n", program_name, strerror(errno)); + } + + /* TODO: use other_side_pid, for ratio display. */ + + return pv_main_loop(state); +} + + /* * Run in monitor mode: run a process and run the main transfer loop on its * input, output, or both. Returns the appropriate exit status. @@ -276,7 +320,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) int pipefd_in[2]; int pipefd_out[2]; int retcode; - pid_t command_pid; + pid_t command_pid, in_monitor_pid, out_monitor_pid; /* Arguments check. */ if ((NULL == opts->argv) || (opts->argc < 1) || (NULL == opts->argv[0])) { @@ -359,9 +403,16 @@ static int pv__monitor(pvstate_t state, opts_t opts) } /* Execute the command. */ - (void) execvp(opts->argv[0], (char * const *) (opts->argv)); + (void) execvp(opts->argv[0], (char *const *) (opts->argv)); /* flawfinder: ignore */ fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); exit(EXIT_FAILURE); + + /* + * flawfinder recommends using a library call instead of + * executing another program with execvp(), but that isn't + * appropriate here, the whole purpose of monitor mode is to + * execute a process and monitor it. + */ } /* Main process, not the command process. */ @@ -373,8 +424,66 @@ static int pv__monitor(pvstate_t state, opts_t opts) if (-1 != pipefd_out[1]) (void) close(pipefd_out[1]); - /* TODO: fork for out monitor if monitoring both. */ - /* TODO: monitor the appropriate side; if both, use name1/format1 for the in side. */ + /* + * If monitoring both input and output, create a process for + * monitoring the output side. + */ + in_monitor_pid = -1; + out_monitor_pid = -1; + if (PV_SIDE_BOTH == opts->side) { + in_monitor_pid = (pid_t) getpid(); + out_monitor_pid = (pid_t) fork(); + if (out_monitor_pid < 0) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + if (-1 != pipefd_in[1]) + (void) close(pipefd_in[1]); + if (-1 != pipefd_out[0]) + (void) close(pipefd_out[0]); + (void) kill(command_pid, SIGTERM); + return PV_ERROREXIT_MONITOR; + } else if (0 == out_monitor_pid) { + /* Output side monitoring process. */ + + /* Close the write end of the "in" pipe. */ + if (-1 != pipefd_in[1]) + (void) close(pipefd_in[1]); + + return pv__run_monitor(opts->program_name, state, PV_SIDE_OUT, pipefd_out[0], in_monitor_pid); + } + } + + /* Monitor the remaining side. */ + switch (opts->side) { + case PV_SIDE_NONE: + retcode = PV_ERROREXIT_MONITOR; + break; + case PV_SIDE_BOTH: + /* Use name1, format1 for the "in" side. */ + if (NULL != opts->name1) { + pv_state_name_set(state, opts->name1); + } + if (NULL != opts->format1) { + pv_state_format_string_set(state, opts->format1); + } + /* TODO: call pv_state_set_format(). */ + /*@fallthrough@ */ + /* falling through as "out" is in another process (above). */ + __attribute__((fallthrough)); + case PV_SIDE_IN: + /* Close the read end of the "out" pipe. */ + if (-1 != pipefd_out[0]) + (void) close(pipefd_out[0]); + pipefd_out[0] = -1; + retcode = pv__run_monitor(opts->program_name, state, PV_SIDE_IN, pipefd_in[1], out_monitor_pid); + break; + case PV_SIDE_OUT: + /* Close the write end of the "in" pipe. */ + if (-1 != pipefd_in[1]) + (void) close(pipefd_in[1]); + pipefd_in[1] = -1; + retcode = pv__run_monitor(opts->program_name, state, PV_SIDE_OUT, pipefd_out[0], in_monitor_pid); + break; + } return retcode; } From 09a55ef62a457d483e5645aa464c16401700e62f Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 20 Mar 2026 22:14:35 +0000 Subject: [PATCH 16/33] Put the constant on the left hand side to avoid accidental assignment if an "=" is omitted (#67). --- src/main/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/main.c b/src/main/main.c index 0fd10db..b8a27a3 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -342,7 +342,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) pipefd_out[1] = -1; /* Pipe for the input side of the command, if we're monitoring it. */ - if ((opts->side == PV_SIDE_IN) || (opts->side == PV_SIDE_BOTH)) { + if ((PV_SIDE_IN == opts->side) || (PV_SIDE_BOTH == opts->side)) { if (0 != pipe(pipefd_in)) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); return PV_ERROREXIT_MONITOR; @@ -350,7 +350,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) } /* Pipe for the output side of the command, if we're monitoring it. */ - if ((opts->side == PV_SIDE_OUT) || (opts->side == PV_SIDE_BOTH)) { + if ((PV_SIDE_OUT == opts->side) || (PV_SIDE_BOTH == opts->side)) { if (0 != pipe(pipefd_out)) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); if (-1 != pipefd_in[0]) From b3896f93b26c1102f2314b8218e210f87a99366d Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 20 Mar 2026 23:00:44 +0000 Subject: [PATCH 17/33] Add more debugging, and set the pipefd entries to -1 after closing them to avoid double-close (#67). --- src/main/main.c | 26 ++++++++++++++++++++++---- src/pv/loop.c | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/main.c b/src/main/main.c index b8a27a3..ef84612 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -131,10 +131,12 @@ static int pv__set_output(pvstate_t state, opts_t opts, /*@null@ */ const char * return 0; if (NULL == output_file || 0 == strcmp(output_file, "-")) { + debug("%s", "setting output to stdout"); pv_state_output_set(state, STDOUT_FILENO, "(stdout)"); return 0; } + debug("%s: %s", "setting output", output_file); output_fd = open(output_file, O_WRONLY | O_CREAT | O_TRUNC, 0600); /* flawfinder: ignore */ /* * flawfinder rationale: the output filename has been @@ -347,6 +349,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); return PV_ERROREXIT_MONITOR; } + debug("pipefd_in[]=(%d,%d)", pipefd_in[0], pipefd_in[1]); } /* Pipe for the output side of the command, if we're monitoring it. */ @@ -359,6 +362,7 @@ static int pv__monitor(pvstate_t state, opts_t opts) (void) close(pipefd_in[1]); return PV_ERROREXIT_MONITOR; } + debug("pipefd_out[]=(%d,%d)", pipefd_out[0], pipefd_out[1]); } /* Create a process to run the command. */ @@ -378,28 +382,38 @@ static int pv__monitor(pvstate_t state, opts_t opts) /* Command process. */ /* Close the write end of the "in" pipe. */ - if (-1 != pipefd_in[1]) + if (-1 != pipefd_in[1]) { (void) close(pipefd_in[1]); + pipefd_in[1] = -1; + } /* Close the read end of the "out" pipe. */ - if (-1 != pipefd_out[0]) + if (-1 != pipefd_out[0]) { (void) close(pipefd_out[0]); + pipefd_out[0] = -1; + } /* Put the read end of the "in" pipe on stdin. */ if (-1 != pipefd_in[0]) { + debug("replacing command stdin with fd %d", pipefd_in[0]); if (dup2(pipefd_in[0], STDIN_FILENO) < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); exit(EXIT_FAILURE); } (void) close(pipefd_in[0]); + debug("replaced command stdin with fd %d", pipefd_in[0]); + pipefd_in[0] = -1; } /* Put the write end of the "out" pipe on stdout. */ if (-1 != pipefd_out[1]) { + debug("replacing command stdout with fd %d", pipefd_out[1]); if (dup2(pipefd_out[1], STDOUT_FILENO) < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); exit(EXIT_FAILURE); } (void) close(pipefd_out[1]); + debug("replaced command stdout with fd %d", pipefd_out[1]); + pipefd_out[1] = -1; } /* Execute the command. */ @@ -418,11 +432,15 @@ static int pv__monitor(pvstate_t state, opts_t opts) /* Main process, not the command process. */ /* Close the read end of the "in" pipe. */ - if (-1 != pipefd_in[0]) + if (-1 != pipefd_in[0]) { (void) close(pipefd_in[0]); + pipefd_in[0] = -1; + } /* Close the write end of the "out" pipe. */ - if (-1 != pipefd_out[1]) + if (-1 != pipefd_out[1]) { (void) close(pipefd_out[1]); + pipefd_out[1] = -1; + } /* * If monitoring both input and output, create a process for diff --git a/src/pv/loop.c b/src/pv/loop.c index cc533d8..776c832 100644 --- a/src/pv/loop.c +++ b/src/pv/loop.c @@ -213,7 +213,7 @@ int pv_main_loop(pvstate_t state) /*@-type@ */ if ((sb.st_mode & S_IFMT) == S_IFIFO) { output_is_pipe = true; - debug("%s", "output is a pipe"); + debug("%s (fd %d)", "output is a pipe", output_fd); } /*@+type@ *//* splint says st_mode is __mode_t, not mode_t */ } else { From 7be959ece19014b18710444b6a2671f42fd5889a Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 20 Mar 2026 23:13:33 +0000 Subject: [PATCH 18/33] Add more debugging, and pass a dummy input file list of just "-" to the monitoring transfer loop (#67). --- src/main/main.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/main.c b/src/main/main.c index ef84612..7fe973b 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -280,15 +280,21 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta * * As side effect, "command_fd" is closed. */ -static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t side, int command_fd, /*@unused@ */ +static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t side, int command_fd, + /*@unused@ */ __attribute__((unused)) pid_t other_side_pid) { + const char *dummy_argv[1]; /* flawfinder: ignore */ + + /* flawfinder - the array length is passed along with the array. */ + switch (side) { case PV_SIDE_NONE: /* fall through */ case PV_SIDE_BOTH: return PV_ERROREXIT_MONITOR; case PV_SIDE_IN: /* Replace stdout with the pipe to the command. */ + debug("replacing stdout with fd %d", command_fd); if (dup2(command_fd, STDOUT_FILENO) < 0) { fprintf(stderr, "%s: %s\n", program_name, strerror(errno)); return PV_ERROREXIT_MONITOR; @@ -296,6 +302,7 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s break; case PV_SIDE_OUT: /* Replace stdin with the pipe from the command. */ + debug("replacing stdin with fd %d", command_fd); if (dup2(command_fd, STDIN_FILENO) < 0) { fprintf(stderr, "%s: %s\n", program_name, strerror(errno)); return PV_ERROREXIT_MONITOR; @@ -309,6 +316,11 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s /* TODO: use other_side_pid, for ratio display. */ + /*@-observertrans@ */ + dummy_argv[0] = "-"; + /*@+observertrans@ */ + pv_state_inputfiles(state, 1, dummy_argv); + return pv_main_loop(state); } @@ -486,7 +498,9 @@ static int pv__monitor(pvstate_t state, opts_t opts) /* TODO: call pv_state_set_format(). */ /*@fallthrough@ */ /* falling through as "out" is in another process (above). */ +#ifndef SPLINT __attribute__((fallthrough)); +#endif case PV_SIDE_IN: /* Close the read end of the "out" pipe. */ if (-1 != pipefd_out[0]) From 6a20139f5c5d89bd21cf85d85a53859be15187a9 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 20 Mar 2026 23:48:28 +0000 Subject: [PATCH 19/33] Add a macro to remove __attribute__(()) if not compiling with GCC. --- src/include/config-aux.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/include/config-aux.h b/src/include/config-aux.h index 45a5f3b..2dcbc27 100644 --- a/src/include/config-aux.h +++ b/src/include/config-aux.h @@ -51,3 +51,8 @@ typedef bool _Bool; #define PV_REMOTE_CONTROL 1 # endif #endif + +/* Remove __attribute__(()) if not using GCC. */ +#ifndef __GNUC__ +#define __attribute__(x) /* GCC-only feature */ +#endif From c8e13d226b8524570995c68b66a76682a8521266 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 21 Mar 2026 15:47:07 +0000 Subject: [PATCH 20/33] Wait for the other processes associated with monitoring mode before returning, closing stdout beforehand so the monitored command will end (#67). --- src/main/main.c | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/main/main.c b/src/main/main.c index 7fe973b..7b671e1 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -19,6 +19,7 @@ #include #include #include +#include #ifdef HAVE_LANGINFO_H #include #endif @@ -333,8 +334,8 @@ static int pv__monitor(pvstate_t state, opts_t opts) { int pipefd_in[2]; int pipefd_out[2]; - int retcode; - pid_t command_pid, in_monitor_pid, out_monitor_pid; + int retcode, pid_status; + pid_t command_pid, in_monitor_pid, out_monitor_pid, waited_pid; /* Arguments check. */ if ((NULL == opts->argv) || (opts->argc < 1) || (NULL == opts->argv[0])) { @@ -517,6 +518,37 @@ static int pv__monitor(pvstate_t state, opts_t opts) break; } + /* + * If monitoring the "in" side, close stdout to signal EOF, + * otherwise when we wait for the monitored command, we'll wait + * forever. + */ + if (PV_SIDE_IN == opts->side || PV_SIDE_BOTH == opts->side) { + if (close(STDOUT_FILENO) < 0) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + } + } + + /* Wait for the monitored command to exit. */ + do { + /*@-type@ */ + /* splint disagreement about __pid_t vs pid_t. */ + pid_status = 0; + waited_pid = waitpid(command_pid, &pid_status, 0); + /*@+type@ */ + } while (-1 == waited_pid && EINTR == errno); + + /* If monitoring both sides, wait for the "out" side to exit. */ + if (PV_SIDE_BOTH == opts->side && -1 != out_monitor_pid) { + do { + /*@-type@ */ + /* splint disagreement about __pid_t vs pid_t. */ + pid_status = 0; + waited_pid = waitpid(out_monitor_pid, &pid_status, 0); + /*@+type@ */ + } while (-1 == waited_pid && EINTR == errno); + } + return retcode; } From 5c84305817e5be54e77b63831c1f15f67c090f28 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 21 Mar 2026 15:48:35 +0000 Subject: [PATCH 21/33] Hide the __attribute__() definition from splint so it doesn't generate spurious warnings. --- src/include/config-aux.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/include/config-aux.h b/src/include/config-aux.h index 2dcbc27..45fed1b 100644 --- a/src/include/config-aux.h +++ b/src/include/config-aux.h @@ -52,7 +52,9 @@ typedef bool _Bool; # endif #endif +#ifndef SPLINT /* Remove __attribute__(()) if not using GCC. */ #ifndef __GNUC__ #define __attribute__(x) /* GCC-only feature */ #endif +#endif From d98237134b98a2e13f6f45f04aa036ef3bae73c1 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Mon, 23 Mar 2026 22:33:25 +0000 Subject: [PATCH 22/33] Adjust the -M help so it specifies the range of accepted values (#67). --- src/main/help.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/help.c b/src/main/help.c index 5664bee..dd02ebd 100644 --- a/src/main/help.c +++ b/src/main/help.c @@ -372,7 +372,7 @@ void display_help(void) N_("show progress of process PID"), { 0, 0, 0, 0} }, #endif /* PV_REMOTE_CONTROL */ - { "-M", "--monitor", N_("SIDE"), + { "-M", "--monitor", "in|0|out|1|both|2", N_("run a command and monitor its standard input, output, or both"), { 0, 0, 0, 0} }, { "", NULL, NULL, NULL, { 0, 0, 0, 0} }, From 7edbd3e9516fe2b15a5b16cdae7e9bdb8cf9b32c Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Tue, 24 Mar 2026 23:06:36 +0000 Subject: [PATCH 23/33] Add a TODO about how to get ratio information (#67). --- src/main/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/main.c b/src/main/main.c index 7b671e1..b860809 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -316,6 +316,7 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s } /* TODO: use other_side_pid, for ratio display. */ + /* TODO: exchange transfer information like -Q. */ /*@-observertrans@ */ dummy_argv[0] = "-"; From 8281b918effb9a285e3194e18062234079b75cb0 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 27 Mar 2026 22:28:07 +0000 Subject: [PATCH 24/33] Add a TODO marker for monitor mode suggesting another way to pass ratio information between in and out (#67). --- src/main/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/main.c b/src/main/main.c index b860809..d662f8c 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -317,6 +317,7 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s /* TODO: use other_side_pid, for ratio display. */ /* TODO: exchange transfer information like -Q. */ + /* TODO: ...or arrange pipes between the two PIDs. */ /*@-observertrans@ */ dummy_argv[0] = "-"; From 2a54d4edcc5f3096c65720cc150a037dee534a67 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sat, 28 Mar 2026 22:40:45 +0000 Subject: [PATCH 25/33] Set up a pair of pipes for the in and out monitors to use to pass progress details between each other, and replace repeated "close if open" code with a macro (#67). --- src/main/main.c | 212 +++++++++++++++++++++++++++++++----------------- 1 file changed, 138 insertions(+), 74 deletions(-) diff --git a/src/main/main.c b/src/main/main.c index d662f8c..7429f60 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -274,16 +274,26 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta * Run the main transfer loop for the given side of monitor mode, using the * "command_fd" file descriptor as the transfer loop's output (on the "in" * side, it's a pipe to the command) or as its input (on the "out" side, - * it's a pipe from the command). If both sides are active, - * "other_side_pid" is the PID of the other side. + * it's a pipe from the command). + * + * If both sides are active, "othermonitor_pid" is the PID of the monitor on + * the other side, "othermonitor_read_fd" is a pipe file descriptor to read + * info from the other monitor, and "othermonitor_write_fd" is for writing + * info to the other monitor. * * Returns the appropriate exit status. * - * As side effect, "command_fd" is closed. + * As a side effect, "command_fd" is closed. */ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t side, int command_fd, /*@unused@ */ - __attribute__((unused)) pid_t other_side_pid) + __attribute__((unused)) pid_t othermonitor_pid, + /*@unused@ */ + __attribute__((unused)) + int othermonitor_read_fd, + /*@unused@ */ + __attribute__((unused)) + int othermonitor_write_fd) { const char *dummy_argv[1]; /* flawfinder: ignore */ @@ -315,9 +325,7 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s fprintf(stderr, "%s: %s\n", program_name, strerror(errno)); } - /* TODO: use other_side_pid, for ratio display. */ - /* TODO: exchange transfer information like -Q. */ - /* TODO: ...or arrange pipes between the two PIDs. */ + /* TODO: pass othermonitor_* into the state for ratio display. */ /*@-observertrans@ */ dummy_argv[0] = "-"; @@ -329,13 +337,21 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s /* - * Run in monitor mode: run a process and run the main transfer loop on its - * input, output, or both. Returns the appropriate exit status. + * Monitor mode: run a process and run the main transfer loop on its input, + * output, or both. Returns the appropriate exit status. + * + * When monitoring both sides, two pipes are set up between the monitor + * processes for each side - in to out, and out to in - for them to exchange + * information about the transfer, so both sides can see how many bytes the + * other side has transferred. This is what allows the in:out ratio to be + * displayed. */ static int pv__monitor(pvstate_t state, opts_t opts) { - int pipefd_in[2]; - int pipefd_out[2]; + int pipefd_cmd_in[2]; /* pipe from the monitor to the command */ + int pipefd_cmd_out[2]; /* pipe from the command to the monitor */ + int pipefd_in_to_out[2]; /* from in monitor to out monitor */ + int pipefd_out_to_in[2]; /* from out monitor to in monitor */ int retcode, pid_status; pid_t command_pid, in_monitor_pid, out_monitor_pid, waited_pid; @@ -353,82 +369,79 @@ static int pv__monitor(pvstate_t state, opts_t opts) * Create the pipes for communicating with the command. */ - pipefd_in[0] = -1; - pipefd_in[1] = -1; - pipefd_out[0] = -1; - pipefd_out[1] = -1; + pipefd_cmd_in[0] = -1; + pipefd_cmd_in[1] = -1; + pipefd_cmd_out[0] = -1; + pipefd_cmd_out[1] = -1; /* Pipe for the input side of the command, if we're monitoring it. */ if ((PV_SIDE_IN == opts->side) || (PV_SIDE_BOTH == opts->side)) { - if (0 != pipe(pipefd_in)) { + if (0 != pipe(pipefd_cmd_in)) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); return PV_ERROREXIT_MONITOR; } - debug("pipefd_in[]=(%d,%d)", pipefd_in[0], pipefd_in[1]); + debug("pipefd_cmd_in[]=(%d,%d)", pipefd_cmd_in[0], pipefd_cmd_in[1]); } /* Pipe for the output side of the command, if we're monitoring it. */ if ((PV_SIDE_OUT == opts->side) || (PV_SIDE_BOTH == opts->side)) { - if (0 != pipe(pipefd_out)) { + if (0 != pipe(pipefd_cmd_out)) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); - if (-1 != pipefd_in[0]) - (void) close(pipefd_in[0]); - if (-1 != pipefd_in[1]) - (void) close(pipefd_in[1]); + if (-1 != pipefd_cmd_in[0]) + (void) close(pipefd_cmd_in[0]); + if (-1 != pipefd_cmd_in[1]) + (void) close(pipefd_cmd_in[1]); return PV_ERROREXIT_MONITOR; } - debug("pipefd_out[]=(%d,%d)", pipefd_out[0], pipefd_out[1]); + debug("pipefd_cmd_out[]=(%d,%d)", pipefd_cmd_out[0], pipefd_cmd_out[1]); } + /* Common idiom to close an fd, and set it to -1, if it's open. */ +#define close_if_open(x) if (-1 != x) { \ +(void) close(x); \ +x = 1; \ +} + /* Create a process to run the command. */ command_pid = (pid_t) fork(); if (command_pid < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); - if (-1 != pipefd_in[0]) - (void) close(pipefd_in[0]); - if (-1 != pipefd_in[1]) - (void) close(pipefd_in[1]); - if (-1 != pipefd_out[0]) - (void) close(pipefd_out[0]); - if (-1 != pipefd_out[1]) - (void) close(pipefd_out[1]); + close_if_open(pipefd_cmd_in[0]); + close_if_open(pipefd_cmd_in[1]); + close_if_open(pipefd_cmd_out[0]); + close_if_open(pipefd_cmd_out[1]); return PV_ERROREXIT_MONITOR; } else if (0 == command_pid) { /* Command process. */ /* Close the write end of the "in" pipe. */ - if (-1 != pipefd_in[1]) { - (void) close(pipefd_in[1]); - pipefd_in[1] = -1; - } + close_if_open(pipefd_cmd_in[1]); + /* Close the read end of the "out" pipe. */ - if (-1 != pipefd_out[0]) { - (void) close(pipefd_out[0]); - pipefd_out[0] = -1; - } + close_if_open(pipefd_cmd_out[0]); /* Put the read end of the "in" pipe on stdin. */ - if (-1 != pipefd_in[0]) { - debug("replacing command stdin with fd %d", pipefd_in[0]); - if (dup2(pipefd_in[0], STDIN_FILENO) < 0) { + if (-1 != pipefd_cmd_in[0]) { + debug("replacing command stdin with fd %d", pipefd_cmd_in[0]); + if (dup2(pipefd_cmd_in[0], STDIN_FILENO) < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); exit(EXIT_FAILURE); } - (void) close(pipefd_in[0]); - debug("replaced command stdin with fd %d", pipefd_in[0]); - pipefd_in[0] = -1; + (void) close(pipefd_cmd_in[0]); + debug("replaced command stdin with fd %d", pipefd_cmd_in[0]); + pipefd_cmd_in[0] = -1; } /* Put the write end of the "out" pipe on stdout. */ - if (-1 != pipefd_out[1]) { - debug("replacing command stdout with fd %d", pipefd_out[1]); - if (dup2(pipefd_out[1], STDOUT_FILENO) < 0) { + if (-1 != pipefd_cmd_out[1]) { + debug("replacing command stdout with fd %d", pipefd_cmd_out[1]); + if (dup2(pipefd_cmd_out[1], STDOUT_FILENO) < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); exit(EXIT_FAILURE); } - (void) close(pipefd_out[1]); - debug("replaced command stdout with fd %d", pipefd_out[1]); - pipefd_out[1] = -1; + (void) close(pipefd_cmd_out[1]); + debug("replaced command stdout with fd %d", pipefd_cmd_out[1]); + pipefd_cmd_out[1] = -1; } /* Execute the command. */ @@ -447,14 +460,38 @@ static int pv__monitor(pvstate_t state, opts_t opts) /* Main process, not the command process. */ /* Close the read end of the "in" pipe. */ - if (-1 != pipefd_in[0]) { - (void) close(pipefd_in[0]); - pipefd_in[0] = -1; - } + close_if_open(pipefd_cmd_in[0]); + /* Close the write end of the "out" pipe. */ - if (-1 != pipefd_out[1]) { - (void) close(pipefd_out[1]); - pipefd_out[1] = -1; + close_if_open(pipefd_cmd_out[1]); + + /* + * If monitoring both input and output, create pipes for the two + * monitors to talk to each other in both directions. + */ + pipefd_in_to_out[0] = -1; + pipefd_in_to_out[1] = -1; + pipefd_out_to_in[0] = -1; + pipefd_out_to_in[1] = -1; + if (PV_SIDE_BOTH == opts->side) { + if (0 != pipe(pipefd_in_to_out)) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + close_if_open(pipefd_cmd_in[1]); + close_if_open(pipefd_cmd_out[0]); + (void) kill(command_pid, SIGTERM); + return PV_ERROREXIT_MONITOR; + } + debug("pipefd_in_to_out[]=(%d,%d)", pipefd_in_to_out[0], pipefd_in_to_out[1]); + if (0 != pipe(pipefd_out_to_in)) { + fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); + close_if_open(pipefd_in_to_out[0]); + close_if_open(pipefd_in_to_out[1]); + close_if_open(pipefd_cmd_in[1]); + close_if_open(pipefd_cmd_out[0]); + (void) kill(command_pid, SIGTERM); + return PV_ERROREXIT_MONITOR; + } + debug("pipefd_out_to_in[]=(%d,%d)", pipefd_out_to_in[0], pipefd_out_to_in[1]); } /* @@ -468,21 +505,44 @@ static int pv__monitor(pvstate_t state, opts_t opts) out_monitor_pid = (pid_t) fork(); if (out_monitor_pid < 0) { fprintf(stderr, "%s: %s\n", opts->program_name, strerror(errno)); - if (-1 != pipefd_in[1]) - (void) close(pipefd_in[1]); - if (-1 != pipefd_out[0]) - (void) close(pipefd_out[0]); + close_if_open(pipefd_in_to_out[0]); + close_if_open(pipefd_in_to_out[1]); + close_if_open(pipefd_out_to_in[0]); + close_if_open(pipefd_out_to_in[1]); + close_if_open(pipefd_cmd_in[1]); + close_if_open(pipefd_cmd_out[0]); (void) kill(command_pid, SIGTERM); return PV_ERROREXIT_MONITOR; } else if (0 == out_monitor_pid) { /* Output side monitoring process. */ - /* Close the write end of the "in" pipe. */ - if (-1 != pipefd_in[1]) - (void) close(pipefd_in[1]); + /* Close the write end of the command "in" pipe. */ + close_if_open(pipefd_cmd_in[1]); - return pv__run_monitor(opts->program_name, state, PV_SIDE_OUT, pipefd_out[0], in_monitor_pid); + /* Close the write end of the in-to-out pipe. */ + close_if_open(pipefd_in_to_out[1]); + + /* Close the read end of the out-to-in pipe. */ + close_if_open(pipefd_out_to_in[0]); + + retcode = + pv__run_monitor(opts->program_name, state, PV_SIDE_OUT, pipefd_cmd_out[0], in_monitor_pid, + pipefd_in_to_out[0], pipefd_out_to_in[1]); + + /* Close the other ends of the intra-monitor pipes. */ + close_if_open(pipefd_in_to_out[0]); + close_if_open(pipefd_out_to_in[1]); + + return retcode; } + + /* Input side monitoring process. */ + + /* Close the read end of the in-to-out pipe. */ + close_if_open(pipefd_in_to_out[0]); + + /* Close the write end of the out-to-in pipe. */ + close_if_open(pipefd_out_to_in[1]); } /* Monitor the remaining side. */ @@ -506,17 +566,17 @@ static int pv__monitor(pvstate_t state, opts_t opts) #endif case PV_SIDE_IN: /* Close the read end of the "out" pipe. */ - if (-1 != pipefd_out[0]) - (void) close(pipefd_out[0]); - pipefd_out[0] = -1; - retcode = pv__run_monitor(opts->program_name, state, PV_SIDE_IN, pipefd_in[1], out_monitor_pid); + close_if_open(pipefd_cmd_out[0]); + retcode = + pv__run_monitor(opts->program_name, state, PV_SIDE_IN, pipefd_cmd_in[1], out_monitor_pid, + pipefd_out_to_in[0], pipefd_in_to_out[1]); break; case PV_SIDE_OUT: /* Close the write end of the "in" pipe. */ - if (-1 != pipefd_in[1]) - (void) close(pipefd_in[1]); - pipefd_in[1] = -1; - retcode = pv__run_monitor(opts->program_name, state, PV_SIDE_OUT, pipefd_out[0], in_monitor_pid); + close_if_open(pipefd_cmd_in[1]); + retcode = + pv__run_monitor(opts->program_name, state, PV_SIDE_OUT, pipefd_cmd_out[0], in_monitor_pid, + pipefd_in_to_out[0], pipefd_out_to_in[1]); break; } @@ -542,6 +602,10 @@ static int pv__monitor(pvstate_t state, opts_t opts) /* If monitoring both sides, wait for the "out" side to exit. */ if (PV_SIDE_BOTH == opts->side && -1 != out_monitor_pid) { + /* Close our ends of the intra-monitor pipes. */ + close_if_open(pipefd_in_to_out[1]); + close_if_open(pipefd_out_to_in[0]); + /* Wait for the "out" side to exit. */ do { /*@-type@ */ /* splint disagreement about __pid_t vs pid_t. */ From 00f06f2b2cab6009dae7ede462045a28a41bce20 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Sun, 29 Mar 2026 21:49:47 +0100 Subject: [PATCH 26/33] Pass the othermonitor_* values into the main state, and add TODO markers for next steps (#67). --- src/include/pv-internal.h | 3 +++ src/include/pv.h | 1 + src/main/main.c | 11 ++--------- src/main/options.c | 1 + src/pv/display.c | 1 + src/pv/loop.c | 7 +++++++ src/pv/state.c | 10 ++++++++++ 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/include/pv-internal.h b/src/include/pv-internal.h index ba9830c..d7f6491 100644 --- a/src/include/pv-internal.h +++ b/src/include/pv-internal.h @@ -201,6 +201,9 @@ struct pvstate_s { off_t size; /* total size of data */ unsigned int skip_errors; /* skip read errors counter */ int output_fd; /* fd to write output to */ + pid_t othermonitor_pid; /* pid of the other monitor, in "-M both" mode */ + int othermonitor_read_fd; /* fd to read transfer counts from other monitor */ + int othermonitor_write_fd; /* fd to write transfer counts to other monitor */ unsigned int average_rate_window; /* time window in seconds for average rate calculations */ unsigned int history_interval; /* seconds between each average rate calc history entry */ pvdisplay_width_t width; /* screen width */ diff --git a/src/include/pv.h b/src/include/pv.h index c7690c0..a8009ab 100644 --- a/src/include/pv.h +++ b/src/include/pv.h @@ -239,6 +239,7 @@ extern void pv_state_extra_display_set(pvstate_t, /*@null@*/ const char *); extern void pv_state_output_set(pvstate_t, int, const char *); extern void pv_state_average_rate_window_set(pvstate_t, unsigned int); extern void pv_state_set_terminal_supports_utf8(pvstate_t, bool); +extern void pv_state_othermonitor_set(pvstate_t, pid_t, int, int); extern void pv_state_inputfiles(pvstate_t, unsigned int, const char **); extern void pv_state_watchfds(pvstate_t, unsigned int, const pid_t *, const int *); diff --git a/src/main/main.c b/src/main/main.c index 7429f60..67e4a20 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -286,14 +286,7 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta * As a side effect, "command_fd" is closed. */ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t side, int command_fd, - /*@unused@ */ - __attribute__((unused)) pid_t othermonitor_pid, - /*@unused@ */ - __attribute__((unused)) - int othermonitor_read_fd, - /*@unused@ */ - __attribute__((unused)) - int othermonitor_write_fd) + pid_t othermonitor_pid, int othermonitor_read_fd, int othermonitor_write_fd) { const char *dummy_argv[1]; /* flawfinder: ignore */ @@ -325,7 +318,7 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s fprintf(stderr, "%s: %s\n", program_name, strerror(errno)); } - /* TODO: pass othermonitor_* into the state for ratio display. */ + pv_state_othermonitor_set(state, othermonitor_pid, othermonitor_read_fd, othermonitor_write_fd); /*@-observertrans@ */ dummy_argv[0] = "-"; diff --git a/src/main/options.c b/src/main/options.c index ead226e..cf85a6f 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -1290,6 +1290,7 @@ opts_t opts_parse(unsigned int argc, char **argv) opts->eta = true; opts->rate = true; opts->bytes = true; + /* TODO: add ratio format if "-M both" */ } /* If -Z was given but not -E, pretend one -E was given too. */ diff --git a/src/pv/display.c b/src/pv/display.c index 2c7a1ff..365b9b0 100644 --- a/src/pv/display.c +++ b/src/pv/display.c @@ -544,6 +544,7 @@ pvdisplay_bytecount_t pv_formatter_segmentcontent(char *content, pvformatter_arg { "{sgr:colour,...}", &pv_formatter_sgr, false }, { NULL, NULL, false } }; + /* TODO: formatter for ratio, for "-M both" */ return format_component_array; } diff --git a/src/pv/loop.c b/src/pv/loop.c index 776c832..5c82bc9 100644 --- a/src/pv/loop.c +++ b/src/pv/loop.c @@ -185,6 +185,10 @@ int pv_main_loop(pvstate_t state) unsigned int file_idx; bool output_is_pipe; + /* TODO: treat "-F ''" as "-q" so there's no extra blank line. */ + /* TODO: if "-q", turn off "-c", again so there's no blank line. */ + /* May need local no_display and cursor flags for this. */ + /* * "written" is ALWAYS bytes written by the last transfer. * @@ -338,6 +342,9 @@ int pv_main_loop(pvstate_t state) pv_elapsedtime_add_nsec(&next_remotecheck, REMOTE_INTERVAL); } + /* TODO: occasionally check othermonitor_read_fd, calculate ratio */ + /* TODO: occasionally send to othermonitor_write_fd */ + if (1 == state->flags.trigger_exit) break; diff --git a/src/pv/state.c b/src/pv/state.c index 2345576..81cba2c 100644 --- a/src/pv/state.c +++ b/src/pv/state.c @@ -191,6 +191,9 @@ pvstate_t pv_state_alloc(void) state->watchfd.count = 0; state->control.output_fd = -1; + state->control.othermonitor_pid = 0; + state->control.othermonitor_read_fd = -1; + state->control.othermonitor_write_fd = -1; #ifdef HAVE_IPC state->cursor.shmid = -1; state->cursor.pvcount = 1; @@ -769,6 +772,13 @@ void pv_state_set_terminal_supports_utf8(pvstate_t state, bool val) state->status.terminal_supports_utf8 = val; } +void pv_state_othermonitor_set(pvstate_t state, pid_t pid, int read_fd, int write_fd) +{ + state->control.othermonitor_pid = pid; + state->control.othermonitor_read_fd = read_fd; + state->control.othermonitor_write_fd = write_fd; +} + /* * Set the array of input files. */ From 8ef4ff2e848ec7016a092901f4c45426f53c64ac Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Mon, 30 Mar 2026 21:41:16 +0100 Subject: [PATCH 27/33] If an empty format string is supplied, such as -F "", make that imply -q and cancel -c, so that -F "" -F "%b %t %r %p %e" -M both will monitor both sides but only display the output side - which will be useful when ratio is added (#67). --- src/include/pv.h | 1 + src/main/main.c | 3 +++ src/pv/loop.c | 4 ---- src/pv/state.c | 12 ++++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/include/pv.h b/src/include/pv.h index a8009ab..c467ac0 100644 --- a/src/include/pv.h +++ b/src/include/pv.h @@ -240,6 +240,7 @@ extern void pv_state_output_set(pvstate_t, int, const char *); extern void pv_state_average_rate_window_set(pvstate_t, unsigned int); extern void pv_state_set_terminal_supports_utf8(pvstate_t, bool); extern void pv_state_othermonitor_set(pvstate_t, pid_t, int, int); +extern void pv_state_cancel_output_if_empty_format_string(pvstate_t); extern void pv_state_inputfiles(pvstate_t, unsigned int, const char **); extern void pv_state_watchfds(pvstate_t, unsigned int, const pid_t *, const int *); diff --git a/src/main/main.c b/src/main/main.c index 67e4a20..604189f 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -325,6 +325,7 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s /*@+observertrans@ */ pv_state_inputfiles(state, 1, dummy_argv); + pv_state_cancel_output_if_empty_format_string(state); return pv_main_loop(state); } @@ -892,10 +893,12 @@ int main(int argc, char **argv) break; case PV_ACTION_TRANSFER: /* Normal "transfer data" mode. */ + pv_state_cancel_output_if_empty_format_string(state); retcode = pv_main_loop(state); break; case PV_ACTION_STORE_AND_FORWARD: /* Store-and-forward transfer mode. */ + pv_state_cancel_output_if_empty_format_string(state); retcode = pv__store_and_forward(state, opts, can_have_eta); break; case PV_ACTION_WATCHFD: diff --git a/src/pv/loop.c b/src/pv/loop.c index 5c82bc9..c500fb7 100644 --- a/src/pv/loop.c +++ b/src/pv/loop.c @@ -185,10 +185,6 @@ int pv_main_loop(pvstate_t state) unsigned int file_idx; bool output_is_pipe; - /* TODO: treat "-F ''" as "-q" so there's no extra blank line. */ - /* TODO: if "-q", turn off "-c", again so there's no blank line. */ - /* May need local no_display and cursor flags for this. */ - /* * "written" is ALWAYS bytes written by the last transfer. * diff --git a/src/pv/state.c b/src/pv/state.c index 81cba2c..38f4e9e 100644 --- a/src/pv/state.c +++ b/src/pv/state.c @@ -779,6 +779,18 @@ void pv_state_othermonitor_set(pvstate_t state, pid_t pid, int read_fd, int writ state->control.othermonitor_write_fd = write_fd; } +/* If the format string is set to an empty string, stop all display output. */ +void pv_state_cancel_output_if_empty_format_string(pvstate_t state) +{ + if (NULL == state->control.format_string) + return; + if ('\0' != state->control.format_string[0]) + return; + debug("%s", "empty format string - setting no_display and turning off cursor positioning"); + state->control.no_display = true; + state->control.cursor = false; +} + /* * Set the array of input files. */ From 80358e84f32618a3f211c93706eac446dad8e596 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Thu, 2 Apr 2026 23:08:49 +0100 Subject: [PATCH 28/33] If no display options are passed and "-M both" is active, set a "ratio" option flag to display the in:out ratio as well (#67). --- src/include/options.h | 1 + src/main/options.c | 3 ++- src/pv/state.c | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/include/options.h b/src/include/options.h index ecf0b23..3997895 100644 --- a/src/include/options.h +++ b/src/include/options.h @@ -93,6 +93,7 @@ struct opts_s { bool rate; /* rate counter flag */ bool average_rate; /* average rate counter flag */ bool bytes; /* bytes transferred flag */ + bool ratio; /* in:out ratio flag */ bool bits; /* report transfer size in bits */ bool decimal_units; /* decimal prefix flag */ bool bufpercent; /* transfer buffer percentage flag */ diff --git a/src/main/options.c b/src/main/options.c index cf85a6f..f8877e7 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -1290,7 +1290,8 @@ opts_t opts_parse(unsigned int argc, char **argv) opts->eta = true; opts->rate = true; opts->bytes = true; - /* TODO: add ratio format if "-M both" */ + if (PV_SIDE_BOTH == opts->side) + opts->ratio = true; } /* If -Z was given but not -E, pretend one -E was given too. */ diff --git a/src/pv/state.c b/src/pv/state.c index 38f4e9e..298cdd2 100644 --- a/src/pv/state.c +++ b/src/pv/state.c @@ -412,6 +412,8 @@ void pv_state_free(pvstate_t state) /* * Set the formatting string, given a set of old-style formatting options. + * + * TODO: allow opts->ratio to be passed here as well. */ void pv_state_set_format(pvstate_t state, bool progress, bool timer, bool eta, bool fineta, bool rate, bool average_rate, bool bytes, bool bufpercent, size_t lastwritten, /*@null@ */ const char *name) From d7327aaf954e4e431e2e8d6d443054964f331a19 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 3 Apr 2026 20:00:42 +0100 Subject: [PATCH 29/33] Replace pv_state_set_format() with pv_set_format_options(), which takes a structure rather than a long list of arguments, to make it easier to pass around - and rely on pv_state_name_set() being called first rather than doing it within this function, so it is only doing one job (#67). --- src/include/pv-internal.h | 12 +------ src/include/pv.h | 22 ++++++++----- src/main/main.c | 36 ++++++++++++-------- src/pv/remote.c | 27 ++++++++++----- src/pv/state.c | 69 ++++++++++++++++++--------------------- 5 files changed, 87 insertions(+), 79 deletions(-) diff --git a/src/include/pv-internal.h b/src/include/pv-internal.h index d7f6491..a801505 100644 --- a/src/include/pv-internal.h +++ b/src/include/pv-internal.h @@ -209,17 +209,7 @@ struct pvstate_s { pvdisplay_width_t width; /* screen width */ unsigned int height; /* screen height */ unsigned int extra_displays; /* bitmask of extra display destinations */ - struct { /* old-style format options (used by -R) */ - size_t lastwritten; /* --last-written (amount) */ - bool progress; /* --progress */ - bool timer; /* --timer */ - bool eta; /* --eta */ - bool fineta; /* --fineta */ - bool rate; /* --rate */ - bool average_rate; /* --average-rate */ - bool bytes; /* --bytes */ - bool bufpercent; /* --buffer-percent */ - } format_option; + pvformatoptions_s format_option; /* old-style format options (used by -R) */ bool force; /* display even if not on terminal */ bool cursor; /* use cursor positioning */ bool numeric; /* numeric output only */ diff --git a/src/include/pv.h b/src/include/pv.h index c467ac0..8537b3b 100644 --- a/src/include/pv.h +++ b/src/include/pv.h @@ -193,15 +193,21 @@ extern /*@null@*/ /*@only@*/ pvstate_t pv_state_alloc(void); extern void pv_state_reset(pvstate_t state); /* - * Set the formatting string, given a set of old-style formatting options. + * Set the format options and use them to build a default formatting string. + * The default string is used if no format string is explicitly set. */ -extern void pv_state_set_format(pvstate_t state, bool progress, - bool timer, bool eta, - bool fineta, bool rate, - bool average_rate, bool bytes, - bool bufpercent, - size_t lastwritten, - /*@null@*/ const char *name); +typedef struct { + size_t lastwritten; /* --last-written (amount) */ + bool progress; /* --progress */ + bool timer; /* --timer */ + bool eta; /* --eta */ + bool fineta; /* --fineta */ + bool rate; /* --rate */ + bool average_rate; /* --average-rate */ + bool bytes; /* --bytes */ + bool bufpercent; /* --buffer-percent */ +} pvformatoptions_s; +extern void pv_state_set_format_options(pvstate_t, pvformatoptions_s); /* * Set the various options. diff --git a/src/main/main.c b/src/main/main.c index 604189f..98e9c28 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -163,7 +163,7 @@ static int pv__set_output(pvstate_t state, opts_t opts, /*@null@ */ const char * * with the input file list forced to be just the store-and-forward file. * Returns nonzero on error. */ -static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta) +static int pv__store_and_forward(pvstate_t state, opts_t opts, pvformatoptions_s format_options) { char tmp_filename[4096]; /* flawfinder: ignore */ bool use_temporary_file; @@ -224,11 +224,10 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta if (0 != retcode) goto end_store_and_forward; - /* Reset the formatting to set the displayed name to "(input)". */ + /* Set the displayed name to "(input)" and trigger a format reparse. */ /*@-mustfreefresh@ */ - pv_state_set_format(state, opts->progress, opts->timer, can_have_eta ? opts->eta : false, - can_have_eta ? opts->fineta : false, opts->rate, opts->average_rate, - opts->bytes, opts->bufpercent, opts->lastwritten, _("(input)")); + pv_state_name_set(state, _("(input)")); + pv_state_set_format_options(state, format_options); /*@+mustfreefresh@ *//* see below about gettext _() calls. */ /* Run the main loop as normal. */ @@ -250,10 +249,12 @@ static int pv__store_and_forward(pvstate_t state, opts_t opts, bool can_have_eta /* Recalculate the input size. */ pv_state_size_set(state, pv_calc_total_size(state)); + /* Set the displayed name to whatever was requested. */ + pv_state_name_set(state, opts->name); /* Reset the format, since we might have been asked to show ETA. */ - pv_state_set_format(state, opts->progress, opts->timer, opts->eta, - opts->fineta, opts->rate, opts->average_rate, - opts->bytes, opts->bufpercent, opts->lastwritten, opts->name); + format_options.eta = opts->eta; + format_options.fineta = opts->fineta; + pv_state_set_format_options(state, format_options); /* Reset calculated values in the state. */ pv_state_reset(state); @@ -552,7 +553,7 @@ x = 1; \ if (NULL != opts->format1) { pv_state_format_string_set(state, opts->format1); } - /* TODO: call pv_state_set_format(). */ + /* TODO: call pv_state_set_format_options(). */ /*@fallthrough@ */ /* falling through as "out" is in another process (above). */ #ifndef SPLINT @@ -624,6 +625,7 @@ int main(int argc, char **argv) int retcode = 0; bool can_have_eta = true; bool terminal_supports_utf8 = false; + pvformatoptions_s format_options; #if ! HAVE_SETPROCTITLE initproctitle(argc, argv); @@ -880,9 +882,17 @@ int main(int argc, char **argv) pv_state_extra_display_set(state, opts->extra_display); pv_state_average_rate_window_set(state, opts->average_rate_window); - pv_state_set_format(state, opts->progress, opts->timer, can_have_eta ? opts->eta : false, - can_have_eta ? opts->fineta : false, opts->rate, opts->average_rate, - opts->bytes, opts->bufpercent, opts->lastwritten, opts->name); + format_options.progress = opts->progress; + format_options.timer = opts->timer; + format_options.eta = can_have_eta ? opts->eta : false; + format_options.fineta = can_have_eta ? opts->fineta : false; + format_options.rate = opts->rate; + format_options.average_rate = opts->average_rate; + format_options.bytes = opts->bytes; + format_options.bufpercent = opts->bufpercent; + format_options.lastwritten = opts->lastwritten; + + pv_state_set_format_options(state, format_options); debug("%s: %s", "terminal_supports_utf8", terminal_supports_utf8 ? "true" : "false"); pv_state_set_terminal_supports_utf8(state, terminal_supports_utf8); @@ -899,7 +909,7 @@ int main(int argc, char **argv) case PV_ACTION_STORE_AND_FORWARD: /* Store-and-forward transfer mode. */ pv_state_cancel_output_if_empty_format_string(state); - retcode = pv__store_and_forward(state, opts, can_have_eta); + retcode = pv__store_and_forward(state, opts, format_options); break; case PV_ACTION_WATCHFD: /* "Watch file descriptor(s) of another process" mode. */ diff --git a/src/pv/remote.c b/src/pv/remote.c index aeeec91..4867a9f 100644 --- a/src/pv/remote.c +++ b/src/pv/remote.c @@ -247,8 +247,8 @@ int pv_remote_set(pvstate_t state, pid_t remote) * If a message was received, update the current process's options with the * ones in the message. * - * Note that this relies on pv_state_set_format() causing the output format - * to be reparsed. + * Note that this relies on pv_state_set_format_options() causing the output + * format to be reparsed. */ static bool pv__rxsignal_usr2(pvstate_t state) { @@ -256,6 +256,7 @@ static bool pv__rxsignal_usr2(pvstate_t state) char control_filename[4096]; /* flawfinder: ignore */ FILE *control_fptr; struct remote_msg msgbuf; + pvformatoptions_s format_options; /* flawfinder rationale: as above. */ @@ -306,11 +307,19 @@ static bool pv__rxsignal_usr2(pvstate_t state) msgbuf.format[sizeof(msgbuf.format) - 1] = '\0'; msgbuf.extra_display[sizeof(msgbuf.extra_display) - 1] = '\0'; - pv_state_set_format(state, msgbuf.progress, msgbuf.timer, - msgbuf.eta, msgbuf.fineta, msgbuf.rate, - msgbuf.average_rate, - msgbuf.bytes, msgbuf.bufpercent, - msgbuf.lastwritten, '\0' == msgbuf.name[0] ? NULL : msgbuf.name); + pv_state_name_set(state, '\0' == msgbuf.name[0] ? NULL : msgbuf.name); + + format_options.progress = msgbuf.progress; + format_options.timer = msgbuf.timer; + format_options.eta = msgbuf.eta; + format_options.fineta = msgbuf.fineta; + format_options.rate = msgbuf.rate; + format_options.average_rate = msgbuf.average_rate; + format_options.bytes = msgbuf.bytes; + format_options.bufpercent = msgbuf.bufpercent; + format_options.lastwritten = msgbuf.lastwritten; + + pv_state_set_format_options(state, format_options); if (msgbuf.rate_limit > 0) pv_state_rate_limit_set(state, msgbuf.rate_limit); @@ -475,8 +484,8 @@ static bool pv__rxsignal_usr1(pvstate_t state, pid_t match_sender) * send our transfer state to the sending process, depending on the content * of the message. * - * NB --remote relies on pv_state_set_format() causing the output format to - * be reparsed. + * NB --remote relies on pv_state_set_format_options() causing the output + * format to be reparsed. * * Returns true if a --remote message was received, false otherwise. */ diff --git a/src/pv/state.c b/src/pv/state.c index 298cdd2..9229133 100644 --- a/src/pv/state.c +++ b/src/pv/state.c @@ -411,12 +411,15 @@ void pv_state_free(pvstate_t state) /* - * Set the formatting string, given a set of old-style formatting options. + * Set the format options and use them to build a default formatting string. + * The default string is used if no format string is explicitly set. + * + * Call this *after* setting a name, so it can determine whether there + * should be a name in the default format. * * TODO: allow opts->ratio to be passed here as well. */ -void pv_state_set_format(pvstate_t state, bool progress, bool timer, bool eta, bool fineta, bool rate, bool average_rate, bool bytes, bool bufpercent, size_t lastwritten, /*@null@ */ - const char *name) +void pv_state_set_format_options(pvstate_t state, pvformatoptions_s format) { #define PV_ADDFORMAT(x,y) if (x) { \ if (state->control.default_format[0] != '\0') \ @@ -424,15 +427,15 @@ void pv_state_set_format(pvstate_t state, bool progress, bool timer, bool eta, b (void) pv_strlcat(state->control.default_format, y, sizeof(state->control.default_format)); \ } - state->control.format_option.progress = progress; - state->control.format_option.timer = timer; - state->control.format_option.eta = eta; - state->control.format_option.fineta = fineta; - state->control.format_option.rate = rate; - state->control.format_option.average_rate = average_rate; - state->control.format_option.bytes = bytes; - state->control.format_option.bufpercent = bufpercent; - state->control.format_option.lastwritten = lastwritten; + state->control.format_option.progress = format.progress; + state->control.format_option.timer = format.timer; + state->control.format_option.eta = format.eta; + state->control.format_option.fineta = format.fineta; + state->control.format_option.rate = format.rate; + state->control.format_option.average_rate = format.average_rate; + state->control.format_option.bytes = format.bytes; + state->control.format_option.bufpercent = format.bufpercent; + state->control.format_option.lastwritten = format.lastwritten; state->control.default_format[0] = '\0'; @@ -443,21 +446,21 @@ void pv_state_set_format(pvstate_t state, bool progress, bool timer, bool eta, b * Add the format strings for the enabled options in a * standard order. */ - PV_ADDFORMAT(name, "%N"); - PV_ADDFORMAT(bytes, "%b"); - PV_ADDFORMAT(bufpercent, "%T"); - PV_ADDFORMAT(timer, "%t"); - PV_ADDFORMAT(rate, "%r"); - PV_ADDFORMAT(average_rate, "%a"); - PV_ADDFORMAT(progress, "%p"); - PV_ADDFORMAT(eta, "%e"); - PV_ADDFORMAT(fineta, "%I"); + PV_ADDFORMAT(NULL != state->control.name, "%N"); + PV_ADDFORMAT(format.bytes, "%b"); + PV_ADDFORMAT(format.bufpercent, "%T"); + PV_ADDFORMAT(format.timer, "%t"); + PV_ADDFORMAT(format.rate, "%r"); + PV_ADDFORMAT(format.average_rate, "%a"); + PV_ADDFORMAT(format.progress, "%p"); + PV_ADDFORMAT(format.eta, "%e"); + PV_ADDFORMAT(format.fineta, "%I"); - if (lastwritten > 0) { + if (format.lastwritten > 0) { char buf[16]; /* flawfinder: ignore */ memset(buf, 0, sizeof(buf)); - (void) pv_snprintf(buf, sizeof(buf), "%%%uA", (unsigned int) lastwritten); - PV_ADDFORMAT(lastwritten > 0, buf); + (void) pv_snprintf(buf, sizeof(buf), "%%%uA", (unsigned int) format.lastwritten); + PV_ADDFORMAT(format.lastwritten > 0, buf); /* * flawfinder rationale: large enough for string, * zeroed before use, only written to by @@ -468,25 +471,15 @@ void pv_state_set_format(pvstate_t state, bool progress, bool timer, bool eta, b } else { /* Numeric mode has different behaviour. */ - PV_ADDFORMAT(timer, "%t"); - PV_ADDFORMAT(bytes, "%b"); - PV_ADDFORMAT(rate, "%r"); - PV_ADDFORMAT(!(bytes || rate), "%{progress-amount-only}"); + PV_ADDFORMAT(format.timer, "%t"); + PV_ADDFORMAT(format.bytes, "%b"); + PV_ADDFORMAT(format.rate, "%r"); + PV_ADDFORMAT(!(format.bytes || format.rate), "%{progress-amount-only}"); } debug("%s: [%s]", "default format set", state->control.default_format); - /* Free any previously set name. */ - if (NULL != state->control.name) { - free(state->control.name); - state->control.name = NULL; - } - - /* Set a new name if one was given. */ - if (NULL != name) - state->control.name = pv_strdup(name); - /* Tell pv_format() that the format has changed. */ state->flags.reparse_display = 1; } From 7892e54d7ee3cd1aa6df932fd8a7a6818c9a2f1b Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 3 Apr 2026 20:20:50 +0100 Subject: [PATCH 30/33] Rather than setting an option flag for ratio, add it to the default format string on the "out" side (#67). --- src/include/pv.h | 3 +++ src/main/main.c | 12 +++++++++--- src/main/options.c | 2 -- src/pv/state.c | 13 +++++++++++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/include/pv.h b/src/include/pv.h index 8537b3b..031f1c2 100644 --- a/src/include/pv.h +++ b/src/include/pv.h @@ -209,6 +209,9 @@ typedef struct { } pvformatoptions_s; extern void pv_state_set_format_options(pvstate_t, pvformatoptions_s); +/* Append a string to the default format. */ +void pv_state_append_to_default_format(pvstate_t, /*@null@ */ const char *); + /* * Set the various options. */ diff --git a/src/main/main.c b/src/main/main.c index 98e9c28..dda3c55 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -341,7 +341,7 @@ static int pv__run_monitor(const char *program_name, pvstate_t state, pvside_t s * other side has transferred. This is what allows the in:out ratio to be * displayed. */ -static int pv__monitor(pvstate_t state, opts_t opts) +static int pv__monitor(pvstate_t state, opts_t opts, pvformatoptions_s format_options) { int pipefd_cmd_in[2]; /* pipe from the monitor to the command */ int pipefd_cmd_out[2]; /* pipe from the command to the monitor */ @@ -520,6 +520,11 @@ x = 1; \ /* Close the read end of the out-to-in pipe. */ close_if_open(pipefd_out_to_in[0]); + /* Add ratio to the default format on the out side. */ + if (PV_SIDE_BOTH == opts->side) { + pv_state_append_to_default_format(state, "%{ratio}"); + } + retcode = pv__run_monitor(opts->program_name, state, PV_SIDE_OUT, pipefd_cmd_out[0], in_monitor_pid, pipefd_in_to_out[0], pipefd_out_to_in[1]); @@ -553,7 +558,8 @@ x = 1; \ if (NULL != opts->format1) { pv_state_format_string_set(state, opts->format1); } - /* TODO: call pv_state_set_format_options(). */ + /* Trigger a format reparse. */ + pv_state_set_format_options(state, format_options); /*@fallthrough@ */ /* falling through as "out" is in another process (above). */ #ifndef SPLINT @@ -925,7 +931,7 @@ int main(int argc, char **argv) break; case PV_ACTION_MONITOR: /* Run a process and monitor its input and output. */ - retcode = pv__monitor(state, opts); + retcode = pv__monitor(state, opts, format_options); break; } diff --git a/src/main/options.c b/src/main/options.c index f8877e7..ead226e 100644 --- a/src/main/options.c +++ b/src/main/options.c @@ -1290,8 +1290,6 @@ opts_t opts_parse(unsigned int argc, char **argv) opts->eta = true; opts->rate = true; opts->bytes = true; - if (PV_SIDE_BOTH == opts->side) - opts->ratio = true; } /* If -Z was given but not -E, pretend one -E was given too. */ diff --git a/src/pv/state.c b/src/pv/state.c index 9229133..be226e7 100644 --- a/src/pv/state.c +++ b/src/pv/state.c @@ -416,8 +416,6 @@ void pv_state_free(pvstate_t state) * * Call this *after* setting a name, so it can determine whether there * should be a name in the default format. - * - * TODO: allow opts->ratio to be passed here as well. */ void pv_state_set_format_options(pvstate_t state, pvformatoptions_s format) { @@ -485,6 +483,17 @@ void pv_state_set_format_options(pvstate_t state, pvformatoptions_s format) } +/* + * Append the given string to the default format, and trigger a format + * reparse. + */ +void pv_state_append_to_default_format(pvstate_t state, /*@null@ */ const char *val) +{ + PV_ADDFORMAT(NULL != val, val); + state->flags.reparse_display = 1; +} + + void pv_state_force_set(pvstate_t state, bool val) { state->control.force = val; From 6c3a08af2b07f1d576a229bb1be2ced8912aa0ab Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 3 Apr 2026 20:32:14 +0100 Subject: [PATCH 31/33] Document the ratio formatting sequence (#67). --- docs/pv.1 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/pv.1 b/docs/pv.1 index 8e5a57a..92f9e28 100644 --- a/docs/pv.1 +++ b/docs/pv.1 @@ -426,9 +426,6 @@ monitored command's options separate. With a \fISIDE\fR of \*(lq\fBboth\fR\*(rq, two progress bars will be shown. All of \fBpv\fR's display switches, output modifiers, and data transfer modifiers will apply to both sides. -The \*(lq\fB\-\-format\fR\*(rq and \*(lq\fB\-\-name\fR\*(rq options may be -specified twice, in which case the first one applies to the input side and -the second applies to the output side. .IP For example, \*(lq\fBpv\~\-\-cursor\~\-\-monitor\~both\~\-\-\~gzip\~\-9\fR\*(rq is @@ -436,12 +433,18 @@ equivalent to \*(lq\fBpv\~\-\-cursor\~|\~gzip\~\-9\~|\~pv\~\-\-cursor\fR\*(rq, except that the latter can't show the input:output ratio. .IP +The \*(lq\fB\-\-format\fR\*(rq and \*(lq\fB\-\-name\fR\*(rq options may be +specified twice, in which case the first one applies to the input side and +the second applies to the output side. +If no \*(lq\fB\-\-format\fR\*(rq options are specified, the default format +based on the other display switches is used as normal, except that the +ratio is added to the output side when \fISIDE\fR is \*(lq\fBboth\fR\*(rq. +.IP The values \*(lq\fB0\fR\*(rq, \*(lq\fB1\fR\*(rq, and \*(lq\fB2\fR\*(rq may be used as synonyms for \*(lq\fBin\fR\*(rq, \*(lq\fBout\fR\*(rq, and \*(lq\fBboth\fR\*(rq. .IP This option cannot be used with \*(lq\fB\-\-store\-and\-forward\*(rq. -.\"TODO: formatting option for input:output ratio .\" .SS "Other options" .TP @@ -531,6 +534,10 @@ Equivalent to \*(lq\fB\-\-bytes\fR\*(rq. If \*(lq\fB\-\-bits\fR\*(rq was specified, \*(lq\fB%b\fR\*(rq shows the bits transferred so far, not bytes. .TP +.B %{ratio} +Show the ratio of bytes transferred by the other side to the bytes +transferred by this side, in \*(lq\fB\-\-monitor\~both\fR\*(rq mode. +.TP .BR %T ", " %{buffer\-percent} Percentage of the transfer buffer in use. Equivalent to \*(lq\fB\-\-buffer\-percent\fR\*(rq. From 8f19633dd7b34d22af4bf81f52dcef995f505639 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 3 Apr 2026 21:12:13 +0100 Subject: [PATCH 32/33] In "-M both" mode, exchange information about the total amount transferred with the other side (#67). --- src/include/pv-internal.h | 3 + src/pv/loop.c | 130 +++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/include/pv-internal.h b/src/include/pv-internal.h index a801505..d0231e9 100644 --- a/src/include/pv-internal.h +++ b/src/include/pv-internal.h @@ -26,6 +26,7 @@ extern "C" { #define RATE_GRANULARITY 100000000 /* nsec between -L rate chunks */ #define RATE_BURST_WINDOW 5 /* rate burst window (multiples of rate) */ #define REMOTE_INTERVAL 100000000 /* nsec between checks for -R and -Q */ +#define MONITOR_EXCHANGE_INTERVAL 100000000 /* nsec between "-M both" data exchanges */ #define BUFFER_SIZE (size_t) 409600 /* default transfer buffer size */ #define BUFFER_SIZE_MAX (size_t) 524288 /* max auto transfer buffer size */ #define MAX_READ_AT_ONCE (size_t) 524288 /* max to read() in one go */ @@ -420,6 +421,8 @@ struct pvstate_s { off_t total_written; /* total bytes or lines written */ off_t transferred; /* amount transferred (written - unconsumed) */ + off_t otherside_transferred; /* amount transferred by the other side ("-M both") */ + /* Keep track of line positions to backtrack written_but_not_consumed. */ /*@only@*/ /*@null@*/ off_t *line_positions; /* line separator write positions (circular buffer) */ size_t line_positions_capacity; /* total size of line position array */ diff --git a/src/pv/loop.c b/src/pv/loop.c index c500fb7..90aa016 100644 --- a/src/pv/loop.c +++ b/src/pv/loop.c @@ -166,6 +166,121 @@ static long double pv__elapsed_transfer_time(const struct timespec *loop_start_t } +/* + * Exchange information with the other side of the "-M both" monitor about + * the amount of data transferred. + */ +static void pv__monitor_exchange(pvstate_t state) +{ + bool written_yet = false; + + while ((state->control.othermonitor_read_fd >= 0) || (state->control.othermonitor_write_fd >= 0)) { + struct timeval tv; + fd_set readfds; + fd_set writefds; + fd_set exceptfds; + int max_fd; + int result; + + max_fd = -1; + if (state->control.othermonitor_read_fd > max_fd) + max_fd = state->control.othermonitor_read_fd; + if ((!written_yet) && (state->control.othermonitor_write_fd > max_fd)) + max_fd = state->control.othermonitor_write_fd; + + memset(&tv, 0, sizeof(tv)); + +#if SPLINT + /* splint doesn't like FD_ZERO, FD_SET, FD_ISSET. */ + /* Refer to src/pv/transfer.c for more details. */ + memset(&readfds, 0, sizeof(readfds)); + memset(&writefds, 0, sizeof(writefds)); + memset(&exceptfds, 0, sizeof(exceptfds)); +#else /* !SPLINT */ + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&exceptfds); + if (state->control.othermonitor_read_fd >= 0) + FD_SET(state->control.othermonitor_read_fd, &readfds); + if ((!written_yet) && (state->control.othermonitor_write_fd >= 0)) + FD_SET(state->control.othermonitor_write_fd, &writefds); +#endif /* !SPLINT */ + + tv.tv_sec = 0; + tv.tv_usec = 0; + + result = select(max_fd + 1, &readfds, &writefds, &exceptfds, &tv); + + if (result <= 0) + break; + + if ((state->control.othermonitor_read_fd >= 0) +#ifndef SPLINT + && (FD_ISSET(state->control.othermonitor_read_fd, &readfds)) +#endif + ) { + off_t otherside_transferred; + ssize_t nread; + + otherside_transferred = 0; + + nread = read(state->control.othermonitor_read_fd, &otherside_transferred, /* flawfinder: ignore */ + sizeof(otherside_transferred)); + /* + * flawfinder rationale: no buffer overflow possible + * as we read a fixed size to a fixed point. + */ + + if (0 == nread) { + /* EOF - close. */ + debug("%d: %s", state->control.othermonitor_read_fd, "EOF - closing"); + (void) close(state->control.othermonitor_read_fd); + state->control.othermonitor_read_fd = -1; + } else if (nread < 0) { + debug("%d: %s: %s", state->control.othermonitor_read_fd, "error on read", + strerror(errno)); + } else if (nread != (ssize_t) (sizeof(otherside_transferred))) { + debug("%d: %d: %s", state->control.othermonitor_read_fd, (int) nread, + "incorrect byte count - ignoring"); + } else { + debug("%s: %lu", "other side transfer amount", (unsigned long) otherside_transferred); + state->transfer.otherside_transferred = otherside_transferred; + } + } + + if ((!written_yet) && (state->control.othermonitor_write_fd >= 0) +#ifndef SPLINT + && (FD_ISSET(state->control.othermonitor_write_fd, &writefds)) +#endif + ) { + ssize_t nwritten; + + nwritten = + write(state->control.othermonitor_write_fd, &(state->transfer.transferred), + sizeof(state->transfer.transferred)); + + if ((nwritten < 0) && ((EINTR == errno) || (EAGAIN == errno))) { + debug("%d: %s: %s", state->control.othermonitor_write_fd, "transient write error", + strerror(errno)); + } else if (nwritten < 0) { + debug("%d: %s: %s", state->control.othermonitor_write_fd, "write error - closing", + strerror(errno)); + (void) close(state->control.othermonitor_write_fd); + state->control.othermonitor_write_fd = -1; + } else if (nwritten != (ssize_t) (sizeof(state->transfer.transferred))) { + debug("%d: %d: %s", state->control.othermonitor_write_fd, (int) nwritten, + "incorrect byte count written - closing"); + (void) close(state->control.othermonitor_write_fd); + state->control.othermonitor_write_fd = -1; + } else { + written_yet = true; + } + + } + } +} + + /* * Pipe data from a list of files to standard output, giving information * about the transfer on standard error according to the given options. @@ -180,7 +295,7 @@ int pv_main_loop(pvstate_t state) long double target; bool eof_in, eof_out, final_update; struct timespec start_time, next_update, next_ratecheck, cur_time; - struct timespec next_remotecheck; + struct timespec next_remotecheck, next_monitor_exchange; int input_fd, output_fd; unsigned int file_idx; bool output_is_pipe; @@ -250,10 +365,12 @@ int pv_main_loop(pvstate_t state) memset(&next_ratecheck, 0, sizeof(next_ratecheck)); memset(&next_remotecheck, 0, sizeof(next_remotecheck)); + memset(&next_monitor_exchange, 0, sizeof(next_monitor_exchange)); memset(&next_update, 0, sizeof(next_update)); pv_elapsedtime_copy(&next_ratecheck, &cur_time); pv_elapsedtime_copy(&next_remotecheck, &cur_time); + pv_elapsedtime_copy(&next_monitor_exchange, &cur_time); pv_elapsedtime_copy(&next_update, &cur_time); if ((state->control.delay_start > 0) && (state->control.delay_start > state->control.interval)) { @@ -338,8 +455,15 @@ int pv_main_loop(pvstate_t state) pv_elapsedtime_add_nsec(&next_remotecheck, REMOTE_INTERVAL); } - /* TODO: occasionally check othermonitor_read_fd, calculate ratio */ - /* TODO: occasionally send to othermonitor_write_fd */ + /* + * Exchange messages with the other side of the monitor + * every short while, in monitor mode. + */ + if ((state->control.othermonitor_pid > 0) + && (pv_elapsedtime_compare(&cur_time, &next_monitor_exchange) > 0)) { + pv__monitor_exchange(state); + pv_elapsedtime_add_nsec(&next_monitor_exchange, MONITOR_EXCHANGE_INTERVAL); + } if (1 == state->flags.trigger_exit) break; From d8cc637b568db8958653e11c0f350354c02bf2ce Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Fri, 3 Apr 2026 21:49:41 +0100 Subject: [PATCH 33/33] Add the "%{ratio}" formatter (#67). --- Makefile.am | 1 + src/include/pv-internal.h | 2 ++ src/pv/display.c | 3 ++- src/pv/format/ratio.c | 50 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/pv/format/ratio.c diff --git a/Makefile.am b/Makefile.am index 936b152..486b230 100644 --- a/Makefile.am +++ b/Makefile.am @@ -32,6 +32,7 @@ src/pv/format/name.c \ src/pv/format/previousline.c \ src/pv/format/progressbar.c \ src/pv/format/rate.c \ +src/pv/format/ratio.c \ src/pv/format/sgr.c \ src/pv/format/timer.c \ src/pv/loop.c \ diff --git a/src/include/pv-internal.h b/src/include/pv-internal.h index d0231e9..1faf224 100644 --- a/src/include/pv-internal.h +++ b/src/include/pv-internal.h @@ -318,6 +318,7 @@ struct pvstate_s { bool showing_timer; /* set if showing timer */ bool showing_bytes; /* set if showing byte/line count */ + bool showing_ratio; /* set if showing monitoring in:out ratio */ bool showing_rate; /* set if showing transfer rate */ bool showing_last_written; /* set if displaying the last few bytes written */ bool showing_previous_line; /* set if displaying the previously output line */ @@ -602,6 +603,7 @@ pvdisplay_bytecount_t pv_formatter_fineta(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_rate(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_average_rate(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_bytes(pvformatter_args_t); +pvdisplay_bytecount_t pv_formatter_ratio(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_buffer_percent(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_last_written(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_previous_line(pvformatter_args_t); diff --git a/src/pv/display.c b/src/pv/display.c index 365b9b0..551e0b9 100644 --- a/src/pv/display.c +++ b/src/pv/display.c @@ -533,6 +533,7 @@ pvdisplay_bytecount_t pv_formatter_segmentcontent(char *content, pvformatter_arg { "b", &pv_formatter_bytes, false }, { "{bytes}", &pv_formatter_bytes, false }, { "{transferred}", &pv_formatter_bytes, false }, + { "{ratio}", &pv_formatter_ratio, false }, { "T", &pv_formatter_buffer_percent, false }, { "{buffer-percent}", &pv_formatter_buffer_percent, false }, { "A", &pv_formatter_last_written, false }, @@ -544,7 +545,6 @@ pvdisplay_bytecount_t pv_formatter_segmentcontent(char *content, pvformatter_arg { "{sgr:colour,...}", &pv_formatter_sgr, false }, { NULL, NULL, false } }; - /* TODO: formatter for ratio, for "-M both" */ return format_component_array; } @@ -621,6 +621,7 @@ static void pv__format_init(pvprogramstatus_t status, readonly_pvcontrol_t contr display->showing_timer = false; display->showing_bytes = false; + display->showing_ratio = false; display->showing_rate = false; display->showing_last_written = false; display->showing_previous_line = false; diff --git a/src/pv/format/ratio.c b/src/pv/format/ratio.c new file mode 100644 index 0000000..cce8567 --- /dev/null +++ b/src/pv/format/ratio.c @@ -0,0 +1,50 @@ +/* + * Formatter function for ratio of bytes transferred by the other monitored + * side to bytes transferred by this side. + * + * Copyright 2026 Andrew Wood + * + * License GPLv3+: GNU GPL version 3 or later; see `docs/COPYING'. + */ + +#include "config.h" +#include "pv.h" +#include "pv-internal.h" + + +/* + * Ratio of bytes or lines transferred by the other monitored side ("-M + * both") to the bytes or lines transferred by this side. + */ +pvdisplay_bytecount_t pv_formatter_ratio(pvformatter_args_t args) +{ + char content[128]; /* flawfinder: ignore - always bounded */ + + args->display->showing_ratio = true; + + if (0 == args->buffer_size) + return 0; + + content[0] = '\0'; + + if (0 == args->transfer->otherside_transferred || 0 == args->transfer->transferred) { + (void) pv_snprintf(content, sizeof(content), "%s:%s", "-", "-"); + } else if (args->transfer->otherside_transferred == args->transfer->transferred) { + (void) pv_snprintf(content, sizeof(content), "%d:%d", 1, 1); + } else { + long double side1, side2, ratio; + + side1 = (long double) (args->transfer->otherside_transferred); + side2 = (long double) (args->transfer->transferred); + + if (side1 > side2) { + ratio = side1 / side2; + (void) pv_snprintf(content, sizeof(content), "%.4Lg:%d", ratio, 1); + } else { + ratio = side2 / side1; + (void) pv_snprintf(content, sizeof(content), "%d:%.4Lg", 1, ratio); + } + } + + return pv_formatter_segmentcontent(content, args); +}