diff --git a/clipboard_watcher/app.py b/clipboard_watcher/app.py index 934793c..234a6b5 100644 --- a/clipboard_watcher/app.py +++ b/clipboard_watcher/app.py @@ -1,3 +1,7 @@ +"""Main script to execute the application + +This module provides the entrypoint to CLI app/script +""" import logging import sys from argparse import ArgumentParser @@ -22,9 +26,15 @@ def set_logger_settings(level_name: str) -> None: def process_notifications(q: Queue): while True: req = q.get(block=True) + window_info = f"Window info: {req['window_name']} (id: {req['id']})" + if process := req.get("process"): + process_info = f"Process info: {process.path} (pid: {process.pid})" + else: + process_info = "Process info: Unknown" + display_desktop_notification( - f"New access to {req['selection']}({req['target']}) detected.", - f"Window: {req['window_name']} (id: {req['id']})\nPossible PID: {req['pid']} | Extra Info: {req['extra']}", + f"Access to Clipboard ({req['selection']}) detected.", + f"{window_info}\n{process_info}", ) diff --git a/clipboard_watcher/models.py b/clipboard_watcher/models.py index a88dcf9..b8d4e5f 100644 --- a/clipboard_watcher/models.py +++ b/clipboard_watcher/models.py @@ -1,3 +1,5 @@ +"""Structures to store the existing clipboard data""" + from dataclasses import dataclass from typing import Optional, Any, Dict, List import logging diff --git a/clipboard_watcher/process_info.py b/clipboard_watcher/process_info.py new file mode 100644 index 0000000..a04f281 --- /dev/null +++ b/clipboard_watcher/process_info.py @@ -0,0 +1,52 @@ +"""Process Info Module + +This module provides the tools to fetch all the necessary information +about running processes. + +It is used to populate the information displayed to the user about who +is accessing the clipboard information. +""" +from __future__ import annotations +from datetime import datetime +from typing import Optional +from dataclasses import dataclass +from pathlib import Path + +import psutil + + +@dataclass() +class ProcessInfo: + pid: int + name: Optional[str] + path: Optional[Path] + + parent: int + user: str + started_at: Optional[datetime] + + @classmethod + def collect(cls, pid: int) -> ProcessInfo: + p = psutil.Process(pid) + details = p.as_dict( + attrs=["ppid", "exe", "create_time", "name", "username"], ad_value=None + ) + + if timestamp := details.get("create_time"): + date = datetime.fromtimestamp(timestamp) + else: + date = None + + if exe := details.get("exe"): + path = Path(exe) + else: + path = None + + return cls( + pid, + details["name"], + path, + details["ppid"], + details["username"], + date, + ) diff --git a/clipboard_watcher/xoperations.py b/clipboard_watcher/xoperations.py index 356b5ba..0480c79 100644 --- a/clipboard_watcher/xoperations.py +++ b/clipboard_watcher/xoperations.py @@ -1,3 +1,15 @@ +"""X Operations Module + +This module abstracts some of the functionality of Xlib into +easy to use functions. + +The current functionality contains: +* Querying the existing clipboard targets +* Fetching existing clipboard data +* Processing requests for clipboard data +* Handling the loss of ownership on any clipboard data +""" + import logging from typing import List, Optional, Tuple from queue import Queue @@ -6,6 +18,8 @@ from Xlib import X, Xatom from Xlib.ext.res import query_client_ids, LocalClientPIDMask from Xlib.protocol import event +from .process_info import ProcessInfo + logger = logging.getLogger("ClipboardWatcher") @@ -67,7 +81,7 @@ def get_selection_data(disp, win, selection: str, target: str) -> Optional[Tuple return (data, r.format, r.property_type) -def _handle_incr(d, w, data_atom): +def _handle_incr(d, w, data_atom) -> bytes: """Handle the selection's data, when it is provided in chunks""" w.change_attributes(event_mask=X.PropertyChangeMask) data = None @@ -99,7 +113,7 @@ def _handle_incr(d, w, data_atom): data += r.value -def process_selection_request_event(d, cb_data, e): +def process_selection_request_event(d, cb_data, e) -> None: logger.debug("Selection %s request from %s", e.selection, e.requestor.get_wm_name()) client = e.requestor targets_atom = d.get_atom("TARGETS") @@ -188,7 +202,7 @@ def process_selection_request_event(d, cb_data, e): ) -def process_selection_clear_event(d, cb_data, e): +def process_selection_clear_event(d, cb_data, e) -> None: logger.warning("New content on %s, assuming ownership", d.get_atom_name(e.atom)) if e.atom == d.get_atom("PRIMARY"): cb_data.refresh_primary() @@ -200,7 +214,7 @@ def process_selection_clear_event(d, cb_data, e): logger.warning("Owner again") -def process_event_loop(d, w, q: Queue, cb_data): +def process_event_loop(d, w, q: Queue, cb_data) -> None: while True: e = d.next_event() if ( @@ -210,25 +224,28 @@ def process_event_loop(d, w, q: Queue, cb_data): ): req_id = e.requestor.id req_name = e.requestor.get_wm_name() + client_ids = query_client_ids(d, [(e.requestor.id, LocalClientPIDMask)]) + client_id = client_ids.ids[0] + req_pid = e.requestor.get_property( d.get_atom("_NET_WM_PID"), d.get_atom("CARDINAL"), 0, 1024 ) - extra = d.get_atom_name(e.property) - client_ids = query_client_ids(d, [(e.requestor.id, LocalClientPIDMask)]) - client_id = client_ids.ids[0] if client_id.spec.mask & LocalClientPIDMask: req_pid = client_id.value[0] - print(req_pid) + + # We must collect this information before processing the request + # due to the risk of the requestor no longer be running afterwards + proc_info = ProcessInfo.collect(req_pid) if req_pid else None + process_selection_request_event(d, cb_data, e) if d.get_atom_name(e.target) != "TARGETS": q.put( { "id": req_id, "window_name": req_name, - "pid": req_pid, + "process": proc_info, "target": d.get_atom_name(e.target), "selection": d.get_atom_name(e.selection), - "extra": extra, }, block=False, ) diff --git a/poetry.lock b/poetry.lock index 427a1da..7b3a0dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -131,6 +131,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "psutil" +version = "5.9.0" +description = "Cross-platform lib for process and system monitoring in Python." +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] + [[package]] name = "py" version = "1.11.0" @@ -218,7 +229,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "6231f2ce7abc15e8e1fb0d98ea4705c356e6322e24ab4f9cfae2a223813c7bbb" +content-hash = "5de6bab639719a04f7cf4b791199fabcec0f9c75e6bb12ce6039a299313a9f3c" [metadata.files] atomicwrites = [ @@ -268,6 +279,40 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +psutil = [ + {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"}, + {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618"}, + {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2"}, + {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd"}, + {file = "psutil-5.9.0-cp27-none-win32.whl", hash = "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3"}, + {file = "psutil-5.9.0-cp27-none-win_amd64.whl", hash = "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c"}, + {file = "psutil-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492"}, + {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3"}, + {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2"}, + {file = "psutil-5.9.0-cp310-cp310-win32.whl", hash = "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d"}, + {file = "psutil-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b"}, + {file = "psutil-5.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d"}, + {file = "psutil-5.9.0-cp36-cp36m-win32.whl", hash = "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64"}, + {file = "psutil-5.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94"}, + {file = "psutil-5.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0"}, + {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce"}, + {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5"}, + {file = "psutil-5.9.0-cp37-cp37m-win32.whl", hash = "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9"}, + {file = "psutil-5.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4"}, + {file = "psutil-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2"}, + {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d"}, + {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a"}, + {file = "psutil-5.9.0-cp38-cp38-win32.whl", hash = "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666"}, + {file = "psutil-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841"}, + {file = "psutil-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf"}, + {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07"}, + {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d"}, + {file = "psutil-5.9.0-cp39-cp39-win32.whl", hash = "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845"}, + {file = "psutil-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3"}, + {file = "psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, diff --git a/pyproject.toml b/pyproject.toml index 7797855..0d4c038 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = ["Gonçalo Valério "] python = ">=3.9,<3.11" python-xlib = "^0.31" dbus-python = "^1.2.18" +psutil = "^5.9.0" [tool.poetry.dev-dependencies] pytest = "^5.2"