// main.go package main import ( "database/sql" "flag" "log" "os/exec" "regexp" "sort" "strconv" "strings" "time" _ "github.com/mattn/go-sqlite3" ) const ( // time between processing xinput output SLEEP_TIME = 3 * time.Second // Buffer that we read from xinput KEYBOARD_BUFER_SIZE = 10000 // Database that stores keylog and other settings DATABASE_NAME = "file:gokeystat.db?cache=shared&mode=rwc" // time in seconds between capturing keyboard to db CAPTURE_TIME = 900 ) // StatForTime stotres pressed keys and beginning time type StatForTime struct { time int64 keys map[uint8]int } // Init set time to Now().Unix() and keys to empty map func (stat *StatForTime) Init(timePeriodEnd int64) { stat.time = timePeriodEnd - CAPTURE_TIME stat.keys = make(map[uint8]int) } // GetKeymap returns map from key numbers to key names like "F1", "Tab", "d" func GetKeymap() map[uint8]string { return GetKeymapFromOutput(GetKeymapOutput()) } // GetKeymapOutput returns output of utility that prints system keymap func GetKeymapOutput() []byte { cmd := exec.Command("xmodmap", "-pke") out, err := cmd.Output() if err != nil { log.Fatal(err) } return out } // GetKeymapFromOutput returns map with keymap from text func GetKeymapFromOutput(buf []byte) map[uint8]string { const KEY_NUM_STRING_RE = "\\d+[ ]*=[ ]*\\S+" re := regexp.MustCompile(KEY_NUM_STRING_RE) resByte := re.FindAll(buf, -1) keyMap := make(map[uint8]string) for _, line := range resByte { lineSpitted := strings.Split(string(line), " ") if key, err := strconv.Atoi(lineSpitted[0]); err == nil { keyMap[uint8(key)] = lineSpitted[2] } } return keyMap } // GetKeyNumsFromOutput extract pressed keys from bufer buf // It returns slice with key numbers in the same order func GetKeyNumsFromOutput(buf []byte) []uint8 { const KEY_NUM_STRING_RE = "press[ ]+(\\d+)" re := regexp.MustCompile(KEY_NUM_STRING_RE) resByte := re.FindAll(buf, -1) keyNums := make([]uint8, len(resByte)) re = regexp.MustCompile("\\d+") for i, line := range resByte { numByte := re.Find(line) if num, err := strconv.Atoi(string(numByte)); err == nil { keyNums[i] = uint8(num) } else { log.Fatal(err) } } return keyNums } // GetKeyNumsFromKeyMap returns sorted slice with key nums of keyMap func GetKeyNumsFromKeyMap(keyMap map[uint8]string) []int { res := make([]int, 0, len(keyMap)) for keyNum := range keyMap { res = append(res, int(keyNum)) } sort.Ints(res) return res } // InitDb creates tables, inserts keymap to db func InitDb(db *sql.DB, keyMap map[uint8]string) { keyNums := GetKeyNumsFromKeyMap(keyMap) sqlInit := `CREATE TABLE IF NOT EXISTS keylog ( time INTEGER primary key` for _, keyNum := range keyNums { sqlInit += ",\n" + "KEY" + strconv.Itoa(keyNum) + " INTEGER" } sqlInit += "\n);" // Inserting keymap to table sqlInit += ` CREATE TABLE IF NOT EXISTS keymap ( num INTEGER primary key, value STRING );` _, err := db.Exec(sqlInit) if err != nil { log.Fatalf("%q: %s\n", err, sqlInit) } rows, err := db.Query("SELECT COUNT(*) FROM keymap") if err != nil { log.Fatal(err) } var rowsCount int rows.Next() rows.Scan(&rowsCount) if rowsCount > 0 { // already inserted keymap return } rows.Close() tx, err := db.Begin() if err != nil { log.Fatal(err) } stmt, err := tx.Prepare("INSERT INTO keymap(num, value) VALUES(?, ?)") if err != nil { log.Fatal(err) } defer stmt.Close() for keyNum, keyName := range keyMap { _, err = stmt.Exec(keyNum, keyName) if err != nil { log.Fatal(err) } } tx.Commit() } //AddStatTimeToDb insert statTime to db with using keyMap for extracting key nums func AddStatTimeToDb(db *sql.DB, statTime StatForTime, keyMap map[uint8]string) { keyNums := GetKeyNumsFromKeyMap(keyMap) sqlStmt := "insert into keylog(time" for _, keyNum := range keyNums { sqlStmt += ",\n" + "KEY" + strconv.Itoa(keyNum) } sqlStmt += ") values " sqlStmt += "(" + strconv.FormatInt(statTime.time, 10) for _, keyNum := range keyNums { keyNumber, _ := statTime.keys[uint8(keyNum)] sqlStmt += ",\n" + strconv.Itoa(keyNumber) } sqlStmt += ")" tx, err := db.Begin() if err != nil { log.Fatal(err) } _, err = tx.Exec(sqlStmt) if err != nil { log.Printf("%q: %s\n", err, sqlStmt) } tx.Commit() } // GetStatTimesFromDb returns slice with StatForTime objects that func GetStatTimesFromDb(db *sql.DB, fromTime int64, keyMap map[uint8]string) []StatForTime { sqlStmt := "select * from keylog where time > " + strconv.FormatInt(fromTime, 10) rows, err := db.Query(sqlStmt) if err != nil { log.Fatalln("Error with query", sqlStmt, " is ", err) } defer rows.Close() cols, err := rows.Columns() if err != nil { log.Fatalln("Failed to get columns", err) } rawResult := make([][]byte, len(cols)) result := make([]int64, len(cols)) dest := make([]interface{}, len(cols)) // A temporary interface{} slice for i := range rawResult { dest[i] = &rawResult[i] // Put pointers to each string in the interface slice } // keyNums[i] stores i-th keynum keyNums := GetKeyNumsFromKeyMap(keyMap) // result var res []StatForTime for rows.Next() { err = rows.Scan(dest...) if err != nil { log.Fatalln("Failed to scan row", err) } for i, raw := range rawResult { if raw == nil { result[i] = 0 } else { // Only numbers in db: converting it to int64 result[i], err = strconv.ParseInt(string(raw), 10, 64) if err != nil { log.Fatalln("Error when parsing ", raw, " from db:", err) } } } var resStatTime StatForTime resStatTime.time = result[0] resStatTime.keys = make(map[uint8]int) for index, val := range result[1:] { if val == 0 { continue } resStatTime.keys[uint8(keyNums[index])] = int(val) } res = append(res, resStatTime) } if err = rows.Err(); err != nil { log.Fatalln("Error when iterating over rows", err) } return res } // GetFileType extract extension from path if we support it // Allowed if non-supported result with non supported path is wrong func GetFileType(path string) string { if len(path) == 0 { return "" } Point := false for _, c := range path { if c == '.' { Point = true } } if !Point { return "" } path = strings.ToLower(path) if len(path) > 3 { if path[len(path)-3:] == ".gz" { return GetFileType(path[:len(path)-3]) + ".gz" } } i := len(path) - 1 for path[i] != '.' { i-- if i < 0 { break } } return path[i+1:] } func main() { keyboardID := flag.Int("id", -1, "Your keyboard id") outputPath := flag.String("o", "", "Path to export file") fullExport := flag.Bool("full", false, "Export full stats") flag.Parse() log.Println("keyboardID =", *keyboardID, "outputPath =", *outputPath) // Opening database db, err := sql.Open("sqlite3", DATABASE_NAME) if err != nil { log.Fatal(err) } db.SetMaxIdleConns(5) db.SetMaxOpenConns(5) defer db.Close() keyMap := GetKeymap() InitDb(db, keyMap) switch { case *keyboardID == -1 && *outputPath == "": flag.PrintDefaults() return case *keyboardID != -1: cmd := exec.Command("xinput", "test", strconv.Itoa(*keyboardID)) stdout, err := cmd.StdoutPipe() if err != nil { log.Fatal(err) } if err := cmd.Start(); err != nil { log.Fatal(err) } // output of xinput command buf := make([]byte, KEYBOARD_BUFER_SIZE) timePeriodEnd := time.Now().Unix() - time.Now().Unix()%CAPTURE_TIME + CAPTURE_TIME var curStat StatForTime curStat.Init(timePeriodEnd) for { n, err := stdout.Read(buf) if err != nil { log.Fatal(err) } // processing buf here for _, keyNum := range GetKeyNumsFromOutput(buf[:n]) { oldKeyCount, _ := curStat.keys[keyNum] curStat.keys[keyNum] = oldKeyCount + 1 } // Every CAPTURE_TIME seconds save to BD if time.Now().Unix()-timePeriodEnd > 0 { AddStatTimeToDb(db, curStat, keyMap) timePeriodEnd += CAPTURE_TIME curStat.Init(timePeriodEnd) } time.Sleep(SLEEP_TIME) } case *outputPath != "": exportingData := GetStatTimesFromDb(db, 0, keyMap) filetype := GetFileType(*outputPath) log.Println(filetype) switch filetype { case "csv": SaveToCsvFile(exportingData, keyMap, *outputPath, *fullExport) case "json": SaveToJSONFile(exportingData, keyMap, *outputPath, *fullExport) case "jsl": SaveToJSLFile(exportingData, keyMap, *outputPath, *fullExport) case "csv.gz": SaveToCsvGzFile(exportingData, keyMap, *outputPath, *fullExport) case "json.gz": SaveToJSONGzFile(exportingData, keyMap, *outputPath, *fullExport) case "jsl.gz": SaveToJSLGzFile(exportingData, keyMap, *outputPath, *fullExport) default: log.Fatal("Incorrect file type") } } }