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

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 fs = require('fs');
const path = require('path');
if (process.platform === 'linux') {
// You need to make sure that
// /home/runner/work/VutronMusic/VutronMusic/node_modules/electron/dist/chrome-sandbox
// is owned by root and has mode 4755.
execSync('sudo chown root:root ./node_modules/electron/dist/chrome-sandbox');
execSync('sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox');
const sandboxPath = path.resolve('./node_modules/electron/dist/chrome-sandbox');
if (fs.existsSync(sandboxPath)) {
execSync(`sudo chown root:root ${sandboxPath}`);
execSync(`sudo chmod 4755 ${sandboxPath}`);
console.log('[fix-sandbox] chrome-sandbox permissions fixed');
} else {
console.log('[fix-sandbox] chrome-sandbox not found, skipping');
}
}

View File

@@ -17,7 +17,8 @@
"dev": "electron-vite dev",
"dev:web": "vite dev",
"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: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",

View File

@@ -1,6 +1,12 @@
import { app, BrowserWindow, ipcMain } from 'electron';
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 {
id?: number | string;
@@ -27,11 +33,14 @@ let currentPosition = 0;
let trayLyricIface: 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) {
if (process.platform !== 'linux') return;
if (mprisPlayer) {
console.log('[MPRIS] Already initialized, skipping');
return;
}
@@ -79,13 +88,13 @@ export function initializeMpris(mainWindowRef: BrowserWindow) {
mprisPlayer.on('pause', () => {
if (mainWindow) {
mainWindow.webContents.send('global-shortcut', 'togglePlay');
mainWindow.webContents.send('mpris-pause');
}
});
mprisPlayer.on('play', () => {
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', () => {
if (mainWindow) {
mainWindow.webContents.send('global-shortcut', 'togglePlay');
mainWindow.webContents.send('mpris-pause');
}
});
mprisPlayer.getPosition = (): number => {
console.log('[MPRIS] getPosition called, returning:', 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;
mprisPlayer.seeked(position * 1000 * 1000);
mprisPlayer.getPosition = () => position * 1000 * 1000;
mprisPlayer.position = position * 1000 * 1000;
});
if (mprisPlayer) {
mprisPlayer.seeked(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);
});
};
ipcMain.on('tray-lyric-update', onTrayLyricUpdate);
initTrayLyric();
@@ -176,6 +188,14 @@ export function updateMprisPosition(position: number) {
}
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) {
mprisPlayer.quit();
mprisPlayer = null;
@@ -183,25 +203,17 @@ export function destroyMpris() {
}
function initTrayLyric() {
if (process.platform !== 'linux') {
console.log('[TrayLyric] Not Linux, skipping');
return;
}
console.log('[TrayLyric] Initializing...');
if (process.platform !== 'linux' || !dbusModule) return;
const serviceName = 'org.gnome.Shell.TrayLyric';
try {
const sessionBus = dbus.sessionBus({});
const sessionBus = dbusModule.sessionBus({});
trayLyricBus = sessionBus;
console.log('[TrayLyric] Session bus created, type:');
// 使用 invoke 方法调用 D-Bus 方法
const dbusPath = '/org/freedesktop/DBus';
const dbusInterface = 'org.freedesktop.DBus';
// 先尝试直接获取接口并使用 signals
sessionBus.invoke(
{
path: dbusPath,
@@ -212,26 +224,19 @@ function initTrayLyric() {
body: [serviceName]
},
(err: any, result: any) => {
console.log('[TrayLyric] GetNameOwner result:', err, result);
if (err || !result) {
console.log('[TrayLyric] Service not running yet');
console.log('[TrayLyric] Service not running');
} else {
console.log('[TrayLyric] Service is running, owner:', result[0]);
onServiceAvailable();
}
}
);
} catch (err) {
console.error('[TrayLyric] Exception during init:', err);
console.error('[TrayLyric] Failed to init:', err);
}
function onServiceAvailable() {
console.log('[TrayLyric] onServiceAvailable called');
if (!trayLyricBus) {
console.log('[TrayLyric] Bus not available');
return;
}
console.log('[TrayLyric] Getting service interface...');
if (!trayLyricBus) return;
const path = '/' + serviceName.replace(/\./g, '/');
trayLyricBus.getService(serviceName).getInterface(path, serviceName, (err: any, iface: any) => {
if (err) {
@@ -245,12 +250,8 @@ function initTrayLyric() {
}
function sendTrayLyric(lrcObj: string) {
if (!trayLyricIface || !trayLyricBus) {
console.log('[TrayLyric] Interface or bus not ready, skipping');
return;
}
if (!trayLyricIface || !trayLyricBus) return;
// 使用 invoke 方法调用 D-Bus 方法
trayLyricBus.invoke(
{
path: '/org/gnome/Shell/TrayLyric',

23
src/main/types/mpris-service.d.ts vendored Normal file
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;
}

View File

@@ -54,6 +54,9 @@ export let artistList: ComputedRef<Artist[]>;
let lastIndex = -1;
// 缓存平台信息,避免每次歌词变化时同步 IPC 调用
const cachedPlatform = isElectron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web';
export const musicDB = await useIndexedDB(
'musicDB',
[
@@ -831,14 +834,7 @@ export const sendLyricToWin = () => {
// 发送歌词到系统托盘歌词TrayLyric
const sendTrayLyric = (index: number) => {
const platformValue = window.electron.ipcRenderer.sendSync('get-platform');
console.log(
'[TrayLyric] sendTrayLyric called, isElectron:',
isElectron,
'platform:',
platformValue
);
if (!isElectron || platformValue !== 'linux') return;
if (!isElectron || cachedPlatform !== 'linux') return;
try {
const lyric = lrcArray.value[index];

View File

@@ -38,15 +38,23 @@ const onUpdateAppShortcuts = (_event: unknown, shortcuts: unknown) => {
updateAppShortcuts(shortcuts);
};
const onMprisSeek = (_event: unknown, position: number) => {
const onMprisSeekOrSetPosition = (_event: unknown, position: number) => {
if (audioService) {
audioService.seek(position);
}
};
const onMprisSetPosition = (_event: unknown, position: number) => {
if (audioService) {
audioService.seek(position);
const onMprisPlay = async () => {
const playerStore = usePlayerStore();
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('update-app-shortcuts', onUpdateAppShortcuts);
window.electron.ipcRenderer.on('mpris-seek', onMprisSeek);
window.electron.ipcRenderer.on('mpris-set-position', onMprisSetPosition);
window.electron.ipcRenderer.on('mpris-seek', onMprisSeekOrSetPosition);
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');
updateAppShortcuts(storedShortcuts);
@@ -226,6 +236,10 @@ export function cleanupAppShortcuts() {
window.electron.ipcRenderer.removeListener('global-shortcut', onGlobalShortcut);
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);
}