forked from noxious/server
Renamed command manager to console manager, improved log reading
This commit is contained in:
parent
0b99d4098e
commit
4f1b9cf024
59
src/application/console/commandRegistry.ts
Normal file
59
src/application/console/commandRegistry.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import Logger, { LoggerType } from '#application/logger'
|
||||||
|
import { getAppPath } from '#application/storage'
|
||||||
|
import { Command } from '#application/types'
|
||||||
|
|
||||||
|
export class CommandRegistry {
|
||||||
|
private readonly commands: Map<string, Command> = new Map()
|
||||||
|
private readonly logger = Logger.type(LoggerType.COMMAND)
|
||||||
|
|
||||||
|
public getCommand(name: string): Command | undefined {
|
||||||
|
return this.commands.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadCommands(): Promise<void> {
|
||||||
|
const directory = getAppPath('commands')
|
||||||
|
this.logger.info(`Loading commands from: ${directory}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await fs.promises.readdir(directory, { withFileTypes: true })
|
||||||
|
await Promise.all(
|
||||||
|
files
|
||||||
|
.filter(file => this.isValidCommandFile(file))
|
||||||
|
.map(file => this.loadCommandFile(file))
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to read commands directory: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidCommandFile(file: fs.Dirent): boolean {
|
||||||
|
return file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.js'))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadCommandFile(file: fs.Dirent): Promise<void> {
|
||||||
|
const fullPath = getAppPath('commands', file.name)
|
||||||
|
const commandName = path.basename(file.name, path.extname(file.name))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const module = await import(fullPath)
|
||||||
|
if (typeof module.default !== 'function') {
|
||||||
|
this.logger.warn(`Unrecognized export in ${file.name}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerCommand(commandName, module.default)
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error loading command ${file.name}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerCommand(name: string, CommandClass: Command): void {
|
||||||
|
if (this.commands.has(name)) {
|
||||||
|
this.logger.warn(`Command '${name}' is already registered. Overwriting...`)
|
||||||
|
}
|
||||||
|
this.commands.set(name, CommandClass)
|
||||||
|
this.logger.info(`Registered command: ${name}`)
|
||||||
|
}
|
||||||
|
}
|
33
src/application/console/consolePrompt.ts
Normal file
33
src/application/console/consolePrompt.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import * as readline from 'readline'
|
||||||
|
|
||||||
|
export class ConsolePrompt {
|
||||||
|
private readonly rl: readline.Interface
|
||||||
|
private isClosed: boolean = false
|
||||||
|
|
||||||
|
constructor(private readonly commandHandler: (command: string) => void) {
|
||||||
|
this.rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
})
|
||||||
|
|
||||||
|
this.rl.on('close', () => {
|
||||||
|
this.isClosed = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(): void {
|
||||||
|
if (this.isClosed) return
|
||||||
|
this.promptCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.rl.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptCommand(): void {
|
||||||
|
this.rl.question('> ', (command: string) => {
|
||||||
|
this.commandHandler(command)
|
||||||
|
this.promptCommand()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
76
src/application/console/logReader.ts
Normal file
76
src/application/console/logReader.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import Logger, { LoggerType } from '#application/logger'
|
||||||
|
|
||||||
|
export class LogReader {
|
||||||
|
private logger = Logger.type(LoggerType.CONSOLE)
|
||||||
|
private watchers: fs.FSWatcher[] = []
|
||||||
|
private readonly logsDirectory: string
|
||||||
|
|
||||||
|
constructor(rootPath: string) {
|
||||||
|
this.logsDirectory = path.join(rootPath, 'logs')
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(): void {
|
||||||
|
this.logger.info('Starting log reader...')
|
||||||
|
this.watchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
this.watchers.forEach(watcher => watcher.close())
|
||||||
|
this.watchers = []
|
||||||
|
}
|
||||||
|
|
||||||
|
private watchLogs(): void {
|
||||||
|
// Watch directory for new files
|
||||||
|
const directoryWatcher = fs.watch(this.logsDirectory, (_, filename) => {
|
||||||
|
if (filename?.endsWith('.log')) {
|
||||||
|
this.watchLogFile(filename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.watchers.push(directoryWatcher)
|
||||||
|
|
||||||
|
// Watch existing files
|
||||||
|
try {
|
||||||
|
fs.readdirSync(this.logsDirectory)
|
||||||
|
.filter(file => file.endsWith('.log'))
|
||||||
|
.forEach(file => this.watchLogFile(file))
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error reading logs directory: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private watchLogFile(filename: string): void {
|
||||||
|
const filePath = path.join(this.logsDirectory, filename)
|
||||||
|
let currentPosition = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0
|
||||||
|
|
||||||
|
const watcher = fs.watch(filePath, () => {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath)
|
||||||
|
const newPosition = stat.size
|
||||||
|
|
||||||
|
if (newPosition < currentPosition) {
|
||||||
|
currentPosition = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPosition > currentPosition) {
|
||||||
|
const stream = fs.createReadStream(filePath, {
|
||||||
|
start: currentPosition,
|
||||||
|
end: newPosition
|
||||||
|
})
|
||||||
|
|
||||||
|
stream.on('data', (data) => {
|
||||||
|
process.stdout.write('\r' + `[${filename}]\n${data}`)
|
||||||
|
process.stdout.write('\n> ')
|
||||||
|
})
|
||||||
|
|
||||||
|
currentPosition = newPosition
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
watcher.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.watchers.push(watcher)
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,8 @@ export enum LoggerType {
|
|||||||
QUEUE = 'queue',
|
QUEUE = 'queue',
|
||||||
COMMAND = 'command',
|
COMMAND = 'command',
|
||||||
REPOSITORY = 'repository',
|
REPOSITORY = 'repository',
|
||||||
ENTITY = 'entity'
|
ENTITY = 'entity',
|
||||||
|
CONSOLE = 'console'
|
||||||
}
|
}
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Socket } from 'socket.io'
|
import { Server, Socket } from 'socket.io'
|
||||||
|
|
||||||
import { Character } from '#entities/character'
|
import { Character } from '#entities/character'
|
||||||
import { ZoneEventTile } from '#entities/zoneEventTile'
|
import { ZoneEventTile } from '#entities/zoneEventTile'
|
||||||
@ -51,6 +51,12 @@ export type WorldSettings = {
|
|||||||
fogDensity: number
|
fogDensity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Command {
|
||||||
|
new (io: Server): {
|
||||||
|
execute(args: string[]): Promise<void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// export type TCharacter = Socket & {
|
// export type TCharacter = Socket & {
|
||||||
// user?: User
|
// user?: User
|
||||||
// character?: Character
|
// character?: Character
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
import * as fs from 'fs'
|
|
||||||
import * as path from 'path'
|
|
||||||
import * as readline from 'readline'
|
|
||||||
|
|
||||||
import { Server } from 'socket.io'
|
|
||||||
|
|
||||||
import Logger, { LoggerType } from '#application/logger'
|
|
||||||
import { getAppPath } from '#application/storage'
|
|
||||||
|
|
||||||
class CommandManager {
|
|
||||||
private logger = Logger.type(LoggerType.COMMAND)
|
|
||||||
private commands: Map<string, any> = new Map()
|
|
||||||
private rl: readline.Interface
|
|
||||||
private io: Server | null = null
|
|
||||||
private rlClosed: boolean = false
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
})
|
|
||||||
|
|
||||||
this.rl.on('close', () => {
|
|
||||||
this.rlClosed = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async boot(io: Server) {
|
|
||||||
this.io = io
|
|
||||||
await this.loadCommands()
|
|
||||||
this.logger.info('Command manager loaded')
|
|
||||||
this.startPrompt()
|
|
||||||
}
|
|
||||||
|
|
||||||
private startPrompt() {
|
|
||||||
if (this.rlClosed) return
|
|
||||||
|
|
||||||
this.rl.question('> ', (command: string) => {
|
|
||||||
this.processCommand(command)
|
|
||||||
this.startPrompt()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processCommand(command: string): Promise<void> {
|
|
||||||
const [cmd, ...args] = command.trim().split(' ')
|
|
||||||
if (this.commands.has(cmd)) {
|
|
||||||
const CommandClass = this.commands.get(cmd)
|
|
||||||
const commandInstance = new CommandClass(this.io as Server)
|
|
||||||
await commandInstance.execute(args)
|
|
||||||
} else {
|
|
||||||
this.handleUnknownCommand(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleUnknownCommand(command: string) {
|
|
||||||
switch (command) {
|
|
||||||
case 'exit':
|
|
||||||
this.rl.close()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.error(`Unknown command: ${command}`)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadCommands() {
|
|
||||||
const directory = getAppPath('commands')
|
|
||||||
this.logger.info(`Loading commands from: ${directory}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const files = await fs.promises.readdir(directory, { withFileTypes: true })
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (!file.isFile() || (!file.name.endsWith('.ts') && !file.name.endsWith('.js'))) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = getAppPath('commands', file.name)
|
|
||||||
const commandName = path.basename(file.name, path.extname(file.name))
|
|
||||||
|
|
||||||
try {
|
|
||||||
const module = await import(fullPath)
|
|
||||||
if (typeof module.default !== 'function') {
|
|
||||||
this.logger.warn(`Unrecognized export in ${file.name}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
this.registerCommand(commandName, module.default)
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error loading command ${file.name}: ${error instanceof Error ? error.message : String(error)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to read commands directory: ${error instanceof Error ? error.message : String(error)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerCommand(name: string, CommandClass: any) {
|
|
||||||
if (this.commands.has(name)) {
|
|
||||||
this.logger.warn(`Command '${name}' is already registered. Overwriting...`)
|
|
||||||
}
|
|
||||||
this.commands.set(name, CommandClass)
|
|
||||||
this.logger.info(`Registered command: ${name}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new CommandManager()
|
|
56
src/managers/consoleManager.ts
Normal file
56
src/managers/consoleManager.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { CommandRegistry } from '#application/console/commandRegistry'
|
||||||
|
import { ConsolePrompt } from '#application/console/consolePrompt'
|
||||||
|
import { LogReader } from '#application/console/logReader'
|
||||||
|
import Logger, { LoggerType } from '#application/logger'
|
||||||
|
|
||||||
|
export class ConsoleManager {
|
||||||
|
private readonly logger = Logger.type(LoggerType.COMMAND)
|
||||||
|
private readonly registry: CommandRegistry
|
||||||
|
private readonly prompt: ConsolePrompt
|
||||||
|
private readonly logReader: LogReader
|
||||||
|
private io: Server | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.registry = new CommandRegistry()
|
||||||
|
this.prompt = new ConsolePrompt(
|
||||||
|
(command: string) => this.processCommand(command)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.logReader = new LogReader(process.cwd())
|
||||||
|
}
|
||||||
|
|
||||||
|
public async boot(io: Server): Promise<void> {
|
||||||
|
this.io = io
|
||||||
|
|
||||||
|
await this.registry.loadCommands()
|
||||||
|
this.logReader.start()
|
||||||
|
this.prompt.start()
|
||||||
|
|
||||||
|
this.logger.info('Console manager loaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processCommand(commandLine: string): Promise<void> {
|
||||||
|
const [cmd, ...args] = commandLine.trim().split(' ')
|
||||||
|
|
||||||
|
if (cmd === 'exit') {
|
||||||
|
this.prompt.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandClass = this.registry.getCommand(cmd)
|
||||||
|
if (!CommandClass) {
|
||||||
|
console.error(`Unknown command: ${cmd}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commandInstance = new CommandClass(this.io as Server)
|
||||||
|
await commandInstance.execute(args)
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error executing command ${cmd}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ConsoleManager()
|
@ -11,7 +11,7 @@ import Logger, { LoggerType } from '#application/logger'
|
|||||||
import { getAppPath } from '#application/storage'
|
import { getAppPath } from '#application/storage'
|
||||||
import { TSocket } from '#application/types'
|
import { TSocket } from '#application/types'
|
||||||
import { HttpRouter } from '#http/router'
|
import { HttpRouter } from '#http/router'
|
||||||
import CommandManager from '#managers/commandManager'
|
import ConsoleManager from '#managers/consoleManager'
|
||||||
import DateManager from '#managers/dateManager'
|
import DateManager from '#managers/dateManager'
|
||||||
import QueueManager from '#managers/queueManager'
|
import QueueManager from '#managers/queueManager'
|
||||||
import UserManager from '#managers/userManager'
|
import UserManager from '#managers/userManager'
|
||||||
@ -49,8 +49,6 @@ export class Server {
|
|||||||
* Start the server
|
* Start the server
|
||||||
*/
|
*/
|
||||||
public async start() {
|
public async start() {
|
||||||
// Read log file and print to console for debugging
|
|
||||||
|
|
||||||
// Connect to database
|
// Connect to database
|
||||||
try {
|
try {
|
||||||
await Database.initialize()
|
await Database.initialize()
|
||||||
@ -86,8 +84,8 @@ export class Server {
|
|||||||
// Load zoneEditor manager
|
// Load zoneEditor manager
|
||||||
await ZoneManager.boot()
|
await ZoneManager.boot()
|
||||||
|
|
||||||
// Load command manager
|
// Load console manager
|
||||||
await CommandManager.boot(this.io)
|
await ConsoleManager.boot(this.io)
|
||||||
|
|
||||||
// Listen for socket connections
|
// Listen for socket connections
|
||||||
this.io.on('connection', this.handleConnection.bind(this))
|
this.io.on('connection', this.handleConnection.bind(this))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user