diff --git a/lang/en.json b/lang/en.json index cd450a0b2..18328570a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -78,6 +78,7 @@ "filters": "Filters", "copyText": "Copy Text", "clearLog": "Clear Log", + "copy404Urls": "Copy 404 URLs", "uploadLog": "Upload Log", "copiedToClipboard": "Copied to Clipboard" }, diff --git a/package.json b/package.json index 9983327c5..a5b748d34 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "sqlite3": "4.2.0", + "tail": "2.0.3", "typeorm": "0.2.22", "typesafe-actions": "4.4.2", "uuid": "3.3.2", @@ -74,6 +75,7 @@ "@types/react-redux": "7.1.7", "@types/react-router-dom": "4.3.4", "@types/react-virtualized": "9.21.9", + "@types/tail": "2.0.0", "@types/uuid": "3.4.5", "@types/uuid-validate": "0.0.1", "@types/which": "1.3.2", diff --git a/src/back/ServicesFile.ts b/src/back/ServicesFile.ts index 8cf93451f..45bc3322d 100644 --- a/src/back/ServicesFile.ts +++ b/src/back/ServicesFile.ts @@ -4,6 +4,7 @@ import { parseVarStr, readJsonFile } from '@shared/Util'; import { Coerce } from '@shared/utils/Coerce'; import { IObjectParserProp, ObjectParser } from '@shared/utils/ObjectParser'; import * as path from 'path'; +import { ServiceFileData } from './types'; const { str } = Coerce; @@ -31,6 +32,7 @@ export namespace ServicesFile { server: [], start: [], stop: [], + watch: [], }; const parser = new ObjectParser({ input: data, @@ -39,6 +41,7 @@ export namespace ServicesFile { parser.prop('server').array(item => parsed.server.push(parseNamedBackProcessInfo(item, config))); parser.prop('start').array(item => parsed.start.push(parseBackProcessInfo(item, config))); parser.prop('stop').array(item => parsed.stop.push(parseBackProcessInfo(item, config))); + parser.prop('watch').arrayRaw(item => parsed.watch.push(parseVarStr(str(item), config))); return parsed; } @@ -69,11 +72,3 @@ export namespace ServicesFile { return parsed; } } - -export type ServiceFileData = { - server: INamedBackProcessInfo[]; - /** Processes to run before the launcher starts. */ - start: IBackProcessInfo[]; - /** Processes to run when the launcher closes. */ - stop: IBackProcessInfo[]; -} diff --git a/src/back/index.ts b/src/back/index.ts index dd7f5f24e..9eddd763d 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -22,6 +22,7 @@ import * as path from 'path'; import 'reflect-metadata'; // Required for the DB Models to function import 'sqlite3'; +import { Tail } from 'tail'; import { ConnectionOptions, createConnection } from 'typeorm'; import { ConfigFile } from './ConfigFile'; import { CONFIG_FILENAME, PREFERENCES_FILENAME, SERVICES_SOURCE } from './constants'; @@ -32,7 +33,7 @@ import { SocketServer } from './SocketServer'; import { BackState, ImageDownloadItem } from './types'; import { EventQueue } from './util/EventQueue'; import { FolderWatcher } from './util/FolderWatcher'; -import { createContainer, exit, log, runService } from './util/misc'; +import { createContainer, exit, log, newLogEntry, runService } from './util/misc'; // Make sure the process.send function is available type Required = T extends undefined ? never : T; @@ -145,6 +146,22 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { const chosenServer = state.serviceInfo.server.find(i => i.name === state.config.server); state.services.server = runService(state, 'server', 'Server', chosenServer || state.serviceInfo.server[0]); } + // Start file watchers + for (let i = 0; i < state.serviceInfo.watch.length; i++) { + const filePath = state.serviceInfo.watch[i]; + try { + const tail = new Tail(filePath, { follow: true }); + tail.on('line', (data) => { + log(state, newLogEntry('Log Watcher', data)); + }); + tail.on('error', (error) => { + log(state, newLogEntry('Log Watcher', `Error while watching file "${filePath}" - ${error}`)); + }); + log(state, newLogEntry('Log Watcher', `Watching file "${filePath}"`)); + } catch (error) { + log(state, newLogEntry('Log Watcher', `Failed to watch file "${filePath}" - ${error}`)); + } + } } // Init language diff --git a/src/back/types.ts b/src/back/types.ts index 56ad6e07c..8e52e9f72 100644 --- a/src/back/types.ts +++ b/src/back/types.ts @@ -103,6 +103,8 @@ export type ServiceFileData = { start: IBackProcessInfo[]; /** Processes to run when the launcher closes. */ stop: IBackProcessInfo[]; + /** Files to watch and run continous logging on */ + watch: string[]; }; export type ThemeListItem = Theme & { diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index 28ca5f204..89035d113 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -319,3 +319,10 @@ export async function waitForServiceDeath(service: ManagedChildProcess) : Promis }); } } + +export function newLogEntry(source: string, content: string): ILogPreEntry { + return { + source: source, + content: content + }; +} \ No newline at end of file diff --git a/src/renderer/components/pages/LogsPage.tsx b/src/renderer/components/pages/LogsPage.tsx index f437ab1f6..56d935cf0 100644 --- a/src/renderer/components/pages/LogsPage.tsx +++ b/src/renderer/components/pages/LogsPage.tsx @@ -16,13 +16,15 @@ type OwnProps = {}; export type LogsPageProps = OwnProps & WithPreferencesProps; +const urlRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w\-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\\w]*))?)/; const labels = [ 'Background Services', 'Game Launcher', 'Language', 'Redirector', - 'Router', + 'Server', 'Curation', + 'Log Watcher', ]; export type LogsPageState = { @@ -117,6 +119,16 @@ export class LogsPage extends React.Component { className='simple-button simple-center__vertical-inner' /> + {/* Copy 404 URLs Button */} +
+
+ +
+
{/* Upload Logs Button */}
@@ -154,6 +166,23 @@ export class LogsPage extends React.Component { this.forceUpdate(); } + onCopy404Click = (): void => { + // Store found URLs + const urls: string[] = []; + for (const entry of window.Shared.log.entries) { + // All 404 entries start with 404 + if (entry && entry.content.startsWith('404')) { + // Extract URL with regex + const match = urlRegex.exec(entry.content); + if (match && match.length > 0) { + urls.push(match[1]); + } + } + } + // Copy with each URL on a new line + clipboard.writeText(urls.join('\n')); + } + onUploadClick = async (): Promise => { this.setState({ uploading: true }); const strings = this.context; diff --git a/src/shared/lang.ts b/src/shared/lang.ts index df21e0fa5..2df7055df 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -85,6 +85,7 @@ const langTemplate = { 'filters', 'copyText', 'clearLog', + 'copy404Urls', 'uploadLog', 'copiedToClipboard', ] as const, diff --git a/static/window/styles/fancy.css b/static/window/styles/fancy.css index 9b9ef59b8..06d881df1 100644 --- a/static/window/styles/fancy.css +++ b/static/window/styles/fancy.css @@ -89,8 +89,9 @@ --layout__log-source-game-launcher: #e67e22; --layout__log-source-language: #b157ec; --layout__log-source-redirector: #00ffff; - --layout__log-source-router: #00ff00; + --layout__log-source-server: #00ff00; --layout__log-source-curation: #efff00; + --layout__log-source-log-watcher: #cf0000; /* Credits */ --layout__credits-tooltip-border: #000000; --layout__credits-tooltip-background: #000000DD; @@ -266,12 +267,15 @@ body { .log__source--redirector { color: var(--layout__log-source-redirector); } -.log__source--router { - color: var(--layout__log-source-router); +.log__source--server { + color: var(--layout__log-source-server); } .log__source--curation { color: var(--layout__log-source-curation); } +.log__source--log-watcher { + color: var(--layout__log-source-log-watcher); +} /* ------ Image Preview ------ */