/*
 * Copyright (C) 2018-2022 Intel Corporation.
 * SPDX-License-Identifier: BSD-3-Clause
 */

/*
 * Copyright (C) 2018-2022 Intel Corporation.
 *
 * 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 <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include "crash_dump.h"
#include "packet.h"
#include "log_sys.h"
#include "protocol.h"
#include "version.h"

/**
 * Usercrash works as C/S model: usercrash_c works as usercrash client to
 * collect crash logs and information once crash event occurs. For each time,
 * usercrash_c receives 3 params from core_dump and sends connect request event
 * to usercrash_s, then it receives file fd from server to fill crash info into
 * the file. After this work is done, it will notify server that dump work is
 * completed.
 */

/**
 * @sockfd: the socket fd.
 * set_timeout is used to set timeout for the sockfd, in case client is blocked
 * when client cannot receive the data from server, or send data to server
 */
static int set_timeout(int sockfd)
{
	struct timeval timeout = {50, 0};

	if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout,
				sizeof(timeout)) != 0) {
		LOGE("failed to set receive timeout\n");
		return -1;
	}
	if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout,
				sizeof(timeout)) != 0) {
		LOGE("failed to set send timeout\n");
		return -1;
	}
	return 0;
}

/**
 * @pid: crash process pid.
 * @usercrashd_socket: client socket fd pointer, get the value from
 * usercrashd_connect function.
 * @output_fd: file fd, receives from server to store dump file.
 * @name: crash process name
 * usercrashd_connect is used to connect to server, and get the crash file fd
 * from server
 */
static int usercrashd_connect(int pid, int *usercrashd_socket,
		int *output_fd, const char *name)
{
	int sockfd;
	int tmp_output_fd;
	ssize_t rc;
	int flags;
	struct crash_packet packet = {0};

	if (name == NULL) {
		LOGE("crash process name is NULL\n");
		return -1;
	}
	sockfd = socket_local_client(SOCKET_NAME, strlen(SOCKET_NAME),
			SOCK_SEQPACKET | SOCK_CLOEXEC);
	if (sockfd == -1) {
		LOGE("failed to connect to usercrashd, error (%s)\n",
			strerror(errno));
		return -1;
	}
	packet.packet_type = kDumpRequest;
	packet.pid = pid;
	strncpy(packet.name, name, COMM_NAME_LEN);
	packet.name[COMM_NAME_LEN - 1] = '\0';
	if (set_timeout(sockfd)) {
		close(sockfd);
		return -1;
	}
	if (write(sockfd, &packet, sizeof(packet)) !=
				sizeof(packet)) {
		LOGE("failed to write DumpRequest packet, error (%s)\n",
			strerror(errno));
		close(sockfd);
		return -1;
	}

	rc = recv_fd(sockfd, &packet, sizeof(packet),
				&tmp_output_fd);
	if (rc == -1) {
		LOGE("failed to read response to DumpRequest packet, ");
		LOGE("error (%s)\n", strerror(errno));
		close(sockfd);
		return -1;
	} else if (rc != sizeof(packet)) {
		LOGE("received DumpRequest response packet of incorrect ");
		LOGE("length (expected %lu, got %ld)\n", sizeof(packet), rc);
		goto fail;
	}
	if (packet.packet_type != kPerformDump) {
		LOGE("unexpected dump response:%d\n", packet.packet_type);
		goto fail;
	}

	/**
	 * Make the fd O_APPEND so that our output is guaranteed to be at the
	 * end of a file. (This also makes selinux rules consistent, because
	 * selinux distinguishes between writing to a regular fd, and writing
	 * to an fd with O_APPEND)
	 */
	flags = fcntl(tmp_output_fd, F_GETFL);
	if (fcntl(tmp_output_fd, F_SETFL, flags | O_APPEND) != 0) {
		LOGE("failed to set output fd flags, error (%s)\n",
					strerror(errno));
		goto fail;
	}

	*usercrashd_socket = sockfd;
	*output_fd = tmp_output_fd;

	return 0;
fail:
	close(sockfd);
	close(tmp_output_fd);
	return -1;

}

/**
 * @usercrashd_socket: client socket fd, used to communicate with server.
 * usercrashd_notify_completion is used to tell the server it has done the
 * dump, the server will pop another crash from the queue and execute the dump
 * process
 */
static int usercrashd_notify_completion(int usercrashd_socket)
{
	struct crash_packet packet = {0};

	packet.packet_type = kCompletedDump;
	if (set_timeout(usercrashd_socket)) {
		close(usercrashd_socket);
		return -1;
	}
	if (write(usercrashd_socket, &packet,
				sizeof(packet)) != sizeof(packet)) {
		return -1;
	}
	return 0;
}

static void print_usage(void)
{
	printf("usercrash - tool to dump crash info for the crashes in the ");
	printf("userspace on sos. It's the client role of usercrash.\n");
	printf("[Usage]\n");
	printf("\t--coredump, usercrash_c <pid> <comm> <sig> ");
	printf("(root role to run)\n");
	printf("[Option]\n");
	printf("\t-h: print this usage message\n");
	printf("\t-v: print usercrash_c version\n");
}

int main(int argc, char *argv[])
{
	int pid;
	int sig;
	int out_fd;
	int sock;
	int ret;

	if (argc > 1) {
		if (strcmp(argv[1], "-v") == 0) {
			printf("version is %d.%d-%s, build by %s@%s\n",
				UC_MAJOR_VERSION, UC_MINOR_VERSION,
				UC_BUILD_VERSION, UC_BUILD_USER,
				UC_BUILD_TIME);
			return 0;
		}
		if (strcmp(argv[1], "-h") == 0) {
			print_usage();
			return 0;
		}
	} else
		print_usage();

	if (getuid() != 0) {
		LOGE("failed to execute usercrash_c, root is required\n");
		exit(EXIT_FAILURE);
	}

	if (argc == 4) {
		/* it's from coredump */
		pid = (int)strtol(argv[1], NULL, 10);
		sig = (int)strtol(argv[3], NULL, 10);
		ret = usercrashd_connect(pid, &sock, &out_fd, argv[2]);
		if (ret) {
			LOGE("usercrashd_connect failed, error (%s)\n",
					strerror(errno));
			exit(EXIT_FAILURE);
		}
		crash_dump(pid, sig, out_fd);
		close(out_fd);
		if (usercrashd_notify_completion(sock)) {
			LOGE("failed to notify usercrashd of completion");
			close(sock);
			exit(EXIT_FAILURE);
		}
		close(sock);
	} else {
		print_usage();
		return 0;
	}

	return 0;
}
