401 lines
16 KiB
TypeScript
401 lines
16 KiB
TypeScript
import { ipcMain } from 'electron';
|
|
import type { BrowserWindow, IpcMainInvokeEvent } from 'electron';
|
|
import { DIContainer } from './di-container';
|
|
import type { HostedSessionConfig } from 'core/automation/domain/types/HostedSessionConfig';
|
|
import { StepId } from 'core/automation/domain/value-objects/StepId';
|
|
import { AuthenticationState } from 'core/automation/domain/value-objects/AuthenticationState';
|
|
import { ElectronCheckoutConfirmationAdapter } from 'core/automation/infrastructure//ipc/ElectronCheckoutConfirmationAdapter';
|
|
import type { OverlayAction } from 'core/automation/application/ports/OverlaySyncPort';
|
|
import type { IAutomationLifecycleEmitter } from 'core/automation/infrastructure//IAutomationLifecycleEmitter';
|
|
|
|
let progressMonitorInterval: NodeJS.Timeout | null = null;
|
|
let lifecycleSubscribed = false;
|
|
|
|
export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
|
const container = DIContainer.getInstance();
|
|
const logger = container.getLogger();
|
|
|
|
// Setup checkout confirmation adapter and wire it into the container
|
|
const checkoutConfirmationAdapter = new ElectronCheckoutConfirmationAdapter(mainWindow);
|
|
container.setConfirmCheckoutUseCase(checkoutConfirmationAdapter);
|
|
|
|
// Authentication handlers
|
|
ipcMain.handle('auth:check', async () => {
|
|
try {
|
|
logger.info('Checking authentication status');
|
|
const checkAuthUseCase = container.getCheckAuthenticationUseCase();
|
|
|
|
if (!checkAuthUseCase) {
|
|
logger.error('Authentication use case not available');
|
|
return {
|
|
success: false,
|
|
error: 'Authentication not available - check system configuration'
|
|
};
|
|
}
|
|
|
|
// NO browser connection needed - cookie check reads JSON file directly
|
|
const result = await checkAuthUseCase.execute();
|
|
if (result.isErr()) {
|
|
logger.error('Auth check failed', result.unwrapErr());
|
|
return { success: false, error: result.unwrapErr().message };
|
|
}
|
|
|
|
const state = result.unwrap();
|
|
logger.info('Authentication check complete', { state });
|
|
return { success: true, state };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Auth check failed', err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('auth:login', async () => {
|
|
try {
|
|
logger.info('Starting iRacing login flow (will wait for completion)');
|
|
|
|
const authService = container.getAuthenticationService();
|
|
|
|
if (!authService) {
|
|
// Mock mode - no actual login needed
|
|
logger.warn('Auth service not available in mock mode');
|
|
return { success: true, message: 'Mock mode - login bypassed' };
|
|
}
|
|
|
|
// Use the Playwright browser for login (same browser used for automation)
|
|
// This now waits for login to complete, auto-detects success, and closes browser
|
|
const initiateLoginUseCase = container.getInitiateLoginUseCase();
|
|
if (!initiateLoginUseCase) {
|
|
logger.warn('Initiate login use case not available');
|
|
return { success: false, error: 'Login not available' };
|
|
}
|
|
|
|
// This call now blocks until login is complete or times out
|
|
const result = await initiateLoginUseCase.execute();
|
|
if (result.isErr()) {
|
|
logger.error('Login failed or timed out', result.unwrapErr());
|
|
return { success: false, error: result.unwrapErr().message };
|
|
}
|
|
|
|
logger.info('Login completed successfully');
|
|
return { success: true, message: 'Login completed successfully' };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Login flow failed', err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('auth:confirmLogin', async () => {
|
|
try {
|
|
logger.info('User confirmed login completion');
|
|
const authService = container.getAuthenticationService();
|
|
|
|
if (!authService) {
|
|
logger.warn('Auth service not available in mock mode');
|
|
return { success: true, state: AuthenticationState.AUTHENTICATED };
|
|
}
|
|
|
|
// Call confirmLoginComplete on the adapter if it exists
|
|
if ('confirmLoginComplete' in authService && typeof authService.confirmLoginComplete === 'function') {
|
|
const result = await authService.confirmLoginComplete();
|
|
if (result.isErr()) {
|
|
logger.error('Confirm login failed', result.unwrapErr());
|
|
return { success: false, error: result.unwrapErr().message };
|
|
}
|
|
}
|
|
|
|
logger.info('Login confirmation recorded');
|
|
return { success: true, state: AuthenticationState.AUTHENTICATED };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Failed to confirm login', err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('auth:logout', async () => {
|
|
try {
|
|
logger.info('Clearing session (logout)');
|
|
const clearSessionUseCase = container.getClearSessionUseCase();
|
|
|
|
if (!clearSessionUseCase) {
|
|
logger.warn('Logout not available in mock mode');
|
|
return { success: true, message: 'Mock mode - logout bypassed' };
|
|
}
|
|
|
|
const result = await clearSessionUseCase.execute();
|
|
if (result.isErr()) {
|
|
logger.error('Logout failed', result.unwrapErr());
|
|
return { success: false, error: result.unwrapErr().message };
|
|
}
|
|
|
|
logger.info('Session cleared successfully');
|
|
return { success: true };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Logout failed', err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('auth:getState', async () => {
|
|
try {
|
|
const authService = container.getAuthenticationService();
|
|
if (!authService) {
|
|
return { success: true, state: AuthenticationState.AUTHENTICATED };
|
|
}
|
|
return { success: true, state: authService.getState() };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Failed to get auth state', err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('start-automation', async (_event: IpcMainInvokeEvent, config: HostedSessionConfig) => {
|
|
try {
|
|
const container = DIContainer.getInstance();
|
|
const startAutomationUseCase = container.getStartAutomationUseCase();
|
|
const sessionRepository = container.getSessionRepository();
|
|
const automationEngine = container.getAutomationEngine();
|
|
|
|
logger.info('Starting automation', { sessionName: config.sessionName });
|
|
|
|
if (progressMonitorInterval) {
|
|
clearInterval(progressMonitorInterval);
|
|
progressMonitorInterval = null;
|
|
}
|
|
|
|
const connectionResult = await container.initializeBrowserConnection();
|
|
if (!connectionResult.success) {
|
|
logger.error('Browser connection failed', undefined, { errorMessage: connectionResult.error });
|
|
return { success: false, error: connectionResult.error };
|
|
}
|
|
logger.info('Browser connection established');
|
|
|
|
const checkAuthUseCase = container.getCheckAuthenticationUseCase();
|
|
if (checkAuthUseCase) {
|
|
const authResult = await checkAuthUseCase.execute({
|
|
verifyPageContent: true,
|
|
});
|
|
if (authResult.isOk()) {
|
|
const authState = authResult.unwrap();
|
|
if (authState !== AuthenticationState.AUTHENTICATED) {
|
|
logger.warn('Not authenticated or session expired - automation cannot proceed', { authState });
|
|
return {
|
|
success: false,
|
|
error: 'Not authenticated or session expired. Please login first.',
|
|
authRequired: true,
|
|
authState,
|
|
};
|
|
}
|
|
logger.info('Authentication verified (cookies and page state)');
|
|
} else {
|
|
logger.warn('Auth check failed, proceeding anyway', { error: authResult.unwrapErr().message });
|
|
}
|
|
}
|
|
|
|
const result = await startAutomationUseCase.execute(config);
|
|
logger.info('Automation session created', { sessionId: result.sessionId });
|
|
|
|
const session = await sessionRepository.findById(result.sessionId);
|
|
|
|
if (session) {
|
|
logger.info('Executing step 1');
|
|
await automationEngine.executeStep(StepId.create(1), config);
|
|
}
|
|
|
|
progressMonitorInterval = setInterval(async () => {
|
|
const containerForProgress = DIContainer.getInstance();
|
|
const repoForProgress = containerForProgress.getSessionRepository();
|
|
const updatedSession = await repoForProgress.findById(result.sessionId);
|
|
if (!updatedSession) {
|
|
if (progressMonitorInterval) {
|
|
clearInterval(progressMonitorInterval);
|
|
progressMonitorInterval = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
mainWindow.webContents.send('session-progress', {
|
|
sessionId: result.sessionId,
|
|
currentStep: updatedSession.currentStep.value,
|
|
state: updatedSession.state.value,
|
|
completedSteps: Array.from({ length: updatedSession.currentStep.value - 1 }, (_, i) => i + 1),
|
|
hasError: updatedSession.errorMessage !== undefined,
|
|
errorMessage: updatedSession.errorMessage || null
|
|
});
|
|
|
|
if (updatedSession.state.value === 'COMPLETED' ||
|
|
updatedSession.state.value === 'FAILED' ||
|
|
updatedSession.state.value === 'STOPPED_AT_STEP_18') {
|
|
logger.info('Automation ended', { state: updatedSession.state.value });
|
|
if (progressMonitorInterval) {
|
|
clearInterval(progressMonitorInterval);
|
|
progressMonitorInterval = null;
|
|
}
|
|
}
|
|
}, 100);
|
|
|
|
return {
|
|
success: true,
|
|
sessionId: result.sessionId
|
|
};
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Automation failed', err);
|
|
return {
|
|
success: false,
|
|
error: err.message
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('get-session-status', async (_event: IpcMainInvokeEvent, sessionId: string) => {
|
|
const container = DIContainer.getInstance();
|
|
const sessionRepository = container.getSessionRepository();
|
|
const session = await sessionRepository.findById(sessionId);
|
|
if (!session) {
|
|
return { found: false };
|
|
}
|
|
|
|
return {
|
|
found: true,
|
|
currentStep: session.currentStep.value,
|
|
state: session.state.value,
|
|
completedSteps: Array.from({ length: session.currentStep.value - 1 }, (_, i) => i + 1),
|
|
hasError: session.errorMessage !== undefined,
|
|
errorMessage: session.errorMessage || null
|
|
};
|
|
});
|
|
|
|
ipcMain.handle('pause-automation', async (_event: IpcMainInvokeEvent, _sessionId: string) => {
|
|
return { success: false, error: 'Pause not implemented in POC' };
|
|
});
|
|
|
|
ipcMain.handle('resume-automation', async (_event: IpcMainInvokeEvent, _sessionId: string) => {
|
|
return { success: false, error: 'Resume not implemented in POC' };
|
|
});
|
|
|
|
ipcMain.handle('stop-automation', async (_event: IpcMainInvokeEvent, sessionId: string) => {
|
|
try {
|
|
const container = DIContainer.getInstance();
|
|
const automationEngine = container.getAutomationEngine();
|
|
const sessionRepository = container.getSessionRepository();
|
|
|
|
logger.info('Stopping automation', { sessionId });
|
|
|
|
if (progressMonitorInterval) {
|
|
clearInterval(progressMonitorInterval);
|
|
progressMonitorInterval = null;
|
|
logger.info('Progress monitor cleared');
|
|
}
|
|
|
|
automationEngine.stopAutomation();
|
|
logger.info('Automation engine stopped');
|
|
|
|
const session = await sessionRepository.findById(sessionId);
|
|
if (session) {
|
|
session.fail('User stopped automation');
|
|
await sessionRepository.update(session);
|
|
logger.info('Session marked as failed', { sessionId });
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Stop automation failed', err);
|
|
return {
|
|
success: false,
|
|
error: err.message
|
|
};
|
|
}
|
|
});
|
|
|
|
// Browser mode control handlers
|
|
ipcMain.handle('browser-mode:get', async () => {
|
|
try {
|
|
const loader = container.getBrowserModeConfigLoader();
|
|
if (process.env.NODE_ENV === 'development') {
|
|
return { mode: loader.getDevelopmentMode(), isDevelopment: true };
|
|
}
|
|
return { mode: 'headless', isDevelopment: false };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Failed to get browser mode', err);
|
|
return { mode: 'headless', isDevelopment: false };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('browser-mode:set', async (_event: IpcMainInvokeEvent, mode: 'headed' | 'headless') => {
|
|
try {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const loader = container.getBrowserModeConfigLoader();
|
|
loader.setDevelopmentMode(mode);
|
|
// Ensure runtime automation wiring reflects the new browser mode
|
|
if ('refreshBrowserAutomation' in container) {
|
|
// Call method to refresh /use-cases that depend on browser mode
|
|
container.refreshBrowserAutomation();
|
|
}
|
|
logger.info('Browser mode updated', { mode });
|
|
return { success: true, mode };
|
|
}
|
|
logger.warn('Browser mode change requested but not in development mode');
|
|
return { success: false, error: 'Only available in development mode' };
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
logger.error('Failed to set browser mode', err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
// Handle overlay action requests from renderer and forward to the OverlaySyncService
|
|
ipcMain.handle('overlay-action-request', async (_event: IpcMainInvokeEvent, action: OverlayAction) => {
|
|
try {
|
|
const overlayPort = 'getOverlaySyncPort' in container ? container.getOverlaySyncPort() : null;
|
|
if (!overlayPort) {
|
|
logger.warn('OverlaySyncPort not available');
|
|
return { id: action?.id ?? 'unknown', status: 'failed', reason: 'OverlaySyncPort not available' };
|
|
}
|
|
const ack = await overlayPort.startAction(action);
|
|
return ack;
|
|
} catch (e) {
|
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
logger.error('Overlay action request failed', err);
|
|
const id = typeof action === 'object' && action !== null && 'id' in action
|
|
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(action as { id?: string }).id ?? 'unknown'
|
|
: 'unknown';
|
|
return { id, status: 'failed', reason: err.message };
|
|
}
|
|
});
|
|
|
|
// Subscribe to automation adapter lifecycle events and relay to renderer
|
|
try {
|
|
if (!lifecycleSubscribed) {
|
|
const browserAutomation = container.getBrowserAutomation();
|
|
const candidate = browserAutomation as Partial<IAutomationLifecycleEmitter>;
|
|
if (typeof candidate.onLifecycle === 'function' && typeof candidate.offLifecycle === 'function') {
|
|
candidate.onLifecycle((ev) => {
|
|
try {
|
|
if (mainWindow && mainWindow.webContents) {
|
|
mainWindow.webContents.send('automation-event', ev);
|
|
}
|
|
} catch (e) {
|
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
logger.debug?.('Failed to forward automation-event', { error });
|
|
}
|
|
});
|
|
lifecycleSubscribed = true;
|
|
logger.debug('Subscribed to adapter lifecycle events for renderer relay');
|
|
} else {
|
|
logger.debug?.('Browser automation does not expose lifecycle events; skipping subscription');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
logger.debug?.('Failed to subscribe to adapter lifecycle events', { error });
|
|
}
|
|
|
|
} |