snooper-pkg 0.0.1__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.
- snooper_pkg/__init__.py +1 -0
- snooper_pkg/_modidx.py +102 -0
- snooper_pkg/config.py +54 -0
- snooper_pkg/db.py +304 -0
- snooper_pkg/foreground_tracker.py +98 -0
- snooper_pkg/idle_detector.py +54 -0
- snooper_pkg/main.py +110 -0
- snooper_pkg/monitoring_controller.py +238 -0
- snooper_pkg/process_monitor.py +50 -0
- snooper_pkg/report_window.py +117 -0
- snooper_pkg/reporter.py +157 -0
- snooper_pkg/tray.py +171 -0
- snooper_pkg-0.0.1.dist-info/METADATA +115 -0
- snooper_pkg-0.0.1.dist-info/RECORD +18 -0
- snooper_pkg-0.0.1.dist-info/WHEEL +5 -0
- snooper_pkg-0.0.1.dist-info/entry_points.txt +5 -0
- snooper_pkg-0.0.1.dist-info/licenses/LICENSE +201 -0
- snooper_pkg-0.0.1.dist-info/top_level.txt +1 -0
snooper_pkg/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
snooper_pkg/_modidx.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Autogenerated by nbdev
|
|
2
|
+
|
|
3
|
+
d = { 'settings': { 'branch': 'main',
|
|
4
|
+
'doc_baseurl': '/snooper_pkg',
|
|
5
|
+
'doc_host': 'https://sayanbanerjee32.github.io',
|
|
6
|
+
'git_url': 'https://github.com/sayanbanerjee32/snooper_pkg',
|
|
7
|
+
'lib_path': 'snooper_pkg'},
|
|
8
|
+
'syms': { 'snooper_pkg.config': {},
|
|
9
|
+
'snooper_pkg.db': { 'snooper_pkg.db.ForegroundEvent': ('db.html#foregroundevent', 'snooper_pkg/db.py'),
|
|
10
|
+
'snooper_pkg.db.ProcessEvent': ('db.html#processevent', 'snooper_pkg/db.py'),
|
|
11
|
+
'snooper_pkg.db.Session': ('db.html#session', 'snooper_pkg/db.py'),
|
|
12
|
+
'snooper_pkg.db._db_timestamp': ('db.html#_db_timestamp', 'snooper_pkg/db.py'),
|
|
13
|
+
'snooper_pkg.db.close_stale_open_sessions': ('db.html#close_stale_open_sessions', 'snooper_pkg/db.py'),
|
|
14
|
+
'snooper_pkg.db.delete_old_data': ('db.html#delete_old_data', 'snooper_pkg/db.py'),
|
|
15
|
+
'snooper_pkg.db.end_session': ('db.html#end_session', 'snooper_pkg/db.py'),
|
|
16
|
+
'snooper_pkg.db.get_foreground_events': ('db.html#get_foreground_events', 'snooper_pkg/db.py'),
|
|
17
|
+
'snooper_pkg.db.get_last_completed_session': ('db.html#get_last_completed_session', 'snooper_pkg/db.py'),
|
|
18
|
+
'snooper_pkg.db.get_processes': ('db.html#get_processes', 'snooper_pkg/db.py'),
|
|
19
|
+
'snooper_pkg.db.get_session_by_session_id': ('db.html#get_session_by_session_id', 'snooper_pkg/db.py'),
|
|
20
|
+
'snooper_pkg.db.init_db': ('db.html#init_db', 'snooper_pkg/db.py'),
|
|
21
|
+
'snooper_pkg.db.log_fg_app_events': ('db.html#log_fg_app_events', 'snooper_pkg/db.py'),
|
|
22
|
+
'snooper_pkg.db.log_process_event': ('db.html#log_process_event', 'snooper_pkg/db.py'),
|
|
23
|
+
'snooper_pkg.db.start_session': ('db.html#start_session', 'snooper_pkg/db.py')},
|
|
24
|
+
'snooper_pkg.foreground_tracker': { 'snooper_pkg.foreground_tracker._require_windows': ( 'foreground_tracker.html#_require_windows',
|
|
25
|
+
'snooper_pkg/foreground_tracker.py'),
|
|
26
|
+
'snooper_pkg.foreground_tracker.get_foreground_window_info': ( 'foreground_tracker.html#get_foreground_window_info',
|
|
27
|
+
'snooper_pkg/foreground_tracker.py'),
|
|
28
|
+
'snooper_pkg.foreground_tracker.is_streaming_app': ( 'foreground_tracker.html#is_streaming_app',
|
|
29
|
+
'snooper_pkg/foreground_tracker.py'),
|
|
30
|
+
'snooper_pkg.foreground_tracker.start_foreground_app_monitoring': ( 'foreground_tracker.html#start_foreground_app_monitoring',
|
|
31
|
+
'snooper_pkg/foreground_tracker.py')},
|
|
32
|
+
'snooper_pkg.idle_detector': { 'snooper_pkg.idle_detector.LASTINPUTINFO': ( 'idle_detector.html#lastinputinfo',
|
|
33
|
+
'snooper_pkg/idle_detector.py'),
|
|
34
|
+
'snooper_pkg.idle_detector._require_windows': ( 'idle_detector.html#_require_windows',
|
|
35
|
+
'snooper_pkg/idle_detector.py'),
|
|
36
|
+
'snooper_pkg.idle_detector.get_idle_seconds': ( 'idle_detector.html#get_idle_seconds',
|
|
37
|
+
'snooper_pkg/idle_detector.py'),
|
|
38
|
+
'snooper_pkg.idle_detector.get_last_input_tick': ( 'idle_detector.html#get_last_input_tick',
|
|
39
|
+
'snooper_pkg/idle_detector.py')},
|
|
40
|
+
'snooper_pkg.main': { 'snooper_pkg.main._acquire_single_instance': ( 'main.html#_acquire_single_instance',
|
|
41
|
+
'snooper_pkg/main.py'),
|
|
42
|
+
'snooper_pkg.main.configure_logging': ('main.html#configure_logging', 'snooper_pkg/main.py'),
|
|
43
|
+
'snooper_pkg.main.main': ('main.html#main', 'snooper_pkg/main.py')},
|
|
44
|
+
'snooper_pkg.monitoring_controller': { 'snooper_pkg.monitoring_controller.MonitorController': ( 'monitoring_controller.html#monitorcontroller',
|
|
45
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
46
|
+
'snooper_pkg.monitoring_controller.MonitorController.__init__': ( 'monitoring_controller.html#monitorcontroller.__init__',
|
|
47
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
48
|
+
'snooper_pkg.monitoring_controller.MonitorController._auto_resume': ( 'monitoring_controller.html#monitorcontroller._auto_resume',
|
|
49
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
50
|
+
'snooper_pkg.monitoring_controller.MonitorController._midnight_rollover': ( 'monitoring_controller.html#monitorcontroller._midnight_rollover',
|
|
51
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
52
|
+
'snooper_pkg.monitoring_controller.MonitorController.generate_current_session_stats': ( 'monitoring_controller.html#monitorcontroller.generate_current_session_stats',
|
|
53
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
54
|
+
'snooper_pkg.monitoring_controller.MonitorController.generate_last_report_data': ( 'monitoring_controller.html#monitorcontroller.generate_last_report_data',
|
|
55
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
56
|
+
'snooper_pkg.monitoring_controller.MonitorController.is_paused': ( 'monitoring_controller.html#monitorcontroller.is_paused',
|
|
57
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
58
|
+
'snooper_pkg.monitoring_controller.MonitorController.is_running': ( 'monitoring_controller.html#monitorcontroller.is_running',
|
|
59
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
60
|
+
'snooper_pkg.monitoring_controller.MonitorController.pause_for': ( 'monitoring_controller.html#monitorcontroller.pause_for',
|
|
61
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
62
|
+
'snooper_pkg.monitoring_controller.MonitorController.resume_now': ( 'monitoring_controller.html#monitorcontroller.resume_now',
|
|
63
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
64
|
+
'snooper_pkg.monitoring_controller.MonitorController.run_startup_housekeeping': ( 'monitoring_controller.html#monitorcontroller.run_startup_housekeeping',
|
|
65
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
66
|
+
'snooper_pkg.monitoring_controller.MonitorController.start_monitoring': ( 'monitoring_controller.html#monitorcontroller.start_monitoring',
|
|
67
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
68
|
+
'snooper_pkg.monitoring_controller.MonitorController.stop_monitoring': ( 'monitoring_controller.html#monitorcontroller.stop_monitoring',
|
|
69
|
+
'snooper_pkg/monitoring_controller.py'),
|
|
70
|
+
'snooper_pkg.monitoring_controller.seconds_until_midnight': ( 'monitoring_controller.html#seconds_until_midnight',
|
|
71
|
+
'snooper_pkg/monitoring_controller.py')},
|
|
72
|
+
'snooper_pkg.process_monitor': { 'snooper_pkg.process_monitor.get_current_processes': ( 'process_monitor.html#get_current_processes',
|
|
73
|
+
'snooper_pkg/process_monitor.py'),
|
|
74
|
+
'snooper_pkg.process_monitor.start_process_monitoring': ( 'process_monitor.html#start_process_monitoring',
|
|
75
|
+
'snooper_pkg/process_monitor.py')},
|
|
76
|
+
'snooper_pkg.report_window': { 'snooper_pkg.report_window.fmt_duration': ( 'report_window.html#fmt_duration',
|
|
77
|
+
'snooper_pkg/report_window.py'),
|
|
78
|
+
'snooper_pkg.report_window.show_apps': ( 'report_window.html#show_apps',
|
|
79
|
+
'snooper_pkg/report_window.py'),
|
|
80
|
+
'snooper_pkg.report_window.show_report': ( 'report_window.html#show_report',
|
|
81
|
+
'snooper_pkg/report_window.py'),
|
|
82
|
+
'snooper_pkg.report_window.show_summary': ( 'report_window.html#show_summary',
|
|
83
|
+
'snooper_pkg/report_window.py'),
|
|
84
|
+
'snooper_pkg.report_window.show_titles': ( 'report_window.html#show_titles',
|
|
85
|
+
'snooper_pkg/report_window.py'),
|
|
86
|
+
'snooper_pkg.report_window.update_titles': ( 'report_window.html#update_titles',
|
|
87
|
+
'snooper_pkg/report_window.py')},
|
|
88
|
+
'snooper_pkg.reporter': { 'snooper_pkg.reporter._clean_events': ('reporter.html#_clean_events', 'snooper_pkg/reporter.py'),
|
|
89
|
+
'snooper_pkg.reporter._events_data': ('reporter.html#_events_data', 'snooper_pkg/reporter.py'),
|
|
90
|
+
'snooper_pkg.reporter._session_data': ('reporter.html#_session_data', 'snooper_pkg/reporter.py'),
|
|
91
|
+
'snooper_pkg.reporter._summary_data': ('reporter.html#_summary_data', 'snooper_pkg/reporter.py'),
|
|
92
|
+
'snooper_pkg.reporter.build_session_report_data': ( 'reporter.html#build_session_report_data',
|
|
93
|
+
'snooper_pkg/reporter.py')},
|
|
94
|
+
'snooper_pkg.tray': { 'snooper_pkg.tray.UiDispatcher': ('tray.html#uidispatcher', 'snooper_pkg/tray.py'),
|
|
95
|
+
'snooper_pkg.tray.UiDispatcher.__init__': ('tray.html#uidispatcher.__init__', 'snooper_pkg/tray.py'),
|
|
96
|
+
'snooper_pkg.tray.UiDispatcher._process': ('tray.html#uidispatcher._process', 'snooper_pkg/tray.py'),
|
|
97
|
+
'snooper_pkg.tray.UiDispatcher.submit': ('tray.html#uidispatcher.submit', 'snooper_pkg/tray.py'),
|
|
98
|
+
'snooper_pkg.tray.open_current_session_report': ( 'tray.html#open_current_session_report',
|
|
99
|
+
'snooper_pkg/tray.py'),
|
|
100
|
+
'snooper_pkg.tray.open_last_session_report': ( 'tray.html#open_last_session_report',
|
|
101
|
+
'snooper_pkg/tray.py'),
|
|
102
|
+
'snooper_pkg.tray.run_tray': ('tray.html#run_tray', 'snooper_pkg/tray.py')}}}
|
snooper_pkg/config.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Central configuration values for Snooper's monitoring, data retention, reporting, privacy, and pause behaviour.
|
|
2
|
+
|
|
3
|
+
Docs: https://sayanbanerjee32.github.io/snooper_pkg/config.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_config.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['APP_NAME', 'LOG_FILENAME', 'LOG_LEVEL', 'PROCESS_MONITORING_INTERVAL_SECONDS', 'EXCLUDED_PROCESSES',
|
|
9
|
+
'DATABASE_FILENAME', 'DATA_RETENTION_DAYS', 'FG_APP_MONITORING_INTERVAL_SECONDS',
|
|
10
|
+
'FG_APP_MONITORING_MIN_IDLE_SECONDS', 'STREAMING_APP_MIN_IDLE_SECONDS', 'NUM_TOP_APP_IN_REPORT',
|
|
11
|
+
'NUM_TOP_WINDOW_IN_REPORT', 'SHOW_WINDOW_TITLES', 'DEFAULT_PAUSE_MINUTES']
|
|
12
|
+
|
|
13
|
+
# %% ../nbs/00_config.ipynb #f2a11914
|
|
14
|
+
APP_NAME = "Snooper"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# %% ../nbs/00_config.ipynb #2c47f2da
|
|
18
|
+
LOG_FILENAME = "snooper.log"
|
|
19
|
+
|
|
20
|
+
# %% ../nbs/00_config.ipynb #e0c32039
|
|
21
|
+
LOG_LEVEL = "WARNING" # DEBUG, INFO, WARNING, ERROR
|
|
22
|
+
|
|
23
|
+
# %% ../nbs/00_config.ipynb #f99ee53a
|
|
24
|
+
PROCESS_MONITORING_INTERVAL_SECONDS = 5
|
|
25
|
+
|
|
26
|
+
# %% ../nbs/00_config.ipynb #f8d4d24d
|
|
27
|
+
EXCLUDED_PROCESSES = {'sudo','python'}
|
|
28
|
+
|
|
29
|
+
# %% ../nbs/00_config.ipynb #13babdaf
|
|
30
|
+
DATABASE_FILENAME = "snooper.db"
|
|
31
|
+
|
|
32
|
+
# %% ../nbs/00_config.ipynb #941e072f
|
|
33
|
+
DATA_RETENTION_DAYS = 7
|
|
34
|
+
|
|
35
|
+
# %% ../nbs/00_config.ipynb #d380659a
|
|
36
|
+
FG_APP_MONITORING_INTERVAL_SECONDS = 2
|
|
37
|
+
|
|
38
|
+
# %% ../nbs/00_config.ipynb #97baa2a4
|
|
39
|
+
FG_APP_MONITORING_MIN_IDLE_SECONDS = 60
|
|
40
|
+
|
|
41
|
+
# %% ../nbs/00_config.ipynb #0a8552af
|
|
42
|
+
STREAMING_APP_MIN_IDLE_SECONDS = {'youtube': 600, 'netflix': 600, 'twitch': 600}
|
|
43
|
+
|
|
44
|
+
# %% ../nbs/00_config.ipynb #199d6147
|
|
45
|
+
NUM_TOP_APP_IN_REPORT = 10
|
|
46
|
+
|
|
47
|
+
# %% ../nbs/00_config.ipynb #c58e4eb3
|
|
48
|
+
NUM_TOP_WINDOW_IN_REPORT = 5
|
|
49
|
+
|
|
50
|
+
# %% ../nbs/00_config.ipynb #84bab75b
|
|
51
|
+
SHOW_WINDOW_TITLES = True
|
|
52
|
+
|
|
53
|
+
# %% ../nbs/00_config.ipynb #e2cf94c1
|
|
54
|
+
DEFAULT_PAUSE_MINUTES = 30
|
snooper_pkg/db.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""SQLite persistence APIs for monitoring sessions, foreground activity, and process events.
|
|
2
|
+
|
|
3
|
+
Docs: https://sayanbanerjee32.github.io/snooper_pkg/db.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_db.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['ProcessEvent', 'Session', 'ForegroundEvent', 'init_db', 'log_process_event', 'get_processes', 'start_session',
|
|
9
|
+
'end_session', 'get_last_completed_session', 'close_stale_open_sessions', 'get_session_by_session_id',
|
|
10
|
+
'log_fg_app_events', 'get_foreground_events', 'delete_old_data']
|
|
11
|
+
|
|
12
|
+
# %% ../nbs/01_db.ipynb #f9f7bca0
|
|
13
|
+
import logging
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# %% ../nbs/01_db.ipynb #6421ef53
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from fastlite import *
|
|
20
|
+
from platformdirs import user_data_path
|
|
21
|
+
|
|
22
|
+
# %% ../nbs/01_db.ipynb #e1ae96a1
|
|
23
|
+
import snooper_pkg.config as cf
|
|
24
|
+
|
|
25
|
+
# %% ../nbs/01_db.ipynb #de4ffec3
|
|
26
|
+
class ProcessEvent:
|
|
27
|
+
"A recorded process start or stop event."
|
|
28
|
+
|
|
29
|
+
session_id: str # Monitoring session containing the event
|
|
30
|
+
pid: int # Operating-system process identifier
|
|
31
|
+
app: str # Process or application name
|
|
32
|
+
event_type: str # Event type: `start` or `stop`
|
|
33
|
+
timestamp: str # ISO-formatted event timestamp
|
|
34
|
+
id: int | None = None # Database-generated primary key
|
|
35
|
+
|
|
36
|
+
# %% ../nbs/01_db.ipynb #9ed8b625
|
|
37
|
+
class Session:
|
|
38
|
+
"A single continuous monitoring session."
|
|
39
|
+
|
|
40
|
+
session_id: str # Unique session identifier
|
|
41
|
+
start_time: str # ISO-formatted session start time
|
|
42
|
+
end_time: str | None # ISO-formatted end time; `None` while open
|
|
43
|
+
status: str # Session state: `open` or `closed`
|
|
44
|
+
end_reason: str | None # Reason the session ended
|
|
45
|
+
|
|
46
|
+
# %% ../nbs/01_db.ipynb #038a14fd
|
|
47
|
+
class ForegroundEvent:
|
|
48
|
+
"An interval during which an application or idle state was in the foreground."
|
|
49
|
+
|
|
50
|
+
session_id: str # Monitoring session containing the interval
|
|
51
|
+
app: str # Foreground application name
|
|
52
|
+
pid: int # Foreground process identifier
|
|
53
|
+
window_title: str # Foreground window title
|
|
54
|
+
start_time: str # ISO-formatted interval start time
|
|
55
|
+
end_time: str # ISO-formatted interval end time
|
|
56
|
+
hwnd: int # Windows window handle
|
|
57
|
+
is_idle: bool # Whether the interval represents user idle time
|
|
58
|
+
id: int | None = None # Database-generated primary key
|
|
59
|
+
|
|
60
|
+
# %% ../nbs/01_db.ipynb #3d9026f4
|
|
61
|
+
def _db_timestamp(value: str | datetime) -> str:
|
|
62
|
+
"Convert a timestamp into the string format stored in SQLite."
|
|
63
|
+
return value.isoformat(sep=" ", timespec="seconds") if isinstance(value, datetime) else value
|
|
64
|
+
|
|
65
|
+
# %% ../nbs/01_db.ipynb #fe74016d
|
|
66
|
+
DATA_DIR = user_data_path(cf.APP_NAME, appauthor=False)
|
|
67
|
+
DATABASE_PATH = DATA_DIR / cf.DATABASE_FILENAME
|
|
68
|
+
|
|
69
|
+
db = None
|
|
70
|
+
process_events = sessions = fore_ground_app_events = None
|
|
71
|
+
|
|
72
|
+
# %% ../nbs/01_db.ipynb #75286d2b
|
|
73
|
+
def init_db(
|
|
74
|
+
path=None # database path for the application
|
|
75
|
+
):
|
|
76
|
+
"Open the database connection and create tables. Uses `DATABASE_PATH` if `path` is None."
|
|
77
|
+
global db, process_events, sessions, fore_ground_app_events
|
|
78
|
+
if path is None: path = DATABASE_PATH
|
|
79
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
db = database(path)
|
|
81
|
+
process_events = db.create(ProcessEvent, if_not_exists=True, transform=True)
|
|
82
|
+
sessions = db.create(Session, pk="session_id", if_not_exists=True, transform=True)
|
|
83
|
+
fore_ground_app_events = db.create(ForegroundEvent, if_not_exists=True, transform=True)
|
|
84
|
+
return db
|
|
85
|
+
|
|
86
|
+
# %% ../nbs/01_db.ipynb #35a9d475
|
|
87
|
+
def log_process_event(
|
|
88
|
+
session_id: str, # Session receiving the event
|
|
89
|
+
pid: int, # Operating-system process identifier
|
|
90
|
+
app: str, # Process or application name
|
|
91
|
+
event_type: str, # Event type: `start` or `stop`
|
|
92
|
+
timestamp: str, # ISO-formatted event timestamp
|
|
93
|
+
):
|
|
94
|
+
"Record a process start or stop event."
|
|
95
|
+
return process_events.insert(
|
|
96
|
+
session_id=session_id,
|
|
97
|
+
pid=pid,
|
|
98
|
+
app=app,
|
|
99
|
+
event_type=event_type,
|
|
100
|
+
timestamp=timestamp,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# %% ../nbs/01_db.ipynb #70e04342
|
|
104
|
+
def get_processes(
|
|
105
|
+
session_id: str, # Session whose completed process intervals are requested
|
|
106
|
+
) -> list[dict]:
|
|
107
|
+
"Return paired process start/stop intervals for a session."
|
|
108
|
+
return db.q(
|
|
109
|
+
"""
|
|
110
|
+
SELECT
|
|
111
|
+
s.session_id,
|
|
112
|
+
s.pid,
|
|
113
|
+
s.app,
|
|
114
|
+
s.timestamp AS start_time,
|
|
115
|
+
e.timestamp AS stop_time,
|
|
116
|
+
(julianday(e.timestamp) - julianday(s.timestamp)) * 86400
|
|
117
|
+
AS duration_seconds
|
|
118
|
+
FROM process_event s
|
|
119
|
+
JOIN process_event e
|
|
120
|
+
ON s.pid = e.pid
|
|
121
|
+
AND s.app = e.app
|
|
122
|
+
AND s.session_id = e.session_id
|
|
123
|
+
WHERE s.event_type = 'start'
|
|
124
|
+
AND e.event_type = 'stop'
|
|
125
|
+
AND s.session_id = ?
|
|
126
|
+
""",
|
|
127
|
+
[session_id],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# %% ../nbs/01_db.ipynb #9e0b447c
|
|
131
|
+
def start_session(
|
|
132
|
+
session_id: str, # Unique identifier for the new session
|
|
133
|
+
start_time: str, # ISO-formatted session start time
|
|
134
|
+
):
|
|
135
|
+
"Create and return a new open monitoring session."
|
|
136
|
+
start_time = _db_timestamp(start_time)
|
|
137
|
+
return sessions.insert(
|
|
138
|
+
session_id=session_id,
|
|
139
|
+
start_time=start_time,
|
|
140
|
+
status="open",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# %% ../nbs/01_db.ipynb #e72f7a86
|
|
144
|
+
def end_session(
|
|
145
|
+
session_id: str, # Session to close
|
|
146
|
+
end_time: str, # ISO-formatted session end time
|
|
147
|
+
end_reason: str = "NA", # Reason the session ended
|
|
148
|
+
):
|
|
149
|
+
"Close a monitoring session and record why it ended."
|
|
150
|
+
end_time = _db_timestamp(end_time)
|
|
151
|
+
return sessions.update(
|
|
152
|
+
session_id=session_id,
|
|
153
|
+
end_time=end_time,
|
|
154
|
+
status="closed",
|
|
155
|
+
end_reason=end_reason,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# %% ../nbs/01_db.ipynb #0c1f5f71
|
|
159
|
+
def get_last_completed_session() -> dict:
|
|
160
|
+
"Return the most recently completed session, or an empty dictionary."
|
|
161
|
+
rows = db.q(
|
|
162
|
+
"""
|
|
163
|
+
SELECT session_id, start_time, end_time
|
|
164
|
+
FROM session
|
|
165
|
+
WHERE status = 'closed'
|
|
166
|
+
ORDER BY julianday(end_time) DESC
|
|
167
|
+
LIMIT 1
|
|
168
|
+
"""
|
|
169
|
+
)
|
|
170
|
+
return rows[0] if rows else {}
|
|
171
|
+
|
|
172
|
+
# %% ../nbs/01_db.ipynb #babf2013
|
|
173
|
+
def close_stale_open_sessions(
|
|
174
|
+
end_time: str, # Timestamp used to close stale sessions
|
|
175
|
+
) -> list[str]:
|
|
176
|
+
"Close sessions left open before startup and return their identifiers."
|
|
177
|
+
end_time = _db_timestamp(end_time)
|
|
178
|
+
open_sessions_dict_list = db.q("""
|
|
179
|
+
SELECT
|
|
180
|
+
session_id
|
|
181
|
+
FROM session
|
|
182
|
+
WHERE status = 'open'
|
|
183
|
+
AND julianday(start_time) < julianday(?)
|
|
184
|
+
""", [end_time])
|
|
185
|
+
closed_session_list = []
|
|
186
|
+
if open_sessions_dict_list:
|
|
187
|
+
for open_sessions_dict in open_sessions_dict_list:
|
|
188
|
+
end_session(session_id = open_sessions_dict['session_id'], end_time = end_time, end_reason = 'interrupted')
|
|
189
|
+
closed_session_list.append(open_sessions_dict['session_id'])
|
|
190
|
+
return closed_session_list
|
|
191
|
+
|
|
192
|
+
# %% ../nbs/01_db.ipynb #eacbf030
|
|
193
|
+
def get_session_by_session_id(
|
|
194
|
+
session_id: str, # Identifier of the requested session
|
|
195
|
+
) -> dict:
|
|
196
|
+
"Return a session by ID, using the current time for an open session's end."
|
|
197
|
+
rows = db.q(
|
|
198
|
+
"""
|
|
199
|
+
SELECT
|
|
200
|
+
session_id,
|
|
201
|
+
start_time,
|
|
202
|
+
COALESCE(end_time, ?) AS end_time
|
|
203
|
+
FROM session
|
|
204
|
+
WHERE session_id = ?
|
|
205
|
+
ORDER BY julianday(start_time) DESC
|
|
206
|
+
LIMIT 1
|
|
207
|
+
""",
|
|
208
|
+
[
|
|
209
|
+
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
210
|
+
session_id,
|
|
211
|
+
],
|
|
212
|
+
)
|
|
213
|
+
return rows[0] if rows else {}
|
|
214
|
+
|
|
215
|
+
# %% ../nbs/01_db.ipynb #1602e56e
|
|
216
|
+
def log_fg_app_events(
|
|
217
|
+
session_id: str, # Session receiving the interval
|
|
218
|
+
app: str, # Foreground application name
|
|
219
|
+
pid: int, # Foreground process identifier
|
|
220
|
+
window_title: str, # Foreground window title
|
|
221
|
+
start_time: str, # ISO-formatted interval start time
|
|
222
|
+
end_time: str, # ISO-formatted interval end time
|
|
223
|
+
hwnd: int, # Windows window handle
|
|
224
|
+
is_idle: bool, # Whether this was an idle interval
|
|
225
|
+
):
|
|
226
|
+
"Record a completed foreground-window or idle interval."
|
|
227
|
+
return fore_ground_app_events.insert(
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
app=app,
|
|
230
|
+
pid=pid,
|
|
231
|
+
window_title=window_title,
|
|
232
|
+
start_time=start_time,
|
|
233
|
+
end_time=end_time,
|
|
234
|
+
hwnd=hwnd,
|
|
235
|
+
is_idle=is_idle,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# %% ../nbs/01_db.ipynb #680919a5
|
|
239
|
+
def get_foreground_events(
|
|
240
|
+
session_id: str, # Session whose foreground intervals are requested
|
|
241
|
+
) -> list[dict]:
|
|
242
|
+
"Return a session's foreground intervals in chronological order."
|
|
243
|
+
rows = db.q(
|
|
244
|
+
"""
|
|
245
|
+
SELECT *
|
|
246
|
+
FROM foreground_event
|
|
247
|
+
WHERE session_id = ?
|
|
248
|
+
ORDER BY julianday(start_time)
|
|
249
|
+
""",
|
|
250
|
+
[session_id],
|
|
251
|
+
)
|
|
252
|
+
return rows or []
|
|
253
|
+
|
|
254
|
+
# %% ../nbs/01_db.ipynb #61154a49
|
|
255
|
+
def delete_old_data(
|
|
256
|
+
now_ts: str | datetime | None = None, # Retention reference time; defaults to now
|
|
257
|
+
) -> str:
|
|
258
|
+
"Delete data belonging to closed sessions older than the retention period."
|
|
259
|
+
if isinstance(now_ts, str):
|
|
260
|
+
now_dt = datetime.fromisoformat(now_ts)
|
|
261
|
+
else:
|
|
262
|
+
now_dt = now_ts or datetime.now()
|
|
263
|
+
|
|
264
|
+
retention_days = getattr(cf, "DATA_RETENTION_DAYS", 365)
|
|
265
|
+
cutoff_ts = (
|
|
266
|
+
now_dt - timedelta(days=retention_days)
|
|
267
|
+
).isoformat(timespec="seconds")
|
|
268
|
+
|
|
269
|
+
db.q(
|
|
270
|
+
"""
|
|
271
|
+
DELETE FROM process_event
|
|
272
|
+
WHERE session_id IN (
|
|
273
|
+
SELECT session_id
|
|
274
|
+
FROM session
|
|
275
|
+
WHERE status = 'closed'
|
|
276
|
+
AND julianday(end_time) < julianday(?)
|
|
277
|
+
)
|
|
278
|
+
""",
|
|
279
|
+
[cutoff_ts],
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
db.q(
|
|
283
|
+
"""
|
|
284
|
+
DELETE FROM foreground_event
|
|
285
|
+
WHERE session_id IN (
|
|
286
|
+
SELECT session_id
|
|
287
|
+
FROM session
|
|
288
|
+
WHERE status = 'closed'
|
|
289
|
+
AND julianday(end_time) < julianday(?)
|
|
290
|
+
)
|
|
291
|
+
""",
|
|
292
|
+
[cutoff_ts],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
db.q(
|
|
296
|
+
"""
|
|
297
|
+
DELETE FROM session
|
|
298
|
+
WHERE status = 'closed'
|
|
299
|
+
AND julianday(end_time) < julianday(?)
|
|
300
|
+
""",
|
|
301
|
+
[cutoff_ts],
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return cutoff_ts
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Track the active Windows foreground application and persist completed foreground and idle intervals.
|
|
2
|
+
|
|
3
|
+
Docs: https://sayanbanerjee32.github.io/snooper_pkg/foreground_tracker.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_foreground_tracker.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['get_foreground_window_info', 'is_streaming_app', 'start_foreground_app_monitoring']
|
|
9
|
+
|
|
10
|
+
# %% ../nbs/03_foreground_tracker.ipynb #0f83e8bf
|
|
11
|
+
import logging
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# %% ../nbs/03_foreground_tracker.ipynb #3fc06c45
|
|
16
|
+
import psutil
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from time import sleep
|
|
19
|
+
|
|
20
|
+
# %% ../nbs/03_foreground_tracker.ipynb #4cbec422
|
|
21
|
+
import snooper_pkg.config as cf
|
|
22
|
+
from snooper_pkg.db import *
|
|
23
|
+
from snooper_pkg.idle_detector import *
|
|
24
|
+
|
|
25
|
+
# %% ../nbs/03_foreground_tracker.ipynb #4939c6b1
|
|
26
|
+
import sys
|
|
27
|
+
def _require_windows():
|
|
28
|
+
if sys.platform != "win32":
|
|
29
|
+
raise RuntimeError("Windows idle detection is only available on Windows")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# %% ../nbs/03_foreground_tracker.ipynb #9672487f
|
|
33
|
+
def get_foreground_window_info() -> tuple[str, int, int, str]:
|
|
34
|
+
"Return the foreground window title, handle, process ID, and process name."
|
|
35
|
+
_require_windows()
|
|
36
|
+
import win32gui, win32process
|
|
37
|
+
hwnd = win32gui.GetForegroundWindow()
|
|
38
|
+
title = win32gui.GetWindowText(hwnd) or "Unknown"
|
|
39
|
+
tid, pid = win32process.GetWindowThreadProcessId(hwnd)
|
|
40
|
+
if not isinstance(pid, int) or pid <= 0: return title, hwnd, -1, "UNKNOWN"
|
|
41
|
+
try: process_name = psutil.Process(pid).name()
|
|
42
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, ValueError, psutil.Error): return title, hwnd, -1, "UNKNOWN"
|
|
43
|
+
return title, hwnd, pid, process_name
|
|
44
|
+
|
|
45
|
+
# %% ../nbs/03_foreground_tracker.ipynb #9f7f5ced
|
|
46
|
+
def is_streaming_app(
|
|
47
|
+
title: str, # Foreground window title to inspect
|
|
48
|
+
streaming_app_min_idle_time: dict[str, int | float], # Title substrings mapped to idle thresholds in seconds
|
|
49
|
+
) -> int | float | None:
|
|
50
|
+
"Return the idle threshold for the first configured title match, or `None`."
|
|
51
|
+
for s in streaming_app_min_idle_time.keys():
|
|
52
|
+
if s.lower() in title.lower():
|
|
53
|
+
return streaming_app_min_idle_time[s]
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# %% ../nbs/03_foreground_tracker.ipynb #f101b187
|
|
57
|
+
def start_foreground_app_monitoring(
|
|
58
|
+
session_id: str, # Monitoring session receiving foreground intervals
|
|
59
|
+
stop_event: object, # Stop signal providing an `is_set()` method
|
|
60
|
+
time_interval: int | float = cf.FG_APP_MONITORING_INTERVAL_SECONDS, # Seconds between foreground samples
|
|
61
|
+
min_idle_time_gap: int | float = cf.FG_APP_MONITORING_MIN_IDLE_SECONDS, # General idle threshold in seconds
|
|
62
|
+
streaming_app_min_idle_time: dict[str, int | float] = cf.STREAMING_APP_MIN_IDLE_SECONDS, # Title-specific idle thresholds
|
|
63
|
+
) -> None:
|
|
64
|
+
"Sample foreground state until stopped and record each completed interval."
|
|
65
|
+
fg_dict = {}
|
|
66
|
+
try:
|
|
67
|
+
while not stop_event.is_set():
|
|
68
|
+
title, hwnd, pid, process_name = get_foreground_window_info()
|
|
69
|
+
if is_streaming_app(title, streaming_app_min_idle_time) is not None:
|
|
70
|
+
min_idle_time = is_streaming_app(title, streaming_app_min_idle_time)
|
|
71
|
+
is_idle = get_idle_seconds() > min_idle_time
|
|
72
|
+
else: is_idle = get_idle_seconds() > min_idle_time_gap
|
|
73
|
+
if is_idle:
|
|
74
|
+
title, hwnd, pid, process_name = "Idle", -1, -1, "IDLE"
|
|
75
|
+
if fg_dict.get("pid") != pid or fg_dict.get("hwnd") != hwnd or fg_dict.get("window_title") != title:
|
|
76
|
+
if fg_dict:
|
|
77
|
+
fg_dict["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
78
|
+
log_fg_app_events(**fg_dict)
|
|
79
|
+
fg_dict = {
|
|
80
|
+
"session_id":session_id,
|
|
81
|
+
"window_title": title,
|
|
82
|
+
"pid": pid,
|
|
83
|
+
"hwnd": hwnd,
|
|
84
|
+
"app": process_name,
|
|
85
|
+
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
86
|
+
"is_idle":is_idle,
|
|
87
|
+
"end_time": None
|
|
88
|
+
}
|
|
89
|
+
else:
|
|
90
|
+
fg_dict["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
91
|
+
sleep(time_interval)
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
logger.exception("Stopped by user.")
|
|
94
|
+
finally:
|
|
95
|
+
if fg_dict:
|
|
96
|
+
fg_dict["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
97
|
+
log_fg_app_events(**fg_dict)
|
|
98
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Windows idle-time measurement based on system tick counts for the most recent user input.
|
|
2
|
+
|
|
3
|
+
Docs: https://sayanbanerjee32.github.io/snooper_pkg/idle_detector.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_idle_detector.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['LASTINPUTINFO', 'get_last_input_tick', 'get_idle_seconds']
|
|
9
|
+
|
|
10
|
+
# %% ../nbs/02_idle_detector.ipynb #4cab5d99
|
|
11
|
+
import logging
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# %% ../nbs/02_idle_detector.ipynb #c6affeda
|
|
16
|
+
import ctypes
|
|
17
|
+
from ctypes import wintypes
|
|
18
|
+
|
|
19
|
+
# %% ../nbs/02_idle_detector.ipynb #56597405
|
|
20
|
+
import sys
|
|
21
|
+
def _require_windows():
|
|
22
|
+
if sys.platform != "win32":
|
|
23
|
+
raise RuntimeError("Windows idle detection is only available on Windows")
|
|
24
|
+
|
|
25
|
+
# %% ../nbs/02_idle_detector.ipynb #e6a637cf
|
|
26
|
+
class LASTINPUTINFO(ctypes.Structure):
|
|
27
|
+
"ctypes representation of the Windows `LASTINPUTINFO` structure."
|
|
28
|
+
|
|
29
|
+
_fields_ = [
|
|
30
|
+
("cbSize", wintypes.UINT), # Size of this structure in bytes
|
|
31
|
+
("dwTime", wintypes.DWORD), # Tick count when the last input occurred
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# %% ../nbs/02_idle_detector.ipynb #3f90a87c
|
|
36
|
+
def get_last_input_tick():
|
|
37
|
+
"Call `GetLastInputInfo` and return its last-input tick count in milliseconds."
|
|
38
|
+
_require_windows()
|
|
39
|
+
|
|
40
|
+
info = LASTINPUTINFO()
|
|
41
|
+
info.cbSize = ctypes.sizeof(info)
|
|
42
|
+
|
|
43
|
+
ok = ctypes.windll.user32.GetLastInputInfo(ctypes.byref(info))
|
|
44
|
+
if not ok:
|
|
45
|
+
raise ctypes.WinError()
|
|
46
|
+
return info.dwTime
|
|
47
|
+
|
|
48
|
+
# %% ../nbs/02_idle_detector.ipynb #2083d992
|
|
49
|
+
def get_idle_seconds():
|
|
50
|
+
"Return the elapsed seconds between system uptime and the last input tick."
|
|
51
|
+
_require_windows()
|
|
52
|
+
current_tick = ctypes.windll.kernel32.GetTickCount64()
|
|
53
|
+
last_input_tick = get_last_input_tick()
|
|
54
|
+
return (current_tick - last_input_tick)/1000
|