Skip to content

Commit 091b1a7

Browse files
authored
Execute: ensure pipes are read (#10)
Prior to this PR there were two bugs: One: we default initialized a boolean to indicate whether we were piping child process output to a file or the wrapper's stdout. Default-initializing this causes it to have undefined behavior, as either true or false, which means the reader on the child process' stdout/stderr pipes will spuriously close as they attempt to write to a nonexistent file. Two: The readers for the child process' stdout/stderr pipes can potentially exit early if the child process hangs for a bit without output. This PR resolves both by: - Adding an initialization value to the file check (false) - Ensuring the child process terminates before either of the stdout/stderr reader threads - Ensuring the readers do not exit before the Execute class has indicated the child has termianted This PR adds a test that: - Dumps 64k bytes of output to stdout - expects the compiler wrapper to handle that and exit w/ success without hanging
1 parent fb126c8 commit 091b1a7

File tree

5 files changed

+114
-14
lines changed

5 files changed

+114
-14
lines changed

Makefile

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,28 @@ test_relocate_dll: build_and_check_test_sample
108108
del tester.exe
109109
link main.obj ..\tmp_lib\calc.lib /out:tester.exe
110110
.\tester.exe
111+
cd ../..
112+
113+
test_pipe_overflow: build_and_check_test_sample
114+
set SPACK_CC_TMP=%SPACK_CC%
115+
set SPACK_CC=$(MAKEDIR)\test\lots-of-output.bat
116+
cl /c /EHsc "test\src file\calc.cxx"
117+
set SPACK_CC=%SPACK_CC_TMP%
118+
119+
build_zerowrite_test: test\writezero.obj
120+
link $(LFLAGS) $** Shlwapi.lib /out:writezero.exe
121+
122+
123+
test_zerowrite: build_zerowrite_test
124+
set SPACK_CC_TMP=%SPACK_CC%
125+
set SPACK_CC=$(MAKEDIR)\writezero.exe
126+
cl /c EHsc "test\src file\calc.cxx"
127+
set SPACK_CC=%SPACK_CC_TMP%
111128

112129
test_and_cleanup: test clean-test
113130

114131

115-
test: test_wrapper test_relocate_exe test_relocate_dll
132+
test: test_wrapper test_relocate_exe test_relocate_dll test_pipe_overflow
116133

117134

118135
clean : clean-test clean-cl
@@ -128,4 +145,5 @@ clean-cl :
128145
del cl.exe
129146

130147
clean-test:
131-
rmdir /q /s tmp
148+
-@ if EXIST "tmp" rmdir /q /s "tmp"
149+

src/execute.cxx

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ int ExecuteCommand::PipeChildToStdErr()
161161
CHAR chBuf[BUFSIZE];
162162
BOOL bSuccess = TRUE;
163163
HANDLE hParentOut;
164-
if (this->write_to_file) {
164+
if (this->write_to_file && this->fileout != INVALID_HANDLE_VALUE) {
165165
hParentOut = this->fileout;
166166
}
167167
else {
@@ -171,10 +171,35 @@ int ExecuteCommand::PipeChildToStdErr()
171171
for (;;)
172172
{
173173
bSuccess = ReadFile( this->ChildStdErr_Rd, chBuf, BUFSIZE, &dwRead, NULL);
174-
if( ! bSuccess || dwRead == 0 ) break;
175-
176-
bSuccess = WriteFile(hParentOut, chBuf,
177-
dwRead, &dwWritten, NULL);
174+
// For an explanation behind the use of termianted here
175+
// see the docstring on pipechildtostdout
176+
if( ! bSuccess || (dwRead == 0 && this->terminated) ) break;
177+
if(dwRead != 0) {
178+
bSuccess = WriteFile(hParentOut, chBuf,
179+
dwRead, &dwWritten, NULL);
180+
if (dwWritten < dwRead && bSuccess){
181+
// incomplete write but not a failure
182+
// since bSuccess is true
183+
// So lets write until bSuccess is false or
184+
// until all bytes are written
185+
int currentPos = dwWritten;
186+
while((dwWritten < dwRead) || dwWritten == 0) {
187+
dwRead = dwRead - dwWritten;
188+
CHAR * partialBuf = new CHAR[dwRead];
189+
for(int i = 0; i < dwRead; ++i) {
190+
partialBuf[i] = chBuf[currentPos + i];
191+
}
192+
bSuccess = WriteFile(hParentOut, partialBuf,
193+
dwRead, &dwWritten, NULL);
194+
delete partialBuf;
195+
if (! bSuccess) break;
196+
currentPos += dwWritten;
197+
}
198+
}
199+
if (! bSuccess ) {
200+
break;
201+
}
202+
}
178203
if (! bSuccess ) break;
179204
}
180205
return !bSuccess;
@@ -192,7 +217,7 @@ int ExecuteCommand::PipeChildToStdout()
192217
CHAR chBuf[BUFSIZE];
193218
BOOL bSuccess = TRUE;
194219
HANDLE hParentOut;
195-
if (this->write_to_file) {
220+
if (this->write_to_file && this->fileout != INVALID_HANDLE_VALUE) {
196221
hParentOut = this->fileout;
197222
}
198223
else {
@@ -202,10 +227,41 @@ int ExecuteCommand::PipeChildToStdout()
202227
for (;;)
203228
{
204229
bSuccess = ReadFile( this->ChildStdOut_Rd, chBuf, BUFSIZE, &dwRead, NULL);
205-
if( ! bSuccess || dwRead == 0 ) break;
206-
207-
bSuccess = WriteFile(hParentOut, chBuf,
208-
dwRead, &dwWritten, NULL);
230+
// Typically dwRead == 0 indicates the writer end of the pipe has ceased writing
231+
// however if the writer were to invoke WriteFile with a size of 0, dwRead would
232+
// be 0 but the writer would not have terminated.
233+
// As such we need an explicit indication the writer process has termianted.
234+
// From the MSVC docs:
235+
// If the lpNumberOfBytesRead parameter is zero when ReadFile returns TRUE on a pipe,
236+
// the other end of the pipe called the WriteFile function with nNumberOfBytesToWrite
237+
// set to zero.
238+
if( ! bSuccess || (dwRead == 0 && this->terminated) ) break;
239+
if(dwRead != 0){
240+
bSuccess = WriteFile(hParentOut, chBuf,
241+
dwRead, &dwWritten, NULL);
242+
if (dwWritten < dwRead && bSuccess){
243+
// incomplete write but not a failure
244+
// since bSuccess is true
245+
// So lets write until bSuccess is false or
246+
// until all bytes are written
247+
int currentPos = dwWritten;
248+
while((dwWritten < dwRead) || dwWritten == 0) {
249+
dwRead = dwRead - dwWritten;
250+
CHAR * partialBuf = new CHAR[dwRead];
251+
for(int i = 0; i < dwRead; ++i) {
252+
partialBuf[i] = chBuf[currentPos + i];
253+
}
254+
bSuccess = WriteFile(hParentOut, partialBuf,
255+
dwRead, &dwWritten, NULL);
256+
delete partialBuf;
257+
if (! bSuccess) break;
258+
currentPos += dwWritten;
259+
}
260+
}
261+
if (! bSuccess ) {
262+
break;
263+
}
264+
}
209265
if (! bSuccess ) break;
210266
}
211267
return !bSuccess;
@@ -242,6 +298,7 @@ int ExecuteCommand::ReportExitCode()
242298
if(exit_code != STILL_ACTIVE)
243299
break;
244300
}
301+
this->terminated = true;
245302
CloseHandle(this->procInfo.hProcess);
246303
return exit_code;
247304
}
@@ -294,9 +351,16 @@ bool ExecuteCommand::Execute(const std::string &filename)
294351
*/
295352
int ExecuteCommand::Join()
296353
{
354+
// Join primary thread first
355+
// This process sets the termianted flag
356+
// without which the reader threads will not
357+
// terminate, so the primary thread must be
358+
// joined first so we have a guaruntee that the
359+
// reader processes can exit
360+
int commandError = this->exit_code_future.get();
297361
if(!this->child_out_future.get())
298362
return -999;
299363
if(!this->child_err_future.get())
300364
return -999;
301-
return this->exit_code_future.get();
365+
return commandError;
302366
}

src/execute.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ class ExecuteCommand {
6161
SECURITY_ATTRIBUTES saAttr;
6262
SECURITY_ATTRIBUTES saAttrErr;
6363
HANDLE fileout = INVALID_HANDLE_VALUE;
64-
bool write_to_file;
64+
bool write_to_file = false;
6565
bool cpw_initalization_failure = false;
66+
bool terminated = false;
6667
std::string base_command;
6768
StrList command_args;
6869
};

test/lots-of-output.bat

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@echo OFF
2+
3+
for /l %%x in (1, 1, 1250) do echo Test boilerplate output to fill stdout line number %%x

test/writezero/writezero.cxx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include <iostream>
2+
#include <string>
3+
#include <windows.h>
4+
#include <tchar.h>
5+
#include <stdio.h>
6+
#include <strsafe.h>
7+
8+
#define BUFSIZE 100
9+
10+
int main(int argc, char ** argv) {
11+
HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);
12+
char buff[BUFSIZE];
13+
WriteFile(stdOut, buff, 0, NULL, NULL);
14+
}

0 commit comments

Comments
 (0)