Implement logging functionality and enhance audio capture error handling
Some checks failed
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled

This commit is contained in:
Илья Глазунов 2026-01-15 18:50:55 +03:00
parent 50bb99df37
commit 4edfaf4906
9 changed files with 336 additions and 23 deletions

View File

@ -10,6 +10,8 @@
<true/> <true/>
<key>com.apple.security.cs.disable-library-validation</key> <key>com.apple.security.cs.disable-library-validation</key>
<true/> <true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
<key>com.apple.security.device.microphone</key> <key>com.apple.security.device.microphone</key>

View File

@ -1,5 +1,7 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses'); const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses'); const { FuseV1Options, FuseVersion } = require('@electron/fuses');
const path = require('path');
const fs = require('fs');
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
@ -7,18 +9,37 @@ module.exports = {
extraResource: ['./src/assets/SystemAudioDump'], extraResource: ['./src/assets/SystemAudioDump'],
name: 'Cheating Daddy', name: 'Cheating Daddy',
icon: 'src/assets/logo', icon: 'src/assets/logo',
// Fix executable permissions after packaging
afterCopy: [
(buildPath, electronVersion, platform, arch, callback) => {
if (platform === 'darwin') {
const systemAudioDump = path.join(buildPath, '..', 'Resources', 'SystemAudioDump');
if (fs.existsSync(systemAudioDump)) {
try {
fs.chmodSync(systemAudioDump, 0o755);
console.log('✓ Set executable permissions for SystemAudioDump');
} catch (err) {
console.error('✗ Failed to set permissions:', err.message);
}
} else {
console.warn('SystemAudioDump not found at:', systemAudioDump);
}
}
callback();
},
],
// use `security find-identity -v -p codesigning` to find your identity // use `security find-identity -v -p codesigning` to find your identity
// for macos signing // for macos signing
// also fuck apple // Use ad-hoc signing with entitlements for local development
// osxSign: { osxSign: {
// identity: '<paste your identity here>', identity: '-', // ad-hoc signing (no Apple Developer account needed)
// optionsForFile: (filePath) => { optionsForFile: (filePath) => {
// return { return {
// entitlements: 'entitlements.plist', entitlements: 'entitlements.plist',
// }; };
// }, },
// }, },
// notarize if off cuz i ran this for 6 hours and it still didnt finish // notarize is off - requires Apple Developer account
// osxNotarize: { // osxNotarize: {
// appleId: 'your apple id', // appleId: 'your apple id',
// appleIdPassword: 'app specific password', // appleIdPassword: 'app specific password',

View File

@ -39,6 +39,7 @@
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1", "@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
"@electron-forge/plugin-fuses": "^7.11.1", "@electron-forge/plugin-fuses": "^7.11.1",
"@electron/fuses": "^2.0.0", "@electron/fuses": "^2.0.0",
"@electron/osx-sign": "^2.3.0",
"@reforged/maker-appimage": "^5.1.1", "@reforged/maker-appimage": "^5.1.1",
"electron": "^39.2.7" "electron": "^39.2.7"
} }

17
pnpm-lock.yaml generated
View File

@ -48,6 +48,9 @@ importers:
'@electron/fuses': '@electron/fuses':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
'@electron/osx-sign':
specifier: ^2.3.0
version: 2.3.0
'@reforged/maker-appimage': '@reforged/maker-appimage':
specifier: ^5.1.1 specifier: ^5.1.1
version: 5.1.1 version: 5.1.1
@ -173,6 +176,11 @@ packages:
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
hasBin: true hasBin: true
'@electron/osx-sign@2.3.0':
resolution: {integrity: sha512-qh/BkWUHITP2W1grtCWn8Pm4EML3q1Rfo2tnd/KHo+f1le5gAYotBc6yEfK6X2VHyN21mPZ3CjvOiYOwjimBlA==}
engines: {node: '>=22.12.0'}
hasBin: true
'@electron/packager@18.4.4': '@electron/packager@18.4.4':
resolution: {integrity: sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==} resolution: {integrity: sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==}
engines: {node: '>= 16.13.0'} engines: {node: '>= 16.13.0'}
@ -2487,6 +2495,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@electron/osx-sign@2.3.0':
dependencies:
debug: 4.4.3
isbinaryfile: 4.0.10
plist: 3.1.0
semver: 7.7.3
transitivePeerDependencies:
- supports-color
'@electron/packager@18.4.4': '@electron/packager@18.4.4':
dependencies: dependencies:
'@electron/asar': 3.4.1 '@electron/asar': 3.4.1

View File

@ -170,6 +170,25 @@ export class HelpView extends LitElement {
flex-shrink: 0; flex-shrink: 0;
} }
.open-logs-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-color);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}
.open-logs-btn:hover {
background: var(--hover-background);
}
.usage-steps { .usage-steps {
counter-reset: step-counter; counter-reset: step-counter;
} }
@ -442,9 +461,33 @@ export class HelpView extends LitElement {
</div> </div>
<div class="description">The AI listens to conversations and provides contextual assistance based on what it hears.</div> <div class="description">The AI listens to conversations and provides contextual assistance based on what it hears.</div>
</div> </div>
<div class="option-group">
<div class="option-label">
<span>Troubleshooting</span>
</div>
<div class="description" style="margin-bottom: 12px;">
If you're experiencing issues with audio capture or other features, check the application logs for diagnostic information.
</div>
<button class="open-logs-btn" @click=${this.openLogsFolder}>
📁 Open Logs Folder
</button>
</div>
</div> </div>
`; `;
} }
async openLogsFolder() {
try {
const { ipcRenderer } = require('electron');
const result = await ipcRenderer.invoke('open-logs-folder');
if (!result.success) {
console.error('Failed to open logs folder:', result.error);
}
} catch (err) {
console.error('Error opening logs folder:', err);
}
}
} }
customElements.define('help-view', HelpView); customElements.define('help-view', HelpView);

View File

@ -6,6 +6,7 @@ const { app, BrowserWindow, shell, ipcMain } = require('electron');
const { createWindow, updateGlobalShortcuts } = require('./utils/window'); const { createWindow, updateGlobalShortcuts } = require('./utils/window');
const { setupAIProviderIpcHandlers } = require('./utils/ai-provider-manager'); const { setupAIProviderIpcHandlers } = require('./utils/ai-provider-manager');
const { stopMacOSAudioCapture } = require('./utils/gemini'); const { stopMacOSAudioCapture } = require('./utils/gemini');
const { initLogger, closeLogger, getLogPath } = require('./utils/logger');
const storage = require('./storage'); const storage = require('./storage');
const geminiSessionRef = { current: null }; const geminiSessionRef = { current: null };
@ -24,6 +25,10 @@ function createMainWindow() {
} }
app.whenReady().then(async () => { app.whenReady().then(async () => {
// Initialize file logger first
const logPath = initLogger();
console.log('App starting, log file:', logPath);
// Initialize storage (checks version, resets if needed) // Initialize storage (checks version, resets if needed)
storage.initializeStorage(); storage.initializeStorage();
@ -31,10 +36,14 @@ app.whenReady().then(async () => {
setupAIProviderIpcHandlers(geminiSessionRef); setupAIProviderIpcHandlers(geminiSessionRef);
setupStorageIpcHandlers(); setupStorageIpcHandlers();
setupGeneralIpcHandlers(); setupGeneralIpcHandlers();
// Add handler to get log path from renderer
ipcMain.handle('get-log-path', () => getLogPath());
}); });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
stopMacOSAudioCapture(); stopMacOSAudioCapture();
closeLogger();
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
@ -42,6 +51,7 @@ app.on('window-all-closed', () => {
app.on('before-quit', () => { app.on('before-quit', () => {
stopMacOSAudioCapture(); stopMacOSAudioCapture();
closeLogger();
}); });
app.on('activate', () => { app.on('activate', () => {
@ -284,6 +294,18 @@ function setupGeneralIpcHandlers() {
return app.getVersion(); return app.getVersion();
}); });
ipcMain.handle('open-logs-folder', async () => {
try {
const logPath = getLogPath();
const logsDir = require('path').dirname(logPath);
await shell.openPath(logsDir);
return { success: true, path: logsDir };
} catch (error) {
console.error('Error opening logs folder:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('quit-application', async event => { ipcMain.handle('quit-application', async event => {
try { try {
stopMacOSAudioCapture(); stopMacOSAudioCapture();

View File

@ -418,10 +418,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
// Kill any existing SystemAudioDump processes first // Kill any existing SystemAudioDump processes first
await killExistingSystemAudioDump(); await killExistingSystemAudioDump();
console.log('Starting macOS audio capture with SystemAudioDump...'); console.log('=== Starting macOS audio capture ===');
sendToRenderer('update-status', 'Starting audio capture...');
const { app } = require('electron'); const { app } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs');
let systemAudioPath; let systemAudioPath;
if (app.isPackaged) { if (app.isPackaged) {
@ -430,7 +432,35 @@ async function startMacOSAudioCapture(geminiSessionRef) {
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump'); systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
} }
console.log('SystemAudioDump path:', systemAudioPath); console.log('SystemAudioDump config:', {
path: systemAudioPath,
isPackaged: app.isPackaged,
resourcesPath: process.resourcesPath,
exists: fs.existsSync(systemAudioPath),
});
// Check if file exists
if (!fs.existsSync(systemAudioPath)) {
console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
sendToRenderer('update-status', 'Error: Audio binary not found');
return false;
}
// Check and fix executable permissions
try {
fs.accessSync(systemAudioPath, fs.constants.X_OK);
console.log('SystemAudioDump is executable');
} catch (err) {
console.warn('SystemAudioDump not executable, fixing permissions...');
try {
fs.chmodSync(systemAudioPath, 0o755);
console.log('Fixed executable permissions');
} catch (chmodErr) {
console.error('Failed to fix permissions:', chmodErr);
sendToRenderer('update-status', 'Error: Cannot execute audio binary');
return false;
}
}
const spawnOptions = { const spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
@ -439,10 +469,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
}, },
}; };
console.log('Spawning SystemAudioDump...');
systemAudioProc = spawn(systemAudioPath, [], spawnOptions); systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
if (!systemAudioProc.pid) { if (!systemAudioProc.pid) {
console.error('Failed to start SystemAudioDump'); console.error('FATAL: Failed to start SystemAudioDump - no PID');
sendToRenderer('update-status', 'Error: Audio capture failed to start');
return false; return false;
} }
@ -455,8 +487,16 @@ async function startMacOSAudioCapture(geminiSessionRef) {
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION; const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
let audioBuffer = Buffer.alloc(0); let audioBuffer = Buffer.alloc(0);
let chunkCount = 0;
let firstDataReceived = false;
systemAudioProc.stdout.on('data', data => { systemAudioProc.stdout.on('data', data => {
if (!firstDataReceived) {
firstDataReceived = true;
console.log('First audio data received! Size:', data.length);
sendToRenderer('update-status', 'Listening...');
}
audioBuffer = Buffer.concat([audioBuffer, data]); audioBuffer = Buffer.concat([audioBuffer, data]);
while (audioBuffer.length >= CHUNK_SIZE) { while (audioBuffer.length >= CHUNK_SIZE) {
@ -467,6 +507,11 @@ async function startMacOSAudioCapture(geminiSessionRef) {
const base64Data = monoChunk.toString('base64'); const base64Data = monoChunk.toString('base64');
sendAudioToGemini(base64Data, geminiSessionRef); sendAudioToGemini(base64Data, geminiSessionRef);
chunkCount++;
if (chunkCount % 100 === 0) {
console.log(`Audio: ${chunkCount} chunks processed`);
}
if (process.env.DEBUG_AUDIO) { if (process.env.DEBUG_AUDIO) {
console.log(`Processed audio chunk: ${chunk.length} bytes`); console.log(`Processed audio chunk: ${chunk.length} bytes`);
saveDebugAudio(monoChunk, 'system_audio'); saveDebugAudio(monoChunk, 'system_audio');
@ -480,16 +525,24 @@ async function startMacOSAudioCapture(geminiSessionRef) {
}); });
systemAudioProc.stderr.on('data', data => { systemAudioProc.stderr.on('data', data => {
console.error('SystemAudioDump stderr:', data.toString()); const msg = data.toString();
console.error('SystemAudioDump stderr:', msg);
if (msg.toLowerCase().includes('error')) {
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
}
}); });
systemAudioProc.on('close', code => { systemAudioProc.on('close', code => {
console.log('SystemAudioDump process closed with code:', code); console.log('SystemAudioDump closed with code:', code, 'chunks processed:', chunkCount);
if (code !== 0 && code !== null) {
sendToRenderer('update-status', `Audio stopped (exit: ${code})`);
}
systemAudioProc = null; systemAudioProc = null;
}); });
systemAudioProc.on('error', err => { systemAudioProc.on('error', err => {
console.error('SystemAudioDump process error:', err); console.error('SystemAudioDump spawn error:', err.message);
sendToRenderer('update-status', 'Audio error: ' + err.message);
systemAudioProc = null; systemAudioProc = null;
}); });

97
src/utils/logger.js Normal file
View File

@ -0,0 +1,97 @@
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
let logFile = null;
let logPath = null;
function getLogPath() {
if (logPath) return logPath;
const userDataPath = app.getPath('userData');
const logsDir = path.join(userDataPath, 'logs');
// Create logs directory if it doesn't exist
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Create log file with timestamp
const timestamp = new Date().toISOString().split('T')[0];
logPath = path.join(logsDir, `app-${timestamp}.log`);
return logPath;
}
function initLogger() {
try {
const filePath = getLogPath();
logFile = fs.createWriteStream(filePath, { flags: 'a' });
const startMsg = `\n${'='.repeat(60)}\nApp started at ${new Date().toISOString()}\nPlatform: ${process.platform}, Arch: ${process.arch}\nElectron: ${process.versions.electron}, Node: ${process.versions.node}\nPackaged: ${app.isPackaged}\n${'='.repeat(60)}\n`;
logFile.write(startMsg);
// Override console methods to also write to file
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
console.log = (...args) => {
originalLog.apply(console, args);
writeLog('INFO', args);
};
console.error = (...args) => {
originalError.apply(console, args);
writeLog('ERROR', args);
};
console.warn = (...args) => {
originalWarn.apply(console, args);
writeLog('WARN', args);
};
console.log('Logger initialized, writing to:', filePath);
return filePath;
} catch (err) {
console.error('Failed to initialize logger:', err);
return null;
}
}
function writeLog(level, args) {
if (!logFile) return;
try {
const timestamp = new Date().toISOString();
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch {
return String(arg);
}
}
return String(arg);
}).join(' ');
logFile.write(`[${timestamp}] [${level}] ${message}\n`);
} catch (err) {
// Silently fail - don't want logging errors to crash the app
}
}
function closeLogger() {
if (logFile) {
logFile.write(`\nApp closed at ${new Date().toISOString()}\n`);
logFile.end();
logFile = null;
}
}
module.exports = {
initLogger,
closeLogger,
getLogPath,
};

View File

@ -441,9 +441,11 @@ async function startMacOSAudioCapture() {
// Kill any existing SystemAudioDump processes first // Kill any existing SystemAudioDump processes first
await killExistingSystemAudioDump(); await killExistingSystemAudioDump();
console.log('Starting macOS audio capture with SystemAudioDump for OpenAI SDK...'); console.log('=== Starting macOS audio capture (OpenAI SDK) ===');
sendToRenderer('update-status', 'Starting audio capture...');
const { app } = require('electron'); const { app } = require('electron');
const fs = require('fs');
let systemAudioPath; let systemAudioPath;
if (app.isPackaged) { if (app.isPackaged) {
@ -452,7 +454,35 @@ async function startMacOSAudioCapture() {
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump'); systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
} }
console.log('SystemAudioDump path:', systemAudioPath); console.log('SystemAudioDump config:', {
path: systemAudioPath,
isPackaged: app.isPackaged,
resourcesPath: process.resourcesPath,
exists: fs.existsSync(systemAudioPath),
});
// Check if file exists
if (!fs.existsSync(systemAudioPath)) {
console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
sendToRenderer('update-status', 'Error: Audio binary not found');
return false;
}
// Check and fix executable permissions
try {
fs.accessSync(systemAudioPath, fs.constants.X_OK);
console.log('SystemAudioDump is executable');
} catch (err) {
console.warn('SystemAudioDump not executable, fixing permissions...');
try {
fs.chmodSync(systemAudioPath, 0o755);
console.log('Fixed executable permissions');
} catch (chmodErr) {
console.error('Failed to fix permissions:', chmodErr);
sendToRenderer('update-status', 'Error: Cannot execute audio binary');
return false;
}
}
const spawnOptions = { const spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
@ -461,10 +491,12 @@ async function startMacOSAudioCapture() {
}, },
}; };
console.log('Spawning SystemAudioDump...');
systemAudioProc = spawn(systemAudioPath, [], spawnOptions); systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
if (!systemAudioProc.pid) { if (!systemAudioProc.pid) {
console.error('Failed to start SystemAudioDump'); console.error('FATAL: Failed to start SystemAudioDump - no PID');
sendToRenderer('update-status', 'Error: Audio capture failed to start');
return false; return false;
} }
@ -476,8 +508,16 @@ async function startMacOSAudioCapture() {
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION; const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
let tempBuffer = Buffer.alloc(0); let tempBuffer = Buffer.alloc(0);
let chunkCount = 0;
let firstDataReceived = false;
systemAudioProc.stdout.on('data', data => { systemAudioProc.stdout.on('data', data => {
if (!firstDataReceived) {
firstDataReceived = true;
console.log('First audio data received! Size:', data.length);
sendToRenderer('update-status', 'Listening...');
}
tempBuffer = Buffer.concat([tempBuffer, data]); tempBuffer = Buffer.concat([tempBuffer, data]);
while (tempBuffer.length >= CHUNK_SIZE) { while (tempBuffer.length >= CHUNK_SIZE) {
@ -489,6 +529,11 @@ async function startMacOSAudioCapture() {
// Add to audio buffer for transcription // Add to audio buffer for transcription
audioBuffer = Buffer.concat([audioBuffer, monoChunk]); audioBuffer = Buffer.concat([audioBuffer, monoChunk]);
chunkCount++;
if (chunkCount % 100 === 0) {
console.log(`Audio: ${chunkCount} chunks processed, buffer size: ${audioBuffer.length}`);
}
} }
// Limit buffer size (max 30 seconds of audio) // Limit buffer size (max 30 seconds of audio)
@ -499,21 +544,33 @@ async function startMacOSAudioCapture() {
}); });
systemAudioProc.stderr.on('data', data => { systemAudioProc.stderr.on('data', data => {
console.error('SystemAudioDump stderr:', data.toString()); const msg = data.toString();
console.error('SystemAudioDump stderr:', msg);
if (msg.toLowerCase().includes('error')) {
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
}
}); });
systemAudioProc.on('close', code => { systemAudioProc.on('close', (code, signal) => {
console.log('SystemAudioDump process closed with code:', code); console.log('SystemAudioDump closed:', { code, signal, chunksProcessed: chunkCount, tempBufferSize: tempBuffer.length });
if (code !== 0 && code !== null) {
sendToRenderer('update-status', `Audio stopped (exit: ${code}, signal: ${signal})`);
}
systemAudioProc = null; systemAudioProc = null;
stopTranscriptionTimer(); stopTranscriptionTimer();
}); });
systemAudioProc.on('error', err => { systemAudioProc.on('error', err => {
console.error('SystemAudioDump process error:', err); console.error('SystemAudioDump spawn error:', err.message, err.stack);
sendToRenderer('update-status', 'Audio error: ' + err.message);
systemAudioProc = null; systemAudioProc = null;
stopTranscriptionTimer(); stopTranscriptionTimer();
}); });
systemAudioProc.on('exit', (code, signal) => {
console.log('SystemAudioDump exit event:', { code, signal });
});
// Start periodic transcription // Start periodic transcription
startTranscriptionTimer(); startTranscriptionTimer();