1 /*
2 * Win32OutputCapture.cpp
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16 #include <core/system/OutputCapture.hpp>
17
18 #include <windows.h>
19 #include <io.h>
20
21 #include <stdio.h>
22 #include <fcntl.h>
23
24 #include <core/Log.hpp>
25 #include <shared_core/Error.hpp>
26 #include <core/Thread.hpp>
27
28 #ifndef STDOUT_FILENO
29 # define STDOUT_FILENO 1
30 #endif
31
32 #ifndef STDERR_FILENO
33 # define STDERR_FILENO 2
34 #endif
35
36 namespace rstudio {
37 namespace core {
38 namespace system {
39
40 namespace {
41
42 boost::mutex s_mutex;
43
standardStreamCaptureThread(HANDLE hReadPipe,HANDLE hOrigStdHandle,const boost::function<void (const std::string &)> & outputHandler)44 void standardStreamCaptureThread(
45 HANDLE hReadPipe,
46 HANDLE hOrigStdHandle,
47 const boost::function<void(const std::string&)>& outputHandler)
48 {
49 try
50 {
51 while(true)
52 {
53 const int kBufferSize = 512;
54 char buffer[kBufferSize];
55 DWORD bytesRead = 0;
56 if (::ReadFile(hReadPipe, &buffer, kBufferSize, &bytesRead, nullptr))
57 {
58 if (bytesRead > 0)
59 {
60 outputHandler(std::string(buffer, bytesRead));
61
62 if (hOrigStdHandle)
63 {
64 bool result = false;
65 LOCK_MUTEX(s_mutex)
66 {
67 result = ::WriteFile(hOrigStdHandle, buffer, bytesRead, nullptr, nullptr);
68 }
69 END_LOCK_MUTEX
70
71 if (!result)
72 {
73 LOG_ERROR(LAST_SYSTEM_ERROR());
74 }
75 }
76 }
77 }
78 else
79 {
80 // we don't expect errors to ever occur (since the standard
81 // streams are never closed) so log any that do and continue
82 LOG_ERROR(LAST_SYSTEM_ERROR());
83 }
84 }
85 }
86 CATCH_UNEXPECTED_EXCEPTION
87 }
88
ioError(const std::string & description,const ErrorLocation & location)89 Error ioError(const std::string& description, const ErrorLocation& location)
90 {
91 boost::system::error_code ec(boost::system::errc::io_error,
92 boost::system::system_category());
93 Error error(ec, location);
94 error.addProperty("description", description);
95 return error;
96 }
97
redirectToPipe(DWORD stdHandle,int stdFd,FILE * fpStdFile,HANDLE * phReadPipe)98 Error redirectToPipe(DWORD stdHandle,
99 int stdFd,
100 FILE* fpStdFile,
101 HANDLE* phReadPipe)
102 {
103 // create pipe
104 HANDLE hWritePipe;
105 if (!::CreatePipe(phReadPipe, &hWritePipe, nullptr, 0))
106 {
107 return LAST_SYSTEM_ERROR();
108 }
109
110 // reset win32 standard handle
111 if (!::SetStdHandle(stdHandle, hWritePipe))
112 {
113 return LAST_SYSTEM_ERROR();
114 }
115
116 // reset c runtime library handle
117 int fd = ::_open_osfhandle((intptr_t)hWritePipe, _O_TEXT);
118 if (fd == -1)
119 return ioError("_open_osfhandle", ERROR_LOCATION);
120
121 int stdDupFd = ::_fileno(fpStdFile);
122 if (stdDupFd == -2)
123 {
124 // -2 is a special value that indicates stdout/stderr isn't associated
125 // with an output stream. The below fixes an issue in which, for Windows
126 // users other than the first, QProcess doesn't start the R session in
127 // such a way that it receives stdout/stderr streams (see case 4230); in
128 // this situation, we allocate a new stream for the descriptor so
129 // stdout/stderr will go somewhere.
130 FILE* newStdHandle = ::_fdopen(stdFd, "w");
131 if (newStdHandle == nullptr)
132 return systemError(errno, ERROR_LOCATION);
133 *fpStdFile = *newStdHandle;
134 stdDupFd = stdFd;
135 }
136
137 if (::_dup2(fd, stdDupFd))
138 return systemError(errno, ERROR_LOCATION);
139
140 // turn off buffering
141 if (::setvbuf(fpStdFile, nullptr, _IONBF, 0) != 0)
142 return ioError("setvbuf", ERROR_LOCATION);
143
144 // sync c++ std streams
145 std::ios::sync_with_stdio();
146
147 return Success();
148 }
149
redirectToPipe(DWORD stdHandle,int stdFd,FILE * fpStdFile,HANDLE * phReadPipe,HANDLE * hOrigStdHandle)150 Error redirectToPipe(DWORD stdHandle,
151 int stdFd,
152 FILE* fpStdFile,
153 HANDLE* phReadPipe,
154 HANDLE* hOrigStdHandle)
155 {
156 Error error = redirectToPipe(stdHandle, stdFd, fpStdFile, phReadPipe);
157 if (error)
158 return error;
159
160 HANDLE hOrigHandle = ::CreateFile("CONOUT$",
161 GENERIC_WRITE,
162 FILE_SHARE_WRITE,
163 NULL,
164 OPEN_EXISTING,
165 FILE_ATTRIBUTE_NORMAL,
166 NULL);
167
168 if (hOrigHandle == INVALID_HANDLE_VALUE)
169 {
170 // if for some reason we cannot open a handle to the console we simply log the error instead
171 // of bubling it up - this way, we can continue to redirect output to the pipe but not forward
172 // it to the console, as an error would indicate that the console is not available
173 LOG_ERROR(LAST_SYSTEM_ERROR());
174 }
175 else
176 {
177 *hOrigStdHandle = hOrigHandle;
178 }
179
180 return Success();
181 }
182
183
184 } // anonymous namespace
185
captureStandardStreams(const boost::function<void (const std::string &)> & stdoutHandler,const boost::function<void (const std::string &)> & stderrHandler,bool forwardOutputToOriginalDescriptors)186 Error captureStandardStreams(
187 const boost::function<void(const std::string&)>& stdoutHandler,
188 const boost::function<void(const std::string&)>& stderrHandler,
189 bool forwardOutputToOriginalDescriptors)
190 {
191 try
192 {
193 if (!stdoutHandler && !stderrHandler)
194 {
195 return systemError(boost::system::errc::invalid_argument,
196 "At least one of stdoutHandler and stderrHandler must be set",
197 ERROR_LOCATION);
198 }
199
200 Error error;
201 HANDLE hReadStdoutPipe = nullptr;
202 HANDLE hReadStderrPipe = nullptr;
203 HANDLE hOrigStdoutHandle = nullptr;
204 HANDLE hOrigStderrHandle = nullptr;
205
206 // redirect stdout if handler was provided
207 if (stdoutHandler)
208 {
209 if (!forwardOutputToOriginalDescriptors)
210 {
211 error = redirectToPipe(STD_OUTPUT_HANDLE,
212 STDOUT_FILENO,
213 stdout,
214 &hReadStdoutPipe);
215 }
216 else
217 {
218 error = redirectToPipe(STD_OUTPUT_HANDLE,
219 STDOUT_FILENO,
220 stdout,
221 &hReadStdoutPipe,
222 &hOrigStdoutHandle);
223 }
224
225 if (error)
226 return error;
227
228 // capture stdout
229 boost::thread stdoutThread(boost::bind(standardStreamCaptureThread,
230 hReadStdoutPipe,
231 hOrigStdoutHandle,
232 stdoutHandler));
233 }
234
235 // redirect stderror if handler was provided
236 if (stderrHandler)
237 {
238 if (!forwardOutputToOriginalDescriptors)
239 {
240 error = redirectToPipe(STD_ERROR_HANDLE,
241 STDERR_FILENO,
242 stderr,
243 &hReadStderrPipe);
244 }
245 else
246 {
247 error = redirectToPipe(STD_ERROR_HANDLE,
248 STDERR_FILENO,
249 stderr,
250 &hReadStderrPipe,
251 &hOrigStderrHandle);
252 }
253
254 if (error)
255 return error;
256
257 // capture stderr
258 boost::thread stderrThread(boost::bind(standardStreamCaptureThread,
259 hReadStderrPipe,
260 hOrigStderrHandle,
261 stderrHandler));
262 }
263
264 return Success();
265 }
266 catch(const boost::thread_resource_error& e)
267 {
268 return Error(boost::thread_error::ec_from_exception(e), ERROR_LOCATION);
269 }
270 }
271
272 } // namespace system
273 } // namespace core
274 } // namespace rstudio
275
276