fix(mpris): 修复 MPRIS 模块多项安全和性能问题

- 将 fix-sandbox.js 从 postinstall 移除,避免 npm install 时执行 sudo
- 修复 play/pause/stop 事件语义错误,不再全部映射到 togglePlay
- 缓存平台信息避免 sendSync 阻塞渲染进程
- 修复 cleanupAppShortcuts 中缺少 MPRIS 监听器清理导致的事件泄漏
- destroyMpris 中添加 IPC 监听器清理
- 清理冗余调试日志,安全加载 dbus-native 模块
- 添加 mpris-service 类型声明解决跨平台类型检查问题
This commit is contained in:
alger
2026-04-11 22:37:26 +08:00
parent 3f31278131
commit 030a1f1c85
6 changed files with 105 additions and 58 deletions
+17 -5
View File
@@ -1,9 +1,21 @@
/**
* 修复 Linux 下 Electron sandbox 权限问题
* chrome-sandbox 需要 root 拥有且权限为 4755
*
* 注意:此脚本需要 sudo 权限,仅在 CI 环境或手动执行时使用
* 用法:sudo node fix-sandbox.js
*/
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
if (process.platform === 'linux') { if (process.platform === 'linux') {
// You need to make sure that const sandboxPath = path.resolve('./node_modules/electron/dist/chrome-sandbox');
// /home/runner/work/VutronMusic/VutronMusic/node_modules/electron/dist/chrome-sandbox if (fs.existsSync(sandboxPath)) {
// is owned by root and has mode 4755. execSync(`sudo chown root:root ${sandboxPath}`);
execSync('sudo chown root:root ./node_modules/electron/dist/chrome-sandbox'); execSync(`sudo chmod 4755 ${sandboxPath}`);
execSync('sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox'); console.log('[fix-sandbox] chrome-sandbox permissions fixed');
} else {
console.log('[fix-sandbox] chrome-sandbox not found, skipping');
}
} }
+2 -1
View File
@@ -17,7 +17,8 @@
"dev": "electron-vite dev", "dev": "electron-vite dev",
"dev:web": "vite dev", "dev:web": "vite dev",
"build": "electron-vite build", "build": "electron-vite build",
"postinstall": "node fix-sandbox.js && electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"fix-sandbox": "node fix-sandbox.js",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win --publish never", "build:win": "npm run build && electron-builder --win --publish never",
"build:mac": "npm run build && electron-builder --mac --x64 --publish never && cp dist/latest-mac.yml dist/latest-mac-x64.yml && electron-builder --mac --arm64 --publish never && cp dist/latest-mac.yml dist/latest-mac-arm64.yml && node scripts/merge_latest_mac_yml.mjs dist/latest-mac-x64.yml dist/latest-mac-arm64.yml dist/latest-mac.yml", "build:mac": "npm run build && electron-builder --mac --x64 --publish never && cp dist/latest-mac.yml dist/latest-mac-x64.yml && electron-builder --mac --arm64 --publish never && cp dist/latest-mac.yml dist/latest-mac-arm64.yml && node scripts/merge_latest_mac_yml.mjs dist/latest-mac-x64.yml dist/latest-mac-arm64.yml dist/latest-mac.yml",
+39 -38
View File
@@ -1,6 +1,12 @@
import { app, BrowserWindow, ipcMain } from 'electron'; import { app, BrowserWindow, ipcMain } from 'electron';
import Player from 'mpris-service'; import Player from 'mpris-service';
const dbus = require('@httptoolkit/dbus-native');
let dbusModule: any;
try {
dbusModule = require('@httptoolkit/dbus-native');
} catch {
// dbus-native 不可用(非 Linux 环境)
}
interface SongInfo { interface SongInfo {
id?: number | string; id?: number | string;
@@ -27,11 +33,14 @@ let currentPosition = 0;
let trayLyricIface: any = null; let trayLyricIface: any = null;
let trayLyricBus: any = null; let trayLyricBus: any = null;
// 保存 IPC 处理函数引用,用于清理
let onPositionUpdate: ((event: any, position: number) => void) | null = null;
let onTrayLyricUpdate: ((event: any, lrcObj: string) => void) | null = null;
export function initializeMpris(mainWindowRef: BrowserWindow) { export function initializeMpris(mainWindowRef: BrowserWindow) {
if (process.platform !== 'linux') return; if (process.platform !== 'linux') return;
if (mprisPlayer) { if (mprisPlayer) {
console.log('[MPRIS] Already initialized, skipping');
return; return;
} }
@@ -79,13 +88,13 @@ export function initializeMpris(mainWindowRef: BrowserWindow) {
mprisPlayer.on('pause', () => { mprisPlayer.on('pause', () => {
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('global-shortcut', 'togglePlay'); mainWindow.webContents.send('mpris-pause');
} }
}); });
mprisPlayer.on('play', () => { mprisPlayer.on('play', () => {
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('global-shortcut', 'togglePlay'); mainWindow.webContents.send('mpris-play');
} }
}); });
@@ -97,12 +106,11 @@ export function initializeMpris(mainWindowRef: BrowserWindow) {
mprisPlayer.on('stop', () => { mprisPlayer.on('stop', () => {
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('global-shortcut', 'togglePlay'); mainWindow.webContents.send('mpris-pause');
} }
}); });
mprisPlayer.getPosition = (): number => { mprisPlayer.getPosition = (): number => {
console.log('[MPRIS] getPosition called, returning:', currentPosition);
return currentPosition; return currentPosition;
}; };
@@ -119,16 +127,20 @@ export function initializeMpris(mainWindowRef: BrowserWindow) {
} }
}); });
ipcMain.on('mpris-position-update', (_, position: number) => { onPositionUpdate = (_, position: number) => {
currentPosition = position * 1000 * 1000; currentPosition = position * 1000 * 1000;
mprisPlayer.seeked(position * 1000 * 1000); if (mprisPlayer) {
mprisPlayer.getPosition = () => position * 1000 * 1000; mprisPlayer.seeked(position * 1000 * 1000);
mprisPlayer.position = position * 1000 * 1000; mprisPlayer.getPosition = () => position * 1000 * 1000;
}); mprisPlayer.position = position * 1000 * 1000;
}
};
ipcMain.on('mpris-position-update', onPositionUpdate);
ipcMain.on('tray-lyric-update', async (_, lrcObj: string) => { onTrayLyricUpdate = (_, lrcObj: string) => {
sendTrayLyric(lrcObj); sendTrayLyric(lrcObj);
}); };
ipcMain.on('tray-lyric-update', onTrayLyricUpdate);
initTrayLyric(); initTrayLyric();
@@ -176,6 +188,14 @@ export function updateMprisPosition(position: number) {
} }
export function destroyMpris() { export function destroyMpris() {
if (onPositionUpdate) {
ipcMain.removeListener('mpris-position-update', onPositionUpdate);
onPositionUpdate = null;
}
if (onTrayLyricUpdate) {
ipcMain.removeListener('tray-lyric-update', onTrayLyricUpdate);
onTrayLyricUpdate = null;
}
if (mprisPlayer) { if (mprisPlayer) {
mprisPlayer.quit(); mprisPlayer.quit();
mprisPlayer = null; mprisPlayer = null;
@@ -183,25 +203,17 @@ export function destroyMpris() {
} }
function initTrayLyric() { function initTrayLyric() {
if (process.platform !== 'linux') { if (process.platform !== 'linux' || !dbusModule) return;
console.log('[TrayLyric] Not Linux, skipping');
return;
}
console.log('[TrayLyric] Initializing...');
const serviceName = 'org.gnome.Shell.TrayLyric'; const serviceName = 'org.gnome.Shell.TrayLyric';
try { try {
const sessionBus = dbus.sessionBus({}); const sessionBus = dbusModule.sessionBus({});
trayLyricBus = sessionBus; trayLyricBus = sessionBus;
console.log('[TrayLyric] Session bus created, type:');
// 使用 invoke 方法调用 D-Bus 方法
const dbusPath = '/org/freedesktop/DBus'; const dbusPath = '/org/freedesktop/DBus';
const dbusInterface = 'org.freedesktop.DBus'; const dbusInterface = 'org.freedesktop.DBus';
// 先尝试直接获取接口并使用 signals
sessionBus.invoke( sessionBus.invoke(
{ {
path: dbusPath, path: dbusPath,
@@ -212,26 +224,19 @@ function initTrayLyric() {
body: [serviceName] body: [serviceName]
}, },
(err: any, result: any) => { (err: any, result: any) => {
console.log('[TrayLyric] GetNameOwner result:', err, result);
if (err || !result) { if (err || !result) {
console.log('[TrayLyric] Service not running yet'); console.log('[TrayLyric] Service not running');
} else { } else {
console.log('[TrayLyric] Service is running, owner:', result[0]);
onServiceAvailable(); onServiceAvailable();
} }
} }
); );
} catch (err) { } catch (err) {
console.error('[TrayLyric] Exception during init:', err); console.error('[TrayLyric] Failed to init:', err);
} }
function onServiceAvailable() { function onServiceAvailable() {
console.log('[TrayLyric] onServiceAvailable called'); if (!trayLyricBus) return;
if (!trayLyricBus) {
console.log('[TrayLyric] Bus not available');
return;
}
console.log('[TrayLyric] Getting service interface...');
const path = '/' + serviceName.replace(/\./g, '/'); const path = '/' + serviceName.replace(/\./g, '/');
trayLyricBus.getService(serviceName).getInterface(path, serviceName, (err: any, iface: any) => { trayLyricBus.getService(serviceName).getInterface(path, serviceName, (err: any, iface: any) => {
if (err) { if (err) {
@@ -245,12 +250,8 @@ function initTrayLyric() {
} }
function sendTrayLyric(lrcObj: string) { function sendTrayLyric(lrcObj: string) {
if (!trayLyricIface || !trayLyricBus) { if (!trayLyricIface || !trayLyricBus) return;
console.log('[TrayLyric] Interface or bus not ready, skipping');
return;
}
// 使用 invoke 方法调用 D-Bus 方法
trayLyricBus.invoke( trayLyricBus.invoke(
{ {
path: '/org/gnome/Shell/TrayLyric', path: '/org/gnome/Shell/TrayLyric',
+23
View File
@@ -0,0 +1,23 @@
declare module 'mpris-service' {
interface PlayerOptions {
name: string;
identity: string;
supportedUriSchemes?: string[];
supportedMimeTypes?: string[];
supportedInterfaces?: string[];
}
interface Player {
on(event: string, callback: (...args: any[]) => void): void;
playbackStatus: string;
metadata: Record<string, any>;
position: number;
getPosition: () => number;
seeked(position: number): void;
objectPath(path: string): string;
quit(): void;
}
function Player(options: PlayerOptions): Player;
export = Player;
}
+4 -8
View File
@@ -54,6 +54,9 @@ export let artistList: ComputedRef<Artist[]>;
let lastIndex = -1; let lastIndex = -1;
// 缓存平台信息,避免每次歌词变化时同步 IPC 调用
const cachedPlatform = isElectron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web';
export const musicDB = await useIndexedDB( export const musicDB = await useIndexedDB(
'musicDB', 'musicDB',
[ [
@@ -831,14 +834,7 @@ export const sendLyricToWin = () => {
// 发送歌词到系统托盘歌词(TrayLyric) // 发送歌词到系统托盘歌词(TrayLyric)
const sendTrayLyric = (index: number) => { const sendTrayLyric = (index: number) => {
const platformValue = window.electron.ipcRenderer.sendSync('get-platform'); if (!isElectron || cachedPlatform !== 'linux') return;
console.log(
'[TrayLyric] sendTrayLyric called, isElectron:',
isElectron,
'platform:',
platformValue
);
if (!isElectron || platformValue !== 'linux') return;
try { try {
const lyric = lrcArray.value[index]; const lyric = lrcArray.value[index];
+20 -6
View File
@@ -38,15 +38,23 @@ const onUpdateAppShortcuts = (_event: unknown, shortcuts: unknown) => {
updateAppShortcuts(shortcuts); updateAppShortcuts(shortcuts);
}; };
const onMprisSeek = (_event: unknown, position: number) => { const onMprisSeekOrSetPosition = (_event: unknown, position: number) => {
if (audioService) { if (audioService) {
audioService.seek(position); audioService.seek(position);
} }
}; };
const onMprisSetPosition = (_event: unknown, position: number) => { const onMprisPlay = async () => {
if (audioService) { const playerStore = usePlayerStore();
audioService.seek(position); if (!playerStore.play && playerStore.playMusic?.id) {
await playerStore.setPlay({ ...playerStore.playMusic });
}
};
const onMprisPause = async () => {
const playerStore = usePlayerStore();
if (playerStore.play) {
await playerStore.handlePause();
} }
}; };
@@ -205,8 +213,10 @@ export function initAppShortcuts() {
window.electron.ipcRenderer.on('global-shortcut', onGlobalShortcut); window.electron.ipcRenderer.on('global-shortcut', onGlobalShortcut);
window.electron.ipcRenderer.on('update-app-shortcuts', onUpdateAppShortcuts); window.electron.ipcRenderer.on('update-app-shortcuts', onUpdateAppShortcuts);
window.electron.ipcRenderer.on('mpris-seek', onMprisSeek); window.electron.ipcRenderer.on('mpris-seek', onMprisSeekOrSetPosition);
window.electron.ipcRenderer.on('mpris-set-position', onMprisSetPosition); window.electron.ipcRenderer.on('mpris-set-position', onMprisSeekOrSetPosition);
window.electron.ipcRenderer.on('mpris-play', onMprisPlay);
window.electron.ipcRenderer.on('mpris-pause', onMprisPause);
const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts'); const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');
updateAppShortcuts(storedShortcuts); updateAppShortcuts(storedShortcuts);
@@ -226,6 +236,10 @@ export function cleanupAppShortcuts() {
window.electron.ipcRenderer.removeListener('global-shortcut', onGlobalShortcut); window.electron.ipcRenderer.removeListener('global-shortcut', onGlobalShortcut);
window.electron.ipcRenderer.removeListener('update-app-shortcuts', onUpdateAppShortcuts); window.electron.ipcRenderer.removeListener('update-app-shortcuts', onUpdateAppShortcuts);
window.electron.ipcRenderer.removeListener('mpris-seek', onMprisSeekOrSetPosition);
window.electron.ipcRenderer.removeListener('mpris-set-position', onMprisSeekOrSetPosition);
window.electron.ipcRenderer.removeListener('mpris-play', onMprisPlay);
window.electron.ipcRenderer.removeListener('mpris-pause', onMprisPause);
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', handleKeyDown);
} }