mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 05:12:57 +02:00
improve app logging
- consolidate electron-log and expand support-oriented logging - consolidate log levels to info / trace only - add system/server diagnostics exporter
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createSocket } from 'dgram';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
import { DiscoveredServerItem, ServerType } from '/@/shared/types/types';
|
||||
|
||||
type JellyfinResponse = {
|
||||
@@ -26,7 +27,7 @@ function discoverJellyfin(reply: (server: DiscoveredServerItem) => void) {
|
||||
});
|
||||
} catch (e) {
|
||||
// Got a spurious response, ignore?
|
||||
console.error(e);
|
||||
log.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -51,5 +52,5 @@ ipcMain.on('autodiscover-ping', (ev) => {
|
||||
|
||||
discoverAll((result) => port.postMessage(result))
|
||||
.then(() => port.close())
|
||||
.catch((err) => console.error(err));
|
||||
.catch((err) => log.error(err));
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import { promises as fs, watch as fsWatch } from 'fs';
|
||||
import path from 'path';
|
||||
import { validateHTMLColor } from 'validate-color';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
const defaultUserDataPath = app.getPath('userData');
|
||||
@@ -48,7 +50,7 @@ const sanitizeColors = (
|
||||
sanitized[key] = value;
|
||||
} else {
|
||||
invalidKeys.push(key);
|
||||
console.warn(`Custom theme "${themeId}" has an invalid color for "${key}": ${value}`);
|
||||
log.warn(`Custom theme "${themeId}" has an invalid color for "${key}": ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +130,7 @@ const resolveStylesheetPaths = (themeDir: string, stylesheets?: string[]): strin
|
||||
relativeToThemes.startsWith('..') || path.isAbsolute(relativeToThemes);
|
||||
|
||||
if (escapesThemesDir) {
|
||||
console.warn(
|
||||
log.warn(
|
||||
`Skipping linked file outside themes folder: ${relativePath} (from ${themeDir})`,
|
||||
);
|
||||
continue;
|
||||
@@ -146,7 +148,7 @@ const readStylesheetContents = async (stylesheetPaths: string[]): Promise<string
|
||||
try {
|
||||
return await fs.readFile(stylesheetPath, 'utf8');
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read linked stylesheet ${stylesheetPath}`, error);
|
||||
log.warn(`Failed to read linked stylesheet ${stylesheetPath}`, error);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
@@ -193,9 +195,7 @@ const resolveExtends = (
|
||||
}
|
||||
|
||||
if (visited.has(id) || depth > MAX_EXTENDS_DEPTH) {
|
||||
console.warn(
|
||||
`Custom theme "${id}" has a circular or too-deep "extends" chain, ignoring it`,
|
||||
);
|
||||
log.warn(`Custom theme "${id}" has a circular or too-deep "extends" chain, ignoring it`);
|
||||
return { fields: { mode: 'dark' }, invalidColorKeys: [] };
|
||||
}
|
||||
|
||||
@@ -322,14 +322,14 @@ const reloadThemes = async () => {
|
||||
cache = await loadThemesFromDisk();
|
||||
broadcastThemes(cache);
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom themes', error);
|
||||
log.error('Failed to load custom themes', error);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleReload = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
reloadThemes().catch((error) => console.error('Failed to reload custom themes', error));
|
||||
reloadThemes().catch((error) => log.error('Failed to reload custom themes', error));
|
||||
}, RELOAD_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
@@ -357,7 +357,7 @@ const startWatcher = async () => {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to watch themes folder', error);
|
||||
log.error('Failed to watch themes folder', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -384,7 +384,7 @@ app.whenReady()
|
||||
await startWatcher();
|
||||
await reloadThemes();
|
||||
})
|
||||
.catch((error) => console.error('Failed to initialize custom themes', error));
|
||||
.catch((error) => log.error('Failed to initialize custom themes', error));
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (watcher) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Client, SetActivity } from '@xhayper/discord-rpc';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const FEISHIN_DISCORD_APPLICATION_ID = '1165957668758900787';
|
||||
|
||||
let client: Client | null = null;
|
||||
@@ -21,26 +23,36 @@ const isConnected = () => {
|
||||
|
||||
const setActivity = (activity: SetActivity) => {
|
||||
if (client) {
|
||||
client.user?.setActivity({
|
||||
...activity,
|
||||
void client.user?.setActivity({ ...activity }).catch((error) => {
|
||||
log.warn('Discord RPC set activity failed', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clearActivity = () => {
|
||||
if (client) {
|
||||
client.user?.clearActivity();
|
||||
void client.user?.clearActivity().catch((error) => {
|
||||
log.warn('Discord RPC clear activity failed', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const quit = () => {
|
||||
if (client) {
|
||||
client?.destroy();
|
||||
void client.destroy().catch((error) => {
|
||||
log.error('Discord RPC destroy failed', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ipcMain.handle('discord-rpc-initialize', async (_event, clientId?: string) => {
|
||||
await createClient(clientId);
|
||||
try {
|
||||
await createClient(clientId);
|
||||
log.info('Discord RPC initialized');
|
||||
} catch (error) {
|
||||
log.error('Discord RPC initialize failed', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-is-connected', () => {
|
||||
@@ -48,19 +60,17 @@ ipcMain.handle('discord-rpc-is-connected', () => {
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-set-activity', (_event, activity: SetActivity) => {
|
||||
if (client) {
|
||||
setActivity(activity);
|
||||
}
|
||||
setActivity(activity);
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-clear-activity', () => {
|
||||
if (client) {
|
||||
clearActivity();
|
||||
}
|
||||
clearActivity();
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-quit', () => {
|
||||
quit();
|
||||
client = null;
|
||||
log.info('Discord RPC quit');
|
||||
});
|
||||
|
||||
export const discordRpc = {
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from '.';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const SEARCH_URL = 'https://genius.com/api/search/song';
|
||||
|
||||
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/genius.ts
|
||||
@@ -100,7 +102,7 @@ export async function getLyricsBySongId(url: string): Promise<null | string> {
|
||||
try {
|
||||
result = await axios.get<string>(url, { responseType: 'text' });
|
||||
} catch (e) {
|
||||
console.error('Genius lyrics request got an error!', (e as Error)?.message);
|
||||
log.error('Genius lyrics request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -138,7 +140,7 @@ export async function getSearchResults(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Genius search request got an error!', (e as Error)?.message);
|
||||
log.error('Genius search request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -193,7 +195,7 @@ async function getSongId(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Genius search request got an error!', (e as Error)?.message);
|
||||
log.error('Genius search request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getSearchResults as searchSimpMusic,
|
||||
} from './simpmusic';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
|
||||
export enum LyricSource {
|
||||
@@ -110,7 +111,7 @@ const searchAllSources = async (
|
||||
allSearchResults.push(...result.value.searchResults);
|
||||
} else if (result.status === 'rejected') {
|
||||
const index = settled.indexOf(result);
|
||||
console.error(`Error searching ${sources[index]} for lyrics:`, result.reason);
|
||||
log.error(`Error searching ${sources[index]} for lyrics:`, result.reason);
|
||||
}
|
||||
}
|
||||
return allSearchResults;
|
||||
@@ -174,7 +175,7 @@ const getRemoteLyrics = async (song: Song) => {
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
|
||||
log.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
|
||||
}
|
||||
|
||||
if (lyricsFromSource) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from '.';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const FETCH_URL = 'https://lrclib.net/api/get';
|
||||
const SEEARCH_URL = 'https://lrclib.net/api/search';
|
||||
|
||||
@@ -46,7 +48,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
||||
try {
|
||||
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
||||
} catch (e) {
|
||||
console.error('LrcLib lyrics request got an error!', (e as Error)?.message);
|
||||
log.error('LrcLib lyrics request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,7 +73,7 @@ export async function getSearchResults(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('LrcLib search request got an error!', (e as Error)?.message);
|
||||
log.error('LrcLib search request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ export async function query(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('LrcLib search request got an error!', (e as Error).message);
|
||||
log.error('LrcLib search request got an error!', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import { store } from '../settings';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const SEARCH_URL = 'https://music.163.com/api/search/get';
|
||||
const LYRICS_URL = 'https://music.163.com/api/song/lyric';
|
||||
|
||||
@@ -81,7 +83,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('NetEase lyrics request got an error!', e);
|
||||
log.error('NetEase lyrics request got an error!', e);
|
||||
return null;
|
||||
}
|
||||
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
|
||||
@@ -114,7 +116,7 @@ export async function getSearchResults(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('NetEase search request got an error!', e);
|
||||
log.error('NetEase search request got an error!', e);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '.';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const API_URL = 'https://api-lyrics.simpmusic.org/v1';
|
||||
|
||||
const TIMEOUT_MS = 5000;
|
||||
@@ -38,7 +40,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic lyrics request errored:', (e as Error)?.message);
|
||||
log.error('SimpMusic lyrics request errored:', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -63,7 +65,7 @@ export async function getSearchResults(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic search errored:', (e as Error)?.message);
|
||||
log.error('SimpMusic search errored:', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,7 +97,7 @@ export async function query(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic search errored:', (e as Error).message);
|
||||
log.error('SimpMusic search errored:', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ export async function query(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic lyrics fetch errored:', (e as Error).message);
|
||||
log.error('SimpMusic lyrics fetch errored:', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import console from 'console';
|
||||
import { app, ipcMain, powerMonitor } from 'electron';
|
||||
import { access, rm } from 'fs/promises';
|
||||
import uniq from 'lodash/uniq';
|
||||
@@ -7,7 +6,7 @@ import { pid } from 'node:process';
|
||||
import process from 'process';
|
||||
|
||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||
import { createLog } from '../../../utils';
|
||||
import log from '../../../logger';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { isMacOS, isWindows } from '/@/main/env';
|
||||
@@ -49,24 +48,29 @@ type NodeMpvError = {
|
||||
};
|
||||
|
||||
const mpvLog = (
|
||||
data: { action: string; toast?: 'info' | 'success' | 'warning' },
|
||||
data: {
|
||||
action: string;
|
||||
level?: 'debug' | 'error' | 'info' | 'warn';
|
||||
toast?: 'info' | 'success' | 'warning';
|
||||
},
|
||||
err?: NodeMpvError,
|
||||
) => {
|
||||
const { action, toast } = data;
|
||||
|
||||
if (err) {
|
||||
const message = `[AUDIO PLAYER] ${action} - mpv errorcode ${err.errcode} - ${
|
||||
const message = `${action} - mpv errorcode ${err.errcode} - ${
|
||||
NodeMpvErrorCode[err.errcode as keyof typeof NodeMpvErrorCode]
|
||||
}`;
|
||||
|
||||
sendToastToRenderer({ message, type: 'error' });
|
||||
createLog({ message, type: 'error' });
|
||||
log.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = `[AUDIO PLAYER] ${action}`;
|
||||
createLog({ message, type: 'error' });
|
||||
const level = data.level ?? 'info';
|
||||
log[level](action);
|
||||
if (toast) {
|
||||
sendToastToRenderer({ message, type: toast });
|
||||
sendToastToRenderer({ message: action, type: toast });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,8 +160,9 @@ const createMpv = async (data: {
|
||||
|
||||
try {
|
||||
await mpv.start();
|
||||
log.info('mpv initialized', { binary: resolvedBinaryPath ?? 'bundled/default' });
|
||||
} catch (error: any) {
|
||||
console.error('mpv failed to start', error);
|
||||
log.error('mpv failed to start', error);
|
||||
} finally {
|
||||
await mpv.setMultipleProperties(properties || {});
|
||||
}
|
||||
@@ -265,11 +270,14 @@ const quit = async (instance?: MpvAPI | null) => {
|
||||
};
|
||||
|
||||
const setAudioPlayerFallback = (isError: boolean) => {
|
||||
if (isError) {
|
||||
log.warn('Falling back to web player');
|
||||
}
|
||||
getMainWindow()?.webContents.send('renderer-player-fallback', isError);
|
||||
};
|
||||
|
||||
ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) => {
|
||||
mpvLog({ action: `Setting properties: ${JSON.stringify(data)}` });
|
||||
mpvLog({ action: `Setting properties: ${JSON.stringify(data)}`, level: 'debug' });
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -291,6 +299,7 @@ ipcMain.handle(
|
||||
try {
|
||||
mpvLog({
|
||||
action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
|
||||
level: 'debug',
|
||||
});
|
||||
|
||||
// Clean up previous mpv instance
|
||||
@@ -318,6 +327,7 @@ ipcMain.handle(
|
||||
try {
|
||||
mpvLog({
|
||||
action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
|
||||
level: 'debug',
|
||||
});
|
||||
mpvInstance = await createMpv(data);
|
||||
setAudioPlayerFallback(false);
|
||||
@@ -778,7 +788,7 @@ process.on('SIGTERM', async () => {
|
||||
|
||||
// Handle uncaught exceptions - cleanup mpv before crashing
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
log.error('Uncaught exception:', error);
|
||||
await cleanupMpv(true).catch(() => {
|
||||
// Ignore cleanup errors during crash
|
||||
});
|
||||
@@ -786,7 +796,7 @@ process.on('uncaughtException', async (error) => {
|
||||
|
||||
// Handle unhandled rejections - cleanup mpv
|
||||
process.on('unhandledRejection', async (reason) => {
|
||||
console.error('Unhandled rejection:', reason);
|
||||
log.error('Unhandled rejection:', reason);
|
||||
await cleanupMpv(true).catch(() => {
|
||||
// Ignore cleanup errors
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import manifest from './manifest.json';
|
||||
|
||||
import { isLinux } from '/@/main/env';
|
||||
import { getMainWindow } from '/@/main/index';
|
||||
import log from '/@/main/logger';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
|
||||
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
|
||||
@@ -76,6 +77,10 @@ function send({ client, data, event }: SendData): void {
|
||||
}
|
||||
|
||||
export const shutdownServer = () => {
|
||||
if (wsServer || server) {
|
||||
log.info('Remote server shutting down');
|
||||
}
|
||||
|
||||
if (wsServer) {
|
||||
wsServer.clients.forEach((client) => client.close(4000));
|
||||
wsServer.close();
|
||||
@@ -332,24 +337,44 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(config.port, resolve);
|
||||
let settled = false;
|
||||
const settle = (fn: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
fn();
|
||||
};
|
||||
|
||||
server.listen(config.port, () => {
|
||||
log.info('Remote server listening', { port: config.port });
|
||||
settle(() => resolve());
|
||||
});
|
||||
server.on('error', (error) => {
|
||||
log.error('Remote server listen failed', { error, port: config.port });
|
||||
settle(() => reject(error));
|
||||
});
|
||||
wsServer = new WebSocketServer<typeof StatefulWebSocket>({ server });
|
||||
|
||||
wsServer!.on('connection', (ws: StatefulWebSocket) => {
|
||||
let authFail: number | undefined;
|
||||
ws.alive = true;
|
||||
log.info('Remote client connected', { clients: wsServer?.clients.size });
|
||||
|
||||
if (!settings.username && !settings.password) {
|
||||
ws.auth = true;
|
||||
} else {
|
||||
authFail = setTimeout(() => {
|
||||
if (!ws.auth) {
|
||||
log.warn('Remote client auth timeout');
|
||||
ws.close();
|
||||
}
|
||||
}, 10000) as unknown as number;
|
||||
}
|
||||
|
||||
ws.on('error', console.error);
|
||||
ws.on('error', log.error);
|
||||
|
||||
ws.on('close', () => {
|
||||
log.info('Remote client disconnected', { clients: wsServer?.clients.size });
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
@@ -365,7 +390,9 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
|
||||
if (login === settings.username && password === settings.password) {
|
||||
ws.auth = true;
|
||||
log.info('Remote client authenticated');
|
||||
} else {
|
||||
log.warn('Remote client auth failed');
|
||||
ws.close();
|
||||
}
|
||||
|
||||
@@ -488,7 +515,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -516,7 +543,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Server did not come up'));
|
||||
settle(() => reject(new Error('Server did not come up')));
|
||||
}, UP_TIMEOUT_MS);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -15,6 +15,8 @@ import Store from 'electron-store';
|
||||
import { promises as fs, watch as fsWatch } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const getFrame = () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
@@ -52,7 +54,7 @@ const readCustomCss = async (): Promise<{ content: string; exists: boolean }> =>
|
||||
return { content: '', exists: false };
|
||||
}
|
||||
|
||||
console.error('Failed to read custom css file', error);
|
||||
log.error('Failed to read custom css file', error);
|
||||
return { content: '', exists: false };
|
||||
}
|
||||
};
|
||||
@@ -75,7 +77,7 @@ const scheduleCustomCssUpdate = () => {
|
||||
|
||||
customCssDebounce = setTimeout(() => {
|
||||
notifyCustomCssUpdate().catch((error) => {
|
||||
console.error('Failed to broadcast custom css update', error);
|
||||
log.error('Failed to broadcast custom css update', error);
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
@@ -94,13 +96,13 @@ const startCustomCssWatcher = async () => {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to watch custom css file', error);
|
||||
log.error('Failed to watch custom css file', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const store = new Store<any>({
|
||||
beforeEachMigration: (_store, context) => {
|
||||
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||
log.info(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||
},
|
||||
cwd: storePath,
|
||||
defaults: {
|
||||
@@ -164,6 +166,7 @@ ipcMain.handle('password-get', (_event, server: string): null | string => {
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
log.warn('Password encryption unavailable', { serverId: server });
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -173,6 +176,7 @@ ipcMain.on('password-remove', (_event, server: string) => {
|
||||
delete passwords[server];
|
||||
}
|
||||
store.set({ server: passwords });
|
||||
log.info('Password removed', { serverId: server });
|
||||
});
|
||||
|
||||
ipcMain.handle('password-set', (_event, password: string, server: string) => {
|
||||
@@ -182,8 +186,11 @@ ipcMain.handle('password-set', (_event, password: string, server: string) => {
|
||||
passwords[server] = encrypted.toString('hex');
|
||||
store.set({ server: passwords });
|
||||
|
||||
log.info('Password saved', { serverId: server });
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn('Password encryption unavailable', { serverId: server });
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -226,7 +233,7 @@ ipcMain.handle('custom-css-open-folder', async () => {
|
||||
|
||||
app.whenReady()
|
||||
.then(() => startCustomCssWatcher())
|
||||
.catch((error) => console.error('Failed to start custom css watcher', error));
|
||||
.catch((error) => log.error('Failed to start custom css watcher', error));
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (customCssWatcher) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
|
||||
import Player from 'mpris-service';
|
||||
|
||||
import { getMainWindow } from '/@/main/index';
|
||||
import log from '/@/main/logger';
|
||||
import { MPV_VOLUME_MAX_CEILING } from '/@/shared/constants/volume';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||
@@ -200,7 +201,7 @@ ipcMain.on(
|
||||
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+98
-41
@@ -21,7 +21,6 @@ import {
|
||||
Tray,
|
||||
} from 'electron';
|
||||
import electronLocalShortcut from 'electron-localshortcut';
|
||||
import log from 'electron-log/main';
|
||||
import { AppImageUpdater, autoUpdater, MacUpdater, NsisUpdater } from 'electron-updater';
|
||||
import { access, constants } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
@@ -32,9 +31,10 @@ import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-
|
||||
import { shutdownServer } from './features/core/remote';
|
||||
import { store } from './features/core/settings';
|
||||
import { canHandleVisualizerDisplayMedia } from './features/core/visualizer';
|
||||
import log, { autoUpdaterLogInterface } from './logger';
|
||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||
import './features';
|
||||
import { autoUpdaterLogInterface, createLog, hotkeyToElectronAccelerator } from './utils';
|
||||
import { hotkeyToElectronAccelerator } from './utils';
|
||||
|
||||
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '/@/main/env';
|
||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
@@ -62,13 +62,21 @@ type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoU
|
||||
class AppUpdater {
|
||||
constructor() {
|
||||
const effectiveChannel = store.get('release_channel') as string;
|
||||
console.log('Effective update channel:', effectiveChannel);
|
||||
log.info('Effective update channel:', effectiveChannel);
|
||||
if (effectiveChannel === 'alpha') {
|
||||
checkAllChannelsAndGetBest().then(({ result, updater: updaterInstance }) => {
|
||||
attachUpdaterMilestoneLogs(updaterInstance);
|
||||
|
||||
if (!result?.isUpdateAvailable) {
|
||||
log.info('Updater check complete', { available: false });
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Updater check complete', {
|
||||
available: true,
|
||||
version: result.updateInfo.version,
|
||||
});
|
||||
|
||||
updaterInstance.autoInstallOnAppQuit = true;
|
||||
updaterInstance.autoRunAppAfterInstall = true;
|
||||
if (isMacOS()) {
|
||||
@@ -77,6 +85,7 @@ class AppUpdater {
|
||||
result.updateInfo.version,
|
||||
);
|
||||
} else {
|
||||
log.info('Updater download starting', { version: result.updateInfo.version });
|
||||
updaterInstance.autoDownload = true;
|
||||
updaterInstance.checkForUpdatesAndNotify();
|
||||
}
|
||||
@@ -84,26 +93,65 @@ class AppUpdater {
|
||||
return;
|
||||
}
|
||||
|
||||
configureAndGetUpdater();
|
||||
const updater = configureAndGetUpdater();
|
||||
attachUpdaterMilestoneLogs(updater);
|
||||
|
||||
if (isMacOS()) {
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater
|
||||
.checkForUpdates()
|
||||
.then((result) => {
|
||||
if (result?.isUpdateAvailable) {
|
||||
log.info('Updater check complete', {
|
||||
available: true,
|
||||
version: result.updateInfo.version,
|
||||
});
|
||||
getMainWindow()?.webContents.send(
|
||||
'update-available',
|
||||
result.updateInfo.version,
|
||||
);
|
||||
} else {
|
||||
log.info('Updater check complete', { available: false });
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error('Check for updates failed', err));
|
||||
.catch((err) => log.error('Check for updates failed', err));
|
||||
} else {
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachUpdaterMilestoneLogs(updater: UpdaterInstance): void {
|
||||
let downloadStarted = false;
|
||||
|
||||
updater.on('checking-for-update', () => {
|
||||
log.info('Updater checking for update');
|
||||
});
|
||||
|
||||
updater.on('update-available', (info) => {
|
||||
log.info('Updater update available', { version: info.version });
|
||||
});
|
||||
|
||||
updater.on('update-not-available', (info) => {
|
||||
log.info('Updater update not available', { version: info.version });
|
||||
});
|
||||
|
||||
updater.on('download-progress', () => {
|
||||
if (!downloadStarted) {
|
||||
downloadStarted = true;
|
||||
log.info('Updater download starting');
|
||||
}
|
||||
});
|
||||
|
||||
updater.on('update-downloaded', (info) => {
|
||||
log.info('Updater download complete', { version: info.version });
|
||||
});
|
||||
|
||||
updater.on('error', (error) => {
|
||||
log.error('Updater error', error);
|
||||
});
|
||||
}
|
||||
|
||||
// When release channel is alpha, check alpha and latest for updates and return
|
||||
// the updater + result for the newest version found (so alpha users can receive
|
||||
// latest updates when they are newer than the current alpha).
|
||||
@@ -121,7 +169,7 @@ async function checkAllChannelsAndGetBest(): Promise<{
|
||||
const alphaUpdater = createAlphaUpdaterInstance({ probeOnly: true });
|
||||
|
||||
try {
|
||||
console.log('Checking for updates on alpha channel');
|
||||
log.info('Checking for updates on alpha channel');
|
||||
const alphaResult = await alphaUpdater.checkForUpdates();
|
||||
if (
|
||||
alphaResult?.updateInfo?.version &&
|
||||
@@ -137,7 +185,7 @@ async function checkAllChannelsAndGetBest(): Promise<{
|
||||
|
||||
try {
|
||||
const latestUpdater = createGithubUpdaterInstance('latest', { probeOnly: true });
|
||||
console.log('Checking for updates on latest channel (GitHub)');
|
||||
log.info('Checking for updates on latest channel (GitHub)');
|
||||
const latestResult = await latestUpdater.checkForUpdates();
|
||||
if (
|
||||
latestResult?.updateInfo?.version &&
|
||||
@@ -173,13 +221,13 @@ function configureAndGetUpdater(): UpdaterInstance {
|
||||
let releaseChannel = store.get('release_channel');
|
||||
const isNotConfigured = !releaseChannel;
|
||||
|
||||
console.log('Release channel:', releaseChannel);
|
||||
console.log('Is beta version:', isBetaVersion);
|
||||
console.log('Is alpha version:', isAlphaVersion);
|
||||
console.log('Is not configured:', isNotConfigured);
|
||||
log.info('Release channel:', releaseChannel);
|
||||
log.info('Is beta version:', isBetaVersion);
|
||||
log.info('Is alpha version:', isAlphaVersion);
|
||||
log.info('Is not configured:', isNotConfigured);
|
||||
|
||||
if (isNotConfigured) {
|
||||
console.log('Release channel not configured, setting default channel');
|
||||
log.info('Release channel not configured, setting default channel');
|
||||
const defaultChannel = isAlphaVersion ? 'alpha' : isBetaVersion ? 'beta' : 'latest';
|
||||
store.set('release_channel', defaultChannel);
|
||||
releaseChannel = defaultChannel;
|
||||
@@ -188,11 +236,9 @@ function configureAndGetUpdater(): UpdaterInstance {
|
||||
const effectiveChannel = store.get('release_channel') as string;
|
||||
|
||||
if (effectiveChannel === 'alpha') {
|
||||
log.transports.file.level = 'info';
|
||||
return createAlphaUpdaterInstance();
|
||||
}
|
||||
|
||||
log.transports.file.level = 'info';
|
||||
autoUpdater.logger = autoUpdaterLogInterface;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
@@ -216,7 +262,6 @@ function configureAndGetUpdater(): UpdaterInstance {
|
||||
* Used when checking multiple channels or when the winning channel is beta/latest.
|
||||
*/
|
||||
function configureAutoUpdaterForChannel(channel: 'beta' | 'latest'): void {
|
||||
log.transports.file.level = 'info';
|
||||
autoUpdater.logger = autoUpdaterLogInterface;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
@@ -296,7 +341,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
]);
|
||||
|
||||
process.on('uncaughtException', (error: any) => {
|
||||
console.error('Error in main process', error);
|
||||
log.error('Error in main process', error);
|
||||
});
|
||||
|
||||
if (store.get('ignore_ssl')) {
|
||||
@@ -320,6 +365,13 @@ let currentPrivateMode = false;
|
||||
let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
|
||||
let currentSidebarCollapsed = false;
|
||||
let currentShuffleEnabled = false;
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (isMacOS()) {
|
||||
forceQuit = true;
|
||||
}
|
||||
log.info('App quitting', { reason: exitFromTray ? 'tray' : 'before-quit' });
|
||||
});
|
||||
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
|
||||
let inputFocused = false;
|
||||
|
||||
@@ -357,10 +409,7 @@ const installExtensions = async () => {
|
||||
{ forceDownload },
|
||||
)
|
||||
.then((installedExtensions) => {
|
||||
createLog({
|
||||
message: `Installed extension: ${installedExtensions}`,
|
||||
type: 'info',
|
||||
});
|
||||
log.info(`Installed extension: ${installedExtensions}`);
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore
|
||||
@@ -542,7 +591,7 @@ const validateUrl = (url: string): boolean => {
|
||||
|
||||
async function createWindow(first = true): Promise<void> {
|
||||
if (isDevelopment) {
|
||||
await installExtensions().catch(console.log);
|
||||
await installExtensions().catch((error) => log.error(error));
|
||||
}
|
||||
|
||||
const nativeFrame = store.get('window_window_bar_style', 'linux') === 'linux';
|
||||
@@ -635,6 +684,7 @@ async function createWindow(first = true): Promise<void> {
|
||||
});
|
||||
|
||||
ipcMain.on('window-quit', () => {
|
||||
log.info('App quitting', { reason: 'window-quit' });
|
||||
shutdownServer();
|
||||
mainWindow?.close();
|
||||
app.exit();
|
||||
@@ -700,9 +750,23 @@ async function createWindow(first = true): Promise<void> {
|
||||
mainWindow.show();
|
||||
createWinThumbarButtons();
|
||||
}
|
||||
|
||||
log.info('Main window created', { startMinimized: startWindowMinimized && first });
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||
log.error('Renderer process gone', {
|
||||
exitCode: details.exitCode,
|
||||
reason: details.reason,
|
||||
});
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('unresponsive', () => {
|
||||
log.error('Renderer process unresponsive');
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
log.info('Main window closed');
|
||||
ipcMain.removeHandler('window-clear-cache');
|
||||
ipcMain.removeHandler('app-check-for-updates');
|
||||
mainWindow = null;
|
||||
@@ -715,10 +779,12 @@ async function createWindow(first = true): Promise<void> {
|
||||
|
||||
if (!exitFromTray && store.get('window_exit_to_tray')) {
|
||||
event.preventDefault();
|
||||
log.info('Main window hidden to tray');
|
||||
mainWindow?.hide();
|
||||
}
|
||||
|
||||
if (forceQuit) {
|
||||
log.info('App quitting', { reason: 'forceQuit' });
|
||||
app.exit();
|
||||
}
|
||||
});
|
||||
@@ -726,6 +792,7 @@ async function createWindow(first = true): Promise<void> {
|
||||
(mainWindow as any).on('minimize', (event: any) => {
|
||||
if (store.get('window_minimize_to_tray') === true) {
|
||||
event.preventDefault();
|
||||
log.info('Main window minimized to tray');
|
||||
mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
@@ -734,12 +801,6 @@ async function createWindow(first = true): Promise<void> {
|
||||
app.setAppUserModelId('org.jeffvli.feishin');
|
||||
}
|
||||
|
||||
if (isMacOS()) {
|
||||
app.on('before-quit', () => {
|
||||
forceQuit = true;
|
||||
});
|
||||
}
|
||||
|
||||
menuBuilder = new MenuBuilder(mainWindow);
|
||||
rebuildMainMenu();
|
||||
|
||||
@@ -949,19 +1010,6 @@ ipcMain.on(
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
'logger',
|
||||
(
|
||||
_event,
|
||||
data: {
|
||||
message: string;
|
||||
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
|
||||
},
|
||||
) => {
|
||||
createLog(data);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle('power-save-blocker-start', (_event, { full }: { full: boolean }) => {
|
||||
if (powerSaveBlockerId !== null) {
|
||||
return powerSaveBlockerId;
|
||||
@@ -1044,6 +1092,15 @@ if (!singleInstance) {
|
||||
|
||||
app.whenReady()
|
||||
.then(() => {
|
||||
log.info('App ready', {
|
||||
arch: process.arch,
|
||||
electron: process.versions.electron,
|
||||
ignoreCors: !!store.get('ignore_cors'),
|
||||
ignoreSsl: !!store.get('ignore_ssl'),
|
||||
platform: process.platform,
|
||||
version: packageJson.version,
|
||||
});
|
||||
|
||||
protocol.handle('feishin', async () => {
|
||||
const filePath = store.get('local_font_path');
|
||||
if (typeof filePath !== 'string') {
|
||||
@@ -1114,7 +1171,7 @@ if (!singleInstance) {
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(console.log);
|
||||
.catch((error) => log.error(error));
|
||||
}
|
||||
|
||||
// Register 'open-item' handler globally, ensuring it is only registered once
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import type { LogLevel } from '/@/shared/logger/types';
|
||||
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
||||
import log from 'electron-log/main';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import util from 'node:util';
|
||||
|
||||
import { createZipBuffer } from '/@/main/utils/zip';
|
||||
import { sanitizeForDiagnostics } from '/@/shared/utils/sanitize-for-diagnostics';
|
||||
|
||||
export type { LogLevel };
|
||||
export type { LogSeverity } from '/@/shared/logger/types';
|
||||
|
||||
const PROCESS_WIDTH = 10; // width of "[renderer]"
|
||||
const LEVEL_WIDTH = 5; // width of "DEBUG" / "ERROR"
|
||||
const RESET = '\x1B[0m';
|
||||
|
||||
const levelColors: Record<string, string> = {
|
||||
debug: '\x1B[38;2;100;149;237m', // #6495ED
|
||||
error: '\x1B[38;2;255;100;100m', // #ff6464
|
||||
info: '\x1B[38;2;76;175;80m', // #4caf50
|
||||
warn: '\x1B[38;2;225;125;50m', // #e17d32
|
||||
};
|
||||
|
||||
const formatLogLine = ({
|
||||
colorize = false,
|
||||
data,
|
||||
level,
|
||||
message,
|
||||
}: {
|
||||
colorize?: boolean;
|
||||
data: unknown[];
|
||||
level: string;
|
||||
message: { date: Date; variables?: { processType?: string } };
|
||||
}): string[] => {
|
||||
const processType = message.variables?.processType === 'renderer' ? 'renderer' : 'main';
|
||||
const paddedLevel = String(level).toUpperCase().padEnd(LEVEL_WIDTH, ' ');
|
||||
const levelLabel =
|
||||
colorize && levelColors[level]
|
||||
? `${levelColors[level]}${paddedLevel}${RESET}`
|
||||
: paddedLevel;
|
||||
const processLabel = `[${processType}]`.padEnd(PROCESS_WIDTH, ' ');
|
||||
const text = data
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
|
||||
return util.inspect(item, {
|
||||
breakLength: 80,
|
||||
colors: colorize,
|
||||
compact: false,
|
||||
depth: null,
|
||||
});
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return [`${message.date.toISOString()} ${levelLabel} ${processLabel} ${text}`];
|
||||
};
|
||||
|
||||
const isLogLevel = (value: unknown): value is LogLevel => {
|
||||
return value === 'debug' || value === 'info';
|
||||
};
|
||||
|
||||
export const setLogLevel = (level: LogLevel) => {
|
||||
log.transports.file.level = level;
|
||||
log.transports.console.level = level;
|
||||
};
|
||||
|
||||
log.initialize();
|
||||
setLogLevel(
|
||||
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true' ? 'debug' : 'info',
|
||||
);
|
||||
log.transports.file.format = (params) => formatLogLine({ ...params, colorize: false });
|
||||
log.transports.file.maxSize = 1024 * 1024 * 10; // 10MB
|
||||
log.transports.console.format = (params) => formatLogLine({ ...params, colorize: true });
|
||||
|
||||
ipcMain.on('logger-set-level', (_event, level: unknown) => {
|
||||
if (isLogLevel(level)) {
|
||||
setLogLevel(level);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('logger-open-folder', async () => {
|
||||
const logFilePath = log.transports.file.getFile().path;
|
||||
const logsPath = path.dirname(logFilePath);
|
||||
await shell.openPath(logsPath);
|
||||
return true;
|
||||
});
|
||||
|
||||
type ExportDiagnosticsPayload = {
|
||||
logLevel?: null | string;
|
||||
rendererSettings?: null | Record<string, unknown>;
|
||||
server?: null | Record<string, unknown>;
|
||||
};
|
||||
|
||||
const exportDiagnosticsArchive = async (payload: ExportDiagnosticsPayload = {}) => {
|
||||
const parentWindow = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
|
||||
const stamp = new Date().toISOString().slice(0, 10);
|
||||
const saveOptions = {
|
||||
defaultPath: `feishin-diagnostics-${stamp}.zip`,
|
||||
filters: [{ extensions: ['zip'], name: 'Zip' }],
|
||||
};
|
||||
const result = parentWindow
|
||||
? await dialog.showSaveDialog(parentWindow, saveOptions)
|
||||
: await dialog.showSaveDialog(saveOptions);
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
const logFile = log.transports.file.getFile();
|
||||
const logsDir = path.dirname(logFile.path);
|
||||
const entries: { data: Buffer; name: string }[] = [];
|
||||
|
||||
const { store } = await import('/@/main/features/core/settings');
|
||||
|
||||
const diagnostics = {
|
||||
app: {
|
||||
name: app.getName(),
|
||||
version: app.getVersion(),
|
||||
},
|
||||
arch: process.arch,
|
||||
chrome: process.versions.chrome,
|
||||
electron: process.versions.electron,
|
||||
exportedAt: new Date().toISOString(),
|
||||
locale: app.getLocale(),
|
||||
logLevel: payload.logLevel ?? null,
|
||||
node: process.versions.node,
|
||||
os: {
|
||||
release: os.release(),
|
||||
type: os.type(),
|
||||
version: typeof os.version === 'function' ? os.version() : undefined,
|
||||
},
|
||||
platform: process.platform,
|
||||
server: payload.server ?? null,
|
||||
settings: {
|
||||
ignoreCors: store.get('ignore_cors'),
|
||||
ignoreSsl: store.get('ignore_ssl'),
|
||||
releaseChannel: store.get('release_channel'),
|
||||
},
|
||||
};
|
||||
|
||||
entries.push({
|
||||
data: Buffer.from(`${JSON.stringify(diagnostics, null, 2)}\n`, 'utf8'),
|
||||
name: 'diagnostics.json',
|
||||
});
|
||||
|
||||
try {
|
||||
const configRaw = await fs.readFile(store.path, 'utf8');
|
||||
const configJson = JSON.parse(configRaw) as unknown;
|
||||
entries.push({
|
||||
data: Buffer.from(
|
||||
`${JSON.stringify(sanitizeForDiagnostics(configJson), null, 2)}\n`,
|
||||
'utf8',
|
||||
),
|
||||
name: 'main-config.json',
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn('Failed to read main config for diagnostics export', error);
|
||||
}
|
||||
|
||||
if (payload.rendererSettings) {
|
||||
entries.push({
|
||||
data: Buffer.from(
|
||||
`${JSON.stringify(sanitizeForDiagnostics(payload.rendererSettings), null, 2)}\n`,
|
||||
'utf8',
|
||||
),
|
||||
name: 'renderer-settings.json',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const logFiles = await fs.readdir(logsDir);
|
||||
for (const fileName of logFiles) {
|
||||
if (!fileName.endsWith('.log')) {
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(logsDir, fileName);
|
||||
const data = await fs.readFile(filePath);
|
||||
entries.push({ data, name: `logs/${fileName}` });
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('Failed to read log files for export', error);
|
||||
}
|
||||
|
||||
if (!entries.some((entry) => entry.name.startsWith('logs/'))) {
|
||||
try {
|
||||
entries.push({
|
||||
data: await fs.readFile(logFile.path),
|
||||
name: `logs/${path.basename(logFile.path)}`,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('Failed to read active log file for export', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const zipBuffer = createZipBuffer(entries);
|
||||
await fs.writeFile(result.filePath, zipBuffer);
|
||||
|
||||
return { canceled: false, path: result.filePath };
|
||||
};
|
||||
|
||||
ipcMain.handle('logger-export-diagnostics', async (_event, payload?: ExportDiagnosticsPayload) => {
|
||||
return exportDiagnosticsArchive(payload);
|
||||
});
|
||||
|
||||
export default log;
|
||||
|
||||
export const autoUpdaterLogInterface = {
|
||||
debug: (message: string) => {
|
||||
log.debug(message);
|
||||
},
|
||||
|
||||
error: (message: string) => {
|
||||
log.error(message);
|
||||
},
|
||||
|
||||
info: (message: string) => {
|
||||
log.info(message);
|
||||
},
|
||||
|
||||
warn: (message: string) => {
|
||||
log.warn(message);
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import log from 'electron-log/main';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import { URL } from 'url';
|
||||
@@ -42,46 +41,3 @@ export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
||||
|
||||
return accelerator;
|
||||
};
|
||||
|
||||
const logMethod = {
|
||||
debug: log.debug,
|
||||
error: log.error,
|
||||
info: log.info,
|
||||
success: log.info,
|
||||
verbose: log.verbose,
|
||||
warning: log.warn,
|
||||
};
|
||||
|
||||
const logColor = {
|
||||
debug: 'blue',
|
||||
error: 'red',
|
||||
info: 'blue',
|
||||
success: 'green',
|
||||
verbose: 'blue',
|
||||
warning: 'yellow',
|
||||
};
|
||||
|
||||
export const createLog = (data: {
|
||||
message: string;
|
||||
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
|
||||
}) => {
|
||||
logMethod[data.type](`%c${data.message}`, `color: ${logColor[data.type]}`);
|
||||
};
|
||||
|
||||
export const autoUpdaterLogInterface = {
|
||||
debug: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'debug' });
|
||||
},
|
||||
|
||||
error: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'error' });
|
||||
},
|
||||
|
||||
info: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'info' });
|
||||
},
|
||||
|
||||
warn: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'warning' });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { deflateRawSync } from 'node:zlib';
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
let crc = i;
|
||||
for (let j = 0; j < 8; j += 1) {
|
||||
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
||||
}
|
||||
table[i] = crc >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
const crc32 = (data: Buffer): number => {
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
crc = CRC_TABLE[(crc ^ data[i]) & 0xff]! ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
};
|
||||
|
||||
export type ZipEntry = {
|
||||
data: Buffer;
|
||||
name: string;
|
||||
};
|
||||
|
||||
/** Build a zip archive (deflate) without external dependencies. */
|
||||
export const createZipBuffer = (entries: ZipEntry[]): Buffer => {
|
||||
const localParts: Buffer[] = [];
|
||||
const centralParts: Buffer[] = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const nameBuf = Buffer.from(entry.name, 'utf8');
|
||||
const compressed = deflateRawSync(entry.data);
|
||||
const checksum = crc32(entry.data);
|
||||
|
||||
const localHeader = Buffer.alloc(30);
|
||||
localHeader.writeUInt32LE(0x04034b50, 0);
|
||||
localHeader.writeUInt16LE(20, 4); // version needed
|
||||
localHeader.writeUInt16LE(0, 6); // flags
|
||||
localHeader.writeUInt16LE(8, 8); // deflate
|
||||
localHeader.writeUInt16LE(0, 10); // time
|
||||
localHeader.writeUInt16LE(0, 12); // date
|
||||
localHeader.writeUInt32LE(checksum, 14);
|
||||
localHeader.writeUInt32LE(compressed.length, 18);
|
||||
localHeader.writeUInt32LE(entry.data.length, 22);
|
||||
localHeader.writeUInt16LE(nameBuf.length, 26);
|
||||
localHeader.writeUInt16LE(0, 28); // extra length
|
||||
|
||||
localParts.push(localHeader, nameBuf, compressed);
|
||||
|
||||
const centralHeader = Buffer.alloc(46);
|
||||
centralHeader.writeUInt32LE(0x02014b50, 0);
|
||||
centralHeader.writeUInt16LE(20, 4); // version made by
|
||||
centralHeader.writeUInt16LE(20, 6); // version needed
|
||||
centralHeader.writeUInt16LE(0, 8);
|
||||
centralHeader.writeUInt16LE(8, 10);
|
||||
centralHeader.writeUInt16LE(0, 12);
|
||||
centralHeader.writeUInt16LE(0, 14);
|
||||
centralHeader.writeUInt32LE(checksum, 16);
|
||||
centralHeader.writeUInt32LE(compressed.length, 20);
|
||||
centralHeader.writeUInt32LE(entry.data.length, 24);
|
||||
centralHeader.writeUInt16LE(nameBuf.length, 28);
|
||||
centralHeader.writeUInt16LE(0, 30);
|
||||
centralHeader.writeUInt16LE(0, 32);
|
||||
centralHeader.writeUInt16LE(0, 34);
|
||||
centralHeader.writeUInt16LE(0, 36);
|
||||
centralHeader.writeUInt32LE(0, 38);
|
||||
centralHeader.writeUInt32LE(offset, 42);
|
||||
|
||||
centralParts.push(centralHeader, nameBuf);
|
||||
offset += localHeader.length + nameBuf.length + compressed.length;
|
||||
}
|
||||
|
||||
const centralSize = centralParts.reduce((sum, part) => sum + part.length, 0);
|
||||
const end = Buffer.alloc(22);
|
||||
end.writeUInt32LE(0x06054b50, 0);
|
||||
end.writeUInt16LE(0, 4);
|
||||
end.writeUInt16LE(0, 6);
|
||||
end.writeUInt16LE(entries.length, 8);
|
||||
end.writeUInt16LE(entries.length, 10);
|
||||
end.writeUInt32LE(centralSize, 12);
|
||||
end.writeUInt32LE(offset, 16);
|
||||
end.writeUInt16LE(0, 20);
|
||||
|
||||
return Buffer.concat([...localParts, ...centralParts, end]);
|
||||
};
|
||||
Reference in New Issue
Block a user