Skip to content

Commit 2e715eb

Browse files
committed
don't ignore write errors when passthrough piping
1 parent 618cebb commit 2e715eb

9 files changed

Lines changed: 210 additions & 20 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@
3636
.DS_Store
3737
./doc-gen
3838

39-
__pycache__/
39+
__pycache__/
40+
./runme

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,19 @@ All tests pass on linux & mac. Most pass under mingw & MSVC.
167167
168168
# Changelog
169169
170+
# 0.6.0 TBA
171+
172+
**Breaking Changes**
173+
174+
- `[[nodiscard]]`` added to pipe_x functions. Most likely an error on your part
175+
if the return value is ignored.
176+
177+
**Non-breaking changes**
178+
- Fixed: threads ignoring return value of pipe_write and never terminating
179+
- Changed: internal threads block SIGPIPE on a thread basis. Threads terminate
180+
and close pipes as needed on pipe errors. This breaks the pipe chains which
181+
prevents potential hangs with deep pipe chains.
182+
170183
# 0.5.0 2025-12-09
171184
172185
**Breaking Changes**

src/cpp/subprocess.hpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,20 @@
44
#include "subprocess/pipe.hpp"
55
#include "subprocess/ProcessBuilder.hpp"
66
#include "subprocess/shell_utils.hpp"
7-
#include "subprocess/environ.hpp"
7+
#include "subprocess/environ.hpp"
8+
9+
#ifdef SUBPROCESS_AMALGAMATE_SOURCES
10+
/* To regen:
11+
(for file in *.cpp; do; echo '#include "'subprocess/$file'"'; done;) | sort
12+
*/
13+
#include "subprocess/CowData.cpp"
14+
#include "subprocess/environ.cpp"
15+
#include "subprocess/pipe.cpp"
16+
#include "subprocess/PipeVar.cpp"
17+
#include "subprocess/ProcessBuilder.cpp"
18+
#include "subprocess/ProcessBuilder_posix.cpp"
19+
#include "subprocess/ProcessBuilder_windows.cpp"
20+
#include "subprocess/shell_utils.cpp"
21+
#include "subprocess/utf8_to_utf16.cpp"
22+
23+
#endif

src/cpp/subprocess/ProcessBuilder.cpp

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ namespace subprocess {
4545
message += std::strerror(errno_code);
4646
throw OSError(message);
4747
}
48+
struct NoSigPipe {
49+
#ifndef _WIN32
50+
NoSigPipe() {
51+
// get the current state
52+
sigprocmask(SIG_BLOCK, NULL, &old_state);
53+
54+
sigset_t set = old_state;
55+
sigaddset(&set, SIGPIPE);
56+
sigprocmask(SIG_BLOCK, &set, NULL);
57+
}
58+
59+
~NoSigPipe() {
60+
sigprocmask(SIG_BLOCK, &old_state, NULL);
61+
}
62+
63+
private:
64+
sigset_t old_state;
65+
#endif
66+
};
4867
}
4968
double monotonic_seconds() {
5069
static bool needs_init = true;
@@ -91,6 +110,7 @@ namespace subprocess {
91110
};
92111
std::thread pipe_thread(PipeHandle input, std::ostream* output) {
93112
return std::thread([=]() {
113+
details::NoSigPipe noSigPipe;
94114
AutoClosePipe autoclose(input);
95115
std::vector<char> buffer(2048);
96116
while (true) {
@@ -102,58 +122,83 @@ namespace subprocess {
102122
});
103123
}
104124

125+
[[nodiscard]]
126+
static ssize_t fwrite_fully(FILE* output, const void* buffer, size_t size) {
127+
const uint8_t* cursor = reinterpret_cast<const uint8_t*>(buffer);
128+
ssize_t total = 0;
129+
while (total < size) {
130+
auto transferred = fwrite(cursor, 1, size - total, output);
131+
if (transferred == 0)
132+
break;
133+
cursor += transferred;
134+
total += transferred;
135+
}
136+
return total;
137+
}
138+
105139
std::thread pipe_thread(PipeHandle input, FILE* output) {
106140
return std::thread([=]() {
141+
details::NoSigPipe noSigPipe;
107142
AutoClosePipe autoclose(input);
108143
std::vector<char> buffer(2048);
109144
while (true) {
110-
ssize_t transfered = pipe_read(input, &buffer[0], buffer.size());
111-
if (transfered <= 0)
145+
ssize_t transferred = pipe_read(input, &buffer[0], buffer.size());
146+
if (transferred <= 0)
147+
break;
148+
transferred = fwrite_fully(output, &buffer[0], transferred);
149+
if (transferred <= 0)
112150
break;
113-
fwrite(&buffer[0], 1, transfered, output);
114151
}
115152
});
116153
}
154+
117155
std::thread pipe_thread(FILE* input, PipeHandle output) {
118156
return std::thread([=]() {
157+
details::NoSigPipe noSigPipe;
119158
AutoClosePipe autoclose(output);
120159
std::vector<char> buffer(2048);
121160
while (true) {
122-
ssize_t transfered = fread(&buffer[0], 1, buffer.size(), input);
123-
if (transfered <= 0)
161+
ssize_t transferred = fread(&buffer[0], 1, buffer.size(), input);
162+
if (transferred <= 0)
163+
break;
164+
transferred = pipe_write_fully(output, &buffer[0], transferred);
165+
if (transferred <= 0)
124166
break;
125-
pipe_write(output, &buffer[0], transfered);
126167
}
127168
});
128169
}
129170
std::thread pipe_thread(std::string& input, PipeHandle output) {
130171
return std::thread([input(move(input)), output]() {
172+
details::NoSigPipe noSigPipe;
131173
AutoClosePipe autoclose(output);
132174

133175
std::size_t pos = 0;
134176
while (pos < input.size()) {
135-
ssize_t transfered = pipe_write(output, input.c_str()+pos, input.size() - pos);
136-
if (transfered <= 0)
177+
ssize_t transferred = pipe_write_fully(output, input.c_str()+pos, input.size() - pos);
178+
if (transferred <= 0)
137179
break;
138-
pos += transfered;
180+
pos += transferred;
139181
}
140182
});
141183
}
142184
std::thread pipe_thread(std::istream* input, PipeHandle output) {
143185
return std::thread([=]() {
186+
details::NoSigPipe noSigPipe;
144187
AutoClosePipe autoclose(output);
145188
std::vector<char> buffer(2048);
146189
while (true) {
147190
input->read(&buffer[0], buffer.size());
148-
ssize_t transfered = input->gcount();
191+
ssize_t transferred = input->gcount();
149192
if (input->bad())
150193
break;
151-
if (transfered <= 0) {
194+
if (transferred <= 0) {
152195
if (input->eof())
153196
break;
154197
continue;
155198
}
156-
pipe_write(output, &buffer[0], transfered);
199+
transferred = pipe_write_fully(output, &buffer[0], transferred);
200+
if (transferred <= 0)
201+
break;
157202
}
158203
});
159204
}
@@ -279,6 +324,7 @@ namespace subprocess {
279324
Popen::~Popen() {
280325
close();
281326
}
327+
282328
void Popen::close() {
283329
if (cin_thread.joinable())
284330
cin_thread.join();

src/cpp/subprocess/ProcessBuilder.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ namespace subprocess {
185185
/** equivalent to send_signal(SIGKILL) */
186186
bool kill();
187187

188-
/** Destructs the object and initializes to basic state */
188+
/** Destructs the object and initializes to base state */
189189
void close();
190190
/** Closes the cin pipe */
191191
void close_cin() {

src/cpp/subprocess/pipe.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,23 @@ namespace subprocess {
319319
return second + 1;
320320
}
321321

322+
ssize_t pipe_write_fully(PipeHandle handle, const void* buffer, size_t size) {
323+
ssize_t transferred = 0;
324+
ssize_t total = 0;
325+
const uint8_t* cursor = reinterpret_cast<const uint8_t*>(buffer);
326+
while (total < size) {
327+
ssize_t transferred = pipe_write(handle, cursor, size - total);
328+
if (transferred < 0)
329+
return -total - 1;
330+
if (transferred == 0)
331+
break;
332+
cursor += transferred;
333+
total += transferred;
334+
}
335+
336+
return total;
337+
}
338+
322339
PipeHandle pipe_file(const char* filename, const char* mode) {
323340
using std::strchr;
324341
#ifdef _WIN32

src/cpp/subprocess/pipe.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ namespace subprocess {
4747
};
4848

4949
/** Peak into how many bytes available in pipe to read. */
50+
[[nodiscard]]
5051
ssize_t pipe_peak_bytes(PipeHandle pipe);
5152

5253
/** Closes a pipe handle.
@@ -76,6 +77,7 @@ namespace subprocess {
7677
to make both ends to be inheritble at creation then you likely have
7778
a bug.
7879
*/
80+
[[nodiscard]]
7981
PipePair pipe_create(bool inheritable = false);
8082

8183
/** Set the pipe to be inheritable or not for subprocess.
@@ -90,13 +92,24 @@ namespace subprocess {
9092
@returns -1 on error. if 0 it could be the end, or perhaps wait for
9193
more data.
9294
*/
95+
[[nodiscard]]
9396
ssize_t pipe_read(PipeHandle, void* buffer, size_t size);
97+
9498
/**
9599
@returns -1 on error. if 0 it could be full, or perhaps wait for
96100
more data.
97101
*/
102+
[[nodiscard]]
98103
ssize_t pipe_write(PipeHandle, const void* buffer, size_t size);
99104

105+
/** Like pipe_write but keep writing while return value is > 0.
106+
107+
@returns bytes written on success, (-total_transferred - 1) on error,
108+
if 0 it could be full,
109+
*/
110+
[[nodiscard]]
111+
ssize_t pipe_write_fully(PipeHandle, const void* buffer, size_t size);
112+
100113
/** Sets the blocking bit.
101114
102115
The handle state is first queried as to only change the blocking bit.
@@ -120,6 +133,7 @@ namespace subprocess {
120133
@return all data read from pipe as a string object. This works fine
121134
with binary data.
122135
*/
136+
[[nodiscard]]
123137
std::string pipe_read_all(PipeHandle handle);
124138

125139
/** Waits for the pipes to be change state.
@@ -133,12 +147,14 @@ namespace subprocess {
133147
@param seconds
134148
The timeout in seconds to wait for. -1 for indefinate
135149
*/
150+
[[nodiscard]]
136151
int pipe_wait_for_read(
137152
PipeHandle pipe,
138153
double seconds
139154
);
140155

141156
/** Will read up to size and not block until buffer is filled. */
157+
[[nodiscard]]
142158
ssize_t pipe_read_some(PipeHandle, void* buffer, size_t size);
143159

144160
/** Opens a file and returns the handle.
@@ -157,6 +173,7 @@ namespace subprocess {
157173
158174
@returns the handle to the opened file, or kBadPipeValue on error
159175
*/
176+
[[nodiscard]]
160177
PipeHandle pipe_file(const char* filename, const char* mode);
161178

162179
#if 0

0 commit comments

Comments
 (0)