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