188 lines
5.6 KiB
JavaScript
188 lines
5.6 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const yargs = require('yargs/yargs');
|
|
const { hideBin } = require('yargs/helpers');
|
|
const koffi = require('koffi');
|
|
|
|
// Parse command line arguments
|
|
const argv = yargs(hideBin(process.argv))
|
|
.usage('Usage: $0 [options]')
|
|
.option('verbose', {
|
|
alias: 'v',
|
|
type: 'boolean',
|
|
description: 'Run with verbose logging'
|
|
})
|
|
.option('port', {
|
|
alias: 'p',
|
|
type: 'number',
|
|
description: 'Port to run the server on',
|
|
default: 3001
|
|
})
|
|
.option('host', {
|
|
type: 'string',
|
|
description: 'Host to run the server on',
|
|
default: 'localhost'
|
|
})
|
|
.help()
|
|
.alias('help', 'h')
|
|
.version()
|
|
.argv;
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use(express.static('public'));
|
|
|
|
|
|
// Load Nasal REPL library functions
|
|
let nasalLib;
|
|
try {
|
|
const lib = koffi.load(path.join(__dirname, '../module/libnasal-web.dylib'));
|
|
|
|
nasalLib = {
|
|
nasal_repl_init: lib.func('nasal_repl_init', 'void*', []),
|
|
nasal_repl_cleanup: lib.func('nasal_repl_cleanup', 'void', ['void*']),
|
|
nasal_repl_eval: lib.func('nasal_repl_eval', 'const char*', ['void*', 'const char*']),
|
|
nasal_repl_is_complete: lib.func('nasal_repl_is_complete', 'int', ['void*', 'const char*']),
|
|
nasal_repl_get_version: lib.func('nasal_repl_get_version', 'const char*', [])
|
|
};
|
|
|
|
if (argv.verbose) {
|
|
console.log('REPL Library loaded successfully');
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load REPL library:', err);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Store active REPL sessions
|
|
const replSessions = new Map();
|
|
|
|
// Clean up inactive sessions periodically (30 minutes timeout)
|
|
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [sessionId, session] of replSessions.entries()) {
|
|
if (now - session.lastAccess > SESSION_TIMEOUT) {
|
|
if (argv.verbose) {
|
|
console.log(`Cleaning up inactive session: ${sessionId}`);
|
|
}
|
|
nasalLib.nasal_repl_cleanup(session.context);
|
|
replSessions.delete(sessionId);
|
|
}
|
|
}
|
|
}, 60000); // Check every minute
|
|
|
|
app.post('/repl/init', (req, res) => {
|
|
try {
|
|
const ctx = nasalLib.nasal_repl_init();
|
|
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
const version = nasalLib.nasal_repl_get_version();
|
|
|
|
replSessions.set(sessionId, {
|
|
context: ctx,
|
|
lastAccess: Date.now()
|
|
});
|
|
|
|
if (argv.verbose) {
|
|
console.log(`New REPL session initialized: ${sessionId}`);
|
|
}
|
|
|
|
res.json({
|
|
sessionId,
|
|
version
|
|
});
|
|
} catch (err) {
|
|
if (argv.verbose) {
|
|
console.error('Failed to initialize REPL session:', err);
|
|
}
|
|
res.status(500).json({ error: 'Failed to initialize REPL session' });
|
|
}
|
|
});
|
|
|
|
app.post('/repl/eval', (req, res) => {
|
|
const { sessionId, line, check, buffer } = req.body;
|
|
|
|
if (!sessionId || !replSessions.has(sessionId)) {
|
|
return res.status(400).json({ error: 'Invalid or expired session' });
|
|
}
|
|
|
|
if (!line) {
|
|
return res.status(400).json({ error: 'No code provided' });
|
|
}
|
|
|
|
try {
|
|
const session = replSessions.get(sessionId);
|
|
session.lastAccess = Date.now();
|
|
|
|
if (check) {
|
|
const codeToCheck = buffer ? [...buffer, line].join('\n') : line;
|
|
const isComplete = nasalLib.nasal_repl_is_complete(session.context, codeToCheck);
|
|
|
|
if (isComplete === 1) {
|
|
return res.json({ needsMore: true });
|
|
} else if (isComplete === -1) {
|
|
return res.json({ error: 'Invalid input' });
|
|
}
|
|
}
|
|
|
|
const result = nasalLib.nasal_repl_eval(session.context, line);
|
|
|
|
if (argv.verbose) {
|
|
console.log(`REPL evaluation for session ${sessionId}:`, { line, result });
|
|
}
|
|
|
|
res.json({ result });
|
|
} catch (err) {
|
|
if (argv.verbose) {
|
|
console.error(`REPL evaluation error for session ${sessionId}:`, err);
|
|
}
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
app.post('/repl/cleanup', (req, res) => {
|
|
const { sessionId } = req.body;
|
|
|
|
if (!sessionId || !replSessions.has(sessionId)) {
|
|
return res.status(400).json({ error: 'Invalid session' });
|
|
}
|
|
|
|
try {
|
|
const session = replSessions.get(sessionId);
|
|
nasalLib.nasal_repl_cleanup(session.context);
|
|
replSessions.delete(sessionId);
|
|
|
|
if (argv.verbose) {
|
|
console.log(`REPL session cleaned up: ${sessionId}`);
|
|
}
|
|
|
|
res.json({ message: 'Session cleaned up successfully' });
|
|
} catch (err) {
|
|
if (argv.verbose) {
|
|
console.error(`Failed to cleanup session ${sessionId}:`, err);
|
|
}
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Handle cleanup on server shutdown
|
|
process.on('SIGINT', () => {
|
|
console.log('\nCleaning up REPL sessions before exit...');
|
|
for (const [sessionId, session] of replSessions.entries()) {
|
|
try {
|
|
nasalLib.nasal_repl_cleanup(session.context);
|
|
if (argv.verbose) {
|
|
console.log(`Cleaned up session: ${sessionId}`);
|
|
}
|
|
} catch (err) {
|
|
console.error(`Error cleaning up session ${sessionId}:`, err);
|
|
}
|
|
}
|
|
process.exit(0);
|
|
});
|
|
|
|
const PORT = argv.port || 3001;
|
|
app.listen(PORT, () => {
|
|
console.log(`REPL Server running on http://localhost:${PORT}`);
|
|
if (argv.verbose) console.log('Verbose logging enabled');
|
|
});
|