Update.
This commit is contained in:
parent
f5b47eebe4
commit
8b13dad644
5
tikker.c
5
tikker.c
@ -141,6 +141,7 @@ static void tikker_print_usage(void) {
|
|||||||
printf(" stats top-keys [N] Top N keys (default: 10)\n");
|
printf(" stats top-keys [N] Top N keys (default: 10)\n");
|
||||||
printf(" stats top-words [N] Top N words (default: 10)\n");
|
printf(" stats top-words [N] Top N words (default: 10)\n");
|
||||||
printf(" stats summary Overall summary statistics\n");
|
printf(" stats summary Overall summary statistics\n");
|
||||||
|
printf(" export Export logs to logs_plain/\n");
|
||||||
printf(" decode [FILE] Decode keystroke log file\n");
|
printf(" decode [FILE] Decode keystroke log file\n");
|
||||||
printf("\nOptions:\n");
|
printf("\nOptions:\n");
|
||||||
printf(" --device='NAME' Monitor specific device\n");
|
printf(" --device='NAME' Monitor specific device\n");
|
||||||
@ -309,6 +310,10 @@ int main(int argc, char *argv[]) {
|
|||||||
return tikker_handle_stats_command(argc - 2, argv + 2);
|
return tikker_handle_stats_command(argc - 2, argv + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strcmp(argv[1], "export") == 0) {
|
||||||
|
return tikker_export_logs(tikker_db);
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(argv[1], "decode") == 0) {
|
if (strcmp(argv[1], "decode") == 0) {
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "Error: decode requires a filename\n");
|
fprintf(stderr, "Error: decode requires a filename\n");
|
||||||
|
|||||||
162
tikker_stats.h
162
tikker_stats.h
@ -4,6 +4,9 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include "sormc.h"
|
#include "sormc.h"
|
||||||
#include "tikker_types.h"
|
#include "tikker_types.h"
|
||||||
#include "tikker_db.h"
|
#include "tikker_db.h"
|
||||||
@ -212,59 +215,150 @@ static inline int tikker_stats_summary(int db) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int tikker_stats_top_words(int db, int limit) {
|
static inline int tikker_export_logs(int db) {
|
||||||
sorm_ptr result = sormq(db,
|
struct stat st = {0};
|
||||||
"SELECT STRFTIME('%%Y-%%m-%%d.%%H', timestamp) as date_hour, "
|
if (stat("logs_plain", &st) == -1) {
|
||||||
"GROUP_CONCAT(char, '') as chars "
|
mkdir("logs_plain", 0755);
|
||||||
"FROM kevent WHERE event = 'PRESSED' "
|
|
||||||
"GROUP BY date_hour ORDER BY date_hour");
|
|
||||||
|
|
||||||
printf("word,count\n");
|
|
||||||
|
|
||||||
if (!result) return 0;
|
|
||||||
|
|
||||||
size_t total_len = strlen((char *)result);
|
|
||||||
char *decoded = malloc(total_len + 1);
|
|
||||||
if (!decoded) {
|
|
||||||
free(result);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
char *csv = (char *)result;
|
sorm_ptr hours_result = sormq(db,
|
||||||
|
"SELECT DISTINCT STRFTIME('%%Y-%%m-%%d.%%H', timestamp) as date_hour "
|
||||||
|
"FROM kevent WHERE event = 'PRESSED' ORDER BY date_hour");
|
||||||
|
|
||||||
|
if (!hours_result) {
|
||||||
|
printf("No data to export\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int files_written = 0;
|
||||||
|
char *csv = (char *)hours_result;
|
||||||
char *line = csv;
|
char *line = csv;
|
||||||
char *next;
|
char *next;
|
||||||
size_t decoded_pos = 0;
|
|
||||||
|
|
||||||
while (line && *line) {
|
while (line && *line) {
|
||||||
next = strchr(line, '\n');
|
next = strchr(line, '\n');
|
||||||
if (next) *next = '\0';
|
if (next) *next = '\0';
|
||||||
|
|
||||||
char *chars = strchr(line, ';');
|
if (tikker_csv_is_metadata(line) || strlen(line) < 10) {
|
||||||
if (chars) {
|
if (next) line = next + 1;
|
||||||
chars++;
|
else break;
|
||||||
char *end = strchr(chars, ';');
|
continue;
|
||||||
if (end) *end = '\0';
|
}
|
||||||
|
|
||||||
int len = tikker_decode_buffer(chars, decoded + decoded_pos, total_len - decoded_pos);
|
char *end = strchr(line, ';');
|
||||||
decoded_pos += len;
|
if (end) *end = '\0';
|
||||||
if (decoded_pos < total_len) {
|
|
||||||
decoded[decoded_pos++] = ' ';
|
char date_hour[32];
|
||||||
|
strncpy(date_hour, line, sizeof(date_hour) - 1);
|
||||||
|
date_hour[sizeof(date_hour) - 1] = '\0';
|
||||||
|
|
||||||
|
char date_hour_space[32];
|
||||||
|
strncpy(date_hour_space, date_hour, sizeof(date_hour_space) - 1);
|
||||||
|
date_hour_space[sizeof(date_hour_space) - 1] = '\0';
|
||||||
|
char *dot = strchr(date_hour_space, '.');
|
||||||
|
if (dot) *dot = ' ';
|
||||||
|
|
||||||
|
char start_ts[32], end_ts[32];
|
||||||
|
snprintf(start_ts, sizeof(start_ts), "%s:00:00", date_hour_space);
|
||||||
|
snprintf(end_ts, sizeof(end_ts), "%s:59:59", date_hour_space);
|
||||||
|
|
||||||
|
char sql[512];
|
||||||
|
snprintf(sql, sizeof(sql),
|
||||||
|
"SELECT GROUP_CONCAT(char, '') FROM kevent "
|
||||||
|
"WHERE event = 'PRESSED' AND timestamp >= '%s' AND timestamp <= '%s'",
|
||||||
|
start_ts, end_ts);
|
||||||
|
|
||||||
|
sorm_ptr chars_result = sormq(db, sql);
|
||||||
|
if (chars_result) {
|
||||||
|
char *chars_csv = (char *)chars_result;
|
||||||
|
char *chars_line = strchr(chars_csv, '\n');
|
||||||
|
if (chars_line) chars_line++;
|
||||||
|
else chars_line = chars_csv;
|
||||||
|
|
||||||
|
char *chars_end = strchr(chars_line, ';');
|
||||||
|
if (chars_end) *chars_end = '\0';
|
||||||
|
chars_end = strchr(chars_line, '\n');
|
||||||
|
if (chars_end) *chars_end = '\0';
|
||||||
|
|
||||||
|
char filepath[256];
|
||||||
|
snprintf(filepath, sizeof(filepath), "logs_plain/%s.txt", date_hour);
|
||||||
|
|
||||||
|
FILE *f = fopen(filepath, "w");
|
||||||
|
if (f) {
|
||||||
|
fprintf(f, "**%s:00**: ```%s```\n", date_hour_space, chars_line);
|
||||||
|
fclose(f);
|
||||||
|
files_written++;
|
||||||
|
if (files_written % 100 == 0) {
|
||||||
|
printf("Exported %d files...\r", files_written);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
free(chars_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next) line = next + 1;
|
if (next) line = next + 1;
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
decoded[decoded_pos] = '\0';
|
|
||||||
|
|
||||||
tikker_word_count_t *words = calloc(TIKKER_MAX_WORDS, sizeof(tikker_word_count_t));
|
free(hours_result);
|
||||||
if (!words) {
|
printf("Exported %d hourly log files to logs_plain/\n", files_written);
|
||||||
free(decoded);
|
return 0;
|
||||||
free(result);
|
}
|
||||||
|
|
||||||
|
static inline int tikker_stats_top_words(int db, int limit) {
|
||||||
|
(void)db;
|
||||||
|
|
||||||
|
printf("word,count\n");
|
||||||
|
|
||||||
|
tikker_word_hash_t *hash = tikker_hash_create();
|
||||||
|
if (!hash) return 1;
|
||||||
|
|
||||||
|
DIR *dir = opendir("logs_plain");
|
||||||
|
if (!dir) {
|
||||||
|
fprintf(stderr, "Error: Cannot open logs_plain directory\n");
|
||||||
|
tikker_hash_free(hash);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int word_count = tikker_extract_words(decoded, words, TIKKER_MAX_WORDS);
|
struct dirent *entry;
|
||||||
|
char filepath[512];
|
||||||
|
char buffer[65536];
|
||||||
|
|
||||||
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
|
if (entry->d_name[0] == '.') continue;
|
||||||
|
|
||||||
|
snprintf(filepath, sizeof(filepath), "logs_plain/%s", entry->d_name);
|
||||||
|
FILE *f = fopen(filepath, "r");
|
||||||
|
if (!f) continue;
|
||||||
|
|
||||||
|
while (fgets(buffer, sizeof(buffer), f)) {
|
||||||
|
char *p = buffer;
|
||||||
|
char word[TIKKER_MAX_WORD_LEN];
|
||||||
|
int word_len = 0;
|
||||||
|
|
||||||
|
while (*p) {
|
||||||
|
if (tikker_is_valid_word_char(*p)) {
|
||||||
|
if (word_len < TIKKER_MAX_WORD_LEN - 1) {
|
||||||
|
word[word_len++] = toupper((unsigned char)*p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (word_len >= 2) {
|
||||||
|
word[word_len] = '\0';
|
||||||
|
tikker_hash_insert(hash, word);
|
||||||
|
}
|
||||||
|
word_len = 0;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
|
||||||
|
int word_count;
|
||||||
|
tikker_word_count_t *words = tikker_hash_to_array(hash, &word_count);
|
||||||
|
tikker_hash_free(hash);
|
||||||
|
|
||||||
tikker_sort_words(words, word_count);
|
tikker_sort_words(words, word_count);
|
||||||
|
|
||||||
int output_count = (limit < word_count) ? limit : word_count;
|
int output_count = (limit < word_count) ? limit : word_count;
|
||||||
@ -273,8 +367,6 @@ static inline int tikker_stats_top_words(int db, int limit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
free(words);
|
free(words);
|
||||||
free(decoded);
|
|
||||||
free(result);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
120
tikker_words.h
120
tikker_words.h
@ -6,67 +6,89 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "tikker_types.h"
|
#include "tikker_types.h"
|
||||||
|
|
||||||
|
#define TIKKER_HASH_SIZE 65536
|
||||||
|
|
||||||
|
typedef struct tikker_word_node {
|
||||||
|
char word[TIKKER_MAX_WORD_LEN];
|
||||||
|
int count;
|
||||||
|
struct tikker_word_node *next;
|
||||||
|
} tikker_word_node_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
tikker_word_node_t *buckets[TIKKER_HASH_SIZE];
|
||||||
|
int total_words;
|
||||||
|
} tikker_word_hash_t;
|
||||||
|
|
||||||
static inline int tikker_is_valid_word_char(char c) {
|
static inline int tikker_is_valid_word_char(char c) {
|
||||||
return isalnum((unsigned char)c) || c == '_';
|
return isalnum((unsigned char)c) || c == '_';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline unsigned int tikker_hash_word(const char *word) {
|
||||||
|
unsigned int hash = 5381;
|
||||||
|
while (*word) {
|
||||||
|
hash = ((hash << 5) + hash) + (unsigned char)*word++;
|
||||||
|
}
|
||||||
|
return hash % TIKKER_HASH_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline tikker_word_hash_t *tikker_hash_create(void) {
|
||||||
|
tikker_word_hash_t *h = calloc(1, sizeof(tikker_word_hash_t));
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void tikker_hash_insert(tikker_word_hash_t *h, const char *word) {
|
||||||
|
unsigned int idx = tikker_hash_word(word);
|
||||||
|
tikker_word_node_t *node = h->buckets[idx];
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
if (strcmp(node->word, word) == 0) {
|
||||||
|
node->count++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node = node->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = malloc(sizeof(tikker_word_node_t));
|
||||||
|
strncpy(node->word, word, TIKKER_MAX_WORD_LEN - 1);
|
||||||
|
node->word[TIKKER_MAX_WORD_LEN - 1] = '\0';
|
||||||
|
node->count = 1;
|
||||||
|
node->next = h->buckets[idx];
|
||||||
|
h->buckets[idx] = node;
|
||||||
|
h->total_words++;
|
||||||
|
}
|
||||||
|
|
||||||
static inline int tikker_word_count_compare(const void *a, const void *b) {
|
static inline int tikker_word_count_compare(const void *a, const void *b) {
|
||||||
return ((tikker_word_count_t *)b)->count - ((tikker_word_count_t *)a)->count;
|
return ((tikker_word_count_t *)b)->count - ((tikker_word_count_t *)a)->count;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int tikker_extract_words(const char *text, tikker_word_count_t *words, int max_words) {
|
static inline tikker_word_count_t *tikker_hash_to_array(tikker_word_hash_t *h, int *count) {
|
||||||
int word_count = 0;
|
tikker_word_count_t *arr = malloc(h->total_words * sizeof(tikker_word_count_t));
|
||||||
const char *p = text;
|
int idx = 0;
|
||||||
char word[TIKKER_MAX_WORD_LEN];
|
|
||||||
int word_len = 0;
|
|
||||||
|
|
||||||
while (*p) {
|
for (int i = 0; i < TIKKER_HASH_SIZE; i++) {
|
||||||
if (tikker_is_valid_word_char(*p)) {
|
tikker_word_node_t *node = h->buckets[i];
|
||||||
if (word_len < TIKKER_MAX_WORD_LEN - 1) {
|
while (node) {
|
||||||
word[word_len++] = tolower((unsigned char)*p);
|
strncpy(arr[idx].word, node->word, TIKKER_MAX_WORD_LEN);
|
||||||
}
|
arr[idx].count = node->count;
|
||||||
} else {
|
idx++;
|
||||||
if (word_len >= 2) {
|
node = node->next;
|
||||||
word[word_len] = '\0';
|
|
||||||
|
|
||||||
int found = 0;
|
|
||||||
for (int i = 0; i < word_count; i++) {
|
|
||||||
if (strcmp(words[i].word, word) == 0) {
|
|
||||||
words[i].count++;
|
|
||||||
found = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found && word_count < max_words) {
|
|
||||||
strcpy(words[word_count].word, word);
|
|
||||||
words[word_count].count = 1;
|
|
||||||
word_count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
word_len = 0;
|
|
||||||
}
|
|
||||||
p++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (word_len >= 2) {
|
|
||||||
word[word_len] = '\0';
|
|
||||||
int found = 0;
|
|
||||||
for (int i = 0; i < word_count; i++) {
|
|
||||||
if (strcmp(words[i].word, word) == 0) {
|
|
||||||
words[i].count++;
|
|
||||||
found = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found && word_count < max_words) {
|
|
||||||
strcpy(words[word_count].word, word);
|
|
||||||
words[word_count].count = 1;
|
|
||||||
word_count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return word_count;
|
*count = h->total_words;
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void tikker_hash_free(tikker_word_hash_t *h) {
|
||||||
|
for (int i = 0; i < TIKKER_HASH_SIZE; i++) {
|
||||||
|
tikker_word_node_t *node = h->buckets[i];
|
||||||
|
while (node) {
|
||||||
|
tikker_word_node_t *next = node->next;
|
||||||
|
free(node);
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void tikker_sort_words(tikker_word_count_t *words, int count) {
|
static inline void tikker_sort_words(tikker_word_count_t *words, int count) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user