-
-
Notifications
You must be signed in to change notification settings - Fork 160
Expand file tree
/
Copy pathlinux_permissions.py
More file actions
171 lines (143 loc) · 5.08 KB
/
Copy pathlinux_permissions.py
File metadata and controls
171 lines (143 loc) · 5.08 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
"""Linux device permission checks for Logitech HID++ access."""
from __future__ import annotations
from dataclasses import dataclass
import glob
import os
import sys
LOGITECH_VENDOR_ID = 0x046D
INSTALL_HELPER = "install-linux-permissions.sh"
@dataclass(frozen=True)
class LinuxHidrawNode:
path: str
product_id: int | None = None
product_name: str = ""
bus_id: int | None = None
@dataclass(frozen=True)
class LinuxPermissionReport:
hidraw_nodes: tuple[LinuxHidrawNode, ...]
blocked_hidraw_paths: tuple[str, ...]
input_event_paths: tuple[str, ...]
input_events_readable: bool
uinput_path: str
uinput_writable: bool
uinput_exists: bool
@property
def has_issue(self) -> bool:
return bool(
self.blocked_hidraw_paths
or (self.input_event_paths and not self.input_events_readable)
or not self.uinput_exists
or not self.uinput_writable
)
def issue_parts(self) -> list[str]:
parts: list[str] = []
if self.blocked_hidraw_paths:
paths = ", ".join(self.blocked_hidraw_paths[:3])
if len(self.blocked_hidraw_paths) > 3:
paths += ", ..."
parts.append(f"blocked hidraw access ({paths})")
if self.input_event_paths and not self.input_events_readable:
parts.append("no readable /dev/input/event* nodes")
if not self.uinput_exists:
parts.append("/dev/uinput is missing")
elif not self.uinput_writable:
parts.append("/dev/uinput is not writable")
return parts
def _parse_hid_id(value: str):
try:
bus_hex, vid_hex, pid_hex = value.split(":", 2)
return int(bus_hex, 16), int(vid_hex, 16), int(pid_hex, 16)
except (AttributeError, ValueError):
return None
def _read_uevent_props(path: str) -> dict[str, str]:
props: dict[str, str] = {}
try:
with open(path, encoding="utf-8") as fh:
for line in fh:
key, sep, value = line.strip().partition("=")
if sep:
props[key] = value
except OSError:
pass
return props
def logitech_hidraw_nodes(
*,
sysfs_base: str = "/sys/class/hidraw",
dev_base: str = "/dev",
) -> tuple[LinuxHidrawNode, ...]:
"""Return Logitech hidraw nodes visible through sysfs."""
try:
entries = sorted(os.listdir(sysfs_base))
except OSError:
return ()
nodes: list[LinuxHidrawNode] = []
for entry in entries:
if not entry.startswith("hidraw"):
continue
props = _read_uevent_props(
os.path.join(sysfs_base, entry, "device", "uevent")
)
parsed = _parse_hid_id(props.get("HID_ID", ""))
if not parsed:
continue
bus_id, vendor_id, product_id = parsed
if vendor_id != LOGITECH_VENDOR_ID:
continue
nodes.append(
LinuxHidrawNode(
path=os.path.join(dev_base, entry),
product_id=product_id,
product_name=props.get("HID_NAME", ""),
bus_id=bus_id,
)
)
return tuple(nodes)
def linux_permission_report(
*,
sysfs_base: str = "/sys/class/hidraw",
dev_base: str = "/dev",
input_event_glob: str = "/dev/input/event*",
uinput_path: str = "/dev/uinput",
) -> LinuxPermissionReport | None:
"""Inspect Linux device-node access when a Logitech hidraw node is visible."""
if not sys.platform.startswith("linux"):
return None
hidraw_nodes = logitech_hidraw_nodes(sysfs_base=sysfs_base, dev_base=dev_base)
if not hidraw_nodes:
return LinuxPermissionReport((), (), (), True, uinput_path, True, True)
blocked_hidraw_paths = tuple(
node.path
for node in hidraw_nodes
if not os.access(node.path, os.R_OK | os.W_OK)
)
input_event_paths = tuple(sorted(glob.glob(input_event_glob)))
input_events_readable = (
not input_event_paths
or any(os.access(path, os.R_OK) for path in input_event_paths)
)
uinput_exists = os.path.exists(uinput_path)
uinput_writable = uinput_exists and os.access(uinput_path, os.W_OK)
return LinuxPermissionReport(
hidraw_nodes=hidraw_nodes,
blocked_hidraw_paths=blocked_hidraw_paths,
input_event_paths=input_event_paths,
input_events_readable=input_events_readable,
uinput_path=uinput_path,
uinput_writable=uinput_writable,
uinput_exists=uinput_exists,
)
def linux_permission_status_message(report: LinuxPermissionReport | None) -> str:
if report is None or not report.has_issue:
return ""
return (
"Linux permissions may block Mouser. Run "
f"{INSTALL_HELPER}, reconnect the mouse, then restart Mouser."
)
def linux_permission_log_message(report: LinuxPermissionReport | None) -> str:
if report is None or not report.has_issue:
return ""
return (
"[LinuxPermissions] Device access issue: "
+ "; ".join(report.issue_parts())
+ f". Install the bundled udev rule with {INSTALL_HELPER}."
)