%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%---------------------------------------------------------------------------%
% Copyright (C) 2001-2002, 2004-2006, 2008 The University of Melbourne.
% Copyright (C) 2014, 2017, 2019, 2021 The Mercury team.
% This file may only be copied under the terms of the GNU General
% Public License - see the file COPYING in the Mercury distribution.
%---------------------------------------------------------------------------%
%
% File: timeout.m.
% Author: zs.
%
% This module implements timeouts and cleanup for the deep profiler.
%
% The timeout design we use and its rationale are given in the file DESIGN.
%
% The cleanup system consists of an array of filenames. When the profiler
% creates a temporary file, it adds its name to the array; when it deletes
% the temporary file, it deletes its name from the array. When we get an
% unexpected signal, we clean up by deleting all the temporary files named
% in the array.
%
% We establish the exit action to clean up the files as soon as they are
% created, but we don't want the parent process after the fork to delete them
% while they are still in use by the child process. This is prevented by the
% boolean flag process_is_detached_server.
%
%---------------------------------------------------------------------------%

:- module timeout.
:- interface.

:- import_module bool.
:- import_module io.

%---------------------------------------------------------------------------%

    % Add the given file name to the list of files to be cleaned up.
    %
:- pred register_file_for_cleanup(string::in, io::di, io::uo) is det.

    % Remove the given file name from the list of files to be cleaned up.
    %
:- pred unregister_file_for_cleanup(string::in, io::di, io::uo) is det.

    % Remove all file names from the list of files to be cleaned up.
    %
:- pred unregister_all_files_for_cleanup(io::di, io::uo) is det.

    % Delete all the files on the cleanup list.
    %
:- pred delete_cleanup_files(io::di, io::uo) is det.

    % Set up signal handlers for all the signals we can catch.
    % The three strings specify the name of the mutex file, the name of the
    % directory containing the `want' files, and the prefix of the names of
    % the `want' files.
    %
:- pred setup_signals(string::in, string::in, string::in,
    io::di, io::uo) is det.

    % Set up a timeout for the given number of minutes in the future.
    %
:- pred setup_timeout(int::in, io::di, io::uo) is det.

    % Get the lock on the named mutex file if the bool is `no'.
    % (The mutex file exists iff some process holds the lock.)
    % If the bool is `yes', meaning debugging is enabled, do nothing.
    %
:- pred get_lock(bool::in, string::in, io::di, io::uo) is det.

    % Release the lock on the named mutex file if the bool is `no'.
    % (The mutex file exists iff some process holds the lock.)
    % If the bool is `yes', meaning debugging is enabled, do nothing.
    %
:- pred release_lock(bool::in, string::in,
    io::di, io::uo) is det.

    % Create the `want' file with the given name.
    %
:- pred make_want_file(string::in, io::di, io::uo) is det.

    % Delete the `want' file with the given name.
    %
:- pred remove_want_file(string::in, io::di, io::uo) is det.

%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- implementation.

%---------------------------------------------------------------------------%

:- pragma foreign_decl("C",
"
#ifdef  MR_DEEP_PROFILER_ENABLED

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>  /* for O_CREAT, O_EXCL */
#include <signal.h> /* for signal numbers */
#include <unistd.h> /* for alarm() */
#include <stdio.h>
#include <errno.h>  /* for EEXIST etc */
#include    <dirent.h>
#include ""mercury_signal.h""

#define MP_MAX_CLEANUP_FILES    20  /* this should be plenty */

extern  const char  *MP_cleanup_files[MP_MAX_CLEANUP_FILES];
extern  int     MP_cleanup_file_next;

extern  void        MP_maybe_print_cleanup_files(const char *msg);
extern  void        MP_register_cleanup_file(const char *filename);
extern  void        MP_unregister_cleanup_file(const char *filename);
extern  void        MP_handle_fatal_exception(void *data);
extern  void        MP_delete_cleanup_files(void);
MR_NO_RETURN(extern  void        MP_delete_cleanup_files_and_exit_failure(
                        const char *signal_name));

extern  int         MP_timeout_seconds;
extern  const char  *MP_timeout_mutex_file;
extern  const char  *MP_timeout_want_dir;
extern  const char  *MP_timeout_want_prefix;

typedef struct
{
    int             MP_signum;
    void            (*MP_handler)(void);
} MP_sig_handler;

extern  const MP_sig_handler    MP_signal_structs[];

extern  void    MP_handle_timeout(void);

MR_NO_RETURN(extern  void    MP_handle_sig_term(void));
MR_NO_RETURN(extern  void    MP_handle_sig_hup(void));
MR_NO_RETURN(extern  void    MP_handle_sig_int(void));
MR_NO_RETURN(extern  void    MP_handle_sig_quit(void));
MR_NO_RETURN(extern  void    MP_handle_sig_ill(void));
MR_NO_RETURN(extern  void    MP_handle_sig_abrt(void));
MR_NO_RETURN(extern  void    MP_handle_sig_bus(void));
MR_NO_RETURN(extern  void    MP_handle_sig_fpe(void));
MR_NO_RETURN(extern  void    MP_handle_sig_segv(void));
MR_NO_RETURN(extern  void    MP_handle_sig_pipe(void));

extern  MR_bool MP_do_try_get_lock(const char *mutex_file);
extern  void    MP_do_get_lock(const char *mutex_file);
extern  void    MP_do_release_lock(const char *mutex_file);

#endif
").

:- pragma foreign_code("C",
"
#ifdef  MR_DEEP_PROFILER_ENABLED

#include    <sys/types.h>

const char  *MP_cleanup_files[MP_MAX_CLEANUP_FILES];
int         MP_cleanup_file_next = 0;

int         MP_timeout_seconds = 30 * 60;
const char  *MP_timeout_mutex_file = NULL;
const char  *MP_timeout_want_dir = NULL;
const char  *MP_timeout_want_prefix = NULL;

/* set this variable to MR_TRUE to debug the code cleanup array */
MR_bool     MP_print_cleanup_files = MR_FALSE;

void
MP_maybe_print_cleanup_files(const char *msg)
{
    int i;

    if (MP_print_cleanup_files) {
        fprintf(stderr, ""\\n%s cleanup files:\\n"", msg);
        for (i = 0; i < MP_cleanup_file_next; i++) {
            fprintf(stderr, ""%i %s\\n"", i, MP_cleanup_files[i]);
        }
    }
}

void
MP_register_cleanup_file(const char *filename)
{
    int i;

    if (MP_cleanup_file_next >= MP_MAX_CLEANUP_FILES - 1) {
        MR_fatal_error(""MP_register_cleanup_file: too many entries"");
    }

    for (i = 0; i < MP_cleanup_file_next; i++) {
        if (MR_streq(filename, MP_cleanup_files[i])) {
            MR_fatal_error(""MP_register_cleanup_file: duplicate"");
        }
    }

    MP_cleanup_files[MP_cleanup_file_next] = filename;
    MP_cleanup_file_next++;
    MP_maybe_print_cleanup_files(""register"");
}

void
MP_unregister_cleanup_file(const char *filename)
{
    int i;
    int j;

    for (i = 0; i < MP_cleanup_file_next; i++) {
        if (MR_streq(filename, MP_cleanup_files[i])) {
            /* shift the array entries above index i down one */
            for (j = i + 1; j < MP_cleanup_file_next; j++) {
                MP_cleanup_files[j - 1] = MP_cleanup_files[j];
            }

            MP_cleanup_file_next--;
            MP_maybe_print_cleanup_files(""unregister"");
            return;
        }
    }

    MR_fatal_error(""MP_unregister_cleanup_file: not found"");
}

void
MP_handle_fatal_exception(void *data)
{
    /* we ignore data */
    MP_delete_cleanup_files();
}

void
MP_delete_cleanup_files(void)
{
    int     i;
    MR_bool delayed_mutex_file;

    /*
    ** We want to remove the mutex file only after we have removed the
    ** files manipulated by the critical section it was protecting.
    */

    MP_maybe_print_cleanup_files(""delete"");

    delayed_mutex_file = MR_FALSE;
    for (i = 0; i < MP_cleanup_file_next; i++) {
        if (MR_streq(MP_timeout_mutex_file, MP_cleanup_files[i])) {
            delayed_mutex_file = MR_TRUE;
        } else {
            if (remove(MP_cleanup_files[i]) != 0) {
                perror(MP_cleanup_files[i]);
            }
        }
    }

    if (delayed_mutex_file) {
        if (remove(MP_timeout_mutex_file) != 0) {
            perror(MP_timeout_mutex_file);
        }
    }

    MP_cleanup_file_next = 0;
}

void
MP_delete_cleanup_files_and_exit_failure(const char *signal_name)
{

#ifdef  MP_DEBUG_MDPROF_SIGNAL
    FILE    *fp;
    char    buf[1024];  /* that should be big enough */

    fp = fopen(""/tmp/mdprof_signal"", ""w"");
    if (fp != NULL) {
        fprintf(fp, ""%s\\n"", signal_name);
        (void) fclose(fp);
    }
#endif

    MP_delete_cleanup_files();

#ifdef  MP_DEBUG_MDPROF_SIGNAL
    sprintf(buf, ""Mercury deep profiler: received unexpected signal %s"",
        signal_name);
    MR_fatal_error(buf);
#else
    exit(EXIT_FAILURE);
#endif
}

/*
** SIGALRM alarm signal indicates a timeout. SIGTERM usually indicates the
** machine is being shut down. The others are there to catch forceful shutdowns
** during development, both intentional ones where the programmer sends the
** signal and those caused by bugs in the server code. We would like to include
** all catchable, fatal signals in this list, but that set is somewhat OS
** dependent. The set whose existence we test for here includes all the
** signals that are at all likely to be sent to server process.
**
** We don't test for the existence of SIGALRM, because we want compilation to
** fail if it does not exist. Without alarm signals, server processes will
** never be timed out, and thus constitute a resource leak (mostly of virtual
** memory/swap space).
**
** We could avoid this problem if we had a version of atexit that executed
** its actions even when the program exits after a signal.
*/

const MP_sig_handler MP_signal_structs[] =
{
    { SIGALRM,  MP_handle_timeout },
#ifdef SIGTERM
    { SIGTERM,  MP_handle_sig_term },
#endif
#ifdef SIGHUP
    { SIGHUP,   MP_handle_sig_hup },
#endif
#ifdef SIGINT
    { SIGINT,   MP_handle_sig_int },
#endif
#ifdef SIGQUIT
    { SIGQUIT,  MP_handle_sig_quit },
#endif
#ifdef SIGILL
    { SIGILL,   MP_handle_sig_ill },
#endif
#ifdef SIGABRT
    { SIGABRT,  MP_handle_sig_abrt },
#endif
#ifdef SIGBUS
    { SIGBUS,   MP_handle_sig_bus },
#endif
#ifdef SIGFPE
    { SIGFPE,   MP_handle_sig_fpe },
#endif
#ifdef SIGSEGV
    { SIGSEGV,  MP_handle_sig_segv },
#endif
#ifdef SIGPIPE
    { SIGPIPE,  MP_handle_sig_pipe },
#endif
    { -1,       NULL }
};

void
MP_handle_timeout(void)
{
    DIR             *dir;
    struct  dirent  *dirent;
    size_t          matchlen;
    MR_bool         success;

#ifdef  MP_DEBUG_LOCKS
    {
        FILE    *debug_fp;

        debug_fp = fopen(""/tmp/deep_locks"", ""a"");
        if (debug_fp != NULL) {
            fprintf(debug_fp, ""pid %d MP_handle_timeout at %d\\n"",
                getpid(), (int) time(NULL));
            fclose(debug_fp);
        }
    }
#endif

    if (MP_timeout_want_dir == NULL || MP_timeout_want_prefix == NULL) {
        MR_fatal_error(""MP_handle_timeout: null dir or prefix"");
    }

    matchlen = strlen(MP_timeout_want_prefix);

    success = MP_do_try_get_lock(MP_timeout_mutex_file);
    if (! success) {
        /*
        ** We could not get the lock, so some other process holds it.
        ** We therefore abort the timeout, but schedule the next one.
        */

#ifdef  MP_DEBUG_LOCKS
        {
            FILE    *debug_fp;

            debug_fp = fopen(""/tmp/deep_locks"", ""a"");
            if (debug_fp != NULL) {
                fprintf(debug_fp, ""pid %d aborting timeout: lock\\n"",
                    getpid());
                fclose(debug_fp);
            }
        }
#endif

        (void) alarm(MP_timeout_seconds);
        return;
    }

    dir = opendir(MP_timeout_want_dir);
    if (dir == NULL) {
        char    errbuf[MR_STRERROR_BUF_SIZE];

        MR_fatal_error(""MP_handle_timeout: opendir failed: %s"",
            MR_strerror(errno, errbuf, sizeof(errbuf)));
    }

    while ((dirent = readdir(dir)) != NULL) {
        if (MR_strneq(dirent->d_name, MP_timeout_want_prefix, matchlen)) {

#ifdef  MP_DEBUG_LOCKS
            {
                FILE    *debug_fp;

                debug_fp = fopen(""/tmp/deep_locks"", ""a"");
                if (debug_fp != NULL) {
                    fprintf(debug_fp,
                        ""pid %d aborting timeout: want file\\n"", getpid());
                    fclose(debug_fp);
                }
            }
#endif
            /* abort the timeout */
            (void) closedir(dir);
            (void) alarm(MP_timeout_seconds);
            return;
        }
    }

    (void) closedir(dir);

    /*
    ** This call will delete the mutex file last, releasing the mutex.
    */

    MP_delete_cleanup_files();

#ifdef  MP_DEBUG_LOCKS
    {
        FILE    *debug_fp;

        debug_fp = fopen(""/tmp/deep_locks"", ""a"");
        if (debug_fp != NULL) {
            fprintf(debug_fp, ""pid %d timeout exit\\n"", getpid());
            fclose(debug_fp);
        }
    }
#endif

    exit(EXIT_SUCCESS);
}

void
MP_handle_sig_term(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGTERM"");
}

void
MP_handle_sig_hup(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGHUP"");
}

void
MP_handle_sig_int(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGINT"");
}

void
MP_handle_sig_quit(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGQUIT"");
}

void
MP_handle_sig_ill(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGILL"");
}

void
MP_handle_sig_abrt(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGABRT"");
}

void
MP_handle_sig_bus(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGBUS"");
}

void
MP_handle_sig_fpe(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGFPE"");
}

void
MP_handle_sig_segv(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGSEGV"");
}

void
MP_handle_sig_pipe(void)
{
    MP_delete_cleanup_files_and_exit_failure(""SIGPIPE"");
}

MR_bool
MP_do_try_get_lock(const char *mutex_file)
{
    int     res;
    MR_bool success;

    res = open(mutex_file, O_CREAT | O_EXCL, 0);
    if (res >= 0) {
#ifdef  MP_DEBUG_LOCKS
        FILE    *debug_fp;

        debug_fp = fopen(""/tmp/deep_locks"", ""a"");
        if (debug_fp != NULL) {
            fprintf(debug_fp, ""pid %d try: lock %s\\n"",
                getpid(), mutex_file);
            fclose(debug_fp);
        }
#endif

        (void) close(res);
        MP_register_cleanup_file(mutex_file);
        success = MR_TRUE;
    } else if (res < 0 && errno == EEXIST) {
#ifdef  MP_DEBUG_LOCKS
        FILE    *debug_fp;

        debug_fp = fopen(""/tmp/deep_locks"", ""a"");
        if (debug_fp != NULL) {
            fprintf(debug_fp, ""pid %d try: no lock %s\\n"",
                getpid(), mutex_file);
            fclose(debug_fp);
        }
#endif

        success = MR_FALSE;
    } else {
        char    errbuf[MR_STRERROR_BUF_SIZE];

        MR_fatal_error(""MP_do_try_get_lock failed: %s"",
            MR_strerror(errno, errbuf, sizeof(errbuf)));
    }

    return success;
}

void
MP_do_get_lock(const char *mutex_file)
{
    int res;

    for (;;) {
        res = open(mutex_file, O_CREAT | O_EXCL, 0);
        if (res >= 0) {
#ifdef  MP_DEBUG_LOCKS
            FILE    *debug_fp;

            debug_fp = fopen(""/tmp/deep_locks"", ""a"");
            if (debug_fp != NULL) {
                fprintf(debug_fp, ""pid %d got lock %s\\n"",
                    getpid(), mutex_file);
                fclose(debug_fp);
            }
#endif

            (void) close(res);
            MP_register_cleanup_file(mutex_file);
            return;
        } else if (res < 0 && errno == EEXIST) {
#ifdef  MP_DEBUG_LOCKS
            FILE    *debug_fp;

            debug_fp = fopen(""/tmp/deep_locks"", ""a"");
            if (debug_fp != NULL) {
                fprintf(debug_fp, ""pid %d trying to lock %s ...\\n"",
                    getpid(), mutex_file);
                fclose(debug_fp);
            }
#endif

            sleep(5);
            continue;
        } else {
            char    errbuf[MR_STRERROR_BUF_SIZE];

            MR_fatal_error(""MP_do_get_lock failed: %s"",
                MR_strerror(errno, errbuf, sizeof(errbuf)));
        }
    }
}

void
MP_do_release_lock(const char *mutex_file)
{
#ifdef  MP_DEBUG_LOCKS
    FILE    *debug_fp;

    debug_fp = fopen(""/tmp/deep_locks"", ""a"");
    if (debug_fp != NULL) {
        fprintf(debug_fp, ""pid %d releasing lock %s\\n"",
            getpid(), mutex_file);
        fclose(debug_fp);
    }
#endif

    MP_unregister_cleanup_file(mutex_file);
    (void) unlink(mutex_file);
}

#endif  /* MR_DEEP_PROFILER_ENABLED */
").

:- pragma foreign_proc("C",
    register_file_for_cleanup(File::in, _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_register_cleanup_file(File);
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pragma foreign_proc("C",
    unregister_file_for_cleanup(File::in, _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_unregister_cleanup_file(File);
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pragma foreign_proc("C",
    unregister_all_files_for_cleanup(_IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_cleanup_file_next = 0;
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pragma foreign_proc("C",
    delete_cleanup_files(_IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_delete_cleanup_files();
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pragma foreign_proc("C",
    setup_signals(MutexFile::in, WantDir::in, WantPrefix::in,
        _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    int i;

#ifdef  MP_DEBUG_LOCKS
    {
        FILE    *debug_fp;

        debug_fp = fopen(""/tmp/deep_locks"", ""a"");
        if (debug_fp != NULL) {
            fprintf(debug_fp, ""pid %d setup signals ...\\n"", getpid());
            fprintf(debug_fp, ""mutexfile %s, wantdir %s, wantprefix %s\\n"",
                MutexFile, WantDir, WantPrefix);
            fclose(debug_fp);
        }
    }
#endif

    MP_timeout_mutex_file = MutexFile;
    MP_timeout_want_dir = WantDir;
    MP_timeout_want_prefix = WantPrefix;

    for (i = 0; MP_signal_structs[i].MP_signum >= 0; i++) {
        MR_setup_signal(MP_signal_structs[i].MP_signum,
            MP_signal_structs[i].MP_handler, MR_FALSE,
            ""Mercury deep profiler: cannot setup signal exit"");
    }

    /*
    ** Mercury exceptions do not cause signals. The default exception
    ** handler prints and error message and exits. To ensure that
    ** we delete the files we need to clean up, we get the exit
    ** library function to invoke MP_delete_cleanup_files through
    ** MP_handle_fatal_exception.
    */

    MR_register_exception_cleanup(MP_handle_fatal_exception, NULL);

#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pragma foreign_proc("C",
    setup_timeout(Minutes::in, _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_timeout_seconds = (int) Minutes * 60;
    (void) alarm(MP_timeout_seconds);

#ifdef  MP_DEBUG_LOCKS
    {
        FILE    *debug_fp;

        debug_fp = fopen(""/tmp/deep_locks"", ""a"");
        if (debug_fp != NULL) {
            fprintf(debug_fp, ""pid %d setup timeout at %d for %d\\n"",
                getpid(), (int) time(NULL),
                (int) time(NULL) + MP_timeout_seconds);
            fclose(debug_fp);
        }
    }
#endif
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

%---------------------------------------------------------------------------%

get_lock(Debug, MutexFile, !IO) :-
    (
        Debug = yes
    ;
        Debug = no,
        do_get_lock(MutexFile, !IO)
    ).

release_lock(Debug, MutexFile, !IO) :-
    (
        Debug = yes
    ;
        Debug = no,
        do_release_lock(MutexFile, !IO)
    ).

:- pred do_get_lock(string::in, io::di, io::uo) is det.

:- pragma foreign_proc("C",
    do_get_lock(MutexFile::in, _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure, tabled_for_io],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_do_get_lock(MutexFile);
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pred do_release_lock(string::in, io::di, io::uo) is det.

:- pragma foreign_proc("C",
    do_release_lock(MutexFile::in, _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure, tabled_for_io],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_do_release_lock(MutexFile);
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pragma foreign_proc("C",
    make_want_file(WantFileName::in, _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    int     fd;
    char    errbuf[MR_STRERROR_BUF_SIZE];

    fd = open(WantFileName, O_CREAT, 0);
    if (fd < 0) {
        MR_fatal_error(""make_want_file: open failed: %s"",
            MR_strerror(errno, errbuf, sizeof(errbuf)));
    }
    (void) close(fd);
    MP_register_cleanup_file(WantFileName);
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

:- pragma foreign_proc("C",
    remove_want_file(WantFileName::in, _IO0::di, _IO::uo),
    [will_not_call_mercury, promise_pure],
"
#ifdef  MR_DEEP_PROFILER_ENABLED
    MP_unregister_cleanup_file(WantFileName);
    (void) unlink(WantFileName);
#else
    MR_fatal_error(""deep profiler not enabled"");
#endif
").

%---------------------------------------------------------------------------%
:- end_module timeout.
%---------------------------------------------------------------------------%
