File: pyi_utils_posix.c

package info (click to toggle)
pyinstaller 6.18.0%2Bds-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 11,824 kB
  • sloc: python: 41,828; ansic: 12,123; makefile: 171; sh: 131; xml: 19
file content (970 lines) | stat: -rw-r--r-- 34,408 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
/*
 * ****************************************************************************
 * Copyright (c) 2013-2023, PyInstaller Development Team.
 *
 * Distributed under the terms of the GNU General Public License (version 2
 * or later) with exception for distributing the bootloader.
 *
 * The full license is in the file COPYING.txt, distributed with this software.
 *
 * SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
 * ****************************************************************************
 */

/*
 * Utility functions. This file contains implementations that are specific
 * to POSIX platforms.
 */

/* Having a header included outside of the ifdef block prevents the compilation
 * unit from becoming empty, which is disallowed by pedantic ISO C. */
#include "pyi_global.h"

#ifndef _WIN32

#include <stdio.h>  /* FILE */
#include <stdlib.h>
#include <stddef.h> /* ptrdiff_t */
#include <unistd.h> /* rmdir, unlink, mkdtemp */
#include <string.h>
#include <errno.h>
#include <signal.h> /* kill */
#include <sys/stat.h> /* struct stat */
#include <sys/wait.h>

#if defined(PYI_USE_POSIX_SEMAPHORE)
    #include <sys/mman.h> /* mmap */
    #include <semaphore.h> /* POSIX semaphore API */
    /* Use MAP_ANON as synonym for MAP_ANONYMOUS if the latter is unavailable. */
    #ifndef MAP_ANONYMOUS
        #ifdef MAP_ANON
            #define MAP_ANONYMOUS MAP_ANON
        #else
            #error "Neither MAP_ANONYMOUS nor MAP_ANON is defined."
        #endif
    #endif
#elif defined(PYI_USE_SYSV_SEMAPHORE)
    #include <sys/sem.h> /* SysV semaphore API */
#endif

#include <dirent.h>

#ifndef SIGCLD
    #define SIGCLD SIGCHLD /* not defined on macOS */
#endif

#ifndef sighandler_t
    typedef void (*sighandler_t)(int);
#endif

/* PyInstaller headers. */
#include "pyi_utils.h"
#include "pyi_path.h"
#include "pyi_main.h"
#include "pyi_apple_events.h"


/**********************************************************************\
 *                  Environment variable management                   *
\**********************************************************************/
char *
pyi_getenv(const char *variable)
{
    char *value;

    /* Use standard POSIX getenv(). */
    value = getenv(variable);

    /* On some POSIX platforms, `unsetenv` is not available. In such
     * cases, we "undefine" environment variables by setting them to
     * empty strings. Therefore, treat empty environment variables as
     * being undefined. */
    return (value && value[0]) ? strdup(value) : NULL; /* Return a copy */
}

int
pyi_setenv(const char *variable, const char *value)
{
    /* Standard POSIX function. */
    return setenv(variable, value, 1 /* overwrite */);
}

int
pyi_unsetenv(const char *variable)
{
#if HAVE_UNSETENV
    return unsetenv(variable);
#else /* HAVE_UNSETENV */
    /* If `unsetenv` is unavailable, set the variable to an empty string. */
    return setenv(variable, "", 1 /* overwrite */);
#endif /* HAVE_UNSETENV */
}


/**********************************************************************\
 *         Temporary application top-level directory (onefile)        *
\**********************************************************************/
/*
 * Function 'mkdtemp' (make temporary directory) is missing on some POSIX platforms:
 * - On Solaris function 'mkdtemp' is missing.
 * - On AIX 5.2 function 'mkdtemp' is missing. It is there in version 6.1 but we don't know
 *   the runtime platform at compile time, so we always include our own implementation on AIX.
 */
#if !defined(HAVE_MKDTEMP)

static char *
mkdtemp(char *template)
{
    if (!mktemp(template) ) {
        return NULL;
    }

    if (mkdir(template, 0700) ) {
        return NULL;
    }

    return template;
}

#endif /* !defined(HAVE_MKDTEMP) */

/* Resolve the temporary directory specified by user via runtime_tmpdir
 * option, and create corresponding directory tree. */
static char *
_pyi_create_runtime_tmpdir(const char *runtime_tmpdir)
{
    char directory_tree_path[PYI_PATH_MAX];
    char *subpath_cursor;

    /* Ensure runtime_tmpdir (and thus also its sub-path components)
     * do not exceed path limit. */
    if (strlen(runtime_tmpdir) >= PYI_PATH_MAX) {
        PYI_WARNING("LOADER: length of runtime-tmpdir exceeds maximum path length!\n");
        return NULL;
    }

    /* Recursively create the directory structure
     *
     * NOTE: we call mkdir with mode 0777 for this part of directory
     * tree, as it might be shared by application instances ran by
     * different users. Only the last component (the actual _MEIXXXXXX
     * directory), created by the caller, uses 0700 to restrict access
     * to current user.
     *
     * NOTE2: we ignore errors returned by mkdir; if we actually fail to
     * create (a part of) directory tree here, we will catch the error
     * when we try to resolve the full path to it later on. */
    for(subpath_cursor = strchr(runtime_tmpdir, '/'); subpath_cursor != NULL; subpath_cursor = strchr(++subpath_cursor, '/')) {
        int subpath_length = subpath_cursor - runtime_tmpdir;

        /* Initial / in absolute path */
        if (subpath_length == 0) {
            continue;
        }

        snprintf(directory_tree_path, PYI_PATH_MAX, "%.*s", subpath_length, runtime_tmpdir);
        PYI_DEBUG("LOADER: creating runtime-tmpdir path component: %s\n", directory_tree_path);
        mkdir(directory_tree_path, 0777);
    }

    /* Create full path; necessary if runtime_tmpdir did not end with
     * path separator. */
    PYI_DEBUG("LOADER: creating runtime-tmpdir path: %s\n", runtime_tmpdir);
    mkdir(runtime_tmpdir, 0777);

    /* Now that directory exists, try to resolve full path to it. */
    return realpath(runtime_tmpdir, NULL); /* Let realpath allocate the buffer */
}


/* Append the _MEIXXXXXX string to the temporary directory path template,
 * and try creating the temporary directory. */
static int
_pyi_format_and_create_tmpdir(char *tmpdir_path)
{
    size_t path_len;
    unsigned char needs_separator;

    /* Compute length of the given temporary directory path - to ensure
     * that strcat operations below do not exceed buffer length. */
    path_len = strlen(tmpdir_path);

    /* Check whether the given path ends with separator or not. Typically,
     * it should not, but on macOS, the value from $TMPDIR does. */
    needs_separator = tmpdir_path[path_len - 1] != PYI_SEP;

    /* Add separator , _MEI, and six X characters required by mkdtemp */
    path_len += needs_separator + 4 + 6;
    if (path_len >= PYI_PATH_MAX) {
        return -1;
    }

    if (needs_separator) {
        strcat(tmpdir_path, PYI_SEPSTR);
    }
    strcat(tmpdir_path, "_MEIXXXXXX");

    /* Try creating the directory */
    if (mkdtemp(tmpdir_path) == NULL) {
        return -1;
    }

    return 0;
}

int
pyi_create_temporary_application_directory(struct PYI_CONTEXT *pyi_ctx)
{
    static const char *candidate_env_vars[] = {
        "TMPDIR",
        "TEMP",
        "TMP"
    };

    static const char *candidate_tmp_dirs[] = {
        "/tmp",
        "/var/tmp",
        "/usr/tmp"
    };

    int i;

    /* If specified, use runtime_tmpdir */
    if (pyi_ctx->runtime_tmpdir != NULL) {
        char *resolved_runtime_tmpdir;
        int ret;

        /* Ensure runtime_tmpdir exists, and resolve full path to it */
        resolved_runtime_tmpdir = _pyi_create_runtime_tmpdir(pyi_ctx->runtime_tmpdir);
        if (resolved_runtime_tmpdir == NULL) {
            PYI_WARNING("Failed to create or resolve runtime_tmpdir from given path: %s\n", pyi_ctx->runtime_tmpdir);
            return -1;
        }

        ret = snprintf(pyi_ctx->application_home_dir, PYI_PATH_MAX, "%s", resolved_runtime_tmpdir);
        free(resolved_runtime_tmpdir);
        if (ret >= PYI_PATH_MAX) {
            PYI_WARNING("Length of resolved runtime_tmpdir exceeds maximum path length!\n");
            return -1;
        }

        /* Try to create _MEIXXXXXX directory under the runtime_tmpdir */
        return _pyi_format_and_create_tmpdir(pyi_ctx->application_home_dir);
    }

    /* Check the standard environment variables */
    for (i = 0; i < sizeof(candidate_env_vars)/sizeof(candidate_env_vars[0]); i++) {
        char *env_var_value = pyi_getenv(candidate_env_vars[i]);
        int ret;

        if (env_var_value == NULL) {
            continue;
        }

        ret = snprintf(pyi_ctx->application_home_dir, PYI_PATH_MAX, "%s", env_var_value);
        free(env_var_value);

        if (ret >= PYI_PATH_MAX) {
            continue;
        }

        if (_pyi_format_and_create_tmpdir(pyi_ctx->application_home_dir) == 0) {
            return 0;
        }
    }

    /* Check the standard temporary directory paths */
    for (i = 0; i < sizeof(candidate_tmp_dirs)/sizeof(candidate_tmp_dirs[0]); i++) {
         snprintf(pyi_ctx->application_home_dir, PYI_PATH_MAX, "%s", candidate_tmp_dirs[i]);
         if (_pyi_format_and_create_tmpdir(pyi_ctx->application_home_dir) == 0) {
            return 0;
        }
    }

    return -1; /* No suitable location found */
}


/**********************************************************************\
 *                  Recursive removal of a directory                  *
\**********************************************************************/
int
pyi_recursive_rmdir(const char *dir_path)
{
    DIR *dir_handle;
    struct dirent *dir_entry;
    struct stat stat_buf;
    int dir_path_length;
    int buffer_size;
    char entry_path[PYI_PATH_MAX];

    /* Make a copy of directory path (and append a path separator), into
     * mutable buffer that we will use to construct entries' full paths.
     * Store the length of the directory path string; this allows us to
     * overwrite only the sub-component part of the string, without having
     * to copy the directory path each time. */
    dir_path_length = snprintf(entry_path, PYI_PATH_MAX, "%s%c", dir_path, PYI_SEP);
    if (dir_path_length >= PYI_PATH_MAX) {
        return -1;
    }
    buffer_size = PYI_PATH_MAX - dir_path_length; /* Remaining buffer size */

    /* Open the directory */
    dir_handle = opendir(dir_path);
    if (dir_handle == NULL) {
        return -1;
    }

    /* Iterate over directory contents */
    for (dir_entry = readdir(dir_handle); dir_entry != NULL; dir_entry = readdir(dir_handle))
    {
        /* Skip . and .. */
        if (strcmp(dir_entry->d_name, ".") == 0 || strcmp(dir_entry->d_name, "..") == 0) {
            continue;
        }

        /* Construct the full path, by overwriting the part of string
         * that starts after path directory and separator. */
        if (snprintf(entry_path + dir_path_length, buffer_size, "%s", dir_entry->d_name) >= buffer_size) {
            continue;
        }

        /* Deteremine the type of entry, and remove it. Use lstat()
         * instead of stat() in order to prevent recursion into symlinked
         * directories. On errors, emit debug messages to simplify
         * debugging, and keep going on. We want to remove everything we
         * can; if we fail to remove an entry here, we will also fail
         * to remove the top-level directory, and will return error
         * there and then. */
        if (lstat(entry_path, &stat_buf) == 0) {
            if (S_ISDIR(stat_buf.st_mode) ) {
                /* Recurse into sub-directory */
                if (pyi_recursive_rmdir(entry_path) < 0) {
                    PYI_DEBUG("LOADER: failed to remove directory: %s\n", entry_path);
                }
            } else {
                if (unlink(entry_path) < 0) {
                    PYI_DEBUG("LOADER: failed to remove file: %s\n", entry_path);
                }
            }
        }
    }
    closedir(dir_handle);

    /* Finally, remove the directory; the return value of rmdir (0 on
     * success, -1 on error) maps directly to this function's return. */
    return rmdir(dir_path);
}


/**********************************************************************\
 *                  Child process spawning (onefile)                  *
\**********************************************************************/
#if !defined(__APPLE__)

int
pyi_utils_set_library_search_path(const char *path)
{
    /* On AIX, LIBPATH is used to set dynamic library search path. On
     * other POSIX platforms (other than macOS), LD_LIBRARY_PATH is used. */
#ifdef AIX
    const char *variable_name = "LIBPATH";
    const char *variable_name_copy = "LIBPATH_ORIG";
#else
    const char *variable_name = "LD_LIBRARY_PATH";
    const char *variable_name_copy = "LD_LIBRARY_PATH_ORIG";
#endif

    char *orig_library_path = NULL;

    int rc = 0;

    /* Try retrieving the original value of the library-path environment
     * variable. */
    orig_library_path = pyi_getenv(variable_name);
    if (orig_library_path) {
        char *new_library_path;
        int new_library_path_length;

        /* Variable is set; store a copy (*_ORIG environment variable),
         * so that it can be restored, if necessary. */
        PYI_DEBUG("LOADER: setting %s=%s\n", variable_name_copy, orig_library_path);
        pyi_setenv(variable_name_copy, orig_library_path);

        /* Compute the length of the new environment variable value:
         * given path + separator + original value + terminating NULL. */
        new_library_path_length = strlen(orig_library_path) + strlen(path) + 2;
        new_library_path = malloc(new_library_path_length);
        if (new_library_path == NULL) {
            rc = -1; /* Allocation failed */
        } else {
            snprintf(new_library_path, new_library_path_length, "%s:%s", path, orig_library_path);
            PYI_DEBUG("LOADER: setting %s=%s\n", variable_name, new_library_path);
            rc = pyi_setenv(variable_name, new_library_path);
            free(new_library_path);
        }

        free(orig_library_path);
    } else {
        /* Variable not set; the new search path should contain just the
         * given path. */
        PYI_DEBUG("LOADER: setting %s=%s\n", variable_name, path);
        rc = pyi_setenv(variable_name, path);
    }

    return rc;
}

#endif /* !defined(__APPLE__) */

/*
 * If the program is activated by a systemd socket, systemd will set
 * LISTEN_PID, LISTEN_FDS environment variable for that process.
 *
 * LISTEN_PID is set to the pid of the parent process of bootloader,
 * which is forked by systemd.
 *
 * Bootloader will duplicate LISTEN_FDS to child process, but the
 * LISTEN_PID environment variable remains unchanged.
 *
 * Here we change the LISTEN_PID to the child pid in child process.
 * So the application can detect it and use the LISTEN_FDS created
 * by systemd.
 */
static int
_pyi_set_systemd_env()
{
    const char *env_var_name = "LISTEN_PID";
    char *value;

    value = pyi_getenv(env_var_name);
    if (value != NULL) {
        /* 32 characters should be enough to accommodate the largest
         * value unsigned 64-bit integer (2^64 - 1), which takes up 20
         * characters. Even on contemporary 64-bit linux systems, PID
         * values have theoretical limit of 2^22, so there is a lot of
         * headroom here...   */
        char pid_str[32];

        free(value); /* Free the copy of original value, which we do not need. */

        snprintf(pid_str, 32, "%ld", (unsigned long)getpid());
        return pyi_setenv(env_var_name, pid_str);
    }

    return 0;
}

static void
_ignoring_signal_handler(int signum)
{
    /* Ignore the signal. Avoid generating debug messages as per
     * explanation in _signal_handler(). */
    (void)signum; /* Supress unused argument warnings */
}

static void
_signal_handler(int signum)
{
    int original_errno = errno; /* Store original errno */
    pid_t child_pid = global_pyi_ctx->child_pid; /* Read from volatile global variable */

    /* Forward signal to the child. Avoid generating debug messages, as
     * functions involved are generally not signal safe. Furthermore, it
     * may result in endless spamming of SIGPIPE, as reported and
     * diagnosed in #5270. */

#if defined(LAUNCH_DEBUG)
    global_pyi_ctx->signal_forward_all++;
#endif

    if (child_pid <= 0) {
        /* No-op if child process does not exist (yet or anymore), or
         * if fork() returned -1 due to error. */
#if defined(LAUNCH_DEBUG)
        global_pyi_ctx->signal_forward_noop++;
#endif
        return;
    }

#if defined(LAUNCH_DEBUG)
    if (kill(child_pid, signum) == 0) {
        global_pyi_ctx->signal_forward_ok++;
    } else {
        global_pyi_ctx->signal_forward_error++;
    }
#else
    kill(child_pid, signum);
#endif

    errno = original_errno; /* Restore original errno */
}

/* Start frozen application in a subprocess. The frozen application runs
 * in a subprocess. */
int
pyi_utils_create_child(struct PYI_CONTEXT *pyi_ctx)
{
    pid_t child_pid = 0;
    int rc = 0;
    int wait_rc = -1;

    /* As indicated in signal(7), signal numbers range from 1-31 (standard)
     * and 32-64 (Linux real-time). */
    const size_t num_signals = 65;

    sighandler_t signal_handler;
    int signum;

    /* Create semaphore for synchronizing child and parent; we need to
     * ensure that the child starts executing user's python code only
     * *after* the parent has fully completed the `fork()` call *and* stored
     * the child process ID to `pyi_ctx->child_pid` for the forwarding
     * signal handlers (as well as installed the said handlers).
     * Failing to do so leads to sporadic failures in our signal handling
     * test (`test_onefile_signal_handling`) when performed under heavy
     * CPU load.
     *
     * Our first choice of API are unnamed POSIX semaphores, which should
     * be available on most POSIX platforms. However, they are unavailable
     * on macOS (i.e., only stubs are implemented), so we also need to support
     * the old SysV semaphore API... */

#if defined(PYI_USE_POSIX_SEMAPHORE)
    sem_t *sem_ptr;

    /* Create anonymous shared memory region for the semaphore. */
    PYI_DEBUG("LOADER: creating sync semaphore...\n");
    sem_ptr = mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (sem_ptr == MAP_FAILED) {
        PYI_DEBUG("LOADER: failed to create shared memory region for sync semaphore (errno %d) - disabling sync.\n", errno);
    } else {
        /* Initialize semaphore with pshared=1 and value=0 (locked/acquired). */
        if (sem_init(sem_ptr, 1, 0) < 0) {
            PYI_DEBUG("LOADER: failed to initialize sync semaphore (errno %d) - disabling sync.\n", errno);
            /* Unmap shared memory, and reset pointer to it for later checks. */
            munmap(sem_ptr, sizeof(sem_t));
            sem_ptr = MAP_FAILED;
        }
    }
#elif defined(PYI_USE_SYSV_SEMAPHORE)
    /* Argument for semctl(); according to API, we need to provide our
     * own union definition... */
    union {
        int val;
        struct semid_ds *buf;
        unsigned short *array;
    } sem_arg;

    struct sembuf sem_op;  /* Argument for semop() */
    int sem_id = -1;  /* Semaphore (array) ID */

    /* Create semaphore */
    PYI_DEBUG("LOADER: creating sync semaphore...\n");
    sem_id = semget(IPC_PRIVATE, 1, 0660 | IPC_CREAT | IPC_EXCL);
    if (sem_id < 0) {
        /* Allow semaphore creation to fail, for whatever reason, and
         * disable parent/child sync in that case. Not having the sync
         * should be of little consequence outside of PyInstaller's own
         * POSIX signal test suite, so it should not be considered fatal.
         * For example, under cygwin, semget() is only available when
         * cygserver is running, which might not always be the case.
         * There might be other failure modes on other POSIX systems. */
        PYI_DEBUG("LOADER: failed to create sync semaphore (errno %d) - disabling sync.\n", errno);
    }

    /* Initialize semaphore's value to 0 (= locked/acquired) */
    if (sem_id >= 0) {
        sem_arg.val = 0;
        if (semctl(sem_id, 0, SETVAL, sem_arg) < 0) {
            PYI_PERROR("semctl", "Failed to initialize sync semaphore!\n");
            goto cleanup;
        }
    }
#endif /* defined(PYI_USE_SYSV_SEMAPHORE) */

    /* macOS: Apple Events handling */
#if defined(__APPLE__) && defined(WINDOWED)
    /* Initialize pyi_argc and pyi_argv with original argc and argv.
     * Do this regardless of argv-emulation setting, because
     * pyi_utils_initialize_args() also filters out -psn_xxx argument. */
    if (pyi_utils_initialize_args(pyi_ctx, pyi_ctx->argc, pyi_ctx->argv) < 0) {
        goto cleanup;
    }

    /* Install Apple Event handlers */
    pyi_ctx->ae_ctx = pyi_apple_install_event_handlers(pyi_ctx);
    if (pyi_ctx->ae_ctx == NULL) {
        goto cleanup;
    }

    /* argv emulation; do a short (250 ms) cycle of Apple Events processing
     * before bringing up the child process */
    if (pyi_ctx->macos_argv_emulation) {
        pyi_apple_process_events(pyi_ctx->ae_ctx, 0.25);  /* short timeout (250 ms) */
    }
#endif

    /* Fork the child process. */
    child_pid = fork();
    if (child_pid < 0) {
        PYI_WARNING("LOADER: failed to fork child process: %s\n", strerror(errno));
        goto cleanup;
    }

    /* Child code. */
    if (child_pid == 0) {
        /* Replace process by starting a new application. */
        /* If modified arguments (pyi_ctx->pyi_argv) are available, use
         * those. Otherwise, use the original pyi_ctx->argv. */
        char *const *argv = (pyi_ctx->pyi_argv != NULL) ? pyi_ctx->pyi_argv : pyi_ctx->argv;
        const int argc = (pyi_ctx->pyi_argv != NULL) ? pyi_ctx->pyi_argc : pyi_ctx->argc;

        /* Wait on the sync semaphore */
#if defined(PYI_USE_POSIX_SEMAPHORE)
        if (sem_ptr != MAP_FAILED) {
            PYI_DEBUG("LOADER: waiting on sync semaphore...\n");
            if (sem_wait(sem_ptr) < 0) {
                PYI_PERROR("sem_wait", "Failed to wait on sync semaphore!\n");
            }
        }
#elif defined(PYI_USE_SYSV_SEMAPHORE)
        if (sem_id >= 0) {
            PYI_DEBUG("LOADER: waiting on sync semaphore...\n");
            sem_op.sem_num = 0;
            sem_op.sem_op = -1; /* P/decrement (= acquire semaphore) */
            sem_op.sem_flg = 0;
            if (semop(sem_id, &sem_op, 1) < 0) {
                PYI_PERROR("semop", "Failed to wait on sync semaphore!\n");
            }
        }
#endif /* defined(PYI_USE_SYSV_SEMAPHORE) */

        /* Modify the LISTEN_PID environment variable, if necessary */
        if (_pyi_set_systemd_env() != 0) {
            PYI_WARNING("LOADER: application is started by systemd socket, but we cannot set proper LISTEN_PID on it.\n");
        }

        /* NOTE: if execvp() fails for whatever reason, we must immediately
         * exit the (forked) child process using exit() call. Otherwise,
         * the forked child process will continue executing the cleanup
         * codepath, which is intended for the parent process, and will
         * end up interfering with the cleanup in the actual parent
         * process - for example, there will be two attempts at removing
         * the application's temporary directory. */
        if (pyi_ctx->dynamic_loader_filename[0] != 0) {
            char *const *exec_argv;

            PYI_DEBUG("LOADER: starting child process via execvp and dynamic linker/loader: %s\n", pyi_ctx->dynamic_loader_filename);
            exec_argv = pyi_prepend_dynamic_loader_to_argv(argc, argv, pyi_ctx->dynamic_loader_filename);
            if (exec_argv == NULL) {
                PYI_ERROR("LOADER: failed to allocate argv array for execvp!\n");
                exit(-1);
            }
            if (execvp(pyi_ctx->dynamic_loader_filename, exec_argv) < 0) {
                PYI_ERROR("LOADER: failed to start child process: %s\n", strerror(errno));
                exit(-1);
            }
        } else {
            PYI_DEBUG("LOADER: starting child process via execvp\n");
            if (execvp(pyi_ctx->executable_filename, argv) < 0) {
                PYI_ERROR("LOADER: failed start child process: %s\n", strerror(errno));
                exit(-1);
            }
        }

        /* NOTREACHED */
    }

    /* From here to end-of-function is parent code (since the child exec'd,
     * or exited on exec failure). */
    PYI_DEBUG("LOADER: forked child process with PID: %d\n", child_pid);

    /* Store child PID to context structure for use in forwarding signal
     * handler (as well as Apple event handler on macOS). */
    pyi_ctx->child_pid = child_pid;

    /* Install signal handlers to either forward received signals to the
     * child process, or ignore them (effectively blocking them). */
    if (pyi_ctx->ignore_signals) {
        PYI_DEBUG("LOADER: registering signal handlers to ignore received signals.\n");
        signal_handler = &_ignoring_signal_handler;
    } else {
        PYI_DEBUG("LOADER: registering signal handlers to forward received signals to child.\n");
        signal_handler = &_signal_handler;
    }

    for (signum = 0; signum < num_signals; ++signum) {
        /* Don't mess with SIGCHLD/SIGCLD; it affects our ability
         * to wait() for the child to exit. Similarly, do not change
         * don't change SIGTSP handling to allow Ctrl-Z */
        if (signum == SIGCHLD || signum == SIGCLD || signum == SIGTSTP) {
            continue;
        }
        signal(signum, signal_handler);
    }

    /* Signal the sync semaphore */
#if defined(PYI_USE_POSIX_SEMAPHORE)
    if (sem_ptr != MAP_FAILED) {
        PYI_DEBUG("LOADER: signalling the sync semaphore...\n");
        if (sem_post(sem_ptr) < 0) {
            PYI_PERROR("sem_post", "Failed to signal the sync semaphore!\n");
        }
    }
#elif defined(PYI_USE_SYSV_SEMAPHORE)
    if (sem_id >= 0) {
        PYI_DEBUG("LOADER: signalling the sync semaphore...\n");
        sem_op.sem_num = 0;
        sem_op.sem_op = 1; /* V/increment (= release semaphore) */
        sem_op.sem_flg = 0;
        if (semop(sem_id, &sem_op, 1) < 0) {
            PYI_PERROR("semop", "Failed to signal the sync semaphore!\n");
        }
    }
#endif /* defined(PYI_USE_SYSV_SEMAPHORE) */

#if defined(__APPLE__) && defined(WINDOWED)
    /* macOS: forward events to child */
    do {
        /* The below loop will iterate about once every second on Apple,
         * waiting on the event queue most of that time. */
        wait_rc = waitpid(child_pid, &rc, WNOHANG);
        if (wait_rc == 0) {
            /* Check if we have a pending event that we need to forward... */
            if (pyi_apple_has_pending_event(pyi_ctx->ae_ctx)) {
                /* Attempt to re-send the pending event after 0.5 second delay. */
                if (pyi_apple_send_pending_event(pyi_ctx->ae_ctx, 0.5) != 0) {
                    /* Do not process additional events until the pending one
                     * is successfully forwarded (or cleaned up by error). */
                    continue;
                }
            }
            /* Wait for and process AppleEvents with a 1-second timeout, forwarding
             * events to the child. */
            pyi_apple_process_events(pyi_ctx->ae_ctx, 1.0);  /* long timeout (1 sec) */
        }
    } while (!wait_rc);

    /* Check if we have a pending event to forward (for diagnostics) */
    if (pyi_apple_has_pending_event(pyi_ctx->ae_ctx)) {
        PYI_DEBUG("LOADER [AppleEvent]: child terminated before pending event could be forwarded!\n");
        pyi_apple_cleanup_pending_event(pyi_ctx->ae_ctx);
    }

    /* Uninstall event handlers */
    pyi_apple_uninstall_event_handlers(&pyi_ctx->ae_ctx);
#else
    wait_rc = waitpid(child_pid, &rc, 0);
#endif

    /* Reset stored child PID - this aims to immediately turn forwarding
     * signal handler into no-op (due to `pyi_ctx->child_pid != 0` check). */
     pyi_ctx->child_pid = 0;

    if (wait_rc < 0) {
        PYI_WARNING("LOADER: failed to wait for child process: %s\n", strerror(errno));
    }

    /* After child process exited, reset signal handlers to default values. */
    PYI_DEBUG("LOADER: restoring signal handlers\n");
    for (signum = 0; signum < num_signals; ++signum) {
        signal(signum, SIG_DFL);
    }

    /* Display statistics from forwarding signal-handler. */
#if defined(LAUNCH_DEBUG)
    if (!pyi_ctx->ignore_signals) {
        PYI_DEBUG(
            "LOADER: signal forwarding statistics: all=%u, ok=%u, err=%u, noop=%u\n",
            pyi_ctx->signal_forward_all,
            pyi_ctx->signal_forward_ok,
            pyi_ctx->signal_forward_error,
            pyi_ctx->signal_forward_noop
        );
    }
#endif

cleanup:
    /* Destroy the sync semaphore (if available) */
#if defined(PYI_USE_POSIX_SEMAPHORE)
    if (sem_ptr != MAP_FAILED) {
        if (sem_destroy(sem_ptr) < 0) {
            PYI_WARNING("LOADER: failed to destroy sync semaphore (errno %d)!\n", errno);
        }
        if (munmap(sem_ptr, sizeof(sem_t)) < 0) {
            PYI_WARNING("LOADER: failed to unmap shared memory of sync semaphore (errno %d)!\n", errno);
        }
    }
#elif defined(PYI_USE_SYSV_SEMAPHORE)
    if (sem_id >= 0) {
        if (semctl(sem_id, 0, IPC_RMID) < 0) {
            PYI_WARNING("LOADER: failed to destroy sync semaphore (errno %d)!\n", errno);
        }
    }
#endif /* defined(PYI_USE_SYSV_SEMAPHORE) */

    /* Clean up the modified copy of command-line arguments (currently
     * applicable only to macOS windowed bootloader builds). */
#if defined(__APPLE__) && defined(WINDOWED)
    pyi_utils_free_args(pyi_ctx);
#endif

    /* Either wait() failed, or we jumped to `cleanup` and
     * didn't wait() at all. Either way, exit with error,
     * because rc does not contain a valid process exit code. */
    if (wait_rc < 0) {
        PYI_DEBUG("LOADER: exiting early\n");
        return 1;
    }

    if (WIFEXITED(rc)) {
        PYI_DEBUG("LOADER: returning child exit status %d\n", WEXITSTATUS(rc));
        return WEXITSTATUS(rc);
    }

    /* Process ended abnormally */
    pyi_ctx->child_signalled = WIFSIGNALED(rc);
    if (pyi_ctx->child_signalled) {
        pyi_ctx->child_signal = WTERMSIG(rc);
        PYI_DEBUG("LOADER: child received signal %d; storing for re-raise after cleanup...\n", pyi_ctx->child_signal);
    }
    return 1;
}


/**********************************************************************\
 *                 Argument filtering and modification                *
\**********************************************************************/
/*
 * Initialize private pyi_argc and pyi_argv from the given argc and
 * argv by creating a deep copy. The resulting pyi_argc and pyi_argv
 * can be retrieved directly from PYI_CONTEXT structure, and are
 * freed/cleaned-up by calling pyi_utils_free_args().
 *
 * The pyi_argv contains pyi_argv + 1 elements, with the last element
 * being NULL (i.e., it is execv-compatible NULL-terminated array).
 *
 * On macOS, this function filters out the -psnxxx argument that is
 * passed to executable when .app bundle is launched from Finder:
 * https://stackoverflow.com/questions/10242115/os-x-strange-psn-command-line-parameter-when-launched-from-finder
 */
int pyi_utils_initialize_args(struct PYI_CONTEXT *pyi_ctx, const int argc, char *const argv[])
{
    int i;

    pyi_ctx->pyi_argc = 0;
    pyi_ctx->pyi_argv = (char**)calloc(argc + 1, sizeof(char*));
    if (!pyi_ctx->pyi_argv) {
        PYI_ERROR("LOADER: failed to allocate pyi_argv: %s\n", strerror(errno));
        return -1;
    }

    for (i = 0; i < argc; i++) {
        char *tmp;

        /* Filter out -psnxxx argument that is used on macOS to pass
         * unique process serial number (PSN) to .app bundles launched
         * via Finder. */
#if defined(__APPLE__) && defined(WINDOWED)
        if (strstr(argv[i], "-psn") == argv[i]) {
            continue;
        }
#endif

        /* Copy the argument */
        tmp = strdup(argv[i]);
        if (!tmp) {
            PYI_ERROR("LOADER: failed to strdup argv[%d]: %s\n", i, strerror(errno));
            /* If we cannot allocate basic amounts of memory at this critical point,
             * we should probably just give up. */
            return -1;
        }
        pyi_ctx->pyi_argv[pyi_ctx->pyi_argc++] = tmp;
    }

    return 0;
}

/*
 * Append given argument to private pyi_argv and increment pyi_argc.
 * The pyi_argv array is reallocated accordingly.
 *
 * Returns 0 on success, -1 on failure (due to failed array reallocation).
 * On failure, pyi_argv and pyi_argc remain unchanged.
 */
int pyi_utils_append_to_args(struct PYI_CONTEXT *pyi_ctx, const char *arg)
{
    char **new_pyi_argv;
    char *arg_copy;

    /* Make a copy of new argument */
    arg_copy = strdup(arg);
    if (!arg_copy) {
        return -1;
    }

    /* Reallocate pyi_argv array, making space for new argument plus
     * terminating NULL */
    new_pyi_argv = (char**)realloc(pyi_ctx->pyi_argv, (pyi_ctx->pyi_argc + 2) * sizeof(char *));
    if (!new_pyi_argv) {
        free(arg_copy);
        return -1;
    }
    pyi_ctx->pyi_argv = new_pyi_argv;

    /* Store new argument */
    pyi_ctx->pyi_argv[pyi_ctx->pyi_argc++] = arg_copy;
    pyi_ctx->pyi_argv[pyi_ctx->pyi_argc] = NULL;

    return 0;
}

/*
 * Free/clean-up the private arguments (pyi_argv).
 */
void pyi_utils_free_args(struct PYI_CONTEXT *pyi_ctx)
{
    /* Free each entry */
    int i;
    for (i = 0; i < pyi_ctx->pyi_argc; i++) {
        free(pyi_ctx->pyi_argv[i]);
    }

    /* Free the array itself */
    free(pyi_ctx->pyi_argv);

    /* Clean-up the variables, just in case */
    pyi_ctx->pyi_argc = 0;
    pyi_ctx->pyi_argv = NULL;
}


/*
 * Allocate new argv array and prepend the given dynamic linker/loader
 * name to it. Used when restarting or spawning process via execvp().
 *
 * Since execvp() copies the array, we can create a shallow copy of
 * argv here.
 */
char *const *pyi_prepend_dynamic_loader_to_argv(const int argc, char *const argv[], char *const loader_filename)
{
    char **new_argv;
    int i;

    /* Allocate the new array; loader name + elements of argv + terminating NULL */
    new_argv = (char **)calloc(argc + 2, sizeof(char *));
    if (new_argv == NULL) {
        return NULL;
    }

    /* Shallow copy of the elements. */
    new_argv[0] = loader_filename;
    for (i = 0; i < argc; i++) {
        new_argv[i + 1] = argv[i];
    }

    return new_argv;
}

#endif /* ifndef _WIN32 */