rewind-timeline 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- collectors/__init__.py +0 -0
- collectors/collect.py +52 -0
- collectors/files.py +104 -0
- collectors/packages.py +62 -0
- collectors/performance.py +50 -0
- collectors/services.py +56 -0
- collectors/shell.py +55 -0
- commands/__init__.py +0 -0
- commands/search.py +28 -0
- commands/stats.py +54 -0
- commands/today.py +31 -0
- commands/yesterday.py +35 -0
- database.py +104 -0
- rewind.py +80 -0
- rewind_timeline-0.1.0.dist-info/METADATA +274 -0
- rewind_timeline-0.1.0.dist-info/RECORD +19 -0
- rewind_timeline-0.1.0.dist-info/WHEEL +5 -0
- rewind_timeline-0.1.0.dist-info/licenses/LICENSE +21 -0
- rewind_timeline-0.1.0.dist-info/top_level.txt +4 -0
collectors/__init__.py
ADDED
|
File without changes
|
collectors/collect.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# collect.py
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
sys.path.append(
|
|
8
|
+
str(Path(__file__).resolve().parent.parent)
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from database import Database
|
|
12
|
+
|
|
13
|
+
from collectors.packages import PackageCollector
|
|
14
|
+
from collectors.services import ServiceCollector
|
|
15
|
+
from collectors.performance import PerformanceCollector
|
|
16
|
+
from collectors.shell import ShellCollector
|
|
17
|
+
from collectors.files import FileCollector
|
|
18
|
+
|
|
19
|
+
def start(stop_event=None):
|
|
20
|
+
db = Database()
|
|
21
|
+
|
|
22
|
+
packages = PackageCollector()
|
|
23
|
+
services = ServiceCollector()
|
|
24
|
+
performance = PerformanceCollector()
|
|
25
|
+
shell = ShellCollector()
|
|
26
|
+
|
|
27
|
+
# Start file monitoring (queue-based) in parallel with other collectors.
|
|
28
|
+
# Watching / can be noisy; change watch_root if needed.
|
|
29
|
+
file_collector = FileCollector(watch_path="/", recursive=True)
|
|
30
|
+
file_collector.start()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
print("Rewind monitor started.")
|
|
34
|
+
|
|
35
|
+
while True:
|
|
36
|
+
if stop_event is not None and stop_event.is_set():
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
all_events = []
|
|
40
|
+
|
|
41
|
+
all_events.extend(packages.read_new_events())
|
|
42
|
+
all_events.extend(services.check_changes())
|
|
43
|
+
all_events.extend(performance.check())
|
|
44
|
+
all_events.extend(shell.read_new_commands())
|
|
45
|
+
all_events.extend(file_collector.read_new_events())
|
|
46
|
+
|
|
47
|
+
db.add_events(all_events)
|
|
48
|
+
|
|
49
|
+
time.sleep(10)
|
|
50
|
+
|
|
51
|
+
file_collector.stop()
|
|
52
|
+
db.close()
|
collectors/files.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from queue import Queue, Empty
|
|
6
|
+
|
|
7
|
+
from watchdog.observers import Observer
|
|
8
|
+
from watchdog.events import FileSystemEventHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileCollector(FileSystemEventHandler):
|
|
12
|
+
"""Queue-based file change collector.
|
|
13
|
+
|
|
14
|
+
Produces events into an internal Queue so the main loop can batch them.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
watch_path: str | Path = "/",
|
|
20
|
+
recursive: bool = True,
|
|
21
|
+
throttle_seconds: float = 2.0,
|
|
22
|
+
):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.watch_path = Path(watch_path)
|
|
25
|
+
self.recursive = recursive
|
|
26
|
+
|
|
27
|
+
self._queue: Queue = Queue()
|
|
28
|
+
|
|
29
|
+
# Debounce by (event_type, src_path) and only emit once per throttle window.
|
|
30
|
+
self.throttle_seconds = throttle_seconds
|
|
31
|
+
self._last_emitted = {}
|
|
32
|
+
|
|
33
|
+
self._observer: Observer | None = None
|
|
34
|
+
|
|
35
|
+
def start(self):
|
|
36
|
+
if self._observer is not None:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
observer = Observer()
|
|
40
|
+
observer.schedule(self, str(self.watch_path), recursive=self.recursive)
|
|
41
|
+
observer.start()
|
|
42
|
+
self._observer = observer
|
|
43
|
+
|
|
44
|
+
def stop(self):
|
|
45
|
+
if self._observer is None:
|
|
46
|
+
return
|
|
47
|
+
self._observer.stop()
|
|
48
|
+
self._observer.join(timeout=5)
|
|
49
|
+
self._observer = None
|
|
50
|
+
|
|
51
|
+
def _enqueue(self, event_type: str, src_path: str):
|
|
52
|
+
now = time.time()
|
|
53
|
+
key = (event_type, src_path)
|
|
54
|
+
last = self._last_emitted.get(key)
|
|
55
|
+
if last is not None and (now - last) < self.throttle_seconds:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
self._last_emitted[key] = now
|
|
59
|
+
title = f"{event_type}: {src_path}"
|
|
60
|
+
|
|
61
|
+
# Keep it consistent with other collectors.
|
|
62
|
+
self._queue.put({
|
|
63
|
+
"category": "file",
|
|
64
|
+
"title": title,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
def on_modified(self, event):
|
|
68
|
+
if event.is_directory:
|
|
69
|
+
return
|
|
70
|
+
self._enqueue("Modified", event.src_path)
|
|
71
|
+
|
|
72
|
+
def on_created(self, event):
|
|
73
|
+
if event.is_directory:
|
|
74
|
+
return
|
|
75
|
+
self._enqueue("Created", event.src_path)
|
|
76
|
+
|
|
77
|
+
def on_deleted(self, event):
|
|
78
|
+
if event.is_directory:
|
|
79
|
+
return
|
|
80
|
+
self._enqueue("Deleted", event.src_path)
|
|
81
|
+
|
|
82
|
+
def read_new_events(self, max_events: int = 500):
|
|
83
|
+
events = []
|
|
84
|
+
for _ in range(max_events):
|
|
85
|
+
try:
|
|
86
|
+
events.append(self._queue.get_nowait())
|
|
87
|
+
except Empty:
|
|
88
|
+
break
|
|
89
|
+
return events
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Backwards-compatible function kept for older imports.
|
|
93
|
+
# This project primarily uses FileCollector.
|
|
94
|
+
|
|
95
|
+
def monitor(path: str | Path):
|
|
96
|
+
collector = FileCollector(watch_path=path)
|
|
97
|
+
collector.start()
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
while True:
|
|
101
|
+
time.sleep(1)
|
|
102
|
+
except KeyboardInterrupt:
|
|
103
|
+
collector.stop()
|
|
104
|
+
|
collectors/packages.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# collectors/packages.py
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
PACMAN_LOG = "/var/log/pacman.log"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PackageCollector:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.last_position = 0
|
|
11
|
+
|
|
12
|
+
def read_new_events(self):
|
|
13
|
+
events = []
|
|
14
|
+
|
|
15
|
+
if not os.path.exists(PACMAN_LOG):
|
|
16
|
+
return events
|
|
17
|
+
|
|
18
|
+
with open(PACMAN_LOG, "r") as file:
|
|
19
|
+
file.seek(self.last_position)
|
|
20
|
+
|
|
21
|
+
for line in file:
|
|
22
|
+
line = line.strip()
|
|
23
|
+
|
|
24
|
+
installed = re.search(
|
|
25
|
+
r"\[(.*?)\].*installed (.*?) \(",
|
|
26
|
+
line
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
removed = re.search(
|
|
30
|
+
r"\[(.*?)\].*removed (.*?) \(",
|
|
31
|
+
line
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
upgraded = re.search(
|
|
35
|
+
r"\[(.*?)\].*upgraded (.*?) \(",
|
|
36
|
+
line
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if installed:
|
|
40
|
+
events.append({
|
|
41
|
+
"timestamp": installed.group(1),
|
|
42
|
+
"category": "package",
|
|
43
|
+
"title": f"Installed {installed.group(2)}"
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
elif removed:
|
|
47
|
+
events.append({
|
|
48
|
+
"timestamp": removed.group(1),
|
|
49
|
+
"category": "package",
|
|
50
|
+
"title": f"Removed {removed.group(2)}"
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
elif upgraded:
|
|
54
|
+
events.append({
|
|
55
|
+
"timestamp": upgraded.group(1),
|
|
56
|
+
"category": "package",
|
|
57
|
+
"title": f"Upgraded {upgraded.group(2)}"
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
self.last_position = file.tell()
|
|
61
|
+
|
|
62
|
+
return events
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# collectors/performance.py
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PerformanceCollector:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.cpu_limit = 90
|
|
10
|
+
self.memory_limit = 90
|
|
11
|
+
self.disk_limit = 90
|
|
12
|
+
|
|
13
|
+
# Prevent alert spam when the system stays above threshold.
|
|
14
|
+
self.cooldown_seconds = 300 # 5 minutes
|
|
15
|
+
self._last_alert_ts = {}
|
|
16
|
+
|
|
17
|
+
# Warm up cpu_percent so subsequent calls don't block.
|
|
18
|
+
try:
|
|
19
|
+
psutil.cpu_percent(interval=None)
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def _emit(self, key, title, now, events):
|
|
24
|
+
last = self._last_alert_ts.get(key)
|
|
25
|
+
if last is None or (now - last) >= self.cooldown_seconds:
|
|
26
|
+
self._last_alert_ts[key] = now
|
|
27
|
+
events.append({
|
|
28
|
+
"category": "performance",
|
|
29
|
+
"title": title,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
def check(self):
|
|
33
|
+
events = []
|
|
34
|
+
now = time.time()
|
|
35
|
+
|
|
36
|
+
# Non-blocking sample after warmup.
|
|
37
|
+
cpu = psutil.cpu_percent(interval=None)
|
|
38
|
+
memory = psutil.virtual_memory().percent
|
|
39
|
+
disk = psutil.disk_usage("/").percent
|
|
40
|
+
|
|
41
|
+
if cpu >= self.cpu_limit:
|
|
42
|
+
self._emit("cpu", f"CPU usage reached {cpu}%", now, events)
|
|
43
|
+
|
|
44
|
+
if memory >= self.memory_limit:
|
|
45
|
+
self._emit("memory", f"Memory usage reached {memory}%", now, events)
|
|
46
|
+
|
|
47
|
+
if disk >= self.disk_limit:
|
|
48
|
+
self._emit("disk", f"Disk usage reached {disk}%", now, events)
|
|
49
|
+
|
|
50
|
+
return events
|
collectors/services.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# collectors/services.py
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ServiceCollector:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.previous_states = {}
|
|
9
|
+
|
|
10
|
+
def get_services(self):
|
|
11
|
+
result = subprocess.run(
|
|
12
|
+
[
|
|
13
|
+
"systemctl",
|
|
14
|
+
"list-units",
|
|
15
|
+
"--type=service",
|
|
16
|
+
"--no-pager",
|
|
17
|
+
"--no-legend"
|
|
18
|
+
],
|
|
19
|
+
capture_output=True,
|
|
20
|
+
text=True
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
services = {}
|
|
24
|
+
|
|
25
|
+
for line in result.stdout.splitlines():
|
|
26
|
+
parts = line.split()
|
|
27
|
+
|
|
28
|
+
if len(parts) >= 4:
|
|
29
|
+
name = parts[0]
|
|
30
|
+
active = parts[2]
|
|
31
|
+
|
|
32
|
+
services[name] = active
|
|
33
|
+
|
|
34
|
+
return services
|
|
35
|
+
|
|
36
|
+
def check_changes(self):
|
|
37
|
+
events = []
|
|
38
|
+
|
|
39
|
+
current = self.get_services()
|
|
40
|
+
|
|
41
|
+
for service, state in current.items():
|
|
42
|
+
old_state = self.previous_states.get(service)
|
|
43
|
+
|
|
44
|
+
if old_state is None:
|
|
45
|
+
self.previous_states[service] = state
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if old_state != state:
|
|
49
|
+
events.append({
|
|
50
|
+
"category": "service",
|
|
51
|
+
"title": f"{service} changed: {old_state} → {state}"
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
self.previous_states[service] = state
|
|
55
|
+
|
|
56
|
+
return events
|
collectors/shell.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ShellCollector:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.history_file = Path.home() / ".bash_history"
|
|
8
|
+
|
|
9
|
+
# File offset for incremental reads.
|
|
10
|
+
self.position = 0
|
|
11
|
+
self._last_seen_ts = {}
|
|
12
|
+
|
|
13
|
+
# Cooldown to reduce duplicates for the exact same command.
|
|
14
|
+
self.cooldown_seconds = 60
|
|
15
|
+
|
|
16
|
+
def _current_size(self):
|
|
17
|
+
try:
|
|
18
|
+
return self.history_file.stat().st_size
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return 0
|
|
21
|
+
|
|
22
|
+
def read_new_commands(self):
|
|
23
|
+
events = []
|
|
24
|
+
|
|
25
|
+
if not self.history_file.exists():
|
|
26
|
+
return events
|
|
27
|
+
|
|
28
|
+
current_size = self._current_size()
|
|
29
|
+
|
|
30
|
+
# Handle rotation/truncation: if file shrank, reset offset.
|
|
31
|
+
if current_size < self.position:
|
|
32
|
+
self.position = 0
|
|
33
|
+
|
|
34
|
+
now = time.time()
|
|
35
|
+
|
|
36
|
+
with open(self.history_file, "r", encoding="utf-8", errors="ignore") as file:
|
|
37
|
+
file.seek(self.position)
|
|
38
|
+
|
|
39
|
+
for line in file:
|
|
40
|
+
command = line.strip()
|
|
41
|
+
if not command:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
last = self._last_seen_ts.get(command)
|
|
45
|
+
if last is None or (now - last) >= self.cooldown_seconds:
|
|
46
|
+
self._last_seen_ts[command] = now
|
|
47
|
+
events.append({
|
|
48
|
+
"category": "shell",
|
|
49
|
+
"title": command,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
self.position = file.tell()
|
|
53
|
+
|
|
54
|
+
return events
|
|
55
|
+
|
commands/__init__.py
ADDED
|
File without changes
|
commands/search.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# commands/search.py
|
|
2
|
+
|
|
3
|
+
from database import Database
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(keyword):
|
|
7
|
+
db = Database()
|
|
8
|
+
|
|
9
|
+
results = db.search_events(keyword)
|
|
10
|
+
|
|
11
|
+
print(f"\nResults for: {keyword}\n")
|
|
12
|
+
|
|
13
|
+
if not results:
|
|
14
|
+
print("No matching events found.")
|
|
15
|
+
db.close()
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
for timestamp, category, title, details in results:
|
|
19
|
+
print(
|
|
20
|
+
f"{timestamp} "
|
|
21
|
+
f"[{category.upper()}] "
|
|
22
|
+
f"{title}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if details:
|
|
26
|
+
print(f" {details}")
|
|
27
|
+
|
|
28
|
+
db.close()
|
commands/stats.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# commands/stats.py
|
|
2
|
+
|
|
3
|
+
from database import Database
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run():
|
|
7
|
+
db = Database()
|
|
8
|
+
|
|
9
|
+
cursor = db.cursor
|
|
10
|
+
|
|
11
|
+
# Total events
|
|
12
|
+
cursor.execute("""
|
|
13
|
+
SELECT COUNT(*)
|
|
14
|
+
FROM events
|
|
15
|
+
""")
|
|
16
|
+
|
|
17
|
+
total = cursor.fetchone()[0]
|
|
18
|
+
|
|
19
|
+
print("\nRewind Statistics\n")
|
|
20
|
+
print(f"Total events: {total}\n")
|
|
21
|
+
|
|
22
|
+
# Events per category
|
|
23
|
+
print("By category:")
|
|
24
|
+
|
|
25
|
+
cursor.execute("""
|
|
26
|
+
SELECT category, COUNT(*)
|
|
27
|
+
FROM events
|
|
28
|
+
GROUP BY category
|
|
29
|
+
ORDER BY COUNT(*) DESC
|
|
30
|
+
""")
|
|
31
|
+
|
|
32
|
+
rows = cursor.fetchall()
|
|
33
|
+
|
|
34
|
+
for category, count in rows:
|
|
35
|
+
print(f"{category.upper():15} {count}")
|
|
36
|
+
|
|
37
|
+
# Most active day
|
|
38
|
+
cursor.execute("""
|
|
39
|
+
SELECT DATE(timestamp), COUNT(*)
|
|
40
|
+
FROM events
|
|
41
|
+
GROUP BY DATE(timestamp)
|
|
42
|
+
ORDER BY COUNT(*) DESC
|
|
43
|
+
LIMIT 1
|
|
44
|
+
""")
|
|
45
|
+
|
|
46
|
+
result = cursor.fetchone()
|
|
47
|
+
|
|
48
|
+
if result:
|
|
49
|
+
date, count = result
|
|
50
|
+
|
|
51
|
+
print("\nMost active day:")
|
|
52
|
+
print(f"{date} ({count} events)")
|
|
53
|
+
|
|
54
|
+
db.close()
|
commands/today.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# commands/today.py
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from database import Database
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run():
|
|
7
|
+
db = Database()
|
|
8
|
+
|
|
9
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
10
|
+
|
|
11
|
+
events = db.get_events_by_date(today)
|
|
12
|
+
|
|
13
|
+
print(f"\nRewind - {today}\n")
|
|
14
|
+
|
|
15
|
+
if not events:
|
|
16
|
+
print("No events found.")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
for timestamp, category, title, details in events:
|
|
20
|
+
time_only = timestamp.split()[1]
|
|
21
|
+
|
|
22
|
+
print(
|
|
23
|
+
f"[{time_only}] "
|
|
24
|
+
f"[{category.upper()}] "
|
|
25
|
+
f"{title}"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if details:
|
|
29
|
+
print(f" {details}")
|
|
30
|
+
|
|
31
|
+
db.close()
|
commands/yesterday.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# commands/yesterday.py
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from database import Database
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run():
|
|
8
|
+
db = Database()
|
|
9
|
+
|
|
10
|
+
yesterday = (
|
|
11
|
+
datetime.now() - timedelta(days=1)
|
|
12
|
+
).strftime("%Y-%m-%d")
|
|
13
|
+
|
|
14
|
+
events = db.get_events_by_date(yesterday)
|
|
15
|
+
|
|
16
|
+
print(f"\nRewind - {yesterday}\n")
|
|
17
|
+
|
|
18
|
+
if not events:
|
|
19
|
+
print("No events found.")
|
|
20
|
+
db.close()
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
for timestamp, category, title, details in events:
|
|
24
|
+
time_only = timestamp.split()[1]
|
|
25
|
+
|
|
26
|
+
print(
|
|
27
|
+
f"[{time_only}] "
|
|
28
|
+
f"[{category.upper()}] "
|
|
29
|
+
f"{title}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if details:
|
|
33
|
+
print(f" {details}")
|
|
34
|
+
|
|
35
|
+
db.close()
|
database.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
DB_PATH = Path.home() / ".rewind.db"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Database:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.conn = sqlite3.connect(DB_PATH)
|
|
11
|
+
self.cursor = self.conn.cursor()
|
|
12
|
+
self.create_tables()
|
|
13
|
+
|
|
14
|
+
def create_tables(self):
|
|
15
|
+
self.cursor.execute("""
|
|
16
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
17
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
timestamp TEXT NOT NULL,
|
|
19
|
+
category TEXT NOT NULL,
|
|
20
|
+
title TEXT NOT NULL,
|
|
21
|
+
details TEXT
|
|
22
|
+
)
|
|
23
|
+
""")
|
|
24
|
+
|
|
25
|
+
# Indexes for faster timeline queries as the DB grows.
|
|
26
|
+
self.cursor.execute(
|
|
27
|
+
"CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp)"
|
|
28
|
+
)
|
|
29
|
+
self.cursor.execute(
|
|
30
|
+
"CREATE INDEX IF NOT EXISTS idx_events_category ON events(category)"
|
|
31
|
+
)
|
|
32
|
+
self.cursor.execute(
|
|
33
|
+
"CREATE INDEX IF NOT EXISTS idx_events_title ON events(title)"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.conn.commit()
|
|
37
|
+
|
|
38
|
+
def add_event(self, category, title, details="", timestamp=None):
|
|
39
|
+
if timestamp is None:
|
|
40
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
41
|
+
self.cursor.execute(
|
|
42
|
+
"INSERT INTO events (timestamp, category, title, details) VALUES (?, ?, ?, ?)",
|
|
43
|
+
(timestamp, category, title, details),
|
|
44
|
+
)
|
|
45
|
+
self.conn.commit()
|
|
46
|
+
|
|
47
|
+
def add_events(self, events):
|
|
48
|
+
"""Bulk insert events.
|
|
49
|
+
|
|
50
|
+
events: iterable of dicts with keys: category, title
|
|
51
|
+
optional keys: details, timestamp
|
|
52
|
+
"""
|
|
53
|
+
rows = []
|
|
54
|
+
for event in events:
|
|
55
|
+
category = event.get("category")
|
|
56
|
+
title = event.get("title")
|
|
57
|
+
details = event.get("details", "")
|
|
58
|
+
timestamp = event.get("timestamp")
|
|
59
|
+
if category is None or title is None:
|
|
60
|
+
continue
|
|
61
|
+
if timestamp is None:
|
|
62
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
63
|
+
rows.append((timestamp, category, title, details))
|
|
64
|
+
|
|
65
|
+
if not rows:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
self.cursor.executemany(
|
|
69
|
+
"INSERT INTO events (timestamp, category, title, details) VALUES (?, ?, ?, ?)",
|
|
70
|
+
rows,
|
|
71
|
+
)
|
|
72
|
+
self.conn.commit()
|
|
73
|
+
|
|
74
|
+
def get_all_events(self):
|
|
75
|
+
self.cursor.execute("""
|
|
76
|
+
SELECT id, timestamp, category, title, details
|
|
77
|
+
FROM events
|
|
78
|
+
ORDER BY timestamp DESC
|
|
79
|
+
""")
|
|
80
|
+
|
|
81
|
+
return self.cursor.fetchall()
|
|
82
|
+
|
|
83
|
+
def get_events_by_date(self, date):
|
|
84
|
+
self.cursor.execute("""
|
|
85
|
+
SELECT timestamp, category, title, details
|
|
86
|
+
FROM events
|
|
87
|
+
WHERE DATE(timestamp) = ?
|
|
88
|
+
ORDER BY timestamp
|
|
89
|
+
""", (date,))
|
|
90
|
+
|
|
91
|
+
return self.cursor.fetchall()
|
|
92
|
+
|
|
93
|
+
def search_events(self, keyword):
|
|
94
|
+
self.cursor.execute("""
|
|
95
|
+
SELECT timestamp, category, title, details
|
|
96
|
+
FROM events
|
|
97
|
+
WHERE title LIKE ? OR details LIKE ?
|
|
98
|
+
ORDER BY timestamp DESC
|
|
99
|
+
""", (f"%{keyword}%", f"%{keyword}%"))
|
|
100
|
+
|
|
101
|
+
return self.cursor.fetchall()
|
|
102
|
+
|
|
103
|
+
def close(self):
|
|
104
|
+
self.conn.close()
|
rewind.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import signal
|
|
6
|
+
|
|
7
|
+
from commands import today
|
|
8
|
+
from commands import yesterday
|
|
9
|
+
from commands import search
|
|
10
|
+
from commands import stats
|
|
11
|
+
from collectors import collect
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def show_help():
|
|
16
|
+
print("""
|
|
17
|
+
Rewind - Linux Time Machine
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
rewind today
|
|
21
|
+
rewind yesterday
|
|
22
|
+
rewind search <keyword>
|
|
23
|
+
rewind stats
|
|
24
|
+
rewind help
|
|
25
|
+
""")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
stop_event = threading.Event()
|
|
30
|
+
|
|
31
|
+
def start_collector():
|
|
32
|
+
collect.start(stop_event=stop_event)
|
|
33
|
+
|
|
34
|
+
collector_thread = threading.Thread(target=start_collector, daemon=True)
|
|
35
|
+
|
|
36
|
+
def _handle_signal(signum, frame):
|
|
37
|
+
stop_event.set()
|
|
38
|
+
raise SystemExit(0)
|
|
39
|
+
|
|
40
|
+
signal.signal(signal.SIGINT, _handle_signal)
|
|
41
|
+
signal.signal(signal.SIGTERM, _handle_signal)
|
|
42
|
+
|
|
43
|
+
collector_thread.start()
|
|
44
|
+
|
|
45
|
+
if len(sys.argv) < 2:
|
|
46
|
+
show_help()
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
command = sys.argv[1]
|
|
50
|
+
|
|
51
|
+
if command == "monitor":
|
|
52
|
+
collect.start()
|
|
53
|
+
|
|
54
|
+
elif command == "today":
|
|
55
|
+
today.run()
|
|
56
|
+
|
|
57
|
+
elif command == "yesterday":
|
|
58
|
+
yesterday.run()
|
|
59
|
+
|
|
60
|
+
elif command == "search":
|
|
61
|
+
if len(sys.argv) < 3:
|
|
62
|
+
print("Error: missing search keyword.")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
keyword = " ".join(sys.argv[2:])
|
|
66
|
+
search.run(keyword)
|
|
67
|
+
|
|
68
|
+
elif command == "stats":
|
|
69
|
+
stats.run()
|
|
70
|
+
|
|
71
|
+
elif command == "help":
|
|
72
|
+
show_help()
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
print(f"Unknown command: {command}")
|
|
76
|
+
show_help()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rewind-timeline
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Linux timeline collector and monitoring tool
|
|
5
|
+
Home-page: https://github.com/LaVenganzaDelLadron/rewind.git
|
|
6
|
+
Author: DarkGlitch
|
|
7
|
+
Author-email: darkglitch5417@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# Rewind
|
|
15
|
+
|
|
16
|
+
**Rewind** is a Linux Time Machine that records important system events and allows users to revisit what happened on their machine.
|
|
17
|
+
|
|
18
|
+
It helps answer questions such as:
|
|
19
|
+
|
|
20
|
+
* What changed yesterday?
|
|
21
|
+
* Why did my system become slow?
|
|
22
|
+
* Which package was installed?
|
|
23
|
+
* Which service stopped?
|
|
24
|
+
* What commands did I run?
|
|
25
|
+
* Which files were modified?
|
|
26
|
+
|
|
27
|
+
Rewind stores these events locally in a SQLite database and provides a simple command-line interface for exploring the system timeline.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
* Package monitoring (Pacman support)
|
|
34
|
+
* Service monitoring
|
|
35
|
+
* Performance monitoring
|
|
36
|
+
* Shell history tracking
|
|
37
|
+
* File change monitoring
|
|
38
|
+
* Timeline search
|
|
39
|
+
* Daily activity reports
|
|
40
|
+
* System statistics
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Project Structure
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
rewind/
|
|
48
|
+
├── collect.py
|
|
49
|
+
├── rewind.py
|
|
50
|
+
├── database.py
|
|
51
|
+
│
|
|
52
|
+
├── collectors/
|
|
53
|
+
│ ├── files.py
|
|
54
|
+
│ ├── packages.py
|
|
55
|
+
│ ├── performance.py
|
|
56
|
+
│ ├── services.py
|
|
57
|
+
│ └── shell.py
|
|
58
|
+
│
|
|
59
|
+
├── commands/
|
|
60
|
+
│ ├── search.py
|
|
61
|
+
│ ├── stats.py
|
|
62
|
+
│ ├── today.py
|
|
63
|
+
│ └── yesterday.py
|
|
64
|
+
│
|
|
65
|
+
└── rewind.db
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Requirements
|
|
71
|
+
|
|
72
|
+
* Python 3.10+
|
|
73
|
+
* Linux
|
|
74
|
+
* systemd
|
|
75
|
+
* SQLite
|
|
76
|
+
|
|
77
|
+
Install dependencies:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install psutil watchdog
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Running the Collector
|
|
86
|
+
|
|
87
|
+
The collector continuously monitors the system and stores events.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python collect.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The collector records:
|
|
94
|
+
|
|
95
|
+
* package changes
|
|
96
|
+
* service state changes
|
|
97
|
+
* performance alerts
|
|
98
|
+
* shell history
|
|
99
|
+
* file modifications
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
### View today's events
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python rewind.py today
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### View yesterday's events
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python rewind.py yesterday
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Search the timeline
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python rewind.py search nginx
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### View statistics
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
python rewind.py stats
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Show help
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python rewind.py help
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Example
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
$ python rewind.py today
|
|
141
|
+
|
|
142
|
+
Rewind - 2026-06-27
|
|
143
|
+
|
|
144
|
+
[08:32:10] [PACKAGE] Installed nginx
|
|
145
|
+
[09:15:22] [FILE] Modified /etc/ssh/sshd_config
|
|
146
|
+
[11:10:33] [PERFORMANCE] CPU usage reached 95%
|
|
147
|
+
[13:00:17] [SERVICE] nginx.service restarted
|
|
148
|
+
[13:01:55] [SHELL] sudo systemctl restart nginx
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Database
|
|
154
|
+
|
|
155
|
+
Rewind stores events inside a local SQLite database.
|
|
156
|
+
|
|
157
|
+
Location:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
~/.rewind.db
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Schema:
|
|
164
|
+
|
|
165
|
+
```sql
|
|
166
|
+
CREATE TABLE events (
|
|
167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
168
|
+
timestamp TEXT NOT NULL,
|
|
169
|
+
category TEXT NOT NULL,
|
|
170
|
+
title TEXT NOT NULL,
|
|
171
|
+
details TEXT
|
|
172
|
+
);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Current Collectors
|
|
178
|
+
|
|
179
|
+
### Package Collector
|
|
180
|
+
|
|
181
|
+
Parses:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
/var/log/pacman.log
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Tracks:
|
|
188
|
+
|
|
189
|
+
* installed packages
|
|
190
|
+
* removed packages
|
|
191
|
+
* upgraded packages
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### Service Collector
|
|
196
|
+
|
|
197
|
+
Uses:
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
systemctl
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Tracks:
|
|
204
|
+
|
|
205
|
+
* started services
|
|
206
|
+
* stopped services
|
|
207
|
+
* restarted services
|
|
208
|
+
* failed services
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### Performance Collector
|
|
213
|
+
|
|
214
|
+
Monitors:
|
|
215
|
+
|
|
216
|
+
* CPU usage
|
|
217
|
+
* memory usage
|
|
218
|
+
* disk usage
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### Shell Collector
|
|
223
|
+
|
|
224
|
+
Reads:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
~/.bash_history
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Tracks executed commands.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### File Collector
|
|
235
|
+
|
|
236
|
+
Uses:
|
|
237
|
+
|
|
238
|
+
```text
|
|
239
|
+
watchdog
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Tracks:
|
|
243
|
+
|
|
244
|
+
* created files
|
|
245
|
+
* modified files
|
|
246
|
+
* deleted files
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Roadmap
|
|
251
|
+
|
|
252
|
+
* systemd service support
|
|
253
|
+
* daemon mode
|
|
254
|
+
* multi-distribution package support
|
|
255
|
+
* export reports
|
|
256
|
+
* weekly summaries
|
|
257
|
+
* interactive TUI
|
|
258
|
+
* notifications
|
|
259
|
+
* command learning mode
|
|
260
|
+
* performance history graphs
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT License
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Author
|
|
271
|
+
|
|
272
|
+
Created by DarkGlitch.
|
|
273
|
+
|
|
274
|
+
Rewind aims to become a personal timeline for Linux systems.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
database.py,sha256=jEEnNslQ9AJx-Ag29jql8wgyrDg52aBJbkBgr-yTQGA,3116
|
|
2
|
+
rewind.py,sha256=dfy_iV4jEcFsKOak8meqad_i_2PDRzwYWxcFtvFdqR0,1475
|
|
3
|
+
collectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
collectors/collect.py,sha256=uxVsd8yP-S1faGVoLfaKtYMR_Iy_9hfM07k-LSaSOwU,1349
|
|
5
|
+
collectors/files.py,sha256=0uFEgq2FeRuz184D-IolHk5OXMviSMmKkMwioyOwAEA,2748
|
|
6
|
+
collectors/packages.py,sha256=yM_I1h10CcagxqLat8f6FR-nFrpBCqSR8Axk8sZS0Jo,1686
|
|
7
|
+
collectors/performance.py,sha256=csT3aOoCX28fYH0UNalIU2xjnQ6lHn8vZDf_5bcteUk,1452
|
|
8
|
+
collectors/services.py,sha256=PkmXJxDDk7jlcq1fTBXEZe8O7IpYE_wnLN5yOzI3P6g,1298
|
|
9
|
+
collectors/shell.py,sha256=23ykBbNMTup4i2PBxJ-j8wmjbiqkmeDJuEdK2RXoe-A,1487
|
|
10
|
+
commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
commands/search.py,sha256=7kJZXJZnXrd28fx9hqP_-PzdcXO5Zwl1B7CB_MQ36ns,515
|
|
12
|
+
commands/stats.py,sha256=UpQvCqzaDSTZY9boz7aaX5alNJeA-U6wfi6K3NLDQVY,988
|
|
13
|
+
commands/today.py,sha256=LJ0B2kB-iinAlFjKGs5yrncszDFA7VxqUGAU_2sX1e0,595
|
|
14
|
+
commands/yesterday.py,sha256=3T91mOAQhTff6RHIJGoV3LghvvCMh5WKypd5ugUksj8,678
|
|
15
|
+
rewind_timeline-0.1.0.dist-info/licenses/LICENSE,sha256=zRp28a4eWydIgwuAKXlfZPfHFcpLZh5eHVjAhiC6crU,1067
|
|
16
|
+
rewind_timeline-0.1.0.dist-info/METADATA,sha256=XazGnysTja1Wal3Q-VhJ_rjsBaS9mDh9X4ZVknemjng,3733
|
|
17
|
+
rewind_timeline-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
rewind_timeline-0.1.0.dist-info/top_level.txt,sha256=jyAjhXtmhL_E25CUy3kPoeWOnSxauknzUKwkFqhjAbY,36
|
|
19
|
+
rewind_timeline-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DarkGlitch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|