You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

290 lines
11 KiB

import { serve } from 'bun';
import { Database } from 'bun:sqlite';
import { EventEmitter } from 'events';
const db = new Database('./data/servers.db');
// Initialize the database table if it doesn't exist
db.run(`
CREATE TABLE IF NOT EXISTS servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
hostname TEXT NOT NULL,
port INTEGER NOT NULL,
ping_rate INTEGER NOT NULL,
status TEXT DEFAULT 'unknown',
monitoring INTEGER DEFAULT 1
)
`);
// In-memory state to store server details
const serverStates: {
[id: number]: {
id: number;
name: string;
hostname: string;
port: number;
ping_rate: number;
status: 'up' | 'down' | 'unknown';
monitoring: number;
}
} = {};
// Event emitter to handle server updates
const serverMonitor = new EventEmitter();
const clients: Set<ReadableStreamDefaultController> = new Set()
class ServerState {
id: number | undefined;
name: string | undefined;
hostname: string | undefined;
port: number | undefined;
ping_rate: number | undefined;
status: 'up' | 'down' | 'unknown' | undefined;
monitoring: number | undefined;
}
// Function to initialize server states from the database
async function initializeServerStates() {
const servers = db.query('SELECT * FROM servers').as(ServerState).all();
servers.forEach((server: ServerState) => {
if (server.id && server.name && server.hostname && server.port && server.ping_rate && server.status) {
serverStates[server.id] = {
id: server.id,
name: server.name,
hostname: server.hostname,
port: server.port,
ping_rate: server.ping_rate,
status: server.status,
monitoring: server.monitoring || 0
};
}
});
}
// Start monitoring for a specific server by its ID
async function monitorServer(serverId: number) {
if (!serverStates[serverId]) return
while (true) {
if (serverStates[serverId]) {
await checkServer(serverId);
await new Promise(resolve => setTimeout(resolve, serverStates[serverId].ping_rate * 1000));
} else {
console.log("Stopped monitoring ", serverId)
return false
}
}
}
// Function to check the server availability
async function checkServer(serverId: number) {
const server = serverStates[serverId];
if (!server) return;
if (server.monitoring === 0) return
try {
const checkSocket = await Bun.connect({
hostname: server.hostname,
port: server.port,
socket: {
data(socket, data) { },
open(socket) {
console.log(`[${new Date().toISOString()}] ${server.name} (${server.hostname}:${server.port}) is up.`);
if (serverStates[serverId].status !== 'up') {
db.run('UPDATE servers SET status = ? WHERE id = ?', ['up', serverId]);
serverStates[serverId].status = 'up';
notifyClients()
}
socket.end();
},
connectError(socket, error) {
console.error(`[${new Date().toISOString()}] ${server.name} (${server.hostname}:${server.port}) is down!`);
if (serverStates[serverId].status !== 'down') {
db.run('UPDATE servers SET status = ? WHERE id = ?', ['down', serverId]);
serverStates[serverId].status = 'down';
notifyClients()
}
},
error(socket, error) {
console.error(`[${new Date().toISOString()}] Error connecting to ${server.name} (${server.hostname}:${server.port}):`, error);
if (serverStates[serverId].status !== 'down') {
db.run('UPDATE servers SET status = ? WHERE id = ?', ['down', serverId]);
serverStates[serverId].status = 'down';
notifyClients()
}
},
timeout(socket) {
console.error(`[${new Date().toISOString()}] Connection to ${server.name} (${server.hostname}:${server.port}) timed out!`);
if (serverStates[serverId].status !== 'down') {
db.run('UPDATE servers SET status = ? WHERE id = ?', ['down', serverId]);
serverStates[serverId].status = 'down';
notifyClients()
}
}
}
})
} catch (error) {
console.error(`[${new Date().toISOString()}] Unexpected error checking ${server.name} (${server.hostname}:${server.port}):`, error);
if (serverStates[serverId].status !== 'down') {
db.run('UPDATE servers SET status = ? WHERE id = ?', ['down', serverId]);
serverStates[serverId].status = 'down';
}
}
}
// Notify all connected clients of server state changes
function notifyClients() {
const data = JSON.stringify(Object.values(serverStates));
clients.forEach(client => client.enqueue(`data: ${data}\n\n`));
}
// Start monitoring all servers from the initial state
async function startMonitoring() {
Object.keys(serverStates).forEach(serverId => {
monitorServer(parseInt(serverId, 10));
})
console.log('Monitoring started for existing servers.');
}
// Initialize states and start monitoring
initializeServerStates().then(startMonitoring);
// Event listener for adding new servers
serverMonitor.on('newServer', (serverId) => {
if (!serverStates[serverId]) return;
monitorServer(serverId);
});
// API Server to manage servers
const server = serve({
port: process.env.MONITOR_PORT || 1234,
fetch: async (req) => {
const url = new URL(req.url);
const pathname = url.pathname;
const method = req.method;
if (pathname === '/watch' && method === 'GET') {
const headers = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
};
const client = new Response(
new ReadableStream({
start(controller) {
clients.add(controller);
controller.enqueue(`data: ${JSON.stringify(Object.values(serverStates))}\n\n`)
},
cancel(controller) {
clients.delete(controller);
}
}),
{ headers }
);
return client;
}
if (pathname === '/servers' && method === 'GET') {
return new Response(JSON.stringify(Object.values(serverStates)), {
headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
status: 200
});
}
if (pathname === '/server' && method === 'POST') {
const data = await req.json();
const { name, hostname, port, ping_rate } = data;
if (!name || !hostname || !port || !ping_rate) {
return new Response('Missing fields', { status: 400 });
}
db.run('INSERT INTO servers (name, hostname, port, ping_rate) VALUES (?, ?, ?, ?)', name, hostname, port, ping_rate);
const newServer = db.query('SELECT * FROM servers WHERE id = last_insert_rowid()').as(ServerState).get();
if (newServer && newServer.id && newServer.name && newServer.hostname && newServer.port && newServer.ping_rate && newServer.status && newServer.monitoring) {
serverStates[newServer.id] = {
id: newServer.id,
name: newServer.name,
hostname: newServer.hostname,
port: newServer.port,
ping_rate: newServer.ping_rate,
status: newServer.status || 'unknown',
monitoring: newServer.monitoring
};
}
if (newServer && newServer.id) {
serverMonitor.emit('newServer', newServer.id);
return new Response('Server added', { status: 201, headers: { 'Access-Control-Allow-Origin': '*' } });
}
}
if (pathname.startsWith('/server/') && method === 'PUT') {
const { searchParams } = new URL(req.url)
const id = pathname.split('/').pop()
const name = searchParams.get('name')
const hostname = searchParams.get('hostname')
const port = searchParams.get('port')
const ping_rate = searchParams.get('ping_rate')
const status = searchParams.get('status')
const monitoring = searchParams.get('monitoring')
if (!id) {
return new Response('Invalid ID', { status: 400, headers: { 'Access-Control-Allow-Origin': '*' } });
}
// Fetch the current state of the server
const currentServer = serverStates[parseInt(id)];
if (!currentServer) {
return new Response('Server not found', { status: 404, headers: { 'Access-Control-Allow-Origin': '*' } });
}
// Prepare the updated server data
const updatedServer: any = {
name: name || currentServer.name,
hostname: hostname || currentServer.hostname,
port: port || currentServer.port,
ping_rate: ping_rate || currentServer.ping_rate,
status: status || currentServer.status,
monitoring: monitoring || currentServer.monitoring
};
// Update the database
db.run(
`UPDATE servers SET name = ?, hostname = ?, port = ?, ping_rate = ?, status = ?, monitoring = ? WHERE id = ?`,
updatedServer.name, updatedServer.hostname, updatedServer.port, updatedServer.ping_rate, updatedServer.status, updatedServer.monitoring, id
);
// Update the in-memory state
serverStates[parseInt(id)] = {
...serverStates[parseInt(id)], // Retain existing fields
...updatedServer // Apply updates
};
return new Response('Server updated', { status: 200, headers: { 'Access-Control-Allow-Origin': '*' } });
}
if (pathname.startsWith('/server/') && method === 'DELETE') {
const id = pathname.split('/').pop();
if (id) {
db.run('DELETE FROM servers WHERE id = ?', [parseInt(id)])
delete serverStates[parseInt(id)]
return new Response('Server deleted', { status: 200, headers: { 'Access-Control-Allow-Origin': '*' } })
}
}
return new Response('Not Found', { status: 404, headers: { 'Access-Control-Allow-Origin': '*' } });
}
});
console.log(`API server and monitoring running on http://localhost:${server.port}`);