xapian-core  2.0.0
progclient.cc
Go to the documentation of this file.
1 
4 /* Copyright (C) 2007-2024 Olly Betts
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, see
18  * <https://www.gnu.org/licenses/>.
19  */
20 
21 #include <config.h>
22 
23 #include "progclient.h"
24 
25 #include <xapian/error.h>
26 
27 #include <cerrno>
28 #include <string>
29 #include <string_view>
30 #include <vector>
31 
32 #include "safefcntl.h"
33 
34 #include <sys/types.h>
35 #ifndef __WIN32__
36 # include "safesyssocket.h"
37 # include "safeunistd.h"
38 # include <sys/wait.h>
39 #else
40 # include <cinttypes> // For PRIx64
41 # include <cstdio> // For snprintf().
42 # include <io.h>
43 #endif
44 
45 #include "closefrom.h"
46 #include "debuglog.h"
47 
48 using namespace std;
49 
50 pair<int, string>
51 ProgClient::run_program(string_view progname, string_view args,
52 #ifndef __WIN32__
53  pid_t& child
54 #else
55  HANDLE& child
56 #endif
57  )
58 {
59  LOGCALL_STATIC(DB, RETURN_TYPE(pair<int, string>), "ProgClient::run_program", progname | args | Literal("[&child]"));
60 
61  string context{"remote:prog("};
62  context += progname;
63  context += ' ';
64  context += args;
65  context += ')';
66 
67 #if defined HAVE_SOCKETPAIR && defined HAVE_FORK
68  int fds[2];
69 
70  // Set the close-on-exec flag. Our child will clear it after we fork() but
71  // before the child exec()s so that there's no window where another thread
72  // in the parent process could fork()+exec() and end up with these fds
73  // still open.
74  if (socketpair(PF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, fds) < 0) {
75  throw Xapian::NetworkError("socketpair failed", context, errno);
76  }
77 
78  // We need the program name as a nul-terminated string.
79  string progname_string{progname};
80 
81  // Make a copy of args - we replace spaces with zero bytes and build
82  // a char* array that points into this which we pass to execvp() as argv.
83  //
84  // We do this before we call fork() because we shouldn't do anything
85  // which calls malloc() in the child process.
86  string args_buf{args};
87  vector<char*> argv;
88  argv.push_back(&progname_string[0]);
89  if (!args_buf.empty()) {
90  // Split argument list on spaces.
91  argv.push_back(&args_buf[0]);
92  for (char& ch : args_buf) {
93  if (ch == ' ') {
94  // Drop the previous element if it's empty (either due to leading
95  // spaces or multiple consecutive spaces).
96  if (&ch == argv.back()) argv.pop_back();
97  ch = '\0';
98  argv.push_back(&ch + 1);
99  }
100  }
101  // Drop final element if it's empty (due to trailing space(s)).
102  if (&args_buf.back() == argv.back()) argv.pop_back();
103  }
104  argv.push_back(nullptr);
105 
106  child = fork();
107 
108  if (child != 0) {
109  // Not the child process.
110 
111  // Close the child's end of the pipe.
112  ::close(fds[1]);
113  if (child < 0) {
114  // Couldn't fork.
115  ::close(fds[0]);
116  throw Xapian::NetworkError("fork failed", context, errno);
117  }
118 
119  // Parent process.
120  RETURN({fds[0], context});
121  }
122 
123  // Child process.
124 
125  // Close the parent's end of the pipe.
126  ::close(fds[0]);
127 
128  // Connect pipe to stdin and stdout. If we set the close-on-exec flag
129  // above, we want to ensure that both fds 0 and 1 are the result of a
130  // dup2() call so that their close-on-exec flags are cleared which we
131  // can do with a little care here.
132  int dup_to_first = 0;
133  if (SOCK_CLOEXEC != 0 && fds[1] == 0) {
134  dup_to_first = 1;
135  }
136 
137  dup2(fds[1], dup_to_first);
138 
139  // Make sure we don't hang on to open files which may get deleted but
140  // not have their disk space released until we exit. Do this before
141  // the second dup2() to ensure there's a free file descriptor.
142  closefrom(2);
143 
144  dup2(dup_to_first, dup_to_first ^ 1);
145 
146  // Redirect stderr to /dev/null
147  int devnull = open("/dev/null", O_WRONLY);
148  if (devnull == -1) {
149  // We can't throw an exception here or usefully flag the failure.
150  // Best option seems to be to continue with stderr closed.
151  } else if (rare(devnull != 2)) {
152  // We expect to get fd 2 as that's the first free one, but handle
153  // if we don't for some reason.
154  dup2(devnull, 2);
155  ::close(devnull);
156  }
157 
158  execvp(progname_string.c_str(), argv.data());
159 
160  // execvp() failed - all we can usefully do is exit.
161  _exit(-1);
162 #elif defined __WIN32__
163  LARGE_INTEGER counter;
164  // QueryPerformanceCounter() will always succeed on XP and later
165  // and gives us a counter which increments each CPU clock cycle
166  // on modern hardware (Pentium or newer).
167  QueryPerformanceCounter(&counter);
168  char pipename[256];
169  snprintf(pipename, sizeof(pipename),
170  "\\\\.\\pipe\\xapian-remote-%lx-%lx_%" PRIx64,
171  static_cast<unsigned long>(GetCurrentProcessId()),
172  static_cast<unsigned long>(GetCurrentThreadId()),
173  static_cast<unsigned long long>(counter.QuadPart));
174  pipename[sizeof(pipename) - 1] = '\0';
175  // Create a pipe so we can read stdout from the child process.
176  HANDLE hPipe = CreateNamedPipe(pipename,
177  PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,
178  0,
179  1, 4096, 4096, NMPWAIT_USE_DEFAULT_WAIT,
180  NULL);
181 
182  if (hPipe == INVALID_HANDLE_VALUE) {
183  throw Xapian::NetworkError("CreateNamedPipe failed",
184  context,
185  -int(GetLastError()));
186  }
187 
188  HANDLE hClient = CreateFile(pipename,
189  GENERIC_READ|GENERIC_WRITE, 0, NULL,
190  OPEN_EXISTING,
191  FILE_FLAG_OVERLAPPED, NULL);
192 
193  if (hClient == INVALID_HANDLE_VALUE) {
194  throw Xapian::NetworkError("CreateFile failed",
195  context,
196  -int(GetLastError()));
197  }
198 
199  if (!ConnectNamedPipe(hPipe, NULL) &&
200  GetLastError() != ERROR_PIPE_CONNECTED) {
201  throw Xapian::NetworkError("ConnectNamedPipe failed",
202  context,
203  -int(GetLastError()));
204  }
205 
206  // Set the appropriate handles to be inherited by the child process.
207  SetHandleInformation(hClient, HANDLE_FLAG_INHERIT, 1);
208 
209  // Create the child process.
210  PROCESS_INFORMATION procinfo;
211  memset(&procinfo, 0, sizeof(PROCESS_INFORMATION));
212 
213  STARTUPINFO startupinfo;
214  memset(&startupinfo, 0, sizeof(STARTUPINFO));
215  startupinfo.cb = sizeof(STARTUPINFO);
216  startupinfo.hStdError = hClient;
217  startupinfo.hStdOutput = hClient;
218  startupinfo.hStdInput = hClient;
219  startupinfo.dwFlags |= STARTF_USESTDHANDLES;
220 
221  // We need the program name as a nul-terminated string.
222  string progname_string{progname};
223 
224  string cmdline{progname};
225  cmdline += ' ';
226  cmdline += args;
227  // For some reason Windows wants a modifiable command line so we
228  // pass `&cmdline[0]` rather than `cmdline.c_str()`.
229  BOOL ok = CreateProcess(progname_string.c_str(), &cmdline[0],
230  0, 0, TRUE, 0, 0, 0,
231  &startupinfo, &procinfo);
232  if (!ok) {
233  throw Xapian::NetworkError("CreateProcess failed",
234  context,
235  -int(GetLastError()));
236  }
237 
238  CloseHandle(hClient);
239  CloseHandle(procinfo.hThread);
240  child = procinfo.hProcess;
241  RETURN({_open_osfhandle(intptr_t(hPipe), O_RDWR|O_BINARY), context});
242 #else
243 // This should have been detected at configure time.
244 # error ProgClient needs porting to this platform
245 #endif
246 }
247 
249 {
250  // Close the pipe.
251  try {
252  do_close();
253  } catch (...) {
254  }
255 
256  // Wait for the child process to exit.
257 #ifndef __WIN32__
258  waitpid(child, 0, 0);
259 #else
260  WaitForSingleObject(child, INFINITE);
261 #endif
262 }
static std::pair< int, std::string > run_program(std::string_view progname, std::string_view args, pid_t &child)
Start the child process.
Definition: progclient.cc:51
~ProgClient()
Destructor.
Definition: progclient.cc:248
Indicates a problem communicating with a remote database.
Definition: error.h:791
Implementation of closefrom() function.
#define rare(COND)
Definition: config.h:607
Debug logging macros.
#define RETURN(...)
Definition: debuglog.h:484
#define LOGCALL_STATIC(CATEGORY, TYPE, FUNC, PARAMS)
Definition: debuglog.h:482
Hierarchy of classes which Xapian can throw as exceptions.
int close(FD &fd)
Definition: fd.h:63
void closefrom(int fd)
Definition: closefrom.cc:91
Database open(std::string_view host, unsigned int port, unsigned timeout=10000, unsigned connect_timeout=10000)
Construct a Database object for read-only access to a remote database accessed via a TCP connection.
Implementation of RemoteDatabase using a spawned server.
include <fcntl.h>, but working around broken platforms.
#define O_BINARY
Definition: safefcntl.h:80
include <sys/socket.h> with portability workarounds.
#define SOCK_CLOEXEC
<unistd.h>, but with compat.
Definition: pretty.h:48