Back to OScope
oscope/tools/doctor.mjs
Code
65 lines (56 loc) · 3.77 KB
// npm run doctor: checks everything OScope needs and says how to fix what is missing.
import fs from 'node:fs';
import path from 'node:path';
import net from 'node:net';
import { config, ROOT } from '../src/config.mjs';
const ok = s => console.log(` \x1b[32m✓\x1b[0m ${s}`);
const bad = (s, fix) => { console.log(` \x1b[31m✗\x1b[0m ${s}${fix ? `\n fix: ${fix}` : ''}`); problems++; };
const note = s => console.log(` \x1b[2m· ${s}\x1b[0m`);
let problems = 0;
console.log('\nOScope doctor\n');
// 1. Node.js
const [maj, min] = process.versions.node.split('.').map(Number);
if (maj > 18 || (maj === 18 && min >= 17)) ok(`Node.js ${process.versions.node}`);
else bad(`Node.js ${process.versions.node} is too old`, 'install Node.js 20 or newer (LTS) from https://nodejs.org');
// 2. dependencies
for (const p of ['ws', 'lightweight-charts']) {
if (fs.existsSync(path.join(ROOT, 'node_modules', p, 'package.json'))) ok(`${p} installed`);
else bad(`${p} is not installed`, 'run: npm ci');
}
// 3. the port: free, or OScope already running there
const status = await fetch(`http://127.0.0.1:${config.port}/api/status`, { signal: AbortSignal.timeout(2000) }).then(r => r.json()).catch(() => null);
let localFire = false;
if (status && status.feed) {
ok(`OScope is already running on http://localhost:${config.port} (stream ${status.feed.connected ? 'live' : 'not connected'}, ${status.stats?.trades ?? 0} trades so far)`);
localFire = !!status.firehose && status.feed.connected;
} else {
const free = await new Promise(res => { const s = net.createServer().once('error', () => res(false)).once('listening', () => s.close(() => res(true))).listen(config.port, '127.0.0.1'); });
if (free) ok(`port ${config.port} is free`);
else note(`port ${config.port} is used by another program: OScope will take the next free port and print its address`);
}
// 4. the stream
{
const { default: WebSocket } = await import('ws');
const url = localFire ? `ws://127.0.0.1:${config.port}/firehose` : config.feedUrl;
const r = await new Promise(res => {
let n = 0, opened = false;
const ws = new WebSocket(url, { handshakeTimeout: 8000 });
const done = why => { try { ws.terminate(); } catch {} res({ n, opened, why }); };
ws.on('open', () => { opened = true; setTimeout(() => done('time'), 5000); });
ws.on('message', () => n++);
ws.on('close', code => done('closed ' + code));
ws.on('error', e => done(e.message));
});
if (r.n > 0) ok(`stream works: ${r.n} messages in 5 s from ${url}`);
else if (r.opened) bad(`the stream opened but sent nothing (${r.why})`, 'the stream allows only a few connections per internet address: close other programs reading it, or point FEED_URL at a firehose you already run');
else bad(`cannot reach the stream at ${url} (${r.why})`, 'check your internet connection, VPN or firewall (it needs outgoing websockets on port 443)');
}
// 5. SOL price
const px = await fetch('https://api.coinbase.com/v2/prices/SOL-USD/spot', { signal: AbortSignal.timeout(6000) }).then(r => r.json()).then(j => +j.data.amount).catch(() => 0);
if (px > 0) ok(`SOL price: $${px.toFixed(2)}`);
else note('SOL price source did not answer: OScope tries two more, dollar values wait until one answers');
// 6. data folder
try { fs.mkdirSync(config.dataDir, { recursive: true }); fs.accessSync(config.dataDir, fs.constants.W_OK); ok(`data folder ${path.relative(ROOT, config.dataDir) || config.dataDir} is writable`); }
catch { bad(`cannot write ${config.dataDir}`, 'set DATA_DIR in .env to a folder you can write to'); }
console.log(problems ? `\n${problems} problem(s). Fix them and run npm run doctor again.\n` : status ? `\nAll good. OScope is running: open http://localhost:${config.port}\n` : `\nAll good. Run: npm start (then open http://localhost:${config.port})\n`);
process.exit(problems ? 1 : 0);