1 | /* |
2 | * Copyright (C) 2011 Red Hat, Inc. |
3 | * |
4 | * This work is provided "as is"; redistribution and modification |
5 | * in whole or in part, in any medium, physical or electronic is |
6 | * permitted without restriction. |
7 | * |
8 | * This work is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
11 | * |
12 | * In no event shall the authors or contributors be liable for any |
13 | * direct, indirect, incidental, special, exemplary, or consequential |
14 | * damages (including, but not limited to, procurement of substitute |
15 | * goods or services; loss of use, data, or profits; or business |
16 | * interruption) however caused and on any theory of liability, whether |
17 | * in contract, strict liability, or tort (including negligence or |
18 | * otherwise) arising in any way out of the use of this software, even |
19 | * if advised of the possibility of such damage. |
20 | * |
21 | * Author: Colin Walters <walters@verbum.org> |
22 | */ |
23 | |
24 | #include "config.h" |
25 | |
26 | #include <glib.h> |
27 | #include <locale.h> |
28 | #include <string.h> |
29 | #include <fcntl.h> |
30 | |
31 | #ifdef G_OS_UNIX |
32 | #include <glib-unix.h> |
33 | #include <glib/gstdio.h> |
34 | #include <sys/types.h> |
35 | #include <sys/stat.h> |
36 | #include <unistd.h> |
37 | #endif |
38 | |
39 | #ifdef G_OS_WIN32 |
40 | #include <io.h> |
41 | #define LINEEND "\r\n" |
42 | #else |
43 | #define LINEEND "\n" |
44 | #endif |
45 | |
46 | /* MinGW builds are likely done using a BASH-style shell, so run the |
47 | * normal script there, as on non-Windows builds, as it is more likely |
48 | * that one will run 'make check' in such shells to test the code |
49 | */ |
50 | #if defined (G_OS_WIN32) && defined (_MSC_VER) |
51 | #define SCRIPT_EXT ".bat" |
52 | #else |
53 | #define SCRIPT_EXT |
54 | #endif |
55 | |
56 | static char *echo_prog_path; |
57 | static char *echo_script_path; |
58 | |
59 | typedef struct { |
60 | GMainLoop *loop; |
61 | gboolean child_exited; |
62 | gboolean stdout_done; |
63 | GString *stdout_buf; |
64 | } SpawnAsyncMultithreadedData; |
65 | |
66 | static gboolean |
67 | on_child_exited (GPid pid, |
68 | gint status, |
69 | gpointer datap) |
70 | { |
71 | SpawnAsyncMultithreadedData *data = datap; |
72 | |
73 | data->child_exited = TRUE; |
74 | if (data->child_exited && data->stdout_done) |
75 | g_main_loop_quit (loop: data->loop); |
76 | |
77 | return G_SOURCE_REMOVE; |
78 | } |
79 | |
80 | static gboolean |
81 | on_child_stdout (GIOChannel *channel, |
82 | GIOCondition condition, |
83 | gpointer datap) |
84 | { |
85 | char buf[1024]; |
86 | GError *error = NULL; |
87 | gsize bytes_read; |
88 | SpawnAsyncMultithreadedData *data = datap; |
89 | |
90 | if (condition & G_IO_IN) |
91 | { |
92 | GIOStatus status; |
93 | status = g_io_channel_read_chars (channel, buf, count: sizeof (buf), bytes_read: &bytes_read, error: &error); |
94 | g_assert_no_error (error); |
95 | g_string_append_len (string: data->stdout_buf, val: buf, len: (gssize) bytes_read); |
96 | if (status == G_IO_STATUS_EOF) |
97 | data->stdout_done = TRUE; |
98 | } |
99 | if (condition & G_IO_HUP) |
100 | data->stdout_done = TRUE; |
101 | if (condition & G_IO_ERR) |
102 | g_error ("Error reading from child stdin" ); |
103 | |
104 | if (data->child_exited && data->stdout_done) |
105 | g_main_loop_quit (loop: data->loop); |
106 | |
107 | return !data->stdout_done; |
108 | } |
109 | |
110 | static void |
111 | test_spawn_async (void) |
112 | { |
113 | int tnum = 1; |
114 | GError *error = NULL; |
115 | GPtrArray *argv; |
116 | char *arg; |
117 | GPid pid; |
118 | GMainContext *context; |
119 | GMainLoop *loop; |
120 | GIOChannel *channel; |
121 | GSource *source; |
122 | int child_stdout_fd; |
123 | SpawnAsyncMultithreadedData data; |
124 | |
125 | context = g_main_context_new (); |
126 | loop = g_main_loop_new (context, TRUE); |
127 | |
128 | arg = g_strdup_printf (format: "thread %d" , tnum); |
129 | |
130 | argv = g_ptr_array_new (); |
131 | g_ptr_array_add (array: argv, data: echo_prog_path); |
132 | g_ptr_array_add (array: argv, data: arg); |
133 | g_ptr_array_add (array: argv, NULL); |
134 | |
135 | g_spawn_async_with_pipes (NULL, argv: (char**)argv->pdata, NULL, flags: G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, child_pid: &pid, NULL, |
136 | standard_output: &child_stdout_fd, NULL, error: &error); |
137 | g_assert_no_error (error); |
138 | g_ptr_array_free (array: argv, TRUE); |
139 | |
140 | data.loop = loop; |
141 | data.stdout_done = FALSE; |
142 | data.child_exited = FALSE; |
143 | data.stdout_buf = g_string_new (init: 0); |
144 | |
145 | source = g_child_watch_source_new (pid); |
146 | g_source_set_callback (source, func: (GSourceFunc)on_child_exited, data: &data, NULL); |
147 | g_source_attach (source, context); |
148 | g_source_unref (source); |
149 | |
150 | channel = g_io_channel_unix_new (fd: child_stdout_fd); |
151 | source = g_io_create_watch (channel, condition: G_IO_IN | G_IO_HUP | G_IO_ERR); |
152 | g_source_set_callback (source, func: (GSourceFunc)on_child_stdout, data: &data, NULL); |
153 | g_source_attach (source, context); |
154 | g_source_unref (source); |
155 | |
156 | g_main_loop_run (loop); |
157 | |
158 | g_assert (data.child_exited); |
159 | g_assert (data.stdout_done); |
160 | g_assert_cmpstr (data.stdout_buf->str, ==, arg); |
161 | g_string_free (string: data.stdout_buf, TRUE); |
162 | |
163 | g_io_channel_unref (channel); |
164 | g_main_context_unref (context); |
165 | g_main_loop_unref (loop); |
166 | |
167 | g_free (mem: arg); |
168 | } |
169 | |
170 | /* Windows close() causes failure through the Invalid Parameter Handler |
171 | * Routine if the file descriptor does not exist. |
172 | */ |
173 | static void |
174 | safe_close (int fd) |
175 | { |
176 | if (fd >= 0) |
177 | close (fd: fd); |
178 | } |
179 | |
180 | /* Test g_spawn_async_with_fds() with a variety of different inputs */ |
181 | static void |
182 | test_spawn_async_with_fds (void) |
183 | { |
184 | int tnum = 1; |
185 | GPtrArray *argv; |
186 | char *arg; |
187 | gsize i; |
188 | |
189 | /* Each test has 3 variable parameters: stdin, stdout, stderr */ |
190 | enum fd_type { |
191 | NO_FD, /* pass fd -1 (unset) */ |
192 | FD_NEGATIVE, /* pass fd of negative value (equivalent to unset) */ |
193 | PIPE, /* pass fd of new/unique pipe */ |
194 | STDOUT_PIPE, /* pass the same pipe as stdout */ |
195 | } tests[][3] = { |
196 | { NO_FD, NO_FD, NO_FD }, /* Test with no fds passed */ |
197 | { NO_FD, FD_NEGATIVE, NO_FD }, /* Test another negative fd value */ |
198 | { PIPE, PIPE, PIPE }, /* Test with unique fds passed */ |
199 | { NO_FD, PIPE, STDOUT_PIPE }, /* Test the same fd for stdout + stderr */ |
200 | }; |
201 | |
202 | arg = g_strdup_printf (format: "thread %d" , tnum); |
203 | |
204 | argv = g_ptr_array_new (); |
205 | g_ptr_array_add (array: argv, data: echo_prog_path); |
206 | g_ptr_array_add (array: argv, data: arg); |
207 | g_ptr_array_add (array: argv, NULL); |
208 | |
209 | for (i = 0; i < G_N_ELEMENTS (tests); i++) |
210 | { |
211 | GError *error = NULL; |
212 | GPid pid; |
213 | GMainContext *context; |
214 | GMainLoop *loop; |
215 | GIOChannel *channel = NULL; |
216 | GSource *source; |
217 | SpawnAsyncMultithreadedData data; |
218 | enum fd_type *fd_info = tests[i]; |
219 | gint test_pipe[3][2]; |
220 | int j; |
221 | |
222 | for (j = 0; j < 3; j++) |
223 | { |
224 | switch (fd_info[j]) |
225 | { |
226 | case NO_FD: |
227 | test_pipe[j][0] = -1; |
228 | test_pipe[j][1] = -1; |
229 | break; |
230 | case FD_NEGATIVE: |
231 | test_pipe[j][0] = -5; |
232 | test_pipe[j][1] = -5; |
233 | break; |
234 | case PIPE: |
235 | #ifdef G_OS_UNIX |
236 | g_unix_open_pipe (fds: test_pipe[j], FD_CLOEXEC, error: &error); |
237 | g_assert_no_error (error); |
238 | #else |
239 | g_assert_cmpint (_pipe (test_pipe[j], 4096, _O_BINARY), >=, 0); |
240 | #endif |
241 | break; |
242 | case STDOUT_PIPE: |
243 | g_assert_cmpint (j, ==, 2); /* only works for stderr */ |
244 | test_pipe[j][0] = test_pipe[1][0]; |
245 | test_pipe[j][1] = test_pipe[1][1]; |
246 | break; |
247 | default: |
248 | g_assert_not_reached (); |
249 | } |
250 | } |
251 | |
252 | context = g_main_context_new (); |
253 | loop = g_main_loop_new (context, TRUE); |
254 | |
255 | g_spawn_async_with_fds (NULL, argv: (char**)argv->pdata, NULL, |
256 | flags: G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, child_pid: &pid, |
257 | stdin_fd: test_pipe[0][0], stdout_fd: test_pipe[1][1], stderr_fd: test_pipe[2][1], |
258 | error: &error); |
259 | g_assert_no_error (error); |
260 | safe_close (fd: test_pipe[0][0]); |
261 | safe_close (fd: test_pipe[1][1]); |
262 | if (fd_info[2] != STDOUT_PIPE) |
263 | safe_close (fd: test_pipe[2][1]); |
264 | |
265 | data.loop = loop; |
266 | data.stdout_done = FALSE; |
267 | data.child_exited = FALSE; |
268 | data.stdout_buf = g_string_new (init: 0); |
269 | |
270 | source = g_child_watch_source_new (pid); |
271 | g_source_set_callback (source, func: (GSourceFunc)on_child_exited, data: &data, NULL); |
272 | g_source_attach (source, context); |
273 | g_source_unref (source); |
274 | |
275 | if (test_pipe[1][0] >= 0) |
276 | { |
277 | channel = g_io_channel_unix_new (fd: test_pipe[1][0]); |
278 | source = g_io_create_watch (channel, condition: G_IO_IN | G_IO_HUP | G_IO_ERR); |
279 | g_source_set_callback (source, func: (GSourceFunc)on_child_stdout, |
280 | data: &data, NULL); |
281 | g_source_attach (source, context); |
282 | g_source_unref (source); |
283 | } |
284 | else |
285 | { |
286 | /* Don't check stdout data if we didn't pass a fd */ |
287 | data.stdout_done = TRUE; |
288 | } |
289 | |
290 | g_main_loop_run (loop); |
291 | |
292 | g_assert_true (data.child_exited); |
293 | |
294 | if (test_pipe[1][0] >= 0) |
295 | { |
296 | /* Check for echo on stdout */ |
297 | g_assert_true (data.stdout_done); |
298 | g_assert_cmpstr (data.stdout_buf->str, ==, arg); |
299 | g_io_channel_unref (channel); |
300 | } |
301 | g_string_free (string: data.stdout_buf, TRUE); |
302 | |
303 | g_main_context_unref (context); |
304 | g_main_loop_unref (loop); |
305 | safe_close (fd: test_pipe[0][1]); |
306 | safe_close (fd: test_pipe[1][0]); |
307 | if (fd_info[2] != STDOUT_PIPE) |
308 | safe_close (fd: test_pipe[2][0]); |
309 | } |
310 | |
311 | g_ptr_array_free (array: argv, TRUE); |
312 | g_free (mem: arg); |
313 | } |
314 | |
315 | static void |
316 | test_spawn_sync (void) |
317 | { |
318 | int tnum = 1; |
319 | GError *error = NULL; |
320 | char *arg = g_strdup_printf (format: "thread %d" , tnum); |
321 | /* Include arguments with special symbols to test that they are correctly passed to child. |
322 | * This is tested on all platforms, but the most prone to failure is win32, |
323 | * where args are specially escaped during spawning. |
324 | */ |
325 | const char * const argv[] = { |
326 | echo_prog_path, |
327 | arg, |
328 | "doublequotes\\\"after\\\\\"\"backslashes" , /* this would be special escaped on win32 */ |
329 | "\\\"\"doublequotes spaced after backslashes\\\\\"" , /* this would be special escaped on win32 */ |
330 | "even$$dollars" , |
331 | "even%%percents" , |
332 | "even\"\"doublequotes" , |
333 | "even''singlequotes" , |
334 | "even\\\\backslashes" , |
335 | "even//slashes" , |
336 | "$odd spaced$dollars$" , |
337 | "%odd spaced%spercents%" , |
338 | "\"odd spaced\"doublequotes\"" , |
339 | "'odd spaced'singlequotes'" , |
340 | "\\odd spaced\\backslashes\\" , /* this wasn't handled correctly on win32 in glib <=2.58 */ |
341 | "/odd spaced/slashes/" , |
342 | NULL |
343 | }; |
344 | char *joined_args_str = g_strjoinv (separator: "" , str_array: (char**)argv + 1); |
345 | char *stdout_str; |
346 | int estatus; |
347 | |
348 | g_spawn_sync (NULL, argv: (char**)argv, NULL, flags: 0, NULL, NULL, standard_output: &stdout_str, NULL, exit_status: &estatus, error: &error); |
349 | g_assert_no_error (error); |
350 | g_assert_cmpstr (joined_args_str, ==, stdout_str); |
351 | g_free (mem: arg); |
352 | g_free (mem: stdout_str); |
353 | g_free (mem: joined_args_str); |
354 | } |
355 | |
356 | /* Like test_spawn_sync but uses spawn flags that trigger the optimized |
357 | * posix_spawn codepath. |
358 | */ |
359 | static void |
360 | test_posix_spawn (void) |
361 | { |
362 | int tnum = 1; |
363 | GError *error = NULL; |
364 | GPtrArray *argv; |
365 | char *arg; |
366 | char *stdout_str; |
367 | int estatus; |
368 | GSpawnFlags flags = G_SPAWN_CLOEXEC_PIPES | G_SPAWN_LEAVE_DESCRIPTORS_OPEN; |
369 | |
370 | arg = g_strdup_printf (format: "thread %d" , tnum); |
371 | |
372 | argv = g_ptr_array_new (); |
373 | g_ptr_array_add (array: argv, data: echo_prog_path); |
374 | g_ptr_array_add (array: argv, data: arg); |
375 | g_ptr_array_add (array: argv, NULL); |
376 | |
377 | g_spawn_sync (NULL, argv: (char**)argv->pdata, NULL, flags, NULL, NULL, standard_output: &stdout_str, NULL, exit_status: &estatus, error: &error); |
378 | g_assert_no_error (error); |
379 | g_assert_cmpstr (arg, ==, stdout_str); |
380 | g_free (mem: arg); |
381 | g_free (mem: stdout_str); |
382 | g_ptr_array_free (array: argv, TRUE); |
383 | } |
384 | |
385 | static void |
386 | test_spawn_script (void) |
387 | { |
388 | GError *error = NULL; |
389 | GPtrArray *argv; |
390 | char *stdout_str; |
391 | int estatus; |
392 | |
393 | argv = g_ptr_array_new (); |
394 | g_ptr_array_add (array: argv, data: echo_script_path); |
395 | g_ptr_array_add (array: argv, NULL); |
396 | |
397 | g_spawn_sync (NULL, argv: (char**)argv->pdata, NULL, flags: 0, NULL, NULL, standard_output: &stdout_str, NULL, exit_status: &estatus, error: &error); |
398 | g_assert_no_error (error); |
399 | g_assert_cmpstr ("echo" LINEEND, ==, stdout_str); |
400 | g_free (mem: stdout_str); |
401 | g_ptr_array_free (array: argv, TRUE); |
402 | } |
403 | |
404 | /* Test that spawning a non-existent executable returns %G_SPAWN_ERROR_NOENT. */ |
405 | static void |
406 | test_spawn_nonexistent (void) |
407 | { |
408 | GError *error = NULL; |
409 | GPtrArray *argv = NULL; |
410 | gchar *stdout_str = NULL; |
411 | gint exit_status = -1; |
412 | |
413 | argv = g_ptr_array_new (); |
414 | g_ptr_array_add (array: argv, data: "this does not exist" ); |
415 | g_ptr_array_add (array: argv, NULL); |
416 | |
417 | g_spawn_sync (NULL, argv: (char**) argv->pdata, NULL, flags: 0, NULL, NULL, standard_output: &stdout_str, |
418 | NULL, exit_status: &exit_status, error: &error); |
419 | g_assert_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT); |
420 | g_assert_null (stdout_str); |
421 | g_assert_cmpint (exit_status, ==, -1); |
422 | |
423 | g_ptr_array_free (array: argv, TRUE); |
424 | |
425 | g_clear_error (err: &error); |
426 | } |
427 | |
428 | /* Test that FD assignments in a spawned process don’t overwrite and break the |
429 | * child_err_report_fd which is used to report error information back from the |
430 | * intermediate child process to the parent. |
431 | * |
432 | * https://gitlab.gnome.org/GNOME/glib/-/issues/2097 */ |
433 | static void |
434 | test_spawn_fd_assignment_clash (void) |
435 | { |
436 | #ifdef G_OS_UNIX |
437 | int tmp_fd; |
438 | guint i; |
439 | const guint n_fds = 10; |
440 | gint source_fds[n_fds]; |
441 | gint target_fds[n_fds]; |
442 | const gchar *argv[] = { "/nonexistent" , NULL }; |
443 | gboolean retval; |
444 | GError *local_error = NULL; |
445 | struct stat statbuf; |
446 | |
447 | /* Open a temporary file and duplicate its FD several times so we have several |
448 | * FDs to remap in the child process. */ |
449 | tmp_fd = g_file_open_tmp (tmpl: "glib-spawn-test-XXXXXX" , NULL, NULL); |
450 | g_assert_cmpint (tmp_fd, >=, 0); |
451 | |
452 | for (i = 0; i < (n_fds - 1); ++i) |
453 | { |
454 | int source = fcntl (fd: tmp_fd, F_DUPFD_CLOEXEC, 3); |
455 | g_assert_cmpint (source, >=, 0); |
456 | source_fds[i] = source; |
457 | target_fds[i] = source + n_fds; |
458 | } |
459 | |
460 | source_fds[i] = tmp_fd; |
461 | target_fds[i] = tmp_fd + n_fds; |
462 | |
463 | /* Print out the FD map. */ |
464 | g_test_message (format: "FD map:" ); |
465 | for (i = 0; i < n_fds; i++) |
466 | g_test_message (format: " • %d → %d" , source_fds[i], target_fds[i]); |
467 | |
468 | /* Spawn the subprocess. This should fail because the executable doesn’t |
469 | * exist. */ |
470 | retval = g_spawn_async_with_pipes_and_fds (NULL, argv, NULL, flags: G_SPAWN_DEFAULT, |
471 | NULL, NULL, stdin_fd: -1, stdout_fd: -1, stderr_fd: -1, |
472 | source_fds, target_fds, n_fds, |
473 | NULL, NULL, NULL, NULL, |
474 | error: &local_error); |
475 | g_assert_error (local_error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT); |
476 | g_assert_false (retval); |
477 | |
478 | g_clear_error (err: &local_error); |
479 | |
480 | /* Check nothing was written to the temporary file, as would happen if the FD |
481 | * mapping was messed up to conflict with the child process error reporting FD. |
482 | * See https://gitlab.gnome.org/GNOME/glib/-/issues/2097 */ |
483 | g_assert_no_errno (fstat (tmp_fd, &statbuf)); |
484 | g_assert_cmpuint (statbuf.st_size, ==, 0); |
485 | |
486 | /* Clean up. */ |
487 | for (i = 0; i < n_fds; i++) |
488 | g_close (fd: source_fds[i], NULL); |
489 | #else /* !G_OS_UNIX */ |
490 | g_test_skip ("FD redirection only supported on Unix" ); |
491 | #endif /* !G_OS_UNIX */ |
492 | } |
493 | |
494 | int |
495 | main (int argc, |
496 | char *argv[]) |
497 | { |
498 | char *dirname; |
499 | int ret; |
500 | |
501 | setlocale (LC_ALL, locale: "" ); |
502 | |
503 | g_test_init (argc: &argc, argv: &argv, NULL); |
504 | |
505 | dirname = g_path_get_dirname (file_name: argv[0]); |
506 | echo_prog_path = g_build_filename (first_element: dirname, "test-spawn-echo" EXEEXT, NULL); |
507 | if (!g_file_test (filename: echo_prog_path, test: G_FILE_TEST_EXISTS)) |
508 | { |
509 | g_free (mem: echo_prog_path); |
510 | echo_prog_path = g_build_filename (first_element: dirname, "lt-test-spawn-echo" EXEEXT, NULL); |
511 | } |
512 | echo_script_path = g_build_filename (first_element: dirname, "echo-script" SCRIPT_EXT, NULL); |
513 | if (!g_file_test (filename: echo_script_path, test: G_FILE_TEST_EXISTS)) |
514 | { |
515 | g_free (mem: echo_script_path); |
516 | echo_script_path = g_test_build_filename (file_type: G_TEST_DIST, first_path: "echo-script" SCRIPT_EXT, NULL); |
517 | } |
518 | g_free (mem: dirname); |
519 | |
520 | g_assert (g_file_test (echo_prog_path, G_FILE_TEST_EXISTS)); |
521 | g_assert (g_file_test (echo_script_path, G_FILE_TEST_EXISTS)); |
522 | |
523 | g_test_add_func (testpath: "/gthread/spawn-single-sync" , test_func: test_spawn_sync); |
524 | g_test_add_func (testpath: "/gthread/spawn-single-async" , test_func: test_spawn_async); |
525 | g_test_add_func (testpath: "/gthread/spawn-single-async-with-fds" , test_func: test_spawn_async_with_fds); |
526 | g_test_add_func (testpath: "/gthread/spawn-script" , test_func: test_spawn_script); |
527 | g_test_add_func (testpath: "/gthread/spawn/nonexistent" , test_func: test_spawn_nonexistent); |
528 | g_test_add_func (testpath: "/gthread/spawn-posix-spawn" , test_func: test_posix_spawn); |
529 | g_test_add_func (testpath: "/gthread/spawn/fd-assignment-clash" , test_func: test_spawn_fd_assignment_clash); |
530 | |
531 | ret = g_test_run(); |
532 | |
533 | g_free (mem: echo_script_path); |
534 | g_free (mem: echo_prog_path); |
535 | |
536 | return ret; |
537 | } |
538 | |