log watcher

This commit is contained in:
Alexander Drozdov
2020-09-18 18:10:47 +03:00
parent 05b4b274a7
commit a0fedd5423
10 changed files with 298 additions and 12 deletions
+14
View File
@@ -0,0 +1,14 @@
const dict = {
'channel.system': /^: (?<body>.+)$/,
'channel.trade': /^\$(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.global': /^#(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.party': /^%(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.guild': /^&(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.whisper.to': /^@To (?<char_name>.+?): (?<body>.+)$/,
'channel.whisper.from': /^@From (?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'trade.gem': /^level (?<gem_lvl>\d+) (?<gem_qual>\d+)% (?<gem_name>.+)$/
}
export default dict
export type ClientLogDict = typeof dict
+3
View File
@@ -1,10 +1,12 @@
import type { TranslationDict } from '@/assets/data/en/client_strings'
import type { ClientLogDict } from '@/assets/data/en/client_log'
import type { BaseType, DropEntry, Stat, StatMatcher, UniqueItem } from './interfaces'
import { Config } from '@/web/Config'
export * from './interfaces'
export let CLIENT_STRINGS: TranslationDict
export let CLIENTLOG_STRINGS: ClientLogDict
export let ITEM_NAME_REF_BY_TRANSLATED: Map<string | undefined, string>
export let TRANSLATED_ITEM_NAME_BY_REF: Map<string | undefined, string>
@@ -28,6 +30,7 @@ export const ITEM_DROP = new Map<string, DropEntry>()
;(async function initData () { /* eslint-disable no-lone-blocks */
{
CLIENT_STRINGS = (require(`./${Config.store.language}/client_strings`).default)
CLIENTLOG_STRINGS = (require(`./${Config.store.language}/client_log`).default)
const itemNames: Array<[string, string]> = (require(`./${Config.store.language}/item-names.json`))
TRANSLATED_ITEM_NAME_BY_REF = new Map(itemNames)
+14
View File
@@ -0,0 +1,14 @@
import type { ClientLogDict } from '../en/client_log'
const dict: ClientLogDict = {
'channel.system': /^: (?<body>.+)$/,
'channel.trade': /^\$(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.global': /^#(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.party': /^%(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.guild': /^&(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'channel.whisper.to': /^@Кому (?<char_name>.+?): (?<body>.+)$/,
'channel.whisper.from': /^@От кого (?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
'trade.gem': /^уровень (?<gem_lvl>\d+) (?<gem_qual>\d+)% (?<gem_name>.+)$/
}
export default dict
+2
View File
@@ -17,6 +17,7 @@ import { createOverlayWindow } from './main/overlay-window'
import { setupAltVisibility } from './main/alt-visibility'
import { setupBuiltinBrowser } from './main/builtin-browser'
import { createFileProtocol } from './main/app-file-protocol'
import { LogWatcher } from './main/LogWatcher'
const isDevelopment = process.env.NODE_ENV !== 'production'
if (!app.requestSingleInstanceLock()) {
@@ -70,6 +71,7 @@ app.on('ready', async () => {
setupCfProtection()
setupShortcuts()
setupAltVisibility()
LogWatcher.start()
},
// fixes(linux): window is black instead of transparent
process.platform === 'linux' ? 1000 : 0
+5
View File
@@ -66,3 +66,8 @@ export const OPEN_WIKI = 'MAIN->OVERLAY::open-wiki'
export const IMPORT_FILE = 'OVERLAY->MAIN::import-file'
export const TOGGLE_DELVE_GRID = 'MAIN->OVERLAY::delve-grid'
export const CLIENT_LOG_UPDATE = 'MAIN->OVERLAY::client-log'
export interface IpcClientLog {
lines: string[]
}
+8
View File
@@ -59,6 +59,14 @@ class MainProcessBinding extends EventTarget {
electron.ipcRenderer.on(ipcEvent.TOGGLE_DELVE_GRID, (e) => {
this.dispatchEvent(new CustomEvent(ipcEvent.TOGGLE_DELVE_GRID))
})
electron.ipcRenderer.on(ipcEvent.CLIENT_LOG_UPDATE, (e, data: ipcEvent.IpcClientLog) => {
data.lines.forEach(line => {
this.dispatchEvent(new CustomEvent(ipcEvent.CLIENT_LOG_UPDATE, {
detail: line
}))
})
})
}
}
+90
View File
@@ -0,0 +1,90 @@
import { promises as fs, watchFile, unwatchFile } from 'fs'
import { overlayWindow } from './overlay-window'
import * as ipc from '@/ipc/ipc-event'
import { config } from './config'
import { logger } from './logger'
const COMMON_PATH = [
'C:/Program Files (x86)/Grinding Gear Games/Path of Exile/logs/Client.txt',
'D:/Program Files (x86)/Grinding Gear Games/Path of Exile/logs/Client.txt',
'C:/Program Files (x86)/Steam/steamapps/common/Path of Exile/logs/Client.txt',
'D:/Program Files (x86)/Steam/steamapps/common/Path of Exile/logs/Client.txt'
]
export class LogWatcher {
static offset = 0
static filePath?: string
static file?: fs.FileHandle
static isReading = false
static readBuff = Buffer.allocUnsafe(64 * 1024)
static async start () {
let logFile = config.get('clientLog')
if (!logFile && process.platform === 'win32') {
for (const filePath of COMMON_PATH) {
try {
await fs.access(filePath)
config.set('clientLog', filePath)
logFile = filePath
break
} catch {}
}
}
if (logFile) {
try {
await LogWatcher.watch(logFile)
} catch {
logger.error('Failed to watch', { source: 'log-watcher', file: logFile })
}
}
}
static async watch (path: string) {
if (this.file) {
unwatchFile(this.filePath!)
await this.file.close()
this.file = undefined
this.isReading = false
}
watchFile(path, { interval: 450 }, () => {
this.handleFileChange()
})
this.filePath = path
this.file = await fs.open(path, 'r')
const stats = await this.file.stat()
this.offset = stats.size
}
static handleFileChange () {
if (!this.isReading) {
this.isReading = true
this.readToEOF()
}
}
static async readToEOF () {
if (!this.file) {
this.isReading = false
return
}
const { bytesRead } = await this.file.read(this.readBuff, 0, this.readBuff.length, this.offset)
if (bytesRead) {
const str = this.readBuff.toString('utf8', 0, bytesRead)
const lines = str.split('\n').map(line => line.trim()).filter(line => line.length)
overlayWindow!.webContents.send(ipc.CLIENT_LOG_UPDATE, { lines } as ipc.IpcClientLog)
}
if (bytesRead) {
this.offset += bytesRead
this.readToEOF()
} else {
this.isReading = false
}
}
}
+16 -12
View File
@@ -6,19 +6,16 @@ import { forbidden, forbiddenCtrl } from '@/ipc/KeyToCode'
import { GET_CONFIG, PUSH_CONFIG, CLOSE_SETTINGS_WINDOW } from '@/ipc/ipc-event'
import { overlayWindow } from './overlay-window'
import { logger } from './logger'
import { LogWatcher } from './LogWatcher'
export function setupConfigEvents () {
ipcMain.on(GET_CONFIG, (e) => {
e.returnValue = config.store
})
ipcMain.on(PUSH_CONFIG, (e, cfg) => {
const old = config.store
Object.setPrototypeOf(old, Object.prototype)
if (!isDeepEq(cfg, old)) {
batchUpdateConfig(cfg, false)
}
ipcMain.on(PUSH_CONFIG, (e, cfg: Config) => {
batchUpdateConfig(cfg, false)
})
ipcMain.on(CLOSE_SETTINGS_WINDOW, (e, cfg) => {
ipcMain.on(CLOSE_SETTINGS_WINDOW, (e, cfg: Config | undefined) => {
if (cfg != null) {
batchUpdateConfig(cfg, true)
}
@@ -84,10 +81,17 @@ export const config = (() => {
return store
})()
export function batchUpdateConfig (upd: Config, push = true) {
config.store = upd
logger.verbose('Saved', { source: 'config', push })
if (push) {
overlayWindow!.webContents.send(PUSH_CONFIG, upd)
export function batchUpdateConfig (newCfg: Config, push = true) {
const oldCfg = config.store
Object.setPrototypeOf(oldCfg, Object.prototype)
if (!isDeepEq(newCfg, oldCfg)) {
config.store = newCfg
logger.verbose('Saved', { source: 'config', push })
if (push) {
overlayWindow!.webContents.send(PUSH_CONFIG, newCfg)
}
if (oldCfg.clientLog !== newCfg.clientLog) {
LogWatcher.start()
}
}
}
+141
View File
@@ -0,0 +1,141 @@
/* eslint-disable camelcase */
import { CLIENTLOG_STRINGS as _$, ITEM_NAME_REF_BY_TRANSLATED, TRADE_TAGS } from '@/assets/data'
import { Config } from '@/web/Config'
const enum MessageChannel {
SYSTEM = 'system',
TRADE = 'trade',
GLOBAL = 'global',
PARTY = 'party',
GUILD = 'guild',
WHISPER_FROM = 'whisper.from',
WHISPER_TO = 'whisper.to'
}
interface LogEntry {
сhannel: MessageChannel
guildTag?: string
charName?: string
body: string
trade?: {
item: {
name: string
amount?: number
gem?: { level: number, quality: number }
}
price: {
name: string
amount: number
}
tab?: {
name: string
left: number
top: number
}
}
}
export function handleLine (line: string) {
const text = line.split('] ').slice(1).join('] ')
if (!text) return
const entry = {} as LogEntry
let match: RegExpMatchArray | null
if ((match = text.match(_$['channel.global']))) {
entry.сhannel = MessageChannel.GLOBAL
} else if ((match = text.match(_$['channel.trade']))) {
entry.сhannel = MessageChannel.TRADE
} else if ((match = text.match(_$['channel.system']))) {
entry.сhannel = MessageChannel.SYSTEM
} else if ((match = text.match(_$['channel.whisper.to']))) {
entry.сhannel = MessageChannel.WHISPER_TO
} else if ((match = text.match(_$['channel.whisper.from']))) {
entry.сhannel = MessageChannel.WHISPER_FROM
} else if ((match = text.match(_$['channel.party']))) {
entry.сhannel = MessageChannel.PARTY
} else if ((match = text.match(_$['channel.guild']))) {
entry.сhannel = MessageChannel.GUILD
} else {
return
}
entry.guildTag = match.groups!.guild_tag
entry.charName = match.groups!.char_name
entry.body = match.groups!.body
if (entry.сhannel === MessageChannel.WHISPER_FROM) {
if ((match = entry.body.match(TRADE_WHISPER[Config.store.language]))) {
const [pAmount, pTag] = [
match.groups!.price.split(' ', 1).toString(),
match.groups!.price.split(' ').slice(1).join(' ')
]
const gemMatch = match.groups!.item.match(_$['trade.gem'])
if (gemMatch) {
match.groups!.item = gemMatch.groups!.gem_name
}
entry.trade = {
item: {
name: ITEM_NAME_REF_BY_TRANSLATED.get(match.groups!.item) ||
match.groups!.item
},
price: {
amount: Number(pAmount),
name: TRADE_TAGS.find(pair => pair[1] === pTag)?.[0] || pTag
},
tab: {
name: match.groups!.tab_name,
top: Number(match.groups!.tab_top),
left: Number(match.groups!.tab_left)
}
}
if (gemMatch) {
entry.trade.item.gem = {
level: Number(gemMatch.groups!.gem_lvl),
quality: Number(gemMatch.groups!.gem_qual)
}
}
console.log(entry)
} else if ((match = entry.body.match(TRADE_BULK_WHISPER[Config.store.language]))) {
const [pAmount, pName] = [
match.groups!.price.split(' ', 1).toString(),
match.groups!.price.split(' ').slice(1).join(' ')
]
const [iAmount, iName] = [
match.groups!.item.split(' ', 1).toString(),
match.groups!.item.split(' ').slice(1).join(' ')
]
entry.trade = {
item: {
amount: Number(iAmount),
name: ITEM_NAME_REF_BY_TRANSLATED.get(iName) || iName
},
price: {
amount: Number(pAmount),
name: ITEM_NAME_REF_BY_TRANSLATED.get(pName) || pName
}
}
console.log(entry)
}
}
}
const TRADE_WHISPER = {
en: /^Hi, I would like to buy your (?<item>.+) listed for (?<price>.+) in (?<league>.+) \(stash tab "(?<tab_name>.*)"; position: left (?<tab_left>\d+), top (?<tab_top>\d+)\)(?<message>.+)?$/,
ru: /^Здравствуйте, хочу купить у вас (?<item>.+) за (?<price>.+) в лиге (?<league>.+) \(секция "(?<tab_name>.*)"; позиция: (?<tab_left>\d+) столбец, (?<tab_top>\d+) ряд\)(?<message>.+)?$/,
ko: /^안녕하세요, (?<league>.+)\(보관함 탭 "(?<tab_name>.*)", 위치: 왼쪽 (?<tab_left>\d+), 상단 (?<tab_top>\d+)\)에 (?<price>.+)\(으\)로 올려놓은 (?<item>.+)\(을\)를 구매하고 싶습니다(?<message>.+)?$/,
de: /^Hi, ich möchte '(?<item>.+)' zum angebotenen Preis von (?<price>.+) in der (?<league>.+)-Liga kaufen \(Truhenfach "(?<tab_name>.*)"; Position: (?<tab_left>\d+) von links, (?<tab_top>\d+) von oben\)(?<message>.+)?$/,
fr: /^Bonjour, je souhaiterais t'acheter (?<item>.+) pour (?<price>.+) dans la ligue (?<league>.+) \(onglet de réserve "(?<tab_name>.*)" ; (?<tab_left>\d+)e en partant de la gauche, (?<tab_top>\d+)e en partant du haut\)(?<message>.+)?$/,
es: /^Hola, quisiera comprar tu (?<item>.+) listado por (?<price>.+) en (?<league>.+) \(pestaña de alijo "(?<tab_name>.*)"; posición: izquierda(?<tab_left>\d+), arriba (?<tab_top>\d+)\)(?<message>.+)?$/,
pt: /^Olá, eu gostaria de comprar o seu item (?<item>.+) listado por (?<price>.+) na (?<league>.+) \(aba do baú: "(?<tab_name>.*)"; posição: esquerda (?<tab_left>\d+), topo (?<tab_top>\d+)\)(?<message>.+)?$/,
th: /^สวัสดี, เราต้องการจะชื้อของคุณ (?<item>.+) ใน ราคา (?<price>.+) ใน (?<league>.+) \(stash tab "(?<tab_name>.*)"; ตำแหน่ง: ซ้าย (?<tab_left>\d+), บน (?<tab_top>\d+)\)(?<message>.+)?$/
}
const TRADE_BULK_WHISPER = {
en: /^Hi, I'd like to buy your (?<item>.+) for my (?<price>.+) in (?<league>.+)\.(?<message>.+)?$/,
ru: /^Здравствуйте, хочу купить у вас (?<item>.+) за (?<price>.+) в лиге (?<league>.+)\.(?<message>.+)?$/
}
+5
View File
@@ -1,9 +1,14 @@
import { MainProcess } from '@/ipc/main-process-bindings'
import * as ipc from '@/ipc/ipc-event'
import { openWiki } from './wiki'
import { handleLine } from '../client-log/client-log'
export function registerOtherServices () {
MainProcess.addEventListener(ipc.OPEN_WIKI, (e) => {
openWiki((e as CustomEvent<string>).detail)
})
MainProcess.addEventListener(ipc.CLIENT_LOG_UPDATE, (e) => {
handleLine((e as CustomEvent<string>).detail)
})
}