Files
JRedisDesktop/src/main/ipc-handlers.ts
T
Jokul 1a5e4dd706 fix: patch all P2 quality/performance issues from project review
- ipc-handlers: withLog extracts actual connection ID from args[0] (P2#20)
- updater: add initialized guard to prevent duplicate IPC registration (P2#25)
- win-state: log writeFile errors instead of empty callback (P2#26)
- format: detectFormat samples up to 4096 chars instead of 100 (P2#28)
- commandLogger: add 30+ missing write commands to WRITE_COMMANDS (P2#32)
- ReJsonEditor: bind Monaco theme to monacoTheme computed (P2#19)
- keys: normalize empty SCAN pattern to '*' (P2#21)
- SlowLogView: remove duplicate <style scoped> block (P2#29)
- CliView: deduplicate XGROUP in writeCommands Set (P2#33)
- key store: use connectionStore instead of IPC for separator (P2#22)
- NewConnectionDialog: add visible guard after async watch (P2#30)
- KeyList: remove dangerouslyUseHTMLString, use plain text + i18n (P2#31)
- KeyList: batch DUMP export with Promise.allSettled (batch=20) (P2#23)
- HashEditor: lazy load field TTL on click instead of bulk HTTL (P2#24)
- P2#27 already fixed in P0#2; P2#34 is feature (N4); P2#35 not a bug
- docs: update PROJECT_REVIEW.md with P2 fix status
2026-07-12 14:30:18 +08:00

350 lines
14 KiB
TypeScript

// IPC 通道处理器
import { ipcMain, BrowserWindow, nativeTheme, dialog } from 'electron'
import store, { type ConnectionConfig } from './store'
import { encrypt, decrypt } from './credential'
import * as redis from './redis'
// Helper: wrap handler with command logging
function withLog(cmd: string, fn: (...args: any[]) => any) {
return async (_e: any, ...args: any[]) => {
const start = Date.now()
const connId = args[0] || 'unknown'
try {
const result = await fn(_e, ...args)
redis.log(connId, cmd, Date.now() - start)
return result
} catch (err) {
redis.log(connId, cmd, Date.now() - start)
throw err
}
}
}
export function registerIpcHandlers(): void {
// Window
ipcMain.on('window:minimize', () => BrowserWindow.getFocusedWindow()?.minimize())
ipcMain.on('window:maximize', () => {
const win = BrowserWindow.getFocusedWindow()
if (win) win.isMaximized() ? win.unmaximize() : win.maximize()
})
ipcMain.on('window:close', () => BrowserWindow.getFocusedWindow()?.close())
// Theme
ipcMain.handle('theme:get', () => nativeTheme.shouldUseDarkColors)
ipcMain.on('theme:set', (_e, theme: 'system' | 'dark' | 'light') => {
nativeTheme.themeSource = theme
})
// Dialog
ipcMain.handle('dialog:openFile', async (_e, options: Electron.OpenDialogOptions) => {
const win = BrowserWindow.getFocusedWindow()
if (!win) return null
const result = await dialog.showOpenDialog(win, options)
if (result.canceled || result.filePaths.length === 0) return null
const fs = await import('fs')
try {
return { path: result.filePaths[0], content: fs.readFileSync(result.filePaths[0], 'utf-8') }
} catch { return null }
})
// Credential
ipcMain.handle('credential:encrypt', (_e, text: string) => encrypt(text))
ipcMain.handle('credential:decrypt', (_e, encoded: string) => decrypt(encoded))
// Storage - connections
ipcMain.handle('storage:getConnections', async () => {
const connections = store.get('connections', [])
// Decrypt sensitive fields
for (const conn of connections) {
if (conn.auth && conn.auth.startsWith('enc:')) {
try { conn.auth = await decrypt(conn.auth.slice(4)) } catch { /* keep as-is */ }
}
if (conn.sshPassword && conn.sshPassword.startsWith('enc:')) {
try { conn.sshPassword = await decrypt(conn.sshPassword.slice(4)) } catch { /* keep as-is */ }
}
if (conn.sshPassphrase && conn.sshPassphrase.startsWith('enc:')) {
try { conn.sshPassphrase = await decrypt(conn.sshPassphrase.slice(4)) } catch { /* keep as-is */ }
}
}
return connections
})
ipcMain.handle('storage:saveConnection', async (_e, conn: ConnectionConfig) => {
// Encrypt sensitive fields before storing (create copy to avoid mutation)
const encrypted: ConnectionConfig = { ...conn }
if (encrypted.auth && !encrypted.auth.startsWith('enc:')) {
encrypted.auth = 'enc:' + await encrypt(encrypted.auth)
}
if (encrypted.sshPassword && !encrypted.sshPassword.startsWith('enc:')) {
encrypted.sshPassword = 'enc:' + await encrypt(encrypted.sshPassword)
}
if (encrypted.sshPassphrase && !encrypted.sshPassphrase.startsWith('enc:')) {
encrypted.sshPassphrase = 'enc:' + await encrypt(encrypted.sshPassphrase)
}
const connections = store.get('connections', [])
const idx = connections.findIndex((c: ConnectionConfig) => c.id === encrypted.id)
if (idx >= 0) connections[idx] = encrypted
else connections.push(encrypted)
store.set('connections', connections)
return connections
})
ipcMain.handle('storage:deleteConnection', (_e, id: string) => {
const connections = store.get('connections', []).filter((c: ConnectionConfig) => c.id !== id)
store.set('connections', connections)
return connections
})
ipcMain.handle('storage:reorderConnections', (_e, ids: string[]) => {
const connections = store.get('connections', [])
const reordered = ids.map((id, i) => {
const conn = connections.find((c: ConnectionConfig) => c.id === id)
if (!conn) return null
conn.order = i
return conn
}).filter((c): c is ConnectionConfig => c !== null)
store.set('connections', reordered)
return reordered
})
// Storage - settings
ipcMain.handle('storage:getSettings', () => store.get('settings'))
ipcMain.handle('storage:saveSettings', (_e, settings) => {
store.set('settings', settings)
})
// Redis - 连接
ipcMain.handle('redis:connect', async (_e, id: string, opts) => {
try {
await redis.connect(id, opts)
return { success: true }
} catch (err: any) {
return { success: false, error: err.message }
}
})
ipcMain.handle('redis:disconnect', async (_e, id: string) => {
await redis.disconnect(id)
})
ipcMain.handle('redis:ping', async (_e, id: string) => {
return redis.ping(id)
})
ipcMain.handle('redis:isConnected', (_e, id: string) => {
return redis.isClientConnected(id)
})
// Blocked dangerous commands — blocked in redis:execute but allowed via redis:executeUnsafe (CLI)
const BLOCKED_COMMANDS = new Set([
'FLUSHALL', 'FLUSHDB', 'SHUTDOWN', 'DEBUG',
])
// Redis - 服务器 (safe execute — blocks dangerous commands)
ipcMain.handle('redis:execute', withLog('EXEC', async (_e, id: string, command: string, args: string[]) => {
const cmdUpper = command.toUpperCase().split(/\s+/)[0]
if (BLOCKED_COMMANDS.has(cmdUpper)) {
throw new Error(`Command "${command}" is blocked for safety. Use CLI for administrative commands.`)
}
return redis.execute(id, command, args)
}))
// Redis - unsafe execute (bypasses block — for CLI use only)
ipcMain.handle('redis:executeUnsafe', withLog('EXEC', async (_e, id: string, command: string, args: string[]) => {
return redis.execute(id, command, args)
}))
ipcMain.handle('redis:getInfo', async (_e, id: string) => {
return redis.getServerInfo(id)
})
ipcMain.handle('redis:selectDb', withLog('SELECT', async (_e, id: string, db: number) => {
await redis.selectDb(id, db)
}))
ipcMain.handle('redis:dbSize', async (_e, id: string) => {
return redis.dbSize(id)
})
ipcMain.handle('redis:slowLogGet', async (_e, id: string, count: number) => {
return redis.slowLogGet(id, count)
})
ipcMain.handle('redis:slowLogLen', async (_e, id: string) => {
return redis.slowLogLen(id)
})
// Redis - Key 操作
ipcMain.handle('redis:scanKeys', async (_e, id: string, cursor: string, pattern: string, count: number) => {
return redis.scanKeys(id, cursor, pattern, count)
})
ipcMain.handle('redis:getKeyType', async (_e, id: string, key: string) => {
return redis.getKeyType(id, key)
})
ipcMain.handle('redis:getKeyTTL', async (_e, id: string, key: string) => {
return redis.getKeyTTL(id, key)
})
ipcMain.handle('redis:setTTL', withLog('EXPIRE', async (_e, id: string, key: string, ttl: number) => {
await redis.setKeyTTL(id, key, ttl)
}))
ipcMain.handle('redis:deleteKey', withLog('DEL', async (_e, id: string, key: string) => {
await redis.deleteKey(id, key)
}))
ipcMain.handle('redis:renameKey', withLog('RENAME', async (_e, id: string, oldKey: string, newKey: string) => {
await redis.renameKey(id, oldKey, newKey)
}))
ipcMain.handle('redis:existsKey', async (_e, id: string, key: string) => {
return redis.existsKey(id, key)
})
ipcMain.handle('redis:batchDelete', withLog('DEL batch', async (_e, id: string, keys: string[]) => {
return redis.batchDelete(id, keys)
}))
ipcMain.handle('redis:memoryUsage', async (_e, id: string, key: string) => {
return redis.memoryUsage(id, key)
})
ipcMain.handle('redis:batchMemoryUsage', async (_e, id: string, keys: string[]) => {
return redis.batchKeyMemoryUsage(id, keys)
})
// Redis - String
ipcMain.handle('redis:getString', async (_e, id: string, key: string) => {
return redis.getStringValue(id, key)
})
ipcMain.handle('redis:setString', withLog('SET', async (_e, id: string, key: string, value: string) => {
await redis.setStringValue(id, key, value)
}))
// ReJson
ipcMain.handle('redis:rejsonGet', async (_e, id: string, key: string, path: string) => {
return redis.rejsonGet(id, key, path)
})
ipcMain.handle('redis:rejsonSet', withLog('JSON.SET', async (_e, id: string, key: string, path: string, value: string) => {
await redis.rejsonSet(id, key, path, value)
}))
// Redis - Hash
ipcMain.handle('redis:hashScan', async (_e, id: string, key: string, cursor: string, count: number) => {
return redis.getHashFields(id, key, cursor, count)
})
ipcMain.handle('redis:hashSet', withLog('HSET', async (_e, id: string, key: string, field: string, value: string) => {
await redis.setHashField(id, key, field, value)
}))
ipcMain.handle('redis:hashDel', withLog('HDEL', async (_e, id: string, key: string, field: string) => {
await redis.deleteHashField(id, key, field)
}))
// Redis - List
ipcMain.handle('redis:listRange', async (_e, id: string, key: string, start: number, stop: number) => {
return redis.listRange(id, key, start, stop)
})
ipcMain.handle('redis:listLength', async (_e, id: string, key: string) => {
return redis.listLength(id, key)
})
ipcMain.handle('redis:listPush', withLog('RPUSH', async (_e, id: string, key: string, values: string[]) => {
return redis.listPush(id, key, ...values)
}))
ipcMain.handle('redis:listSet', withLog('LSET', async (_e, id: string, key: string, index: number, value: string) => {
await redis.listSet(id, key, index, value)
}))
ipcMain.handle('redis:listRemove', withLog('LREM', async (_e, id: string, key: string, count: number, value: string) => {
return redis.listRemove(id, key, count, value)
}))
// Redis - Set
ipcMain.handle('redis:setMembers', async (_e, id: string, key: string) => {
return redis.setMembers(id, key)
})
ipcMain.handle('redis:setSize', async (_e, id: string, key: string) => {
return redis.setSize(id, key)
})
ipcMain.handle('redis:setAdd', withLog('SADD', async (_e, id: string, key: string, members: string[]) => {
return redis.setAdd(id, key, ...members)
}))
ipcMain.handle('redis:setRemove', withLog('SREM', async (_e, id: string, key: string, members: string[]) => {
return redis.setRemove(id, key, ...members)
}))
// Redis - Zset
ipcMain.handle('redis:zsetRange', async (_e, id: string, key: string, start: number, stop: number, withScores: boolean) => {
return redis.zsetRange(id, key, start, stop, withScores)
})
ipcMain.handle('redis:zsetSize', async (_e, id: string, key: string) => {
return redis.zsetSize(id, key)
})
ipcMain.handle('redis:zsetAdd', withLog('ZADD', async (_e, id: string, key: string, score: number, member: string) => {
return redis.zsetAdd(id, key, score, member)
}))
ipcMain.handle('redis:zsetRemove', withLog('ZREM', async (_e, id: string, key: string, members: string[]) => {
return redis.zsetRemove(id, key, ...members)
}))
// Redis - Stream
ipcMain.handle('redis:streamInfo', async (_e, id: string, key: string) => {
return redis.streamInfo(id, key)
})
ipcMain.handle('redis:streamRange', async (_e, id: string, key: string, start: string, end: string, count: number) => {
return redis.streamRange(id, key, start, end, count)
})
ipcMain.handle('redis:streamAdd', withLog('XADD', async (_e, id: string, key: string, streamId: string, fieldValues: string[]) => {
return redis.streamAdd(id, key, streamId, fieldValues)
}))
ipcMain.handle('redis:streamTrim', withLog('XTRIM', async (_e, id: string, key: string, maxLen: number) => {
return redis.streamTrim(id, key, maxLen)
}))
ipcMain.handle('redis:streamDel', withLog('XDEL', async (_e, id: string, key: string, ids: string[]) => {
return redis.streamDel(id, key, ...ids)
}))
// Redis - Stream Consumer Groups
ipcMain.handle('redis:streamGroups', async (_e, id: string, key: string) => {
return redis.streamGroups(id, key)
})
ipcMain.handle('redis:streamConsumers', async (_e, id: string, key: string, group: string) => {
return redis.streamConsumers(id, key, group)
})
ipcMain.handle('redis:streamGroupCreate', withLog('XGROUP CREATE', async (_e, id: string, key: string, group: string, startId: string, mkstream: boolean) => {
return redis.streamGroupCreate(id, key, group, startId, mkstream)
}))
ipcMain.handle('redis:streamGroupDestroy', withLog('XGROUP DESTROY', async (_e, id: string, key: string, group: string) => {
return redis.streamGroupDestroy(id, key, group)
}))
ipcMain.handle('redis:streamAck', withLog('XACK', async (_e, id: string, key: string, group: string, ids: string[]) => {
return redis.streamAck(id, key, group, ...ids)
}))
// Command Log
ipcMain.handle('redis:getCommandLog', () => {
return redis.getLog()
})
ipcMain.handle('redis:clearCommandLog', () => {
redis.clearLog()
})
// Format decode
ipcMain.handle('redis:decodeValue', (_e, value: string, format: string) => {
return redis.decodeValue(value, format as any)
})
ipcMain.handle('redis:detectFormat', (_e, value: string) => {
return redis.detectFormat(value)
})
ipcMain.handle('redis:customFormat', (_e, value: string, command: string, key: string) => {
return redis.customFormat(value, command, key)
})
// Pub/Sub
ipcMain.handle('redis:subscribe', async (_e, id: string, channels: string[], isPattern: boolean) => {
try {
redis.startSubscribe(id, channels, isPattern, _e.sender)
return { success: true }
} catch (err: any) {
return { success: false, error: err.message }
}
})
ipcMain.handle('redis:unsubscribe', async (_e, id: string) => {
redis.stopSubscribe(id)
})
// Monitor
ipcMain.handle('redis:monitor', async (_e, id: string) => {
try {
redis.startMonitor(id, _e.sender)
return { success: true }
} catch (err: any) {
return { success: false, error: err.message }
}
})
ipcMain.handle('redis:monitorStop', async (_e, id: string) => {
redis.stopMonitor(id)
})
}