Skip to content

Commit 37d15f9

Browse files
committed
fix(razer): explain macOS Input Monitoring on open failure
1 parent b43b7a1 commit 37d15f9

5 files changed

Lines changed: 57 additions & 4 deletions

File tree

src/devices/razer/TESTING.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
Test in Chrome or Edge over HTTPS. Quit Razer Synapse first — it holds the
44
control interface open and reads then time out.
55

6+
On macOS the browser itself must be granted Input Monitoring permission
7+
(System Settings → Privacy & Security → Input Monitoring) **before** the first
8+
connection, and then quit and reopened. Razer's control interface is a Generic
9+
Desktop Mouse collection, which macOS reserves for its own input stack; without
10+
the permission the browser's `IOHIDDeviceOpen` call is refused and the app
11+
shows "Failed to open the device." while the device still appears in the
12+
picker. This is a system grant, not an app setting.
13+
614
Supported identifiers:
715

816
- `1532:00a5` — Viper V2 Pro, wired
@@ -144,6 +152,18 @@ answers, add the device again and choose another.
144152
The V2 has RGB lighting, but like every other model the panel offers no Razer
145153
lighting controls and device mode is never sent.
146154

155+
## DeathAdder V2 on macOS
156+
157+
Confirmed on macOS: the mouse enumerates as four HID interfaces, the
158+
configuration channel sits on the Generic Desktop Mouse interface, and the
159+
browser is refused from opening it unless it holds the Input Monitoring
160+
permission. The device still shows up in the picker, so the failure looks like
161+
a driver bug: the sidebar lists the mouse as available, and connecting fails
162+
with `NotAllowedError: Failed to open the device.` before any feature report is
163+
exchanged — no Synapse installed. Granting the browser Input Monitoring
164+
(System Settings → Privacy & Security → Input Monitoring) and restarting it is
165+
the fix; the app now says exactly that when the open is refused on macOS.
166+
147167
## Verified against firmware 1.12
148168

149169
| Read | Class / ID | Notes |

src/devices/razer/hid-open.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const IS_MAC = typeof navigator !== "undefined" && /Mac/i.test(navigator.userAgent);
2+
3+
const MACOS_INPUT_MONITORING_HINT =
4+
"On macOS the browser is refused access to mouse-class HID interfaces unless it"
5+
+ " has Input Monitoring permission. Enable it in System Settings → Privacy &"
6+
+ " Security → Input Monitoring, then quit and reopen the browser.";
7+
8+
/**
9+
* Opens a Razer control interface, and turns the browser's bare "Failed to open
10+
* the device." on macOS into an actionable message.
11+
*
12+
* Razer's configuration channel lives on a Generic Desktop Mouse collection,
13+
* which macOS reserves for its own input stack: without the Input Monitoring
14+
* TCC permission the browser's `IOHIDDeviceOpen` call fails with
15+
* `kIOReturnNotPermitted`, which WebHID surfaces as a generic NotAllowedError.
16+
* Nothing in the app can grant that — it is a system permission on the browser
17+
* itself — so the hint is the whole fix.
18+
*/
19+
export async function openRazerDevice(device: HIDDevice): Promise<void> {
20+
if (device.opened) return;
21+
try {
22+
await device.open();
23+
} catch (error) {
24+
if (IS_MAC && error instanceof Error) {
25+
throw new Error(`${error.message} ${MACOS_INPUT_MONITORING_HINT}`);
26+
}
27+
throw error;
28+
}
29+
}

src/devices/razer/hid.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { MouseStatus } from "../mouse-types.ts";
22
import { VENDOR_ID } from "../vendors.ts";
3+
import { openRazerDevice } from "./hid-open.ts";
34
import {
45
RAZER_LANDING_MAX,
56
RAZER_LANDING_MIN,
@@ -188,7 +189,7 @@ export class RazerHidClient {
188189
}
189190

190191
async open(): Promise<void> {
191-
if (!this.device.opened) await this.device.open();
192+
await openRazerDevice(this.device);
192193
}
193194

194195
async close(): Promise<void> {

src/devices/razer/viper-mini-hid.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { MouseStatus } from "../mouse-types.ts";
22
import { VENDOR_ID } from "../vendors.ts";
3+
import { openRazerDevice } from "./hid-open.ts";
34
import {
45
RAZER_READ,
56
RAZER_REPORT_ID,
@@ -69,8 +70,9 @@ export class RazerViperMiniHidClient {
6970
// On Linux "Failed to open the device" means the hidraw node for 1532:008a
7071
// is root-owned or the razermouse kernel driver claimed the interface.
7172
// Fix: udev rule granting plugdev access to that vendor/product, then
72-
// `sudo rmmod razermouse`.
73-
if (!this.device.opened) await this.device.open();
73+
// `sudo rmmod razermouse`. On macOS the same message means the browser
74+
// lacks Input Monitoring permission — see `openRazerDevice`.
75+
await openRazerDevice(this.device);
7476
}
7577

7678
async close(): Promise<void> {

src/devices/razer/viper-v4-pro-hid.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { MouseStatus } from "../mouse-types.ts";
2+
import { openRazerDevice } from "./hid-open.ts";
23

34
export const RAZER_VENDOR_ID = 0x1532;
45
export const VIPER_V4_PRO_PRODUCTS = new Map<number, { wireless: boolean }>([
@@ -79,7 +80,7 @@ export class RazerViperV4ProHidClient {
7980
}
8081

8182
async open(): Promise<void> {
82-
if (!this.device.opened) await this.device.open();
83+
await openRazerDevice(this.device);
8384
}
8485

8586
async close(): Promise<void> {

0 commit comments

Comments
 (0)