2 * Copyright (C) 2011 Red Hat, Inc.
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.
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.
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.
21 * Author: Colin Walters <walters@verbum.org>
32 #include <glib-unix.h>
33 #include <glib/gstdio.h>
34 #include <sys/types.h>
41 #define LINEEND "\r\n"
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
50 #if defined (G_OS_WIN32) && defined (_MSC_VER)
51 #define SCRIPT_EXT ".bat"
56 static char *echo_prog_path;
57 static char *echo_script_path;
61 gboolean child_exited;
64 } SpawnAsyncMultithreadedData;
67 on_child_exited (GPid pid,
71 SpawnAsyncMultithreadedData *data = datap;
73 data->child_exited = TRUE;
74 if (data->child_exited && data->stdout_done)
75 g_main_loop_quit (data->loop);
77 return G_SOURCE_REMOVE;
81 on_child_stdout (GIOChannel *channel,
82 GIOCondition condition,
88 SpawnAsyncMultithreadedData *data = datap;
90 if (condition & G_IO_IN)
93 status = g_io_channel_read_chars (channel, buf, sizeof (buf), &bytes_read, &error);
94 g_assert_no_error (error);
95 g_string_append_len (data->stdout_buf, buf, (gssize) bytes_read);
96 if (status == G_IO_STATUS_EOF)
97 data->stdout_done = TRUE;
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");
104 if (data->child_exited && data->stdout_done)
105 g_main_loop_quit (data->loop);
107 return !data->stdout_done;
111 test_spawn_async (void)
114 GError *error = NULL;
118 GMainContext *context;
123 SpawnAsyncMultithreadedData data;
125 context = g_main_context_new ();
126 loop = g_main_loop_new (context, TRUE);
128 arg = g_strdup_printf ("thread %d", tnum);
130 argv = g_ptr_array_new ();
131 g_ptr_array_add (argv, echo_prog_path);
132 g_ptr_array_add (argv, arg);
133 g_ptr_array_add (argv, NULL);
135 g_spawn_async_with_pipes (NULL, (char**)argv->pdata, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid, NULL,
136 &child_stdout_fd, NULL, &error);
137 g_assert_no_error (error);
138 g_ptr_array_free (argv, TRUE);
141 data.stdout_done = FALSE;
142 data.child_exited = FALSE;
143 data.stdout_buf = g_string_new (0);
145 source = g_child_watch_source_new (pid);
146 g_source_set_callback (source, (GSourceFunc)on_child_exited, &data, NULL);
147 g_source_attach (source, context);
148 g_source_unref (source);
150 channel = g_io_channel_unix_new (child_stdout_fd);
151 source = g_io_create_watch (channel, G_IO_IN | G_IO_HUP | G_IO_ERR);
152 g_source_set_callback (source, (GSourceFunc)on_child_stdout, &data, NULL);
153 g_source_attach (source, context);
154 g_source_unref (source);
156 g_main_loop_run (loop);
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 (data.stdout_buf, TRUE);
163 g_io_channel_unref (channel);
164 g_main_context_unref (context);
165 g_main_loop_unref (loop);
170 /* Windows close() causes failure through the Invalid Parameter Handler
171 * Routine if the file descriptor does not exist.
180 /* Test g_spawn_async_with_fds() with a variety of different inputs */
182 test_spawn_async_with_fds (void)
189 /* Each test has 3 variable parameters: stdin, stdout, stderr */
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 */
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 */
202 arg = g_strdup_printf ("thread %d", tnum);
204 argv = g_ptr_array_new ();
205 g_ptr_array_add (argv, echo_prog_path);
206 g_ptr_array_add (argv, arg);
207 g_ptr_array_add (argv, NULL);
209 for (i = 0; i < G_N_ELEMENTS (tests); i++)
211 GError *error = NULL;
213 GMainContext *context;
215 GIOChannel *channel = NULL;
217 SpawnAsyncMultithreadedData data;
218 enum fd_type *fd_info = tests[i];
219 gint test_pipe[3][2];
222 for (j = 0; j < 3; j++)
227 test_pipe[j][0] = -1;
228 test_pipe[j][1] = -1;
231 test_pipe[j][0] = -5;
232 test_pipe[j][1] = -5;
236 g_unix_open_pipe (test_pipe[j], FD_CLOEXEC, &error);
237 g_assert_no_error (error);
239 g_assert_cmpint (_pipe (test_pipe[j], 4096, _O_BINARY), >=, 0);
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];
248 g_assert_not_reached ();
252 context = g_main_context_new ();
253 loop = g_main_loop_new (context, TRUE);
255 g_spawn_async_with_fds (NULL, (char**)argv->pdata, NULL,
256 G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid,
257 test_pipe[0][0], test_pipe[1][1], test_pipe[2][1],
259 g_assert_no_error (error);
260 safe_close (test_pipe[0][0]);
261 safe_close (test_pipe[1][1]);
262 if (fd_info[2] != STDOUT_PIPE)
263 safe_close (test_pipe[2][1]);
266 data.stdout_done = FALSE;
267 data.child_exited = FALSE;
268 data.stdout_buf = g_string_new (0);
270 source = g_child_watch_source_new (pid);
271 g_source_set_callback (source, (GSourceFunc)on_child_exited, &data, NULL);
272 g_source_attach (source, context);
273 g_source_unref (source);
275 if (test_pipe[1][0] >= 0)
277 channel = g_io_channel_unix_new (test_pipe[1][0]);
278 source = g_io_create_watch (channel, G_IO_IN | G_IO_HUP | G_IO_ERR);
279 g_source_set_callback (source, (GSourceFunc)on_child_stdout,
281 g_source_attach (source, context);
282 g_source_unref (source);
286 /* Don't check stdout data if we didn't pass a fd */
287 data.stdout_done = TRUE;
290 g_main_loop_run (loop);
292 g_assert_true (data.child_exited);
294 if (test_pipe[1][0] >= 0)
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);
301 g_string_free (data.stdout_buf, TRUE);
303 g_main_context_unref (context);
304 g_main_loop_unref (loop);
305 safe_close (test_pipe[0][1]);
306 safe_close (test_pipe[1][0]);
307 if (fd_info[2] != STDOUT_PIPE)
308 safe_close (test_pipe[2][0]);
311 g_ptr_array_free (argv, TRUE);
316 test_spawn_sync (void)
319 GError *error = NULL;
320 char *arg = g_strdup_printf ("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.
325 const char * const argv[] = {
328 "doublequotes\\\"after\\\\\"\"backslashes", /* this would be special escaped on win32 */
329 "\\\"\"doublequotes spaced after backslashes\\\\\"", /* this would be special escaped on win32 */
332 "even\"\"doublequotes",
333 "even''singlequotes",
334 "even\\\\backslashes",
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/",
344 char *joined_args_str = g_strjoinv ("", (char**)argv + 1);
348 g_spawn_sync (NULL, (char**)argv, NULL, 0, NULL, NULL, &stdout_str, NULL, &estatus, &error);
349 g_assert_no_error (error);
350 g_assert_cmpstr (joined_args_str, ==, stdout_str);
353 g_free (joined_args_str);
356 /* Like test_spawn_sync but uses spawn flags that trigger the optimized
357 * posix_spawn codepath.
360 test_posix_spawn (void)
363 GError *error = NULL;
368 GSpawnFlags flags = G_SPAWN_CLOEXEC_PIPES | G_SPAWN_LEAVE_DESCRIPTORS_OPEN;
370 arg = g_strdup_printf ("thread %d", tnum);
372 argv = g_ptr_array_new ();
373 g_ptr_array_add (argv, echo_prog_path);
374 g_ptr_array_add (argv, arg);
375 g_ptr_array_add (argv, NULL);
377 g_spawn_sync (NULL, (char**)argv->pdata, NULL, flags, NULL, NULL, &stdout_str, NULL, &estatus, &error);
378 g_assert_no_error (error);
379 g_assert_cmpstr (arg, ==, stdout_str);
382 g_ptr_array_free (argv, TRUE);
386 test_spawn_script (void)
388 GError *error = NULL;
393 argv = g_ptr_array_new ();
394 g_ptr_array_add (argv, echo_script_path);
395 g_ptr_array_add (argv, NULL);
397 g_spawn_sync (NULL, (char**)argv->pdata, NULL, 0, NULL, NULL, &stdout_str, NULL, &estatus, &error);
398 g_assert_no_error (error);
399 g_assert_cmpstr ("echo" LINEEND, ==, stdout_str);
401 g_ptr_array_free (argv, TRUE);
404 /* Test that spawning a non-existent executable returns %G_SPAWN_ERROR_NOENT. */
406 test_spawn_nonexistent (void)
408 GError *error = NULL;
409 GPtrArray *argv = NULL;
410 gchar *stdout_str = NULL;
411 gint exit_status = -1;
413 argv = g_ptr_array_new ();
414 g_ptr_array_add (argv, "this does not exist");
415 g_ptr_array_add (argv, NULL);
417 g_spawn_sync (NULL, (char**) argv->pdata, NULL, 0, NULL, NULL, &stdout_str,
418 NULL, &exit_status, &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);
423 g_ptr_array_free (argv, TRUE);
425 g_clear_error (&error);
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.
432 * https://gitlab.gnome.org/GNOME/glib/-/issues/2097 */
434 test_spawn_fd_assignment_clash (void)
439 const guint n_fds = 10;
440 gint source_fds[n_fds];
441 gint target_fds[n_fds];
442 const gchar *argv[] = { "/nonexistent", NULL };
444 GError *local_error = NULL;
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 ("glib-spawn-test-XXXXXX", NULL, NULL);
450 g_assert_cmpint (tmp_fd, >=, 0);
452 for (i = 0; i < (n_fds - 1); ++i)
454 int source = fcntl (tmp_fd, F_DUPFD_CLOEXEC, 3);
455 g_assert_cmpint (source, >=, 0);
456 source_fds[i] = source;
457 target_fds[i] = source + n_fds;
460 source_fds[i] = tmp_fd;
461 target_fds[i] = tmp_fd + n_fds;
463 /* Print out the FD map. */
464 g_test_message ("FD map:");
465 for (i = 0; i < n_fds; i++)
466 g_test_message (" • %d → %d", source_fds[i], target_fds[i]);
468 /* Spawn the subprocess. This should fail because the executable doesn’t
470 retval = g_spawn_async_with_pipes_and_fds (NULL, argv, NULL, G_SPAWN_DEFAULT,
471 NULL, NULL, -1, -1, -1,
472 source_fds, target_fds, n_fds,
473 NULL, NULL, NULL, NULL,
475 g_assert_error (local_error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT);
476 g_assert_false (retval);
478 g_clear_error (&local_error);
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);
487 for (i = 0; i < n_fds; i++)
488 g_close (source_fds[i], NULL);
489 #else /* !G_OS_UNIX */
490 g_test_skip ("FD redirection only supported on Unix");
491 #endif /* !G_OS_UNIX */
501 setlocale (LC_ALL, "");
503 g_test_init (&argc, &argv, NULL);
505 dirname = g_path_get_dirname (argv[0]);
506 echo_prog_path = g_build_filename (dirname, "test-spawn-echo" EXEEXT, NULL);
507 if (!g_file_test (echo_prog_path, G_FILE_TEST_EXISTS))
509 g_free (echo_prog_path);
510 echo_prog_path = g_build_filename (dirname, "lt-test-spawn-echo" EXEEXT, NULL);
512 echo_script_path = g_build_filename (dirname, "echo-script" SCRIPT_EXT, NULL);
513 if (!g_file_test (echo_script_path, G_FILE_TEST_EXISTS))
515 g_free (echo_script_path);
516 echo_script_path = g_test_build_filename (G_TEST_DIST, "echo-script" SCRIPT_EXT, NULL);
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));
523 g_test_add_func ("/gthread/spawn-single-sync", test_spawn_sync);
524 g_test_add_func ("/gthread/spawn-single-async", test_spawn_async);
525 g_test_add_func ("/gthread/spawn-single-async-with-fds", test_spawn_async_with_fds);
526 g_test_add_func ("/gthread/spawn-script", test_spawn_script);
527 g_test_add_func ("/gthread/spawn/nonexistent", test_spawn_nonexistent);
528 g_test_add_func ("/gthread/spawn-posix-spawn", test_posix_spawn);
529 g_test_add_func ("/gthread/spawn/fd-assignment-clash", test_spawn_fd_assignment_clash);
533 g_free (echo_script_path);
534 g_free (echo_prog_path);