/** @file
 * @brief Class representing worker process.
 */
/* Copyright (C) 2005-2026 Olly Betts
 * Copyright (C) 2019 Bruno Baruffaldi
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "worker.h"
#include "worker_comms.h"

#include <cinttypes>
#include <csignal>
#include <cstring>
#include <cerrno>
#include "safefcntl.h"
#include "safeunistd.h"
#include "safesyssocket.h"
#include "safesyswait.h"
#include "safesysexits.h"
#include <utility>

#include "closefrom.h"
#include "freemem.h"
#include "handler.h"
#include "parseint.h"
#include "pkglibbindir.h"
#include "str.h"

#ifdef __WIN32__
# include "safewindows.h"
#endif

using namespace std;

bool Worker::ignoring_sigpipe = false;

int
Worker::start_worker_subprocess()
{
    static bool keep_stderr = (getenv("XAPIAN_OMEGA_DEBUG_WORKERS") != nullptr);

    if (error_prefix.empty()) {
        // The first time we're called, set error_prefix to the leafname of
        // filter_module followed by ": ", and qualify filter_module if it's
        // just a leafname.
        auto slash = filter_module.rfind('/');
        if (slash == string::npos) {
            error_prefix = std::move(filter_module);
            // Look for unqualified filters in pkglibbindir.
            filter_module = get_pkglibbindir();
            filter_module += '/';
            filter_module += error_prefix;
        } else {
            error_prefix.assign(filter_module, slash + 1);
        }
        error_prefix += ": ";
    }

#if defined HAVE_SOCKETPAIR && defined HAVE_FORK
    int fds[2];
    if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, fds) < 0) {
        error = string("socketpair failed: ") + strerror(errno);
        return 1;
    }

    child = fork();
    if (child == 0) {
        // Child process.
        close(fds[0]);

        // Connect pipe to fd 3.  Don't use stdin and stdout in case the
        // filter library tries to read from stdin or write debug or progress
        // info to stdout.
        dup2(fds[1], 3);

        // Make sure we don't hang on to open files which may get deleted but
        // not have their disk space released until we exit.
        closefrom(4);

        // Connect stdin, stdout and (conditionally) stderr to /dev/null.
        int devnull = open("/dev/null", O_RDWR);
        dup2(devnull, 0);
        dup2(devnull, 1);
        if (!keep_stderr)
            dup2(devnull, 2);
        if (devnull > 3) close(devnull);

        // FIXME: For filters which support a file descriptor as input, we
        // could open the file here, and pass the file descriptor across the
        // socket to the worker process using sendmsg().  Then the worker
        // process could be chroot()-ed to a sandbox directory, which means
        // we're reasonably protected from security bugs in the filter.

#ifdef HAVE_SETRLIMIT
#if defined RLIMIT_AS || defined RLIMIT_VMEM || defined RLIMIT_DATA
        // Set a memory limit if it is possible
        long mem = get_free_physical_memory();
        if (mem > 0) {
            struct rlimit ram_limit = {
                static_cast<rlim_t>(mem),
                RLIM_INFINITY
            };
#ifdef RLIMIT_AS
            // Limit size of the process's virtual memory
            // (address space) in bytes.
            setrlimit(RLIMIT_AS, &ram_limit);
#elif defined RLIMIT_VMEM
            // Limit size of a process's mapped address space in bytes.
            setrlimit(RLIMIT_VMEM, &ram_limit);
#else
            // Limit size of the process's data segment.
            setrlimit(RLIMIT_DATA, &ram_limit);
#endif
        }
#endif
#endif
        // Replacing the current process image with a new process image
        const char* mod = filter_module.c_str();
        execl(mod, mod, static_cast<void*>(NULL));
        _exit(EX_OSERR);
    }

    // Main process
    int fork_errno = errno;
    close(fds[1]);
    if (child == -1) {
        close(fds[0]);
        error = string("fork failed: ") + strerror(fork_errno);
        return 1;
    }

    sockt = fdopen(fds[0], "r+");

    if (!ignoring_sigpipe) {
        ignoring_sigpipe = true;
        signal(SIGPIPE, SIG_IGN);
    }
#elif defined __WIN32__
    LARGE_INTEGER counter;
    // QueryPerformanceCounter() will always succeed on XP and later
    // and gives us a counter which increments each CPU clock cycle
    // on modern hardware (Pentium or newer).
    QueryPerformanceCounter(&counter);
    char pipename[256];
    snprintf(pipename, sizeof(pipename),
             "\\\\.\\pipe\\xapian-omega-worker-%lx-%lx_%" PRIx64,
             static_cast<unsigned long>(GetCurrentProcessId()),
             static_cast<unsigned long>(GetCurrentThreadId()),
             static_cast<unsigned long long>(counter.QuadPart));
    pipename[sizeof(pipename) - 1] = '\0';
    // Create a pipe so we can read stdout from the child process.
    HANDLE hPipe = CreateNamedPipe(pipename,
                                   PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,
                                   0,
                                   1, 4096, 4096, NMPWAIT_USE_DEFAULT_WAIT,
                                   NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        error = "CreateNamedPipe failed: " + str(GetLastError());
        return 1;
    }

    HANDLE hClient = CreateFile(pipename,
                                GENERIC_READ|GENERIC_WRITE, 0, NULL,
                                OPEN_EXISTING,
                                FILE_FLAG_OVERLAPPED, NULL);

    if (hClient == INVALID_HANDLE_VALUE) {
        error = "CreateFile failed: " + str(GetLastError());
        return 1;
    }

    if (!ConnectNamedPipe(hPipe, NULL) &&
        GetLastError() != ERROR_PIPE_CONNECTED) {
        error = "ConnectNamedPipe failed: " + str(GetLastError());
        return 1;
    }

    // Set the appropriate handles to be inherited by the child process.
    SetHandleInformation(hClient, HANDLE_FLAG_INHERIT, 1);

    // Create the child process.
    PROCESS_INFORMATION procinfo;
    memset(&procinfo, 0, sizeof(PROCESS_INFORMATION));

    STARTUPINFO startupinfo;
    memset(&startupinfo, 0, sizeof(STARTUPINFO));
    startupinfo.cb = sizeof(STARTUPINFO);
    // FIXME: Is NULL the way to say "/dev/null"?
    // It's what GetStdHandle() is documented to return if "an application does
    // not have associated standard handles"...
    startupinfo.hStdError = keep_stderr ? GetStdHandle(STD_ERROR_HANDLE) : NULL;
    startupinfo.hStdOutput = hClient;
    startupinfo.hStdInput = hClient;
    startupinfo.dwFlags |= STARTF_USESTDHANDLES;

    string cmdline{filter_module};
    // For some reason Windows wants a modifiable command line so we
    // pass `&cmdline[0]` rather than `cmdline.c_str()`.
    BOOL ok = CreateProcess(filter_module.c_str(), &cmdline[0],
                            0, 0, TRUE, 0, 0, 0,
                            &startupinfo, &procinfo);
    if (!ok) {
        if (GetLastError() == ERROR_FILE_NOT_FOUND) {
            error = error_prefix + "failed to run helper";
            filter_module = string();
            return -1;
        }
        error = "CreateProcess failed: " + str(GetLastError());
        return 1;
    }

    CloseHandle(hClient);
    CloseHandle(procinfo.hThread);
    child = procinfo.hProcess;

    sockt = _fdopen(_open_osfhandle(intptr_t(hPipe), O_RDWR|O_BINARY), "r+");
#else
# error Omega needs porting to this platform
#endif

    int field_code = GETC(sockt);
    if (field_code != FIELD_END) {
        error = error_prefix;
        if (field_code != FIELD_ERROR) {
            error += "Unexpected first response from worker module";
            return -1;
        }

        if (!read_string(sockt, error)) goto comms_error;
        if (error.empty())
            error += "Initialisation failed";
        return -1;
    }
    return 0;

comms_error:
    error = error_prefix;
    error += "(init) ";
    return handle_comms_error();
}

int
Worker::extract(const std::string& filename,
                const std::string& mimetype,
                std::string& dump,
                std::string& title,
                std::string& keywords,
                std::string& author,
                std::string& to,
                std::string& cc,
                std::string& bcc,
                std::string& message_id,
                int& pages,
                time_t& created)
{
    if (filter_module.empty()) {
        error = error_prefix + "hard failed earlier in the current run";
        return -1;
    }

    if (sockt) {
#ifdef HAVE_WAITPID
        // Check if the worker process is still alive - if it is, waitpid()
        // with WNOHANG returns 0.
        int status;
        if (waitpid(child, &status, WNOHANG) != 0) {
            fclose(sockt);
            sockt = NULL;
        }
#elif defined __WIN32__
        // Check if the worker process is still alive by trying to wait for it
        // with a timeout of 0ms.
        if (WaitForSingleObject(child, 0) != WAIT_TIMEOUT) {
            fclose(sockt);
            sockt = NULL;
        }
#else
# error Omega needs porting to this platform
#endif
    }
    if (!sockt) {
        int r = start_worker_subprocess();
        if (r != 0) return r;
    }

    error = error_prefix;
    string attachment_filename;
    // Send a filename and wait for the reply.
    if (write_string(sockt, filename) && write_string(sockt, mimetype)) {
        while (true) {
            int field_code = GETC(sockt);
            string* value;
            switch (field_code) {
              case FIELD_PAGE_COUNT: {
                unsigned u_pages;
                if (!read_unsigned(sockt, u_pages)) {
                    error += " read_unsigned after FIELD_PAGE_COUNT";
                    goto comms_error;
                }
                pages = int(u_pages);
                continue;
              }
              case FIELD_CREATED_DATE: {
                unsigned u_created;
                if (!read_unsigned(sockt, u_created)) {
                    error += " read_unsigned after FIELD_CREATED_DATE";
                    goto comms_error;
                }
                created = time_t(long(u_created));
                continue;
              }
              case FIELD_BODY:
                value = &dump;
                break;
              case FIELD_TITLE:
                value = &title;
                break;
              case FIELD_KEYWORDS:
                value = &keywords;
                break;
              case FIELD_AUTHOR:
                value = &author;
                break;
              case FIELD_TO:
                value = &to;
                break;
              case FIELD_CC:
                value = &cc;
                break;
              case FIELD_BCC:
                value = &bcc;
                break;
              case FIELD_MESSAGE_ID:
                value = &message_id;
                break;
              case FIELD_ERROR:
                if (!read_string(sockt, error)) {
                    error += " read_unsigned after FIELD_ERROR";
                    goto comms_error;
                }
                // Fields shouldn't be empty but the protocol allows them to be.
                if (error.empty())
                    error = error_prefix + "Couldn't extract text";
                else
                    error.insert(0, error_prefix);
                return 1;
              case FIELD_END:
                return 0;
              case EOF:
                error += " read_unsigned after EOF";
                goto comms_error;
              case FIELD_ATTACHMENT:
                value = &attachment_filename;
                break;
              default:
                error = error_prefix + "Unknown field code ";
                error += str(field_code);
                return 1;
            }
            if (value->empty()) {
                // First instance of this field for this document.
                if (!read_string(sockt, *value)) break;
            } else {
                // Repeat instance of this field.
                string s;
                if (!read_string(sockt, s)) break;
                *value += ' ';
                *value += s;
            }
            if (field_code == FIELD_ATTACHMENT) {
                // FIXME: Do something more useful with attachment_filename!
                attachment_filename.clear();
            }
        }
    }

comms_error:
    error = error_prefix;
    return handle_comms_error();
}

int
Worker::handle_comms_error()
{
    // Check if the worker process already died, so we can report if it
    // was killed by a segmentation fault, etc.
#ifdef HAVE_WAITPID
    int status;
    int result = waitpid(child, &status, WNOHANG);
    int waitpid_errno = errno;

    fclose(sockt);
    sockt = NULL;

    if (result == 0) {
        // The worker is still alive, so terminate it.
        kill(child, SIGTERM);
        waitpid(child, &status, 0);
        error += "failed";
    } else if (result < 0) {
        // waitpid() failed with an error.
        error += "failed: ";
        error += strerror(waitpid_errno);
    } else if (WIFEXITED(status)) {
        int rc = WEXITSTATUS(status);
        switch (rc) {
          case OMEGA_EX_SOCKET_READ_ERROR:
            error += "subprocess failed to read data from us";
            break;
          case OMEGA_EX_SOCKET_WRITE_ERROR:
            error += "subprocess failed to write data to us";
            break;
          case EX_TEMPFAIL:
            error += "timed out";
            break;
          case EX_UNAVAILABLE:
            error += "failed to initialise";
            filter_module = string();
            return -1;
          case EX_OSERR:
            error += "failed to run helper";
            filter_module = string();
            return -1;
          default:
            error += "exited with status ";
            error += str(rc);
            break;
        }
    } else {
        error += "killed by signal ";
        error += str(WTERMSIG(status));
    }
#elif defined __WIN32__
    DWORD result = WaitForSingleObject(child, 0);

    fclose(sockt);
    sockt = NULL;

    if (result == WAIT_TIMEOUT) {
        // The worker is still alive, so terminate it.  We need to specify an
        // exit code here, 255 is an arbitrary choice.
        TerminateProcess(child, 255);
        WaitForSingleObject(child, INFINITE);
        error += "failed";
        CloseHandle(child);
    } else if (result == WAIT_FAILED) {
        // WaitForSingleObject() failed in an unexpected way.
        error += "failed: ";
        error += str(GetLastError());
        CloseHandle(child);
    } else {
        DWORD rc;
        while (GetExitCodeProcess(child, &rc) && rc == STILL_ACTIVE) {
            Sleep(100);
        }
        switch (rc) {
          case OMEGA_EX_SOCKET_READ_ERROR:
            error += "subprocess failed to read data from us";
            break;
          case OMEGA_EX_SOCKET_WRITE_ERROR:
            error += "subprocess failed to write data to us";
            break;
          case EX_TEMPFAIL:
            error += "timed out";
            break;
          case EX_UNAVAILABLE:
            error += "failed to initialise";
            filter_module = string();
            return -1;
          case EX_OSERR:
            error += "failed to run helper";
            filter_module = string();
            return -1;
          default:
            error += "exited with status ";
            error += str(rc);
            break;
        }
    }
#else
# error Omega needs porting to this platform
#endif
    return 1;
}
