/** @file
 * @brief BackendManager subclass for remotetcp databases.
 */
/* Copyright (C) 2006,2007,2008,2009,2013,2015,2023 Olly Betts
 * Copyright (C) 2008 Lemur Consulting Ltd
 *
 * 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 "backendmanager_remotetcp.h"

#include <xapian.h>

#include <stdio.h> // For fdopen().
#include <cerrno>
#include <cstring>

#include "safesysexits.h"

#ifdef HAVE_FORK
# include <signal.h>
# include <sys/types.h>
# include "safesyssocket.h"
# include <sys/wait.h>
# include <unistd.h>
#endif

#ifdef __WIN32__
# include <io.h> // For _open_osfhandle().
# include "safefcntl.h"
# include "safewindows.h"
# include <cstdlib> // For free().
#endif

#include "errno_to_string.h"
#include "str.h"

#include <string>
#include <vector>

#ifdef HAVE_VALGRIND
# include <valgrind/memcheck.h>
#endif

using namespace std;

// We've had problems on some hosts which run tinderbox tests with "localhost"
// not being set in /etc/hosts - using the IP address equivalent seems more
// reliable.
#define LOCALHOST "127.0.0.1"

// Start at port DEFAULT_PORT and increment until one isn't already in use.
#define DEFAULT_PORT 1239

class ServerData {
#ifdef HAVE_FORK
    typedef pid_t pid_type;
#elif defined __WIN32__
    typedef DWORD pid_type;
#else
# error Neither HAVE_FORK nor __WIN32__ is defined
#endif

    /// Value of pid which indicates an entry kill_remote() was called on.
    static constexpr pid_type DEAD_PID = 0;

    /** The remote server process ID.
     *
     *  Under Unix, this will actually be the /bin/sh process.
     */
    pid_type pid;

#ifdef __WIN32__
    HANDLE handle;
#endif

    /** The internal pointer of the Database object.
     *
     *  We use this to find the entry for a given Xapian::Database object (in
     *  kill_remote()).
     */
    const void* db_internal;

  public:
#ifndef __WIN32__
    void init(pid_type pid_) {
	pid = pid_;
	db_internal = nullptr;
    }
#else
    void init(pid_type pid_, HANDLE handle_) {
	pid = pid_;
	handle = handle_;
	db_internal = nullptr;
    }
#endif

    void set_db_internal(const void* dbi) { db_internal = dbi; }

    void clean_up() {
	if (pid == DEAD_PID) return;
#ifdef HAVE_FORK
	int status;
	while (waitpid(pid, &status, 0) == -1 && errno == EINTR) { }
	// Other possible error from waitpid is ECHILD, which it seems can
	// only mean that the child has already exited and SIGCHLD was set
	// to SIG_IGN.  If we did somehow see that, it seems reasonable to
	// treat the child as successfully cleaned up.
#elif defined __WIN32__
	WaitForSingleObject(handle, INFINITE);
	CloseHandle(handle);
#endif
    }

    bool kill_remote(const void* dbi) {
	if (pid == DEAD_PID || dbi != db_internal) return false;
#ifdef HAVE_FORK
	// Kill the process group that we put the server in so that we kill
	// the server itself and not just the /bin/sh that launched it.
	if (kill(-pid, SIGKILL) < 0) {
	    throw Xapian::DatabaseError("Couldn't kill remote server",
					errno);
	}
#elif defined __WIN32__
	// We want to kill the whole process group so we need to use
	// GenerateConsoleCtrlEvent() - TerminateProcess() can only
	// terminate one process given its handle.
	if (!GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)) {
	    throw Xapian::DatabaseError("Couldn't kill remote server",
					-int(GetLastError()));
	}
#endif
	clean_up();
	pid = DEAD_PID;
	return true;
    }
};

// We can't dynamically resize this on demand (e.g. by using a std::vector)
// because it would confuse the leak detector.  We clean up after each testcase
// so this only needs to store the children launched by a single testcase,
// which is at most 6 currently, but the entries are tiny so allocate enough
// that future testcases shouldn't hit the limit either.
//
// We need to linear scan up to first_unused_server_data entries to implement
// BackendManagerRemoteTcp::kill_remote(), but with a small fixed bound on the
// number of entries that's effectively O(1); also very few testcases use this
// feature anyway.
static ServerData server_data[16];

static unsigned first_unused_server_data = 0;

#ifdef HAVE_FORK

static std::pair<int, ServerData&>
launch_xapian_tcpsrv(const string & args)
{
    int port = DEFAULT_PORT;

try_next_port:
    string cmd = XAPIAN_TCPSRV " --one-shot --interface " LOCALHOST " --port ";
    cmd += str(port);
    cmd += " ";
    cmd += args;
#ifdef HAVE_VALGRIND
    if (RUNNING_ON_VALGRIND) cmd = "./runsrv " + cmd;
#endif
    int fds[2];
    if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, PF_UNSPEC, fds) < 0) {
	string msg("Couldn't create socketpair: ");
	errno_to_string(errno, msg);
	throw msg;
    }

    pid_t child = fork();
    if (child == 0) {
	// Put this process into its own process group so that we can kill the
	// server itself easily by killing the process group.  Just killing
	// `child` only kills the /bin/sh and leaves the server running.
	setpgid(0, 0);
	// Child process.
	close(fds[0]);
	// Connect stdout and stderr to the socket.
	//
	// Make sure the socket isn't fd 1 or 2.  We need to ensure that
	// FD_CLOEXEC isn't set for stdout or stderr (which creating them with
	// dup2() achieves), and that we close fds[1].  The cleanest way to
	// address this seems to be to turn the unusual situation into the
	// usual one.
	if (fds[1] == 1 || fds[1] == 2) {
	    dup2(fds[1], 3);
	    fds[1] = 3;
	}
	dup2(fds[1], 1);
	dup2(fds[1], 2);
	close(fds[1]);
	execl("/bin/sh", "/bin/sh", "-c", cmd.c_str(), static_cast<void*>(0));
	_exit(-1);
    }

    close(fds[1]);
    if (child == -1) {
	// Couldn't fork.
	int fork_errno = errno;
	close(fds[0]);
	string msg("Couldn't fork: ");
	errno_to_string(fork_errno, msg);
	throw msg;
    }

    // Parent process.

    // Wrap the file descriptor in a FILE * so we can read lines using fgets().
    FILE * fh = fdopen(fds[0], "r");
    if (fh == NULL) {
	string msg("Failed to run command '");
	msg += cmd;
	msg += "': ";
	errno_to_string(errno, msg);
	throw msg;
    }

    string output;
    while (true) {
	char buf[256];
	if (fgets(buf, sizeof(buf), fh) == NULL) {
	    fclose(fh);
	    // Wait for the child to exit.
	    int status;
	    if (waitpid(child, &status, 0) == -1) {
		string msg("waitpid failed: ");
		errno_to_string(errno, msg);
		throw msg;
	    }
	    if (++port < 65536 && status != 0) {
		if (WIFEXITED(status) &&
		    WEXITSTATUS(status) == EX_UNAVAILABLE) {
		    // Exit code EX_UNAVAILABLE from xapian-tcpsrv means the
		    // specified port was in use.
		    goto try_next_port;
		}
	    }
	    string msg("Failed to get 'Listening...' from command '");
	    msg += cmd;
	    msg += "' (output: ";
	    msg += output;
	    msg += ")";
	    throw msg;
	}
	if (strcmp(buf, "Listening...\n") == 0) break;
	output += buf;
    }
    fclose(fh);

    if (first_unused_server_data >= std::size(server_data)) {
	// We used to quietly ignore not finding a slot, but it's helpful to
	// know if we haven't allocated enough.
	throw Xapian::DatabaseError("Not enough ServerData slots");
    }

    auto& data = server_data[first_unused_server_data++];
    data.init(child);
    return {port, data};
}

#elif defined __WIN32__

[[noreturn]]
static void win32_throw_error_string(const char * str)
{
    string msg(str);
    char * error = 0;
    DWORD len;
    len = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_ALLOCATE_BUFFER,
			0, GetLastError(), 0, (CHAR*)&error, 0, 0);
    if (error) {
	// Remove any trailing \r\n from output of FormatMessage.
	if (len >= 2 && error[len - 2] == '\r' && error[len - 1] == '\n')
	    len -= 2;
	if (len) {
	    msg += ": ";
	    msg.append(error, len);
	}
	LocalFree(error);
    }
    throw msg;
}

// This implementation uses the WIN32 API to start xapian-tcpsrv as a child
// process and read its output using a pipe.
static std::pair<int, ServerData&>
launch_xapian_tcpsrv(const string & args)
{
    int port = DEFAULT_PORT;

try_next_port:
    string cmd = XAPIAN_TCPSRV " --one-shot --interface " LOCALHOST " --port ";
    cmd += str(port);
    cmd += " ";
    cmd += args;

    // Create a pipe so we can read stdout/stderr from the child process.
    HANDLE hRead, hWrite;
    if (!CreatePipe(&hRead, &hWrite, 0, 0))
	win32_throw_error_string("Couldn't create pipe");

    // Set the write handle to be inherited by the child process.
    SetHandleInformation(hWrite, 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);
    startupinfo.hStdError = hWrite;
    startupinfo.hStdOutput = hWrite;
    startupinfo.hStdInput = INVALID_HANDLE_VALUE;
    startupinfo.dwFlags |= STARTF_USESTDHANDLES;

    // For some reason Windows wants a modifiable command line string
    // so pass a pointer to the first character rather than using c_str().
    if (!CreateProcess(XAPIAN_TCPSRV, &cmd[0], 0, 0, TRUE,
		       CREATE_NEW_PROCESS_GROUP, 0, 0,
		       &startupinfo, &procinfo)) {
	win32_throw_error_string("Couldn't create child process");
    }

    CloseHandle(hWrite);
    CloseHandle(procinfo.hThread);

    string output;
    FILE *fh = fdopen(_open_osfhandle(intptr_t(hRead), O_RDONLY), "r");
    while (true) {
	char buf[256];
	if (fgets(buf, sizeof(buf), fh) == NULL) {
	    fclose(fh);
	    DWORD rc;
	    // This doesn't seem to be necessary on the machine I tested on,
	    // but I guess it could be on a slow machine...
	    while (GetExitCodeProcess(procinfo.hProcess, &rc) && rc == STILL_ACTIVE) {
		Sleep(100);
	    }
	    CloseHandle(procinfo.hProcess);
	    if (++port < 65536 && rc == EX_UNAVAILABLE) {
		// Exit code EX_UNAVAILABLE from xapian-tcpsrv means the
		// specified port was in use.
		goto try_next_port;
	    }
	    string msg("Failed to get 'Listening...' from command '");
	    msg += cmd;
	    msg += "' (output: ";
	    msg += output;
	    msg += ")";
	    throw msg;
	}
	if (strcmp(buf, "Listening...\r\n") == 0) break;
	output += buf;
    }
    fclose(fh);

    if (first_unused_server_data >= std::size(server_data)) {
	// We used to quietly ignore not finding a slot, but it's helpful to
	// know if we haven't allocated enough.
	throw Xapian::DatabaseError("Not enough ServerData slots");
    }

    auto& data = server_data[first_unused_server_data++];
    data.init(procinfo.dwProcessId, procinfo.hProcess);
    return {port, data};
}

#else
# error Neither HAVE_FORK nor __WIN32__ is defined
#endif

static Xapian::Database
get_remotetcp_db(const string& args, int* port_ptr = nullptr)
{
    auto [port, server] = launch_xapian_tcpsrv(args);
    if (port_ptr) *port_ptr = port;
    auto db = Xapian::Remote::open(LOCALHOST, port);
    server.set_db_internal(db.internal.get());
    return db;
}

static Xapian::WritableDatabase
get_remotetcp_writable_db(const string& args)
{
    auto [port, server] = launch_xapian_tcpsrv(args);
    auto db = Xapian::Remote::open_writable(LOCALHOST, port);
    server.set_db_internal(db.internal.get());
    return db;
}

BackendManagerRemoteTcp::~BackendManagerRemoteTcp() {
    BackendManagerRemoteTcp::clean_up();
}

Xapian::Database
BackendManagerRemoteTcp::do_get_database(const vector<string> & files)
{
    // Default to a long (5 minute) timeout so that tests won't fail just
    // because the host is slow or busy.
    return BackendManagerRemoteTcp::get_remote_database(files, 300000,
							nullptr);
}

Xapian::WritableDatabase
BackendManagerRemoteTcp::get_writable_database(const string & name,
					       const string & file)
{
    return get_remotetcp_writable_db(get_writable_database_args(name, file));
}

Xapian::Database
BackendManagerRemoteTcp::get_remote_database(const vector<string> & files,
					     unsigned int timeout,
					     int* port_ptr)
{
    return get_remotetcp_db(get_remote_database_args(files, timeout), port_ptr);
}

Xapian::Database
BackendManagerRemoteTcp::get_database_by_path(const string& path)
{
    return get_remotetcp_db(get_remote_database_args(path, 300000));
}

Xapian::Database
BackendManagerRemoteTcp::get_writable_database_as_database()
{
    return get_remotetcp_db(get_writable_database_as_database_args());
}

Xapian::WritableDatabase
BackendManagerRemoteTcp::get_writable_database_again()
{
    return get_remotetcp_writable_db(get_writable_database_again_args());
}

void
BackendManagerRemoteTcp::kill_remote(const Xapian::Database& db)
{
    const void* db_internal = db.internal.get();
    for (unsigned i = 0; i != first_unused_server_data; ++i) {
	if (server_data[i].kill_remote(db_internal))
	    return;
    }
    throw Xapian::DatabaseError("No known server for remote DB");
}

void
BackendManagerRemoteTcp::clean_up()
{
    while (first_unused_server_data) {
	server_data[--first_unused_server_data].clean_up();
    }
}
