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
56static char *echo_prog_path;
57static char *echo_script_path;
58
59typedef struct {
60 GMainLoop *loop;
61 gboolean child_exited;
62 gboolean stdout_done;
63 GString *stdout_buf;
64} SpawnAsyncMultithreadedData;
65
66static gboolean
67on_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
80static gboolean
81on_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
110static void
111test_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 */
173static void
174safe_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 */
181static void
182test_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
315static void
316test_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 */
359static void
360test_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
385static void
386test_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. */
405static void
406test_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 */
433static void
434test_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
494int
495main (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

source code of gtk/subprojects/glib/glib/tests/spawn-singlethread.c