diff --git a/config.default.json b/config.default.json index 0773b8b..b48f1b4 100644 --- a/config.default.json +++ b/config.default.json @@ -28,5 +28,6 @@ "user": "root", "password": "root", "database": "acore_world" - } + }, + "dbQueryTimeout": 10000 } diff --git a/src/armory/Armory.ts b/src/armory/Armory.ts index 807c61f..1b3a5e7 100644 --- a/src/armory/Armory.ts +++ b/src/armory/Armory.ts @@ -7,7 +7,7 @@ import * as morgan from "morgan"; import { Pool, createPool } from "mysql2/promise"; import { engine as handlebarsEngine } from "express-handlebars"; -import { Config } from "./Config"; +import { Config, IRealmConfig } from "./Config"; import { DbcManager } from "./data/DbcReader"; import { CharacterCustomization } from "./data/CharacterCustomization"; import { IndexController } from "./controllers/IndexController"; @@ -148,14 +148,12 @@ export class Armory { // Respond with html page if (req.accepts("html")) { - res.render("error.html", this.getErrorViewData(404, req)); - return; + return res.render("error.html", this.getErrorViewData(404, req)); } // Respond with json if (req.accepts("json")) { - res.json({ error: this.errorNames[404] }); - return; + return res.json({ error: this.errorNames[404] }); } // Default to plain-text @@ -172,6 +170,10 @@ export class Armory { return this.charsDbs[realm.toLowerCase()]; } + public getRealm(realm: string): IRealmConfig { + return this.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase()); + } + public gc(): void { if (this.config.loadDbcs) { return; diff --git a/src/armory/Config.ts b/src/armory/Config.ts index c846d0d..231c1f1 100644 --- a/src/armory/Config.ts +++ b/src/armory/Config.ts @@ -32,6 +32,7 @@ export class Config { public hideGameMasters: boolean; public realms: IRealmConfig[]; public worldDatabase: IDatabaseConfig; + public dbQueryTimeout: number; private static checkedMissingField: boolean = false; diff --git a/src/armory/DataTablesSsp.ts b/src/armory/DataTablesSsp.ts index 03fb697..36324f9 100644 --- a/src/armory/DataTablesSsp.ts +++ b/src/armory/DataTablesSsp.ts @@ -1,5 +1,5 @@ +import { Pool } from "mysql2/promise"; import { Query } from "express-serve-static-core"; -import { Connection, RowDataPacket } from "mysql2/promise"; export interface IResult { recordsTotal: number; @@ -30,7 +30,7 @@ export class DataTablesSsp { public joins: IColumnJoin[] = []; public extraDataColumns: string[] = []; - private db: Connection; + private db: Pool; private table: string; private primaryKey: string; private columnSettings: IColumnSettings[]; @@ -65,7 +65,7 @@ export class DataTablesSsp { private orderSql: string = ""; private joinSql: string = ""; - public constructor(query: Query, db: Connection, table: string, primaryKey: string, columnSettings: IColumnSettings[]) { + public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) { this.start = parseInt(query.start as string, 10); this.length = parseInt(query.length as string, 10); this.draw = parseInt(query.draw as string, 10); @@ -198,7 +198,7 @@ export class DataTablesSsp { `; } - public async run(): Promise { + public async run(queryTimeout: number = 10_000): Promise { this.limit() .order() .join() @@ -206,16 +206,25 @@ export class DataTablesSsp { const bindings = [...this.filterBindings, ...this.customBindings]; - let [rows, fields] = await this.db.query(this.buildTotalCountSql(), this.customBindings); + let [rows, fields] = await this.db.query({ + sql: this.buildTotalCountSql(), + values: this.customBindings, + timeout: queryTimeout, + }); const recordsTotal = rows[0].count; - [rows, fields] = await this.db.query(this.buildFilteredCountSql(), bindings); + [rows, fields] = await this.db.query({ + sql: this.buildFilteredCountSql(), + values: bindings, + timeout: queryTimeout, + }); const recordsFiltered = rows[0].count; [rows, fields] = await this.db.query({ sql: this.sql(), rowsAsArray: true, values: bindings, + timeout: queryTimeout, }); rows = (rows as any[][]).map(row => { for (let i = 0; i < this.columnSettings.length; ++i) { diff --git a/src/armory/controllers/CharacterController.ts b/src/armory/controllers/CharacterController.ts index 4e58af5..245cdb2 100644 --- a/src/armory/controllers/CharacterController.ts +++ b/src/armory/controllers/CharacterController.ts @@ -116,7 +116,10 @@ export class CharacterController { } this.itemSocketBonuses = {}; - let [rows, fields] = await this.armory.worldDb.query("SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0"); + let [rows, fields] = await this.armory.worldDb.query({ + sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0", + timeout: this.armory.config.dbQueryTimeout, + }); for (const row of rows as RowDataPacket[]) { this.itemSocketBonuses[row.entry] = row.socketBonus; } @@ -151,7 +154,7 @@ export class CharacterController { const realmName = req.params.realm; const charName = req.params.name; - const realm = this.getRealm(realmName); + const realm = this.armory.getRealm(realmName); if (realm === undefined) { // Could not find realm return next(404); @@ -195,7 +198,7 @@ export class CharacterController { const realmName = req.params.realm; const charName = req.params.name; - const realm = this.getRealm(realmName); + const realm = this.armory.getRealm(realmName); if (realm === undefined) { // Could not find realm return next(404); @@ -222,7 +225,7 @@ export class CharacterController { const realmName = req.params.realm; const charName = req.params.name; - const realm = this.getRealm(realmName); + const realm = this.armory.getRealm(realmName); if (realm === undefined) { // Could not find realm return next(404); @@ -244,7 +247,7 @@ export class CharacterController { const realmName = req.params.realm; const character = parseInt(req.params.character) || -1; - const realm = this.getRealm(realmName); + const realm = this.armory.getRealm(realmName); if (realm === undefined) { // Could not find realm return next(404); @@ -275,22 +278,22 @@ export class CharacterController { }; } - private getRealm(realm: string): IRealmConfig { - return this.armory.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase()); - } - private async getCharacterData(realm: IRealmConfig, character: string | number): Promise { const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?"; - const [rows, fields] = await this.armory.getCharactersDb(realm.name).query(` - SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\` - FROM \`characters\` - LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\` - LEFT JOIN \`guild\` ON \`guild\`.\`guildid\` = \`guild_member\`.\`guildid\` - LEFT JOIN \`${realm.authDatabase}\`.\`account_access\` ON \`account_access\`.\`id\` = \`characters\`.\`account\` - WHERE - ${where} - AND (\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0 OR ? = 0) - `, [character, this.armory.config.hideGameMasters ? 1 : 0]); + const [rows, fields] = await this.armory.getCharactersDb(realm.name).query({ + sql: ` + SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\` + FROM \`characters\` + LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\` + LEFT JOIN \`guild\` ON \`guild\`.\`guildid\` = \`guild_member\`.\`guildid\` + LEFT JOIN \`${realm.authDatabase}\`.\`account_access\` ON \`account_access\`.\`id\` = \`characters\`.\`account\` + WHERE + ${where} + AND (\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0 OR ? = 0) + `, + values: [character, this.armory.config.hideGameMasters ? 1 : 0], + timeout: this.armory.config.dbQueryTimeout, + }); if ((rows as RowDataPacket[]).length === 0) { return null; @@ -299,21 +302,29 @@ export class CharacterController { } private async getEquipmentData(realm: string, charGuid: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query(` - SELECT character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId - FROM character_inventory - JOIN item_instance ON item_instance.guid = character_inventory.item - WHERE character_inventory.guid = ? AND character_inventory.bag = 0 AND character_inventory.slot IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18) - `, [charGuid]); + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` + SELECT character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId + FROM character_inventory + JOIN item_instance ON item_instance.guid = character_inventory.item + WHERE character_inventory.guid = ? AND character_inventory.bag = 0 AND character_inventory.slot IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18) + `, + values: [charGuid], + timeout: this.armory.config.dbQueryTimeout, + }); return rows as RowDataPacket[] as IEquipmentData[]; } private async getMounts(realm: string, charGuid: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query(` - SELECT spell - FROM character_spell - WHERE guid = ? AND spell IN (?) - `, [charGuid, this.mountSpells]); + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` + SELECT spell + FROM character_spell + WHERE guid = ? AND spell IN (?) + `, + values:[charGuid, this.mountSpells], + timeout: this.armory.config.dbQueryTimeout, + }); return (rows as RowDataPacket[]) .map(row => this.mountBySpellId[row.spell]) @@ -640,11 +651,15 @@ export class CharacterController { } private async getTalents(realm: string, character: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query(` - SELECT spell, specMask - FROM character_talent - WHERE guid = ? - `, [character]); + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` + SELECT spell, specMask + FROM character_talent + WHERE guid = ? + `, + values: [character], + timeout: this.armory.config.dbQueryTimeout, + }); const talents: number[][] = [[], []]; for (const row of rows as RowDataPacket[]) { @@ -691,11 +706,15 @@ export class CharacterController { } private async getGlyphs(realm: string, character: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query(` - SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6 - FROM character_glyphs - WHERE guid = ? - `, [character]); + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` + SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6 + FROM character_glyphs + WHERE guid = ? + `, + values: [character], + timeout: this.armory.config.dbQueryTimeout, + }); const glyphs = [[], []]; for (const row of rows as RowDataPacket[]) { @@ -731,11 +750,15 @@ export class CharacterController { .toArray(); const achievements = await Promise.all(promises); - const [rows, fields] = await this.armory.getCharactersDb(realm).query(` - SELECT achievement, date - FROM character_achievement - WHERE guid = ? - `, [charData.guid]); + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` + SELECT achievement, date + FROM character_achievement + WHERE guid = ? + `, + values: [charData.guid], + timeout: this.armory.config.dbQueryTimeout, + }); const earned = {}; for (const row of rows as RowDataPacket[]) { earned[row.achievement] = { diff --git a/src/armory/controllers/IndexController.ts b/src/armory/controllers/IndexController.ts index 1599a88..fe0eee3 100644 --- a/src/armory/controllers/IndexController.ts +++ b/src/armory/controllers/IndexController.ts @@ -54,21 +54,23 @@ export class IndexController { } const db = this.armory.getCharactersDb(realm.name); - const conn = await db.getConnection(); if (!(realm.name in this.charsetCache)) { - let [rows, fields] = await db.query(` - SELECT CCSA.character_set_name FROM information_schema.\`TABLES\` T, - information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA - WHERE CCSA.collation_name = T.table_collation - AND T.table_schema = "${conn.config.database}" - AND T.table_name = "characters" - `); + let [rows, fields] = await db.query({ + sql: ` + SELECT CCSA.character_set_name FROM information_schema.\`TABLES\` T, + information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA + WHERE CCSA.collation_name = T.table_collation + AND T.table_schema = "${(await db.getConnection()).config.database}" + AND T.table_name = "characters" + `, + timeout: this.armory.config.dbQueryTimeout, + }); this.charsetCache[realm.name] = rows[0].character_set_name; } const charSet = this.charsetCache[realm.name]; - let ssp = new DataTablesSsp(req.query, conn, "characters", "guid", [ + let ssp = new DataTablesSsp(req.query, db, "characters", "guid", [ { name: "name", collation: `${charSet}_general_ci` }, { table: "guild", name: "name" }, { name: "level" }, @@ -89,7 +91,7 @@ export class IndexController { const result = await ssp .where("`deleteInfos_Account` IS NULL") - .run(); + .run(this.armory.config.dbQueryTimeout); (result as any).realm = realm.name; res.json(result);