/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#if !defined _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <err.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#define MAX_TEST_DURATION 60

time_t start_timer(void);
int timer_active(time_t timer_started);

inline time_t start_timer() {
    return time(NULL);
}

inline int timer_active(time_t timer_started) {
    return time(NULL) < (timer_started + MAX_TEST_DURATION);
}

int main(void) {
    sync(); /* we're probably gonna crash... */

    // set test timer
    const time_t timer_started = start_timer();

    /*
     * We may already be process group leader but want to be session leader;
     * therefore, do everything in a child process.
     */
    pid_t main_task = fork();
    if (main_task == -1) err(EXIT_FAILURE, "initial fork");
    if (main_task != 0) {
        int status;
        if (waitpid(main_task, &status, 0) != main_task) err(EXIT_FAILURE, "waitpid main_task");
        return WEXITSTATUS(status);
    }

    printf("%d:test starts\n", getpid());

    if (prctl(PR_SET_PDEATHSIG, SIGKILL)) err(EXIT_FAILURE, "PR_SET_PDEATHSIG");
    if (getppid() == 1) exit(EXIT_FAILURE);

    /* basic preparation */
    if (signal(SIGTTOU, SIG_IGN)) err(EXIT_FAILURE, "signal");
    if (setsid() == -1) err(EXIT_FAILURE, "start new session");

    /* set up a new pty pair */
    int ptmx = open("/dev/ptmx", O_RDWR);
    if (ptmx == -1) err(EXIT_FAILURE, "open ptmx");
    unlockpt(ptmx);
    int tty = open(ptsname(ptmx), O_RDWR);
    if (tty == -1) err(EXIT_FAILURE, "open tty");

    /*
     * Let a series of children change the ->pgrp pointer
     * protected by the tty's ctrl_lock...
     */
    pid_t child = fork();
    if (child == -1) {
        err(EXIT_FAILURE, "fork");
    }

    // grandchildren creator process
    if (child == 0) {
        int ret = EXIT_SUCCESS;
        if (prctl(PR_SET_PDEATHSIG, SIGKILL)) err(EXIT_FAILURE, "PR_SET_PDEATHSIG");
        if (getppid() == 1) exit(EXIT_FAILURE);

        while (timer_active(timer_started)) {
            pid_t grandchild = fork();
            if (grandchild == -1) {
                err(EXIT_FAILURE, "fork grandchild");
            }
            if (grandchild == 0) {
                if (setpgid(0, 0)) err(EXIT_FAILURE, "setpgid");
                int pgrp = getpid();
                if (ioctl(tty, TIOCSPGRP, &pgrp)) {
                    err(EXIT_FAILURE, "TIOCSPGRP (tty)");
                }
                exit(EXIT_SUCCESS);
            }
            int status;
            if (waitpid(grandchild, &status, 0) != grandchild)
                err(EXIT_FAILURE, "waitpid for grandchild");
            if ((ret = WEXITSTATUS(status)) != EXIT_SUCCESS) {
                break;
            }
        } // end while(time)
        exit(ret);
    } // end grandchildren creator process

    /*
     * ... while the parent changes the same ->pgrp pointer under the
     * ctrl_lock of the other side of the pty pair.
     */
    int status;
    const char* const TIOCSPGRP_ERROR = "TIOCSPGRP (ptmx)";
    const char* const WAITPID_ERROR = "waitpid for grandchildren creator";
    const char* message1 = NULL;
    const char* message2 = NULL;

    while (timer_active(timer_started)) {
        int pgrp = getpid();
        if (ioctl(ptmx, TIOCSPGRP, &pgrp)) {
            message1 = TIOCSPGRP_ERROR;
            break;
        }
    }

    // wait for grandchildren creator to complete
    if (waitpid(child, &status, 0) != child) {
        message2 = WAITPID_ERROR;
    }

    // return exit status
    if (message1 != NULL || message2 != NULL) {
        err(EXIT_FAILURE, "%s %s", message1 != NULL ? message1 : "",
            message2 != NULL ? message2 : "");
    }
    printf("%d:test completed\n", getpid());
    return WEXITSTATUS(status);
}
