Files
orca/src/main/ipc/local-log-tail.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

110 lines
3.5 KiB
TypeScript

import { ipcMain, type WebContents } from 'electron'
import { watch, type FSWatcher } from 'node:fs'
import type { Store } from '../persistence'
import type {
LocalLogTailChangedPayload,
LocalLogTailReadArgs,
LocalLogTailReadResult,
LocalLogTailWatchArgs
} from '../../shared/local-log-tail-types'
import { readLocalLogTailRange } from '../ai-vault/local-log-tail-reader'
import { resolveAuthorizedPath } from './filesystem-auth'
type TailWatch = {
senderId: number
watcher: FSWatcher
}
const tailWatches = new Map<string, TailWatch>()
const senderCleanupRegistered = new Set<number>()
function watchKey(senderId: number, subscriptionId: string): string {
return `${senderId}:${subscriptionId}`
}
function closeWatch(key: string): void {
const subscription = tailWatches.get(key)
if (!subscription) {
return
}
tailWatches.delete(key)
subscription.watcher.close()
}
function closeSenderWatches(senderId: number): void {
senderCleanupRegistered.delete(senderId)
for (const [key, subscription] of tailWatches) {
if (subscription.senderId === senderId) {
closeWatch(key)
}
}
}
function validateSubscriptionId(value: unknown): string {
if (typeof value !== 'string' || value.length === 0 || value.length > 200) {
throw new Error('Invalid local log tail subscription id')
}
return value
}
function registerSenderCleanup(sender: WebContents): void {
if (senderCleanupRegistered.has(sender.id)) {
return
}
senderCleanupRegistered.add(sender.id)
sender.once('destroyed', () => closeSenderWatches(sender.id))
}
export function registerLocalLogTailHandlers(store: Store): void {
ipcMain.handle(
'fs:readLocalLogTail',
async (_event, args: LocalLogTailReadArgs): Promise<LocalLogTailReadResult> => {
const filePath = await resolveAuthorizedPath(args.filePath, store)
return readLocalLogTailRange(filePath, args.fromByteOffset, args.expectedIdentity)
}
)
ipcMain.handle(
'fs:startLocalLogTail',
async (event, args: LocalLogTailWatchArgs): Promise<void> => {
const subscriptionId = validateSubscriptionId(args.subscriptionId)
const filePath = await resolveAuthorizedPath(args.filePath, store)
const key = watchKey(event.sender.id, subscriptionId)
closeWatch(key)
const sendChange = (eventType: 'change' | 'rename'): void => {
if (!tailWatches.has(key) || event.sender.isDestroyed()) {
return
}
const payload: LocalLogTailChangedPayload = { subscriptionId, eventType }
event.sender.send('fs:localLogTailChanged', payload)
}
const watcher = watch(filePath, (eventType) => sendChange(eventType))
watcher.on('error', () => {
// Why: an error commonly accompanies rotation. Signal one final drain so
// the renderer can detect identity change, then release the dead handle.
sendChange('rename')
closeWatch(key)
})
tailWatches.set(key, { senderId: event.sender.id, watcher })
registerSenderCleanup(event.sender)
}
)
ipcMain.handle('fs:stopLocalLogTail', (event, args: { subscriptionId: string }): void => {
closeWatch(watchKey(event.sender.id, validateSubscriptionId(args.subscriptionId)))
})
}
export function closeAllLocalLogTailWatchers(): void {
for (const key of Array.from(tailWatches.keys())) {
closeWatch(key)
}
senderCleanupRegistered.clear()
}
/** Test-only: verifies tab/window teardown does not retain native watchers. */
export function getActiveLocalLogTailWatcherCount(): number {
return tailWatches.size
}