mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-09 20:22:11 +09:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c72f0fb0 |
@@ -1156,6 +1156,11 @@ class KeybindManager {
|
||||
return false;
|
||||
}
|
||||
this.globalKeyHookStarted = true;
|
||||
if (NativePermission.isLinuxWaylandDesktop) {
|
||||
void NativePermission.recheckLinuxInputAccess().then((status) => {
|
||||
if (status === 'blocked') NativePermission.requestLinuxInputAccessNagbar(reason);
|
||||
});
|
||||
}
|
||||
const keyEventUnsub = electronApi.onGlobalKeyEvent?.((event) => {
|
||||
this.handleGlobalKeyEvent(
|
||||
event as {
|
||||
|
||||
@@ -10,6 +10,11 @@ import {createChildLogger} from '@electron/common/Logger';
|
||||
import {type EvdevKeyEvent, type EvdevMouseEvent, getEvdevHook, nameToEvdevKeycode} from '@electron/main/EvdevHook';
|
||||
import {GlobalKeyHookLifecycle} from '@electron/main/GlobalKeyHookLifecycle';
|
||||
import {getLinuxInputHookMode} from '@electron/main/LaunchOptions';
|
||||
import {
|
||||
hasLinuxEvdevAccess,
|
||||
isFullKeyboardKeyCapabilities,
|
||||
type LinuxEvdevAccessProbe,
|
||||
} from '@electron/main/LinuxInputAccess';
|
||||
import {isFlatpakRuntime} from '@electron/main/LinuxSandbox';
|
||||
import {getTccStatus} from '@electron/main/MacTcc';
|
||||
import {getMainWindow} from '@electron/main/Window';
|
||||
@@ -315,6 +320,15 @@ async function startEvdevBackend(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function reopenEvdevDevices(): Promise<void> {
|
||||
if (activeBackend !== 'evdev') return;
|
||||
const evdev = getEvdevHook();
|
||||
evdev.stop();
|
||||
if (!(await evdev.start())) {
|
||||
logger.warn('evdev input backend did not reopen after input access changed');
|
||||
}
|
||||
}
|
||||
|
||||
function startNativeBackend(): boolean {
|
||||
const moduleName = nativeModuleNameForPlatform();
|
||||
if (moduleName === null) return false;
|
||||
@@ -374,7 +388,16 @@ async function startHook(): Promise<boolean> {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (await startEvdevBackend()) {
|
||||
const probe = await probeLinuxEvdevAccess();
|
||||
const keyboardsReadable = hasLinuxEvdevAccess(probe);
|
||||
if (!keyboardsReadable) {
|
||||
logger.warn('Not every keyboard input device is readable', {
|
||||
keyboardDevices: probe.keyboardDevices,
|
||||
readableKeyboardDevices: probe.readableKeyboardDevices,
|
||||
readableEventDevices: probe.readableEventDevices,
|
||||
});
|
||||
}
|
||||
if ((keyboardsReadable || wayland) && (await startEvdevBackend())) {
|
||||
activeBackend = 'evdev';
|
||||
return true;
|
||||
}
|
||||
@@ -495,34 +518,46 @@ async function checkInputMonitoringAccess(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
interface LinuxEvdevAccessProbe {
|
||||
totalEventDevices: number;
|
||||
readableEventDevices: number;
|
||||
inInputGroup: boolean;
|
||||
}
|
||||
|
||||
async function probeLinuxEvdevAccess(): Promise<LinuxEvdevAccessProbe> {
|
||||
if (process.platform !== 'linux') {
|
||||
return {totalEventDevices: 0, readableEventDevices: 0, inInputGroup: true};
|
||||
}
|
||||
const inInputGroup = isFlatpakRuntime() ? false : await isUserInInputGroup();
|
||||
const probe: LinuxEvdevAccessProbe = {
|
||||
totalEventDevices: 0,
|
||||
readableEventDevices: 0,
|
||||
keyboardDevices: 0,
|
||||
readableKeyboardDevices: 0,
|
||||
inInputGroup: true,
|
||||
};
|
||||
if (process.platform !== 'linux') return probe;
|
||||
probe.inInputGroup = isFlatpakRuntime() ? false : await isUserInInputGroup();
|
||||
let entries: Array<string>;
|
||||
try {
|
||||
entries = await readdir('/dev/input');
|
||||
} catch {
|
||||
return {totalEventDevices: 0, readableEventDevices: 0, inInputGroup};
|
||||
return probe;
|
||||
}
|
||||
let total = 0;
|
||||
let readable = 0;
|
||||
for (const name of entries) {
|
||||
if (!name.startsWith('event')) continue;
|
||||
total += 1;
|
||||
probe.totalEventDevices += 1;
|
||||
let readable = false;
|
||||
try {
|
||||
accessSync(path.join('/dev/input', name), fsConstants.R_OK);
|
||||
readable += 1;
|
||||
readable = true;
|
||||
} catch {}
|
||||
if (readable) probe.readableEventDevices += 1;
|
||||
if (await isKeyboardEventDevice(name)) {
|
||||
probe.keyboardDevices += 1;
|
||||
if (readable) probe.readableKeyboardDevices += 1;
|
||||
}
|
||||
}
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function isKeyboardEventDevice(name: string): Promise<boolean> {
|
||||
try {
|
||||
const capabilities = await readFile(path.join('/sys/class/input', name, 'device/capabilities/key'), 'utf8');
|
||||
return isFullKeyboardKeyCapabilities(capabilities);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return {totalEventDevices: total, readableEventDevices: readable, inInputGroup};
|
||||
}
|
||||
|
||||
async function isUserInInputGroup(): Promise<boolean> {
|
||||
@@ -563,6 +598,8 @@ async function _collectLinuxEvdevDiagnostics(): Promise<Record<string, unknown>>
|
||||
flatpak: isFlatpakRuntime(),
|
||||
totalEventDevices: probe.totalEventDevices,
|
||||
readableEventDevices: probe.readableEventDevices,
|
||||
keyboardDevices: probe.keyboardDevices,
|
||||
readableKeyboardDevices: probe.readableKeyboardDevices,
|
||||
inInputGroup: probe.inInputGroup,
|
||||
};
|
||||
}
|
||||
@@ -604,7 +641,7 @@ async function getLinuxEvdevStatus(): Promise<LinuxEvdevStatus> {
|
||||
};
|
||||
}
|
||||
const probe = await probeLinuxEvdevAccess();
|
||||
const hasAccess = probe.inInputGroup || probe.readableEventDevices > 0;
|
||||
const hasAccess = hasLinuxEvdevAccess(probe);
|
||||
let username: string | null = null;
|
||||
try {
|
||||
username = userInfo().username;
|
||||
@@ -701,7 +738,7 @@ async function grantLinuxEvdevAccess(): Promise<LinuxEvdevGrantResult> {
|
||||
};
|
||||
}
|
||||
const before = await probeLinuxEvdevAccess();
|
||||
if (before.inInputGroup || before.readableEventDevices > 0) {
|
||||
if (hasLinuxEvdevAccess(before)) {
|
||||
return {success: true, needsRelogin: false};
|
||||
}
|
||||
const username = (() => {
|
||||
@@ -719,7 +756,8 @@ async function grantLinuxEvdevAccess(): Promise<LinuxEvdevGrantResult> {
|
||||
return {success: false, needsRelogin: false, error: uaccess.error};
|
||||
}
|
||||
const afterUaccess = await waitForLinuxEvdevAccessRefresh();
|
||||
if (afterUaccess.inInputGroup || afterUaccess.readableEventDevices > 0) {
|
||||
if (hasLinuxEvdevAccess(afterUaccess)) {
|
||||
await reopenEvdevDevices();
|
||||
return {success: true, needsRelogin: false};
|
||||
}
|
||||
const groupFallback = await runPkexec(['usermod', '-aG', 'input', username]);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import {describe, test} from 'node:test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
const sourcePath = fileURLToPath(new URL('./LinuxInputAccess.ts', import.meta.url));
|
||||
const source = readFileSync(sourcePath, 'utf8');
|
||||
const transformedSource = esbuild.transformSync(source, {
|
||||
loader: 'ts',
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
}).code;
|
||||
|
||||
function loadLinuxInputAccess() {
|
||||
const module = {exports: {}};
|
||||
const context = vm.createContext({module, exports: module.exports});
|
||||
vm.runInContext(transformedSource, context, {filename: sourcePath});
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
const {isFullKeyboardKeyCapabilities, hasLinuxEvdevAccess} = loadLinuxInputAccess();
|
||||
|
||||
function probe(overrides) {
|
||||
return {
|
||||
totalEventDevices: 4,
|
||||
readableEventDevices: 0,
|
||||
keyboardDevices: 0,
|
||||
readableKeyboardDevices: 0,
|
||||
inInputGroup: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('isFullKeyboardKeyCapabilities', () => {
|
||||
test('accepts a real keyboard bitmap', () => {
|
||||
assert.equal(
|
||||
isFullKeyboardKeyCapabilities(
|
||||
'120013 0 0 0 0 0 0 0 0 1000000000007 ff9f207ac14057ff febeffdfffefffff fffffffffffffffe',
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts a keyboard bitmap from a 32-bit kernel', () => {
|
||||
assert.equal(isFullKeyboardKeyCapabilities('ffefffff fffffffe'), true);
|
||||
});
|
||||
|
||||
test('rejects a mouse bitmap', () => {
|
||||
assert.equal(isFullKeyboardKeyCapabilities('1f0000 0 0 0 0'), false);
|
||||
});
|
||||
|
||||
test('rejects a gamepad bitmap', () => {
|
||||
assert.equal(isFullKeyboardKeyCapabilities('7fdb000000000000 0 0 0 0'), false);
|
||||
});
|
||||
|
||||
test('rejects a media-key-only bitmap', () => {
|
||||
assert.equal(isFullKeyboardKeyCapabilities('10000 0 0 0 1c000000000000 0'), false);
|
||||
});
|
||||
|
||||
test('rejects empty and malformed input', () => {
|
||||
assert.equal(isFullKeyboardKeyCapabilities(''), false);
|
||||
assert.equal(isFullKeyboardKeyCapabilities('zz'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasLinuxEvdevAccess', () => {
|
||||
test('a readable gamepad does not count when the keyboard is unreadable', () => {
|
||||
assert.equal(hasLinuxEvdevAccess(probe({readableEventDevices: 1, keyboardDevices: 1})), false);
|
||||
});
|
||||
|
||||
test('every keyboard must be readable', () => {
|
||||
assert.equal(
|
||||
hasLinuxEvdevAccess(probe({readableEventDevices: 3, keyboardDevices: 2, readableKeyboardDevices: 1})),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
hasLinuxEvdevAccess(probe({readableEventDevices: 4, keyboardDevices: 2, readableKeyboardDevices: 2})),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('input group membership grants access', () => {
|
||||
assert.equal(hasLinuxEvdevAccess(probe({keyboardDevices: 1, inInputGroup: true})), true);
|
||||
});
|
||||
|
||||
test('falls back to any readable device when no keyboard is detected', () => {
|
||||
assert.equal(hasLinuxEvdevAccess(probe({readableEventDevices: 1})), true);
|
||||
assert.equal(hasLinuxEvdevAccess(probe({})), false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const FULL_KEYBOARD_MASK = 0xfffffffe;
|
||||
|
||||
export interface LinuxEvdevAccessProbe {
|
||||
totalEventDevices: number;
|
||||
readableEventDevices: number;
|
||||
keyboardDevices: number;
|
||||
readableKeyboardDevices: number;
|
||||
inInputGroup: boolean;
|
||||
}
|
||||
|
||||
export function isFullKeyboardKeyCapabilities(keyCapabilities: string): boolean {
|
||||
const words = keyCapabilities.trim().split(/\s+/);
|
||||
const lowestWord = words[words.length - 1] ?? '';
|
||||
const lowestBits = Number.parseInt(lowestWord.slice(-8), 16);
|
||||
if (!Number.isFinite(lowestBits)) return false;
|
||||
return (lowestBits & FULL_KEYBOARD_MASK) >>> 0 === FULL_KEYBOARD_MASK;
|
||||
}
|
||||
|
||||
export function hasLinuxEvdevAccess(probe: LinuxEvdevAccessProbe): boolean {
|
||||
if (probe.inInputGroup) return true;
|
||||
if (probe.keyboardDevices > 0) return probe.readableKeyboardDevices === probe.keyboardDevices;
|
||||
return probe.readableEventDevices > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user