/*
 * Copyright (C) 2019 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.
 */

#define LOG_TAG "car-bugreportd"

#include <android-base/errors.h>
#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/macros.h>
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
#include <cutils/sockets.h>
#include <errno.h>
#include <fcntl.h>
#include <ftw.h>
#include <gui/SurfaceComposerClient.h>
#include <log/log_main.h>
#include <private/android_filesystem_config.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <ziparchive/zip_writer.h>

#include <chrono>
#include <string>
#include <vector>

namespace {
// Directory used for keeping temporary files
constexpr const char* kTempDirectory = "/data/user_de/0/com.android.shell/temp_bugreport_files";
// Socket to write the progress information.
constexpr const char* kCarBrProgressSocket = "car_br_progress_socket";
// Socket to write the zipped bugreport file.
constexpr const char* kCarBrOutputSocket = "car_br_output_socket";
// Socket to write the extra bugreport zip file. This zip file contains data that does not exist
// in bugreport file generated by dumpstate.
constexpr const char* kCarBrExtraOutputSocket = "car_br_extra_output_socket";
// The prefix used by bugreportz protocol to indicate bugreport finished successfully.
constexpr const char* kOkPrefix = "OK:";
// Number of connect attempts to dumpstate socket
constexpr const int kMaxDumpstateConnectAttempts = 20;
// Wait time between connect attempts
constexpr const int kWaitTimeBetweenConnectAttemptsInSec = 1;
// Wait time for dumpstate. No timeout in dumpstate is longer than 60 seconds. Choose
// a value that is twice longer.
constexpr const int kDumpstateTimeoutInSec = 120;
// The prefix for screenshot filename in the generated zip file.
constexpr const char* kScreenshotPrefix = "/screenshot";

using android::OK;
using android::PhysicalDisplayId;
using android::status_t;
using android::SurfaceComposerClient;

// Returns a valid socket descriptor or -1 on failure.
int openSocket(const char* service) {
    int s = android_get_control_socket(service);
    if (s < 0) {
        ALOGE("android_get_control_socket(%s): %s", service, strerror(errno));
        return -1;
    }
    fcntl(s, F_SETFD, FD_CLOEXEC);
    if (listen(s, 4) < 0) {
        ALOGE("listen(control socket): %s", strerror(errno));
        return -1;
    }

    struct sockaddr addr;
    socklen_t alen = sizeof(addr);
    int fd = accept(s, &addr, &alen);
    if (fd < 0) {
        ALOGE("accept(control socket): %s", strerror(errno));
        return -1;
    }
    return fd;
}

// Processes the given dumpstate progress protocol |line| and updates
// |out_last_nonempty_line| when |line| is non-empty, and |out_zip_path| when
// the bugreport is finished.
void processLine(const std::string& line, std::string* out_zip_path,
                 std::string* out_last_nonempty_line) {
    // The protocol is documented in frameworks/native/cmds/bugreportz/readme.md
    if (line.empty()) {
        return;
    }
    *out_last_nonempty_line = line;
    if (line.find(kOkPrefix) != 0) {
        return;
    }
    *out_zip_path = line.substr(strlen(kOkPrefix));
    return;
}

// Sends the contents of the zip fileto |outfd|.
// Returns true if success
void zipFilesToFd(const std::vector<std::string>& extra_files, int outfd) {
    // pass fclose as Deleter to close the file when unique_ptr is destroyed.
    std::unique_ptr<FILE, decltype(fclose)*> outfile = {fdopen(outfd, "wb"), fclose};
    if (outfile == nullptr) {
        ALOGE("Failed to open output descriptor");
        return;
    }
    auto writer = std::make_unique<ZipWriter>(outfile.get());

    int error = 0;
    for (const auto& filepath : extra_files) {
        const auto name = android::base::Basename(filepath);

        error = writer->StartEntry(name.c_str(), 0);
        if (error) {
            ALOGE("Failed to start entry %s", writer->ErrorCodeString(error));
            return;
        }
        android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(filepath.c_str(), O_RDONLY)));
        if (fd == -1) {
            return;
        }
        while (1) {
            char buffer[65536];

            ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd, buffer, sizeof(buffer)));
            if (bytes_read == 0) {
                break;
            }
            if (bytes_read == -1) {
                if (errno == EAGAIN) {
                    ALOGE("timed out while reading %s", name.c_str());
                } else {
                    ALOGE("read terminated abnormally (%s)", strerror(errno));
                }
                // fail immediately
                return;
            }
            error = writer->WriteBytes(buffer, bytes_read);
            if (error) {
                ALOGE("WriteBytes() failed %s", ZipWriter::ErrorCodeString(error));
                // fail immediately
                return;
            }
        }

        error = writer->FinishEntry();
        if (error) {
            ALOGE("failed to finish entry %s", writer->ErrorCodeString(error));
            continue;
        }
    }
    error = writer->Finish();
    if (error) {
        ALOGE("failed to finish zip writer %s", writer->ErrorCodeString(error));
    }
}

int copyTo(int fd_in, int fd_out, void* buffer, size_t buffer_len) {
    ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd_in, buffer, buffer_len));
    if (bytes_read == 0) {
        return 0;
    }
    if (bytes_read == -1) {
        // EAGAIN really means time out, so make that clear.
        if (errno == EAGAIN) {
            ALOGE("read timed out");
        } else {
            ALOGE("read terminated abnormally (%s)", strerror(errno));
        }
        return -1;
    }
    // copy all bytes to the output socket
    if (!android::base::WriteFully(fd_out, buffer, bytes_read)) {
        ALOGE("write failed");
        return -1;
    }
    return bytes_read;
}

bool copyFile(const std::string& zip_path, int output_socket) {
    android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(zip_path.c_str(), O_RDONLY)));
    if (fd == -1) {
        return false;
    }
    while (1) {
        char buffer[65536];
        int bytes_copied = copyTo(fd, output_socket, buffer, sizeof(buffer));
        if (bytes_copied == 0) {
            break;
        }
        if (bytes_copied == -1) {
            return false;
        }
    }
    return true;
}

// Triggers a bugreport and waits until it is all collected.
// returns false if error, true if success
bool doBugreport(int progress_socket, size_t* out_bytes_written, std::string* zip_path) {
    // Socket will not be available until service starts.
    android::base::unique_fd s;
    for (int i = 0; i < kMaxDumpstateConnectAttempts; i++) {
        s.reset(socket_local_client("dumpstate", ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_STREAM));
        if (s != -1) break;
        sleep(kWaitTimeBetweenConnectAttemptsInSec);
    }

    if (s == -1) {
        ALOGE("failed to connect to dumpstatez service");
        return false;
    }

    // Set a timeout so that if nothing is read by the timeout, stop reading and quit
    struct timeval tv = {
        .tv_sec = kDumpstateTimeoutInSec,
        .tv_usec = 0,
    };
    if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) {
        ALOGW("Cannot set socket timeout (%s)", strerror(errno));
    }

    std::string line;
    std::string last_nonempty_line;
    while (true) {
        char buffer[65536];
        ssize_t bytes_read = copyTo(s, progress_socket, buffer, sizeof(buffer));
        if (bytes_read == 0) {
            break;
        }
        if (bytes_read == -1) {
            return false;
        }
        // Process the buffer line by line. this is needed for the filename.
        for (int i = 0; i < bytes_read; i++) {
            char c = buffer[i];
            if (c == '\n') {
                processLine(line, zip_path, &last_nonempty_line);
                line.clear();
            } else {
                line.append(1, c);
            }
        }
        *out_bytes_written += bytes_read;
    }
    s.reset();
    // Process final line, in case it didn't finish with newline.
    processLine(line, zip_path, &last_nonempty_line);
    // if doBugReport finished successfully, zip path should be set.
    if (zip_path->empty()) {
        ALOGE("no zip file path was found in bugreportz progress data");
        return false;
    }
    return true;
}

bool waitpid_with_timeout(pid_t pid, int timeout_secs, int* status) {
    sigset_t child_mask, old_mask;
    sigemptyset(&child_mask);
    sigaddset(&child_mask, SIGCHLD);

    if (sigprocmask(SIG_BLOCK, &child_mask, &old_mask) == -1) {
        ALOGE("*** sigprocmask failed: %s\n", strerror(errno));
        return false;
    }

    timespec ts = {.tv_sec = timeout_secs, .tv_nsec = 0};
    int ret = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, nullptr, &ts));
    int saved_errno = errno;

    // Set the signals back the way they were.
    if (sigprocmask(SIG_SETMASK, &old_mask, nullptr) == -1) {
        ALOGE("*** sigprocmask failed: %s\n", strerror(errno));
        if (ret == 0) {
            return false;
        }
    }
    if (ret == -1) {
        errno = saved_errno;
        if (errno == EAGAIN) {
            errno = ETIMEDOUT;
        } else {
            ALOGE("*** sigtimedwait failed: %s\n", strerror(errno));
        }
        return false;
    }

    pid_t child_pid = waitpid(pid, status, WNOHANG);
    if (child_pid != pid) {
        if (child_pid != -1) {
            ALOGE("*** Waiting for pid %d, got pid %d instead\n", pid, child_pid);
        } else {
            ALOGE("*** waitpid failed: %s\n", strerror(errno));
        }
        return false;
    }
    return true;
}

// Runs the given command. Kills the command if it does not finish by timeout.
int runCommand(int timeout_secs, const char* file, std::vector<const char*> args) {
    pid_t pid = fork();

    // handle error case
    if (pid < 0) {
        ALOGE("fork failed %s", strerror(errno));
        return pid;
    }

    // handle child case
    if (pid == 0) {
        /* make sure the child dies when parent dies */
        prctl(PR_SET_PDEATHSIG, SIGKILL);

        /* just ignore SIGPIPE, will go down with parent's */
        struct sigaction sigact;
        memset(&sigact, 0, sizeof(sigact));
        sigact.sa_handler = SIG_IGN;
        sigaction(SIGPIPE, &sigact, nullptr);

        execvp(file, (char**)args.data());
        // execvp's result will be handled after waitpid_with_timeout() below, but
        // if it failed, it's safer to exit dumpstate.
        ALOGE("execvp on command %s failed (error: %s)", file, strerror(errno));
        _exit(EXIT_FAILURE);
    }

    // handle parent case
    int status;
    bool ret = waitpid_with_timeout(pid, timeout_secs, &status);

    if (!ret) {
        if (errno == ETIMEDOUT) {
            ALOGE("command %s timed out (killing pid %d)", file, pid);
        } else {
            ALOGE("command %s: Error (killing pid %d)\n", file, pid);
        }
        kill(pid, SIGTERM);
        if (!waitpid_with_timeout(pid, 5, nullptr)) {
            kill(pid, SIGKILL);
            if (!waitpid_with_timeout(pid, 5, nullptr)) {
                ALOGE("could not kill command '%s' (pid %d) even with SIGKILL.\n", file, pid);
            }
        }
        return -1;
    }

    if (WIFSIGNALED(status)) {
        ALOGE("command '%s' failed: killed by signal %d\n", file, WTERMSIG(status));
    } else if (WIFEXITED(status) && WEXITSTATUS(status) > 0) {
        status = WEXITSTATUS(status);
        ALOGE("command '%s' failed: exit code %d\n", file, status);
    }

    return status;
}

void takeScreenshotForDisplayId(PhysicalDisplayId id, const char* tmp_dir,
        std::vector<std::string>* extra_files) {
    std::string id_as_string = std::to_string(id);
    std::string filename = std::string(tmp_dir) + kScreenshotPrefix + id_as_string + ".png";
    std::vector<const char*> args { "-p", "-d", id_as_string.c_str(), filename.c_str(), nullptr };
    ALOGI("capturing screen for display (%s) as %s", id_as_string.c_str(), filename.c_str());
    int status = runCommand(10, "/system/bin/screencap", args);
    if (status == 0) {
        LOG(INFO) << "Screenshot saved for display:" << id_as_string;
    }
    // add the file regardless of the exit status of the screencap util.
    extra_files->push_back(filename);

    LOG(ERROR) << "Failed to take screenshot for display:" << id_as_string;
}

void takeScreenshot(const char* tmp_dir, std::vector<std::string>* extra_files) {
    // Now send the screencaptures
    std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds();

    for (PhysicalDisplayId display_id : ids) {
        takeScreenshotForDisplayId(display_id, tmp_dir, extra_files);
    }
}

bool recursiveRemoveDir(const std::string& path) {
    auto callback = [](const char* child, const struct stat*, int file_type, struct FTW*) -> int {
        if (file_type == FTW_DP) {
            if (rmdir(child) == -1) {
                ALOGE("rmdir(%s): %s", child, strerror(errno));
                return -1;
            }
        } else if (file_type == FTW_F) {
            if (unlink(child) == -1) {
                ALOGE("unlink(%s): %s", child, strerror(errno));
                return -1;
            }
        }
        return 0;
    };
    // do a file tree walk with a sufficiently large depth.
    return nftw(path.c_str(), callback, 128, FTW_DEPTH) == 0;
}

status_t createTempDir(const char* dir) {
    struct stat sb;
    if (TEMP_FAILURE_RETRY(stat(dir, &sb)) == 0) {
        if (!recursiveRemoveDir(dir)) {
            return -errno;
        }
    } else if (errno != ENOENT) {
        ALOGE("Failed to stat %s ", dir);
        return -errno;
    }
    if (TEMP_FAILURE_RETRY(mkdir(dir, 0700)) == -1) {
        ALOGE("Failed to mkdir %s", dir);
        return -errno;
    }
    return OK;
}

// Removes bugreport
void cleanupBugreportFile(const std::string& zip_path) {
    if (unlink(zip_path.c_str()) != 0) {
        ALOGE("Could not unlink %s (%s)", zip_path.c_str(), strerror(errno));
    }
}

}  // namespace

int main(void) {
    ALOGI("Starting bugreport collecting service");

    auto t0 = std::chrono::steady_clock::now();

    std::vector<std::string> extra_files;
    if (createTempDir(kTempDirectory) == OK) {
        // take screenshots of the physical displays as early as possible
        takeScreenshot(kTempDirectory, &extra_files);
    }

    // Start the dumpstatez service.
    android::base::SetProperty("ctl.start", "car-dumpstatez");

    size_t bytes_written = 0;

    std::string zip_path;
    int progress_socket = openSocket(kCarBrProgressSocket);
    if (progress_socket < 0) {
        // early out. in this case we will not print the final message, but that is ok.
        android::base::SetProperty("ctl.stop", "car-dumpstatez");
        return EXIT_FAILURE;
    }
    bool ret_val = doBugreport(progress_socket, &bytes_written, &zip_path);
    close(progress_socket);

    int output_socket = openSocket(kCarBrOutputSocket);
    if (output_socket != -1 && ret_val) {
        ret_val = copyFile(zip_path, output_socket);
    }
    if (output_socket != -1) {
        close(output_socket);
    }

    int extra_output_socket = openSocket(kCarBrExtraOutputSocket);
    if (extra_output_socket != -1 && ret_val) {
        zipFilesToFd(extra_files, extra_output_socket);
    }
    if (extra_output_socket != -1) {
        close(extra_output_socket);
    }

    auto delta = std::chrono::duration_cast<std::chrono::duration<double>>(
                     std::chrono::steady_clock::now() - t0)
                     .count();

    std::string result = ret_val ? "success" : "failed";
    ALOGI("bugreport %s in %.02fs, %zu bytes written", result.c_str(), delta, bytes_written);
    cleanupBugreportFile(zip_path);

    recursiveRemoveDir(kTempDirectory);

    // No matter how doBugreport() finished, let's try to explicitly stop
    // car-dumpstatez in case it stalled.
    android::base::SetProperty("ctl.stop", "car-dumpstatez");

    return ret_val ? EXIT_SUCCESS : EXIT_FAILURE;
}