/*
 * Copyright 2019 Bloomberg Finance LP
 *
 * 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.
 */

#include <buildboxrun_chrootmanager.h>
#include <buildboxrun_commandfilemanager.h>

#include <buildboxcommon_exception.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_systemutils.h>

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>

namespace buildboxcommon {
namespace buildboxrun {
namespace userchroot {

ChrootManager::ChrootManager(const std::string &userchroot_bin,
                             const std::string &root_path,
                             const std::string &sudoBin,
                             const std::string &user)
    : d_userchroot_bin(SystemUtils::getPathToCommand(userchroot_bin)),
      d_root_path(root_path),
      d_sudoBin(SystemUtils::getPathToCommand(sudoBin)), d_user(user)
{
    BUILDBOX_LOG_INFO("Resolved userchroot path: " << d_userchroot_bin);

    // We need the full path to `userchroot` in order to launch it. If we can't
    // find it now, fail early.
    if (d_userchroot_bin.empty()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Could not find userchroot command \"" +
                                           userchroot_bin + "\"");
    }

    // If `user` or `group` specified, We need the path to `sudo`
    if (!d_user.empty() && d_sudoBin.empty()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Could not find sudo command \"" +
                                           sudoBin + "\"");
    }

    if (!chrootDeviceCommand(Device::INSTALL)) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Failed to install devices");
    }
}

ChrootManager::~ChrootManager()
{
    try {
        chrootDeviceCommand(Device::UNINSTALL);
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_ERROR("Uninstall devices threw: " << e.what());
    }
}
const std::vector<std::string>
ChrootManager::generateCommandLine(const Command &command)
{
    d_command_file = std::make_unique<CommandFileManager>(
        command, d_root_path, s_command_file_location, s_command_file_prefix);
    std::vector<std::string> commandLine;

    // sudo if necessary
    if (!d_user.empty()) {
        commandLine.emplace_back(d_sudoBin);
        commandLine.emplace_back("-u");
        commandLine.emplace_back(d_user);
    }

    commandLine.emplace_back(d_userchroot_bin);
    commandLine.emplace_back(d_root_path);
    commandLine.emplace_back("/bin/sh");
    commandLine.emplace_back("-c");
    commandLine.emplace_back(d_command_file->getFilePathInChroot());

    return commandLine;
}

bool ChrootManager::chrootDeviceCommand(Device device_command)
{
    std::vector<std::string> command{d_userchroot_bin, d_root_path};
    if (device_command == Device::INSTALL) {
        command.emplace_back("--install-devices");
    }
    else {
        command.emplace_back("--uninstall-devices");
    }
    std::string commandString = logging::printableCommandLine(command);

    pid_t pid = fork();
    if (pid == -1) {
        BUILDBOX_LOG_ERROR("Forking child process failed");
        return false;
    }
    else if (pid == 0) {
        BUILDBOX_LOG_DEBUG("Executing command: " << commandString);
        SystemUtils::executeCommand(command, true);
        exit(EXIT_FAILURE);
    }
    else {
        try {
            const int exit_code = SystemUtils::waitPid(pid);
            if (exit_code) {
                BUILDBOX_LOG_ERROR("Command failed with exit code "
                                   << exit_code << " : " << commandString);
                return false;
            }
        }
        catch (const std::runtime_error &e) {
            BUILDBOX_LOG_ERROR(e.what());
            return false;
        }
    }
    return true;
}

const std::string ChrootManager::s_command_file_location = "/tmp";

const std::string ChrootManager::s_command_file_prefix = "command_file_";

} // namespace userchroot
} // namespace buildboxrun
} // namespace buildboxcommon
