| #include <stdlib.h> |
| #include <errno.h> |
| #include <ifaddrs.h> |
| #include <fcntl.h> |
| #include <math.h> |
| #include <net/if.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <sys/ioctl.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <time.h> |
| #include <unistd.h> |
| #include <getopt.h> |
| #include <inttypes.h> |
| #include <netdb.h> |
| |
| #include "log_uploader.h" |
| #include "kvextract.h" |
| #include "upload.h" |
| #include "utils.h" |
| |
| #define DEFAULT_SERVER "https://diag.cpe.gfsvc.com" |
| #define COUNTER_MARKER_FILE "/tmp/loguploadcounter" |
| #define LOGS_UPLOADED_MARKER_FILE "/tmp/logs-uploaded" |
| #define DEFAULT_UPLOAD_TARGET "dmesg" |
| #define MAX_LOG_SIZE 8*1024*1024 // max bytes to upload in one run |
| #define LOG_BUF_EXTRA 65536 // extra bytes for extra compression slop |
| #define DEV_KMSG_PATH "/dev/kmsg" |
| #define NTP_SYNCED_PATH "/tmp/ntp.synced" |
| #define VERSION_PATH "/etc/version" |
| #define SERIAL_PATH "/tmp/serial" |
| #define PLATFORM_PATH "/tmp/platform" |
| // 8192 is the size of the buffer used in printk.c to store a line read from |
| // /dev/kmsg before copying it into our userspace buffer |
| #define LOG_LINE_BUFFER_SIZE 8192 |
| |
| // Use level 1 so we compress for speed, not size since we have lots |
| // of bandwidth for uploading but CPU cycles are something we want to |
| // conserve. |
| #define ZLIB_COMPRESS_LEVEL 1 |
| |
| static const char *interfaces_to_check[] = { "br0", "eth0", "man", "pon0" }; |
| static int num_interfaces = sizeof(interfaces_to_check) / |
| sizeof(interfaces_to_check[0]); |
| |
| volatile static int interrupted = 0; |
| |
| static void got_alarm(int sig) { |
| interrupted = 1; |
| } |
| |
| // To allow overriding for testing. |
| int getnameinfo_resolver(const struct sockaddr* sa, socklen_t salen, char* host, |
| size_t hostlen, char* serv, size_t servlen, int flags) { |
| return getnameinfo(sa, salen, host, hostlen, serv, servlen, flags); |
| } |
| |
| // To allow overriding for testing, gets the MAC of a network interface. |
| int iface_to_mac(const char* iface, char* buf, int len) { |
| // xx:xx:xx:xx:xx:xx format comes back, 17 chars + terminator |
| if (len < 18) |
| return -1; |
| struct ifreq ifreq; |
| int fd; |
| memset(&ifreq, 0, sizeof(ifreq)); |
| fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); |
| if (strlen(iface) + 1 > sizeof(ifreq.ifr_name)) |
| return -1; |
| strcpy(ifreq.ifr_name, iface); |
| if (ioctl(fd, SIOCGIFHWADDR, &ifreq) == -1) { |
| close(fd); |
| return -1; |
| } else { |
| close(fd); |
| snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x", |
| ifreq.ifr_hwaddr.sa_data[0], ifreq.ifr_hwaddr.sa_data[1], |
| ifreq.ifr_hwaddr.sa_data[2], ifreq.ifr_hwaddr.sa_data[3], |
| ifreq.ifr_hwaddr.sa_data[4], ifreq.ifr_hwaddr.sa_data[5]); |
| return 0; |
| } |
| } |
| |
| int standard_read(char* buffer, int len, void* user_data) { |
| return read(*((int*) user_data), buffer, len); |
| } |
| |
| static void usage(const char* progname) { |
| fprintf(stderr, "\nUsage: %s [options...]\n", progname); |
| fprintf(stderr, " --server URL Server URL [" DEFAULT_SERVER "]\n"); |
| fprintf(stderr, " --all Upload entire logs, not just new data\n"); |
| fprintf(stderr, " --logtype TYPE Tell server which log category this is\n"); |
| fprintf(stderr, " --freq SECS Upload logs every SECS seconds [60]\n"); |
| fprintf(stderr, " --stdout Print to stdout instead of uploading\n"); |
| fprintf(stderr, |
| " --stdin NAME Get data from stdin, not /dev/kmsg, and\n" |
| " name uploaded file NAME rather than 'dmesg'\n"); |
| exit(EXIT_SUCCESS); |
| } |
| |
| static int parse_args(struct upload_config* config, int argc, |
| char* const argv[]) { |
| int opt = 0; |
| static struct option long_options[] = { |
| { "server", required_argument, 0, 's' }, |
| { "all", no_argument, 0, 'a' }, |
| { "logtype", required_argument, 0, 'l' }, |
| { "freq", required_argument, 0, 'f' }, |
| { "stdout", no_argument, 0, 'd' }, |
| { "stdin", required_argument, 0, 'i' }, |
| { 0, 0, 0, 0} |
| }; |
| |
| while (1) { |
| opt = getopt_long(argc, argv, "", long_options, NULL); |
| if (opt == -1) |
| break; |
| |
| switch (opt) { |
| case 'a': |
| config->upload_all = 1; |
| break; |
| case 'd': |
| config->use_stdout = 1; |
| break; |
| case 's': |
| snprintf(config->server, sizeof(config->server), "%s", optarg); |
| break; |
| case 'i': |
| config->use_stdin = 1; |
| snprintf(config->upload_target, sizeof(config->upload_target), "%s", |
| optarg); |
| break; |
| case 'l': |
| snprintf(config->logtype, sizeof(config->logtype), "%s", optarg); |
| break; |
| case 'f': |
| config->freq = atoi(optarg); |
| if (config->freq < 0) { |
| fprintf(stderr, "fatal: freq must be >= 0\n"); |
| return -1; |
| } |
| break; |
| default: |
| return -1; |
| } |
| } |
| if (optind < argc) |
| return -1; // extraneous non-option arguments |
| return 0; |
| } |
| |
| static int pick_delay(struct upload_config* config) { |
| // Randomize the sleep time to be near the specified amount, +/- 1/12th. |
| // (1/12th is weird, but it means +/- 5 for 60 seconds, which is nice). |
| int variance = config->freq / 12; |
| return ((config->freq - variance) + |
| (random() % (variance * 2 + 1))); |
| } |
| |
| int main(int argc, char* const argv[]) { |
| setvbuf(stdout, (char *) NULL, _IOLBF, 0); |
| |
| struct upload_config config; |
| memset(&config, 0, sizeof(config)); |
| |
| snprintf(config.server, sizeof(config.server), "%s", DEFAULT_SERVER); |
| snprintf(config.upload_target, sizeof(config.upload_target), "%s", |
| DEFAULT_UPLOAD_TARGET); |
| |
| if (argc > 1) { |
| if (parse_args(&config, argc, argv) < 0) { |
| usage(argv[0]); |
| return 99; |
| } |
| } |
| |
| struct sigaction sa = {}; |
| sa.sa_handler = got_alarm; |
| sigaction(SIGALRM, &sa, NULL); |
| |
| // Initialize the random number generator |
| srandom(getpid() ^ time(NULL)); |
| |
| // Allocate this once and re-use it every time |
| char* log_buffer = (char*) malloc(MAX_LOG_SIZE + LOG_BUF_EXTRA); |
| if (!log_buffer) { |
| fprintf(stderr, "Failed to allocate log_buffer!\n"); |
| return 98; |
| } |
| |
| int kmsg_read_fd = 0; |
| if (!config.use_stdin) { |
| // We never actually close this, it'll get done when the process exits. |
| // Use nonblocking mode so we know when we're consumed all the recent data. |
| kmsg_read_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK); |
| if (kmsg_read_fd < 0) { |
| perror("/dev/kmsg"); |
| return 1; |
| } |
| } |
| |
| // In the kernel struct devkmsg_user this is the size of the buffer |
| // they use to read a line and then copy it to us, so it won't be any |
| // bigger than this. |
| char line_buffer[LOG_LINE_BUFFER_SIZE]; |
| unsigned long total_read; |
| z_stream zstrm; |
| memset(&zstrm, 0, sizeof(zstrm)); |
| |
| struct log_parse_params parse_params; |
| memset(&parse_params, 0, sizeof(parse_params)); |
| parse_params.config = &config; |
| parse_params.user_data = &kmsg_read_fd; |
| parse_params.read_log_data = standard_read; |
| parse_params.dev_kmsg_path = DEV_KMSG_PATH; |
| parse_params.version_path = VERSION_PATH; |
| parse_params.ntp_synced_path = NTP_SYNCED_PATH; |
| // This'll set it to zero if it can't read the file which is fine. |
| parse_params.last_log_counter = read_file_as_uint64(COUNTER_MARKER_FILE); |
| parse_params.log_buffer = log_buffer; |
| parse_params.line_buffer = line_buffer; |
| parse_params.line_buffer_size = sizeof(line_buffer); |
| |
| while (1) { |
| char* log_data_to_use; |
| if (config.use_stdin) { |
| // Read in all the data from stdin |
| int num_read; |
| total_read = num_read = 0; |
| interrupted = 0; |
| alarm(pick_delay(&config)); |
| while ((num_read = read(STDIN_FILENO, log_buffer + total_read, |
| MAX_LOG_SIZE - total_read)) > 0 && !interrupted) { |
| total_read += num_read; |
| } |
| if (num_read < 0 && errno != EINTR) { |
| perror("stdin"); |
| return 2; |
| } |
| if (num_read == 0 && total_read == 0) { |
| fprintf(stderr, "stdin: end of input. done.\n"); |
| return 0; |
| } |
| log_data_to_use = log_buffer; |
| } else { |
| // Remove the marker file to indicate we've completed the upload process. |
| remove(LOGS_UPLOADED_MARKER_FILE); |
| |
| // Normal logs processing, write out the general marker data. |
| // We only need to write this out if we're doing the first upload |
| // otherwise it'll have been written right after we did the last one. |
| if (parse_params.last_log_counter == 0 && |
| logmark_once(DEV_KMSG_PATH, VERSION_PATH, |
| NTP_SYNCED_PATH)) { |
| fprintf(stderr, "failed to execute logmark-once properly\n"); |
| return 3; |
| } |
| |
| parse_params.total_read = MAX_LOG_SIZE; |
| log_data_to_use = parse_and_consume_log_data(&parse_params); |
| if (!log_data_to_use) { |
| fprintf(stderr, "failed with logs parsing, abort!\n"); |
| return 4; |
| } |
| total_read = parse_params.total_read; |
| } |
| |
| // Now we've read all of the log data into our buffer, proceed |
| // with uploading or outputting it. |
| |
| fprintf(stderr, "uploading %lu bytes of logs.\n", total_read); |
| if (config.use_stdout) { |
| // Just print the whole thing to stdout. Note: might be binary. |
| fwrite(log_data_to_use, total_read, 1, stdout); |
| } else { |
| struct ifaddrs* ifaddr; |
| if (getifaddrs(&ifaddr)) { |
| perror("getifaddrs"); |
| return 5; |
| } |
| |
| struct kvextractparams kvparams; |
| memset(&kvparams, 0, sizeof(kvparams)); |
| kvparams.interfaces_to_check = interfaces_to_check; |
| kvparams.num_interfaces = num_interfaces; |
| kvparams.ifaddr = ifaddr; |
| kvparams.platform_path = PLATFORM_PATH; |
| kvparams.serial_path = SERIAL_PATH; |
| kvparams.name_info_resolver = getnameinfo_resolver; |
| kvparams.interface_to_mac = iface_to_mac; |
| kvparams.logtype = config.logtype; |
| struct kvpair* kvpairs = extract_kv_pairs(&kvparams); |
| freeifaddrs(ifaddr); |
| if (!kvpairs) { |
| fprintf(stderr, "failure extracting kv pairs, abort\n"); |
| return 6; |
| } |
| |
| // Adjust this if we moved the pointer. |
| unsigned long compressed_size = MAX_LOG_SIZE + LOG_BUF_EXTRA - |
| (log_data_to_use - log_buffer); |
| int comp_result = deflate_inplace(&zstrm, (unsigned char*)log_data_to_use, |
| total_read, &compressed_size); |
| if (comp_result != Z_OK) { |
| fprintf(stderr, "fatal: deflate_inplace failed\n"); |
| return 7; |
| } |
| |
| int upload_res = upload_file(config.server, config.upload_target, |
| log_data_to_use, compressed_size, kvpairs); |
| free_kv_pairs(kvpairs); |
| if (upload_res) { |
| fprintf(stderr, "upload_file failed\n"); |
| return 8; |
| } |
| if (write_file_as_uint64(COUNTER_MARKER_FILE, |
| parse_params.last_log_counter)) { |
| fprintf(stderr, "unable to write out last log counter\n"); |
| return 9; |
| } |
| // Write the marker file to indicate we finished the upload. |
| int marker_fd = open(LOGS_UPLOADED_MARKER_FILE, O_CREAT | O_WRONLY, |
| RW_FILE_PERMISSIONS); |
| if (marker_fd >= 0) |
| close(marker_fd); |
| } |
| |
| if (!config.use_stdin) { |
| if (write_to_file(DEV_KMSG_PATH, LOG_MARKER_END_LINE) < 0) { |
| perror("end marker"); |
| return 10; |
| } |
| } |
| |
| if (!config.freq) { |
| break; |
| } else if (!config.use_stdin) { |
| // if using stdin, we want to read incrementally instead. |
| sleep(pick_delay(&config)); |
| } |
| } |
| free(log_buffer); |
| return 0; |
| } |