fix: preserve and recover macOS helper service (#2052)

This commit is contained in:
TonyCrane
2026-08-05 19:25:48 +08:00
committed by GitHub
parent 6b56108ec4
commit d7a79bc054
5 changed files with 83 additions and 47 deletions

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>party.mihomo.helper</string>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>party.mihomo.app</string>
</array>
<key>KeepAlive</key>
<true/>
<key>Program</key>
<string>/Library/PrivilegedHelperTools/party.mihomo.helper</string>
<key>StandardErrorPath</key>
<string>/tmp/party.mihomo.helper.err</string>
<key>StandardOutPath</key>
<string>/tmp/party.mihomo.helper.log</string>
</dict>
</plist>

View File

@@ -24,6 +24,7 @@ fi
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
LAUNCH_DAEMON_SOURCE="$APP_PATH/Contents/Resources/files/party.mihomo.helper.plist"
log "Starting installation..."
@@ -74,32 +75,13 @@ fi
# 创建并配置 LaunchDaemon
log "Configuring LaunchDaemon..."
mkdir -p "/Library/LaunchDaemons"
cat << EOF > "$LAUNCH_DAEMON"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>party.mihomo.helper</string>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>party.mihomo.app</string>
</array>
<key>KeepAlive</key>
<true/>
<key>Program</key>
<string>${HELPER_PATH}</string>
<key>StandardErrorPath</key>
<string>/tmp/party.mihomo.helper.err</string>
<key>StandardOutPath</key>
<string>/tmp/party.mihomo.helper.log</string>
</dict>
</plist>
EOF
chown root:wheel "$LAUNCH_DAEMON"
chmod 644 "$LAUNCH_DAEMON"
if [ -f "$LAUNCH_DAEMON_SOURCE" ]; then
/usr/bin/install -o root -g wheel -m 644 "$LAUNCH_DAEMON_SOURCE" "$LAUNCH_DAEMON"
log "LaunchDaemon configured"
else
log "Error: LaunchDaemon file not found at $LAUNCH_DAEMON_SOURCE"
exit 1
fi
# 验证关键文件
log "Verifying installation..."

View File

@@ -7,18 +7,6 @@ if [ "$EUID" -ne 0 ]; then
exit 1
fi
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
# 停止并卸载现有的 LaunchDaemon
if [ -f "$LAUNCH_DAEMON" ]; then
launchctl unload "$LAUNCH_DAEMON" 2>/dev/null || true
rm -f "$LAUNCH_DAEMON"
fi
# 移除 helper 工具
rm -f "$HELPER_PATH"
# 清理可能存在的旧版本文件
rm -rf "/Applications/Clash Party.app"
rm -rf "/Applications/Clash\\ Party.app"

View File

@@ -46,6 +46,9 @@ nsis:
mac:
target:
- pkg
extraResources:
- from: './build/pkg-scripts/party.mihomo.helper.plist'
to: 'files/party.mihomo.helper.plist'
entitlementsInherit: build/entitlements.mac.plist
hardenedRuntime: true
gatekeeperAssess: false

View File

@@ -1,6 +1,7 @@
import { promisify } from 'util'
import { exec } from 'child_process'
import { exec, execFile } from 'child_process'
import fs from 'fs'
import path from 'path'
import { triggerAutoProxy, triggerManualProxy } from 'sysproxy-rs'
import { net } from 'electron'
import axios from 'axios'
@@ -8,9 +9,13 @@ import { getAppConfig, getControledMihomoConfig } from '../config'
import { DEFAULT_MIHOMO_PORTS } from '../../shared/appConfig'
import { pacPort, startPacServer, stopPacServer } from '../resolve/server'
import { proxyLogger } from '../utils/logger'
import { resourcesFilesDir } from '../utils/dirs'
let triggerSysProxyTimer: NodeJS.Timeout | null = null
const helperSocketPath = '/tmp/mihomo-party-helper.sock'
const helperPath = '/Library/PrivilegedHelperTools/party.mihomo.helper'
const helperPlistPath = '/Library/LaunchDaemons/party.mihomo.helper.plist'
const helperService = 'system/party.mihomo.helper'
const defaultBypass: string[] = (() => {
switch (process.platform) {
@@ -180,11 +185,39 @@ async function isHelperRunning(): Promise<boolean> {
}
}
async function startHelperService(): Promise<void> {
async function isHelperServiceRegistered(): Promise<boolean> {
try {
const execPromise = promisify(exec)
const shell = `launchctl kickstart -k system/party.mihomo.helper`
const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`)
await execPromise(`/bin/launchctl print ${helperService}`)
return true
} catch {
return false
}
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'"'"'`)}'`
}
async function startHelperService(forceRepair = false): Promise<void> {
const sourceHelper = shellQuote(path.join(resourcesFilesDir(), 'party.mihomo.helper'))
const sourcePlist = shellQuote(path.join(resourcesFilesDir(), 'party.mihomo.helper.plist'))
const installedHelper = shellQuote(helperPath)
const installedPlist = shellQuote(helperPlistPath)
const shell =
`if ${forceRepair ? 'true' : 'false'} || [ ! -x ${installedHelper} ] || [ ! -f ${installedPlist} ] || ! /bin/launchctl print ${helperService} >/dev/null 2>&1; then ` +
`[ -f ${sourceHelper} ] && [ -f ${sourcePlist} ] || exit 1; ` +
`/bin/launchctl bootout ${helperService} >/dev/null 2>&1 || true; ` +
`/bin/mkdir -p /Library/PrivilegedHelperTools /Library/LaunchDaemons; ` +
`/bin/rm -f ${shellQuote(helperSocketPath)}; ` +
`/usr/bin/install -o root -g wheel -m 544 ${sourceHelper} ${installedHelper}; ` +
`/usr/bin/install -o root -g wheel -m 644 ${sourcePlist} ${installedPlist}; ` +
`/bin/launchctl enable ${helperService} >/dev/null 2>&1 || true; ` +
`/bin/launchctl bootstrap system ${installedPlist}; ` +
`else /bin/launchctl kickstart -k ${helperService}; fi`
const command = `do shell script ${JSON.stringify(shell)} with administrator privileges`
const execFilePromise = promisify(execFile)
await execFilePromise('/usr/bin/osascript', ['-e', command])
await new Promise((resolve) => setTimeout(resolve, 1500))
}
@@ -224,12 +257,13 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2):
)
const helperRunning = await isHelperRunning()
const helperRegistered = await isHelperServiceRegistered()
const socketExists = isSocketFileExists()
if (!helperRunning) {
await proxyLogger.info('Helper process not running, starting service...')
if (!helperRunning || !helperRegistered || !fs.existsSync(helperPlistPath)) {
await proxyLogger.info('Helper service unavailable, repairing...')
try {
await startHelperService()
await startHelperService(!helperRunning)
await proxyLogger.info('Helper service started, retrying...')
continue
} catch (startError) {
@@ -244,6 +278,15 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2):
} catch (signalError) {
await proxyLogger.warn('Failed to request socket recreation', signalError)
}
} else {
await proxyLogger.info('Helper socket is unresponsive, restarting service...')
try {
await startHelperService()
await proxyLogger.info('Helper service restarted, retrying...')
continue
} catch (restartError) {
await proxyLogger.warn('Failed to restart helper service', restartError)
}
}
}