Files
EJClaw/src/ipc.ts
2026-04-29 13:31:11 +09:00

211 lines
6.7 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { DATA_DIR, IPC_POLL_INTERVAL } from './config.js';
import { readJsonFile } from './utils.js';
import { logger } from './logger.js';
import {
claimIpcFile,
moveClaimedIpcFileToError,
quarantineClaimedIpcFiles,
} from './ipc-file-claims.js';
import { forwardAuthorizedIpcMessage } from './ipc-message-forwarding.js';
import { processTaskIpc } from './ipc-task-processor.js';
import type {
IpcDeps,
IpcMessagePayload,
TaskIpcPayload,
} from './ipc-types.js';
export type {
IpcDeps,
IpcMessageForwardResult,
IpcMessagePayload,
TaskIpcPayload,
} from './ipc-types.js';
export {
claimIpcFile,
forwardAuthorizedIpcMessage,
processTaskIpc,
quarantineClaimedIpcFiles,
};
let ipcWatcherRunning = false;
export function startIpcWatcher(deps: IpcDeps): void {
if (ipcWatcherRunning) {
logger.debug('IPC watcher already running, skipping duplicate start');
return;
}
ipcWatcherRunning = true;
const ipcBaseDir = path.join(DATA_DIR, 'ipc');
fs.mkdirSync(ipcBaseDir, { recursive: true });
const processIpcFiles = async () => {
// Scan all group IPC directories (identity determined by directory)
let groupFolders: string[];
try {
groupFolders = fs.readdirSync(ipcBaseDir).filter((f) => {
const stat = fs.statSync(path.join(ipcBaseDir, f));
return stat.isDirectory() && f !== 'errors';
});
} catch (err) {
logger.error({ err }, 'Error reading IPC base directory');
setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
return;
}
const roomBindings = deps.roomBindings();
// Build folder→isMain lookup from registered groups
const folderIsMain = new Map<string, boolean>();
for (const group of Object.values(roomBindings)) {
if (group.isMain) folderIsMain.set(group.folder, true);
}
for (const sourceGroup of groupFolders) {
const isMain = folderIsMain.get(sourceGroup) === true;
const messagesDir = path.join(ipcBaseDir, sourceGroup, 'messages');
const tasksDir = path.join(ipcBaseDir, sourceGroup, 'tasks');
const errorDir = path.join(ipcBaseDir, 'errors');
fs.mkdirSync(errorDir, { recursive: true });
for (const quarantinedPath of quarantineClaimedIpcFiles(
messagesDir,
errorDir,
`${sourceGroup}-message-stale`,
)) {
logger.warn(
{ sourceGroup, quarantinedPath },
'Quarantined previously claimed IPC message after restart',
);
}
for (const quarantinedPath of quarantineClaimedIpcFiles(
tasksDir,
errorDir,
`${sourceGroup}-task-stale`,
)) {
logger.warn(
{ sourceGroup, quarantinedPath },
'Quarantined previously claimed IPC task after restart',
);
}
// Process messages from this group's IPC directory
try {
if (fs.existsSync(messagesDir)) {
const messageFiles = fs
.readdirSync(messagesDir)
.filter((f) => f.endsWith('.json'));
for (const file of messageFiles) {
const filePath = path.join(messagesDir, file);
const claimedPath = claimIpcFile(filePath);
if (!claimedPath) continue;
try {
const data = readJsonFile(claimedPath);
if (!data || typeof data !== 'object')
throw new Error('Invalid JSON');
const msg = data as IpcMessagePayload;
const forwardResult = await forwardAuthorizedIpcMessage(
msg,
sourceGroup,
isMain,
roomBindings,
deps.sendMessage,
deps.injectInboundMessage,
);
if (forwardResult.outcome === 'sent') {
logger.info(
{
transition: 'ipc:auth:allow',
chatJid: forwardResult.chatJid,
sourceGroup,
targetGroup: forwardResult.targetGroup ?? null,
isMainOverride: forwardResult.isMainOverride,
senderRole: forwardResult.senderRole ?? null,
},
'IPC message sent',
);
} else if (forwardResult.outcome === 'blocked') {
logger.warn(
{
transition: 'ipc:auth:deny',
chatJid: forwardResult.chatJid,
sourceGroup,
targetGroup: forwardResult.targetGroup ?? null,
isMainOverride: forwardResult.isMainOverride,
senderRole: forwardResult.senderRole ?? null,
},
'Unauthorized IPC message attempt blocked',
);
}
fs.unlinkSync(claimedPath);
} catch (err) {
logger.error(
{ file, sourceGroup, err },
'Error processing IPC message',
);
moveClaimedIpcFileToError(
claimedPath,
errorDir,
`${sourceGroup}-message-error`,
);
}
}
}
} catch (err) {
logger.error(
{ err, sourceGroup },
'Error reading IPC messages directory',
);
}
// Process tasks from this group's IPC directory
try {
if (fs.existsSync(tasksDir)) {
const taskFiles = fs
.readdirSync(tasksDir)
.filter((f) => f.endsWith('.json'));
for (const file of taskFiles) {
const filePath = path.join(tasksDir, file);
const claimedPath = claimIpcFile(filePath);
if (!claimedPath) continue;
try {
const data = readJsonFile(claimedPath);
if (!data || typeof data !== 'object')
throw new Error('Invalid JSON');
// Pass source group identity to processTaskIpc for authorization
await processTaskIpc(
data as TaskIpcPayload,
sourceGroup,
isMain,
deps,
);
fs.unlinkSync(claimedPath);
} catch (err) {
logger.error(
{ file, sourceGroup, err },
'Error processing IPC task',
);
moveClaimedIpcFileToError(
claimedPath,
errorDir,
`${sourceGroup}-task-error`,
);
}
}
}
} catch (err) {
logger.error({ err, sourceGroup }, 'Error reading IPC tasks directory');
}
}
setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
};
processIpcFiles();
logger.info('IPC watcher started (per-group namespaces)');
}