phper/electron/services/HostsManager.ts
2025-12-26 03:35:54 +08:00

225 lines
6.3 KiB
TypeScript

import { exec } from 'child_process'
import { promisify } from 'util'
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'
import { join } from 'path'
import sudo from 'sudo-prompt'
const execAsync = promisify(exec)
const sudoExec = (command: string, name: string): Promise<{ stdout: string; stderr: string }> => {
return new Promise((resolve, reject) => {
sudo.exec(command, { name }, (error, stdout, stderr) => {
if (error) {
reject(error)
} else {
resolve({ stdout: stdout?.toString() || '', stderr: stderr?.toString() || '' })
}
})
})
}
interface HostEntry {
ip: string
domain: string
comment?: string
}
export class HostsManager {
private hostsPath: string
constructor() {
// Windows hosts 文件路径
this.hostsPath = join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'drivers', 'etc', 'hosts')
}
/**
* 获取 hosts 文件内容
*/
async getHosts(): Promise<HostEntry[]> {
try {
if (!existsSync(this.hostsPath)) {
return []
}
const content = readFileSync(this.hostsPath, 'utf-8')
const entries: HostEntry[] = []
const lines = content.split('\n')
for (const line of lines) {
const trimmed = line.trim()
// 跳过空行和注释
if (!trimmed || trimmed.startsWith('#')) {
continue
}
// 解析行
const match = trimmed.match(/^(\S+)\s+(\S+)(?:\s+#\s*(.*))?$/)
if (match) {
entries.push({
ip: match[1],
domain: match[2],
comment: match[3]
})
}
}
return entries
} catch (error) {
console.error('读取 hosts 文件失败:', error)
return []
}
}
/**
* 添加 hosts 条目
*/
async addHost(domain: string, ip: string = '127.0.0.1'): Promise<{ success: boolean; message: string }> {
try {
// 读取现有内容
let content = ''
if (existsSync(this.hostsPath)) {
content = readFileSync(this.hostsPath, 'utf-8')
}
// 检查是否已存在
const regex = new RegExp(`^\\s*\\S+\\s+${this.escapeRegex(domain)}\\s*$`, 'gm')
if (regex.test(content)) {
// 更新现有条目
content = content.replace(regex, `${ip}\t${domain}`)
} else {
// 添加新条目
const newEntry = `${ip}\t${domain}\t# Added by PHPer Dev Manager`
content = content.trimEnd() + '\n' + newEntry + '\n'
}
// 写入文件(需要管理员权限)
await this.writeHostsFile(content)
return { success: true, message: `已添加 ${domain} -> ${ip}` }
} catch (error: any) {
return { success: false, message: `添加失败: ${error.message}` }
}
}
/**
* 删除 hosts 条目
*/
async removeHost(domain: string): Promise<{ success: boolean; message: string }> {
try {
if (!existsSync(this.hostsPath)) {
return { success: false, message: 'hosts 文件不存在' }
}
let content = readFileSync(this.hostsPath, 'utf-8')
// 删除匹配的行
const lines = content.split('\n')
const newLines = lines.filter(line => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) {
return true
}
const match = trimmed.match(/^\S+\s+(\S+)/)
return !match || match[1] !== domain
})
content = newLines.join('\n')
// 写入文件
await this.writeHostsFile(content)
return { success: true, message: `已删除 ${domain}` }
} catch (error: any) {
return { success: false, message: `删除失败: ${error.message}` }
}
}
/**
* 批量添加 hosts 条目
*/
async addHosts(entries: HostEntry[]): Promise<{ success: boolean; message: string }> {
try {
let content = ''
if (existsSync(this.hostsPath)) {
content = readFileSync(this.hostsPath, 'utf-8')
}
for (const entry of entries) {
const regex = new RegExp(`^\\s*\\S+\\s+${this.escapeRegex(entry.domain)}\\s*(?:#.*)?$`, 'gm')
if (regex.test(content)) {
// 更新现有条目
content = content.replace(regex, `${entry.ip}\t${entry.domain}${entry.comment ? `\t# ${entry.comment}` : ''}`)
} else {
// 添加新条目
const newEntry = `${entry.ip}\t${entry.domain}${entry.comment ? `\t# ${entry.comment}` : ''}`
content = content.trimEnd() + '\n' + newEntry
}
}
content = content.trimEnd() + '\n'
await this.writeHostsFile(content)
return { success: true, message: `已添加 ${entries.length} 个条目` }
} catch (error: any) {
return { success: false, message: `添加失败: ${error.message}` }
}
}
/**
* 刷新 DNS 缓存
*/
async flushDns(): Promise<{ success: boolean; message: string }> {
try {
await execAsync('ipconfig /flushdns')
return { success: true, message: 'DNS 缓存已刷新' }
} catch (error: any) {
return { success: false, message: `刷新失败: ${error.message}` }
}
}
// ==================== 私有方法 ====================
private async writeHostsFile(content: string): Promise<void> {
// 直接写入(需要管理员权限运行应用)
try {
writeFileSync(this.hostsPath, content, 'utf-8')
} catch (error: any) {
if (error.code === 'EPERM' || error.code === 'EACCES') {
// 尝试使用 sudo-prompt 提权写入
const tempPath = join(process.env.TEMP || 'C:\\Temp', 'hosts_phper.tmp')
writeFileSync(tempPath, content, 'utf-8')
// 使用 copy 命令复制临时文件到 hosts
const command = `copy /Y "${tempPath}" "${this.hostsPath}"`
try {
await sudoExec(command, 'PHPer Dev Manager')
// 清理临时文件
try {
unlinkSync(tempPath)
} catch (e) {
// 忽略清理错误
}
} catch (sudoError: any) {
// 清理临时文件
try {
unlinkSync(tempPath)
} catch (e) {
// 忽略清理错误
}
throw new Error(`需要管理员权限修改 hosts 文件: ${sudoError.message}`)
}
} else {
throw error
}
}
}
private escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
}