import express from 'express'; import ffish from 'ffish'; import ChessEngine from './engine.js'; import { getEnginePath, verifyEnginePath } from './engine-path.js'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import os from 'os'; function getLogPath() { if (process.platform === 'linux') { // Use ~/.local/share/ChessBuilder/logs return path.join(os.homedir(), '.local', 'share', 'ChessBuilder', 'logs'); } else if (process.platform === 'win32') { // Use %APPDATA%\ChessBuilder\logs return path.join(os.homedir(), 'AppData', 'Roaming', 'ChessBuilder', 'logs'); } else { // macOS: ~/Library/Logs/ChessBuilder return path.join(os.homedir(), 'Library', 'ChessBuilder', 'logs'); } } // Ensure log directory exists const logDir = getLogPath(); fs.mkdirSync(logDir, { recursive: true }); const logFile = path.join(logDir, 'chess-server.log'); console.log(`Logging to: ${logFile}`); // Create a write stream for logging const logStream = fs.createWriteStream(logFile, { flags: 'a' }); // Redirect console.log and console.error to both console and file const originalConsoleLog = console.log; const originalConsoleError = console.error; console.log = (...args) => { const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg ).join(' ') + '\n'; logStream.write(`[${new Date().toISOString()}] ${message}`); originalConsoleLog.apply(console, args); }; console.error = (...args) => { const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg ).join(' ') + '\n'; logStream.write(`[${new Date().toISOString()}] ERROR: ${message}`); originalConsoleError.apply(console, args); }; try { fs.writeFileSync(logFile, `Server started at ${new Date().toISOString()}\n`, { flag: 'a' }); fs.writeFileSync(logFile, `Process ID: ${process.pid}\n`, { flag: 'a' }); fs.writeFileSync(logFile, `Working directory: ${process.cwd()}\n`, { flag: 'a' }); fs.writeFileSync(logFile, `Node version: ${process.version}\n`, { flag: 'a' }); } catch (error) { console.error('Failed to write startup log:', error); } const app = express(); const port = 27531; let board = null; let engine = null; let isReady = false; let lastResponse = null const SERVER_WAIT_THRESHOLD = 2 * 60 * 1000; const CHECK_INTERVAL = 5000; // Initialize ffish and engine ffish.onRuntimeInitialized = async () => { try { isReady = true; console.log('Fairy-Chess library ready asd'); // Get and verify engine path const enginePath = getEnginePath(); console.log('ENGINE PATH', enginePath); // Initialize Stockfish fs.chmodSync(enginePath, '755'); engine = new ChessEngine(enginePath); await engine.start(); console.log('Chess engine initialized at:', enginePath, engine.isReady); // Set initial engine options if (engine.isReady) { engine.sendCommand('setoption name Threads value 4'); engine.sendCommand('setoption name Hash value 128'); engine.sendCommand('setoption name MultiPV value 1'); engine.sendCommand('setoption name UCI_LimitStrength value true'); engine.sendCommand('uci'); } } catch (error) { console.log('Initialization error:', error); } }; app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@") console.log("@@@@@@@@@@@@@@@@@HEALTH CHECK@@@@@@@@@@@@@@@@@@@@@@") console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@") console.log(JSON.stringify(ffish.variants())) lastResponse = new Date().getTime() res.json({ status: 'ok', engineReady: isReady && engine && engine.isReady, enginePath: getEnginePath(), platform: process.platform, variants: ffish.variants() }); }); // Set Options app.post('/setoptions', (req, res) => { lastResponse = new Date().getTime() const { name, value, options } = req.body; if(name && value !== null){ engine.sendCommand(`setoption name ${name} value ${value}`); res.json({ status: 'ok', success: name, errs: null, }); }else if(options && Array.isArray(options)){ let success = []; let errs = []; for(let x = 0; x < options.length; x++){ let option = options[x]; const {name, value} = option; if(name && value !== null ){ engine.sendCommand(`setoption name ${name} value ${value}`); success.push(name) }else { errs.push(name) } } res.json({ status: 'ok', success: success, errs: errs, }); }else { return res.status(400).json({ error: 'No Params Provided' }); } }); // Validate FEN endpoint app.post('/validate', (req, res) => { lastResponse = new Date().getTime() const { fen, variant = 'chess' } = req.body; if (!fen) { return res.status(400).json({ error: 'FEN string required' }); } try { // Create temporary board to validate FEN const tempBoard = new ffish.Board(variant); const isValid = tempBoard.setFen(fen); tempBoard.delete(); res.json({ status: 'ok', isValid, startingFen: ffish.startingFen(variant) }); } catch (error) { res.status(400).json({ error: 'Invalid FEN or variant' }); } }); // New game endpoint app.post('/new', async (req, res) => { lastResponse = new Date().getTime() const { variant = 'chess' } = req.body; try { if (board) { board.delete(); } board = new ffish.Board(variant); const engineAnalysis = await engine.createNewGame(); res.json({ status: 'ok', fen: board.fen(), legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), turn: board.turn(), moveStack: board.moveStack() }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Set position endpoint app.post('/position', async(req, res) => { lastResponse = new Date().getTime() const { fen, variant = 'chess', start } = req.body; try { //we have a lot of funky rules lets not validate // const isValid = ffish.validateFen(fen) == 1 // if (!isValid) { // return res.status(400).json({ error: 'Invalid FEN string', // fen, // variant, // boardExists: !!board, reqBody: JSON.stringify(req.body) }); // }else { // // if (board) { // // board.delete(); // // } // board.setFen(fen); // } if(start){ await engine.startPos() engine.sendCommand('d'); }else if(fen){ board.setFen(fen); await engine.setBoardPosition(fen) } res.json({ status: 'ok', msg: "Set Position", legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), turn: board.turn(), moveStack: board.moveStack() }); } catch (error) { res.status(500).json({ error: error.message, poserr: true }); } }); // Make move endpoint app.post('/move', (req, res) => { lastResponse = new Date().getTime() const { move, notation = 'uci', variant = 'chess' } = req.body; if (!board) { return res.status(400).json({ error: 'No active board' }); } if (!move) { return res.status(400).json({ error: 'Move required' }); } if (board) { board.delete(); } board = new ffish.Board(variant); try { if (notation === 'san') { board.pushSan(move); } else { board.push(move); } const response = { status: 'ok', fen: board.fen(), legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), turn: board.turn(), moveStack: board.moveStack(), fullmoveNumber: board.fullmoveNumber(), halfmoveClock: board.halfmoveClock() }; if (board.isGameOver()) { response.hasInsufficientMaterial = { white: board.hasInsufficientMaterial(true), // true for white black: board.hasInsufficientMaterial(false) // false for black }; response.gameResult = board.result(); } res.json(response); } catch (error) { res.status(500).json({ error: error.message }); } }); // State endpoint app.get('/state', (req, res) => { lastResponse = new Date().getTime() if (!board) { return res.status(400).json({ error: 'No active board' }); } try { res.json({ status: 'ok', fen: board.fen(), legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), turn: board.turn(), moveStack: board.moveStack(), fullmoveNumber: board.fullmoveNumber(), halfmoveClock: board.halfmoveClock(), variant: board.variant(), is960: board.is960() }); } catch (error) { res.status(500).json({ error: error.message }); } }); /* createNewGame */ app.post('/newgame', async (req, res) => { lastResponse = new Date().getTime() try { const response = { status: 'ok', }; // If engine is available, get engine analysis if (engine && engine.isReady) { const engineAnalysis = await engine.createNewGame(); } res.json(response); } catch (error) { res.status(500).json({ error: error.message }); } }); // Analysis endpoint app.post('/analyze', async (req, res) => { lastResponse = new Date().getTime() if (!board) { return res.status(400).json({ error: 'No active board' }); } try { const { depth = 15, movetime = 1000 } = req.body; // Get basic position analysis const positionAnalysis = { status: 'ok', fen: board.fen(), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), hasInsufficientMaterial: { white: board.hasInsufficientMaterial(true), // true for white black: board.hasInsufficientMaterial(false) // false for black }, legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), moveStack: board.moveStack() }; // If engine is available, get engine analysis if (engine && engine.isReady) { const engineAnalysis = await engine.getAnalysis(board.fen(), { depth, movetime }); positionAnalysis.engineAnalysis = engineAnalysis; } res.json(positionAnalysis); } catch (error) { res.status(500).json({ error: error.message }); } }); // Engine move endpoint app.post('/enginemove', async (req, res) => { lastResponse = new Date().getTime() if (!board) { return res.status(400).json({ error: 'No active board' }); } if (!engine || !engine.isReady) { return res.status(503).json({ error: 'Engine not available', details: 'Chess engine is not initialized or not ready' }); } const { depth = 15, movetime = 1000, nodes = null, fen } = req.body; try { // const fen = board.fen(); const analysis = await engine.getAnalysis(fen, { depth, movetime, nodes }); if (analysis.bestMove) { board.push(analysis.bestMove); } res.json({ status: 'ok', move: analysis.bestMove, analysis: analysis, fen: board.fen(), legalMoves: board.legalMoves().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), turn: board.turn() }); } catch (error) { res.status(500).json({ error: 'Engine analysis failed', details: error.message }); } }); app.post('/shutdown', (req, res) => { lastResponse = new Date().getTime() res.json({ status: 'shutting_down' }); // Give time for response to be sent setTimeout(() => { closeServer() }, 100); }); // Cleanup handling process.on('SIGTERM', () => { console.log('Shutting down...'); closeServer() logStream.end(); }); process.on('SIGINT', () => { console.log('Received SIGINT, shutting down...'); closeServer() logStream.end(); }); app.listen(port, () => { lastResponse = new Date().getTime() startIdleMonitor(); console.log(`Fairy-Chess server running on port ${port}`); }); function startIdleMonitor() { const checkIdle = () => { const currentTime = new Date().getTime(); const timeSinceLastResponse = currentTime - lastResponse; if (timeSinceLastResponse > SERVER_WAIT_THRESHOLD) { console.log(`Server idle for ${timeSinceLastResponse/1000} seconds. Shutting down...`); console.log(currentTime, lastResponse, timeSinceLastResponse, SERVER_WAIT_THRESHOLD) closeServer(); } }; // Start the monitoring interval const monitorInterval = setInterval(checkIdle, CHECK_INTERVAL); // Clean up interval on server close process.on('SIGTERM', () => { clearInterval(monitorInterval); closeServer(); }); process.on('SIGINT', () => { clearInterval(monitorInterval); closeServer(); }); } function closeServer(){ if (board) board.delete(); if (engine) engine.quit(); process.exit(0); }