error handlind, ui status

This commit is contained in:
krk
2026-05-06 09:56:50 +03:00
parent b6e38463e4
commit f9d3f94c68
6 changed files with 155 additions and 134 deletions
+1
View File
@@ -15,3 +15,4 @@ DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your_webhook_url
UPDATE_INTERVAL_SECONDS=300 UPDATE_INTERVAL_SECONDS=300
TRACKER_TOKEN=test TRACKER_TOKEN=test
TRACKER_PORT=12345 TRACKER_PORT=12345
UI_PORT=3000
+2
View File
@@ -3,10 +3,12 @@ import { settings } from './settings';
import { trackerServer } from './tracker/tracker'; import { trackerServer } from './tracker/tracker';
import { kvDataStorage } from './services/kv-data-storage'; import { kvDataStorage } from './services/kv-data-storage';
import { onTrackerAnnounce } from './tracker-announce-flow'; import { onTrackerAnnounce } from './tracker-announce-flow';
import { startUiServer } from './ui-server';
const isWorker = process.argv[2] === 'worker'; const isWorker = process.argv[2] === 'worker';
if (isWorker) { if (isWorker) {
startUiServer(settings.uiPort);
trackerServer.on('announce', onTrackerAnnounce); trackerServer.on('announce', onTrackerAnnounce);
const lastRun = kvDataStorage.get<number>('lastRun') || 0; const lastRun = kvDataStorage.get<number>('lastRun') || 0;
@@ -1,82 +0,0 @@
// @ts-ignore
import TransmissionClient from 'transmission';
import { settings } from '../settings';
type TransmissionOptions = {
url?: string;
host?: string;
port?: number;
ssl?: boolean;
}
export class Transmission {
client: any;
constructor(transmissionOptions: TransmissionOptions) {
this.client = new TransmissionClient(transmissionOptions);
}
async getPort(): Promise<number> {
return new Promise((resolve, reject) => {
this.client.session((err: any, arg: any) => {
if (err) {
reject(err);
}
resolve(arg['peer-port']);
});
});
}
async setPort(port: number) {
return new Promise<void>((resolve, reject) => {
this.client.callServer({ "arguments": { "peer-port": port }, "method": "session-set" }, (err: any, arg: any) => {
if (err) {
reject(err);
}
resolve();
});
});
}
async testPortIfOpen(): Promise<boolean> {
return new Promise((resolve, reject) => {
this.client.callServer({ "method": "port-test" }, (err: any, arg: any) => {
if (err) {
reject(err);
return;
}
if (!('port-is-open' in arg)) {
console.log('port-is-open-arg', arg);
resolve(false);
return;
}
resolve(arg['port-is-open']);
});
});
}
async testPortIfOpen2() {
this.client.callServer({ "method": "port-test" }, (err: any, arg: any) => {
if (err) {
console.error('Error testing port:', err);
return;
}
console.log('Port test result:', arg);
});
}
}
export const transmissionClient = new Transmission({
host: 'localhost',
port: 9091,
});
transmissionClient.getPort().then(port => {
console.log('Current Transmission port:', port);
}).catch(err => {
console.error('Error getting Transmission port:', err);
});
transmissionClient.testPortIfOpen2()
+62 -48
View File
@@ -10,9 +10,9 @@ export async function setPortFlow() {
try { try {
await doSetPortFlow(); await doSetPortFlow();
} catch (e) { } catch (e) {
console.error('Error in set port flow', e); console.error('Unexpected error in set port flow', e);
const reason = e instanceof Error ? e.message : JSON.stringify(e); const reason = e instanceof Error ? e.message : JSON.stringify(e);
await transmissionContainer.downTransmission(`Shutting down: error in set port flow. Reason: ${reason}`); await notificationService.sendNotification(`Unexpected error in set port flow: ${reason}`, true);
} }
await dailyCheckIn(); await dailyCheckIn();
} }
@@ -22,36 +22,43 @@ const checkIfPortOpen = true;
async function doSetPortFlow() { async function doSetPortFlow() {
console.log('Starting set port flow'); console.log('Starting set port flow');
const containerState = await transmissionContainer.getState(); const containerState = await transmissionContainer.getState().catch(() => null);
if (containerState?.state !== 'running') { if (containerState?.state !== 'running') {
console.log('Transmission service is not running'); console.log('Transmission service is not running');
await kvDataStorage.set({ isServiceRunning: false }); await kvDataStorage.set({ isServiceRunning: false });
return; return;
} }
const numRuns = kvDataStorage.get<number>('numRuns') || 0; const numRuns = kvDataStorage.get<number>('numRuns') || 0;
await kvDataStorage.set({ isServiceRunning: true, numRuns: numRuns + 1, lastRun: Date.now() }); await kvDataStorage.set({ isServiceRunning: true, numRuns: numRuns + 1, lastRun: Date.now() });
console.log(`Gathering info`); console.log('Gathering info');
const port = await transmissionClient.getPort();
let port: number;
try {
port = await transmissionClient.getPort();
console.log(`Port: ${port}`); console.log(`Port: ${port}`);
const portOpen = checkIfPortOpen ? await transmissionClient.testPortIfOpen().catch(() => 'error_checking_port') : 'not_checked'; await kvDataStorage.set({ currentPort: port });
} catch (e) {
console.error('Failed to get port from Transmission RPC, skipping cycle', e);
await notificationService.sendNotification('Could not reach Transmission RPC, skipping cycle');
return;
}
const portOpen = checkIfPortOpen
? await transmissionClient.testPortIfOpen().catch(() => 'error_checking_port' as const)
: 'not_checked';
console.log(`Port is open: ${portOpen}`); console.log(`Port is open: ${portOpen}`);
const defaultInterface = await transmissionContainer.getDefaultInterface();
const defaultInterface = await transmissionContainer.getDefaultInterface().catch(() => null);
const ipInfo = await transmissionContainer.getIpInfo().catch(() => null);
console.log(`Default interface: ${defaultInterface?.interface} Internal ip: ${defaultInterface?.ip}`); console.log(`Default interface: ${defaultInterface?.interface} Internal ip: ${defaultInterface?.ip}`);
const ipInfo = await transmissionContainer.getIpInfo();
console.log(`External ip: ${ipInfo?.ip} Country: ${ipInfo?.country}`); console.log(`External ip: ${ipInfo?.ip} Country: ${ipInfo?.country}`);
if (!defaultInterface || !ipInfo) { if (!defaultInterface || !ipInfo) {
await kvDataStorage.set({ console.error(`Could not gather network info, skipping cycle. defaultInterface: ${!!defaultInterface} ipInfo: ${!!ipInfo}`);
country: null, await notificationService.sendNotification(`Could not gather network info (interface: ${!!defaultInterface}, ipInfo: ${!!ipInfo}), skipping cycle`);
internalIp: null, return;
externalIp: null,
interface: null,
})
return await transmissionContainer.downTransmission(
`Shutting down: defaultInterface: ${!!defaultInterface} ipInfo: ${!!ipInfo}`,
);
} }
await kvDataStorage.set({ await kvDataStorage.set({
@@ -59,7 +66,7 @@ async function doSetPortFlow() {
internalIp: defaultInterface.ip, internalIp: defaultInterface.ip,
externalIp: ipInfo.ip, externalIp: ipInfo.ip,
interface: defaultInterface.interface, interface: defaultInterface.interface,
}) });
const countryOK = !settings.disallowedCountries.some((country) => ipInfo.country.includes(country)); const countryOK = !settings.disallowedCountries.some((country) => ipInfo.country.includes(country));
const internalIpOK = !settings.disallowedIntIps.some((ip) => defaultInterface.ip.includes(ip)); const internalIpOK = !settings.disallowedIntIps.some((ip) => defaultInterface.ip.includes(ip));
@@ -79,56 +86,63 @@ async function doSetPortFlow() {
return; return;
} }
console.log(`Port ${port} is not open or not checked (status ${portOpen}). Fetching new port.`); console.log(`Port ${port} is not open or not checked (status: ${portOpen}). Fetching new port.`);
//fetch new port
const request = await fetch(`https://connect.pvdatanet.com/v3/Api/port?ip[]=${defaultInterface.ip}`)
const data = await request.json() as {status: string, supported: boolean}
let data: { status: string; supported: boolean };
try {
const request = await fetch(`https://connect.pvdatanet.com/v3/Api/port?ip[]=${defaultInterface.ip}`);
data = await request.json() as { status: string; supported: boolean };
console.log(`Data received: ${JSON.stringify(data)}`); console.log(`Data received: ${JSON.stringify(data)}`);
} catch (e) {
if (!data.status || !data.supported) { console.error('Failed to fetch new port from PrivateVPN API, skipping cycle', e);
return await transmissionContainer.downTransmission( await notificationService.sendNotification('Could not fetch new port from PrivateVPN API, skipping cycle');
`Shutting down: could not fetch new port, data: ${JSON.stringify(data)}`, return;
);
} }
const newPort= Number(data.status.split(" ")[1]); if (!data.status || !data.supported) {
console.error(`PrivateVPN API returned unexpected data, skipping cycle: ${JSON.stringify(data)}`);
await notificationService.sendNotification(`PrivateVPN API returned unexpected data (${JSON.stringify(data)}), skipping cycle`);
return;
}
const newPort = Number(data.status.split(' ')[1]);
if (!newPort) { if (!newPort) {
return await transmissionContainer.downTransmission( console.error(`Could not parse new port, skipping cycle: ${JSON.stringify(data)}`);
`Shutting down: could not parse new port, data: ${JSON.stringify(data)}`, await notificationService.sendNotification(`Could not parse new port from PrivateVPN API (${JSON.stringify(data)}), skipping cycle`);
); return;
} }
if (newPort === port) { if (newPort === port) {
console.log(`New port ${newPort} is the same as the old port ${port}`); console.log(`New port ${newPort} is the same as the old port`);
return; return;
} }
console.log(`Setting port to ${newPort}`); console.log(`Setting port to ${newPort}`);
const resultOK = await gluetunContainer.setOpenFirewallPort(newPort); const firewallOK = await gluetunContainer.setOpenFirewallPort(newPort);
// TODO - remove old port from firewall? if (!firewallOK) {
if (!resultOK) { console.error('Could not open new port in Gluetun firewall, skipping cycle');
return await transmissionContainer.downTransmission( await notificationService.sendNotification('Could not open new port in Gluetun firewall, skipping cycle');
`Shutting down: could not open new port in gluetun firewall`, return;
);
} }
console.log(`Port in gluetun firewall opened successfully`); console.log('Port in Gluetun firewall opened successfully');
try {
await transmissionClient.setPort(newPort); await transmissionClient.setPort(newPort);
await kvDataStorage.set({ currentPort: newPort });
} catch (e) {
console.error('Failed to set port in Transmission, skipping cycle', e);
await notificationService.sendNotification('Could not set new port in Transmission RPC, skipping cycle');
return;
}
const numPortsChanged = kvDataStorage.get<number>('numPortsChanged') || 0; const numPortsChanged = kvDataStorage.get<number>('numPortsChanged') || 0;
await kvDataStorage.set({ numPortsChanged: numPortsChanged + 1 }); await kvDataStorage.set({ numPortsChanged: numPortsChanged + 1 });
console.log('Port set'); console.log('Port set');
const newPortOpen = checkIfPortOpen ? await transmissionClient.testPortIfOpen().catch(() => 'error_checking_port') : 'not_checked'; const newPortOpen = checkIfPortOpen
? await transmissionClient.testPortIfOpen().catch(() => 'error_checking_port' as const)
: 'not_checked';
console.log(`Port ${newPort} is open: ${newPortOpen}`); console.log(`Port ${newPort} is open: ${newPortOpen}`);
// if (!newPortOpen) {
// return await transmissionContainer.downTransmission(
// `Shutting down: new port is still not open, data: ${JSON.stringify(data)}`,
// );
// }
await notificationService.sendNotification(`Port changed from ${port} to ${newPort} successfully. New port open: ${newPortOpen}.`); await notificationService.sendNotification(`Port changed from ${port} to ${newPort} successfully. New port open: ${newPortOpen}.`);
} }
+2 -1
View File
@@ -23,7 +23,8 @@ export const settings = {
disallowedIntIps: process.env.DISALLOWED_INTERNAL_IPS?.split(',') ?? [], disallowedIntIps: process.env.DISALLOWED_INTERNAL_IPS?.split(',') ?? [],
disallowedExtIps: process.env.DISALLOWED_EXT_IPS?.split(',') ?? [], disallowedExtIps: process.env.DISALLOWED_EXT_IPS?.split(',') ?? [],
discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL, discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL,
updateIntervalMs: (Number(process.env.UPDATE_INTERVAL_SECONDS) ?? 60 * 5) * 1000, updateIntervalMs: (Number(process.env.UPDATE_INTERVAL_SECONDS) || 60 * 5) * 1000,
trackerToken: process.env.TRACKER_TOKEN, trackerToken: process.env.TRACKER_TOKEN,
trackerPort: Number(process.env.TRACKER_PORT), trackerPort: Number(process.env.TRACKER_PORT),
uiPort: Number(process.env.UI_PORT) || 3000,
} as const; } as const;
+85
View File
@@ -0,0 +1,85 @@
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
import { kvDataStorage } from './services/kv-data-storage';
import { transmissionContainer } from './services/transmission-container';
function formatDate(ts: number | null): string {
if (!ts) return 'never';
return new Date(ts).toLocaleString('lt-LT');
}
function row(label: string, value: string | number | null | undefined): string {
return `<tr><td>${label}</td><td>${value ?? '—'}</td></tr>`;
}
function renderHtml(): string {
const isRunning = kvDataStorage.get<boolean>('isServiceRunning');
const statusColor = isRunning ? '#4caf50' : '#f44336';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="30">
<title>pvpn-transmission</title>
<style>
body { font-family: monospace; background: #1a1a1a; color: #e0e0e0; padding: 2rem; max-width: 560px; margin: 0 auto; }
h1 { font-size: 1.1rem; color: #888; margin-bottom: 1.5rem; }
.badge { display: inline-block; padding: 0.2rem 0.7rem; border-radius: 3px; background: ${statusColor}; color: #fff; font-weight: bold; margin-bottom: 1.5rem; }
table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; }
td { padding: 0.35rem 0.5rem; border-bottom: 1px solid #2a2a2a; }
td:first-child { color: #888; width: 45%; }
button { padding: 0.5rem 1.2rem; background: #1976d2; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-family: monospace; font-size: 0.95rem; }
button:hover { background: #1565c0; }
.hint { color: #555; font-size: 0.8rem; margin-top: 1.5rem; }
</style>
</head>
<body>
<h1>pvpn-transmission</h1>
<div class="badge">${isRunning ? 'Running' : 'Stopped'}</div>
<table>
${row('External IP', kvDataStorage.get<string>('externalIp'))}
${row('Internal IP', kvDataStorage.get<string>('internalIp'))}
${row('Country', kvDataStorage.get<string>('country'))}
${row('Interface', kvDataStorage.get<string>('interface'))}
${row('Current port', kvDataStorage.get<number>('currentPort'))}
${row('Last run', formatDate(kvDataStorage.get<number>('lastRun')))}
${row('Last check-in', formatDate(kvDataStorage.get<number>('lastCheckIn')))}
${row('Ports changed', kvDataStorage.get<number>('numPortsChanged') ?? 0)}
${row('Checks run', kvDataStorage.get<number>('numRuns') ?? 0)}
${row('IP mismatches', kvDataStorage.get<number>('ipMismatchCount') ?? 0)}
</table>
${!isRunning ? `<form method="POST" action="/start"><button type="submit">Start Transmission</button></form>` : ''}
<p class="hint">Auto-refreshes every 30s</p>
</body>
</html>`;
}
async function handleRequest(req: IncomingMessage, res: ServerResponse) {
if (req.method === 'GET' && req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(renderHtml());
return;
}
if (req.method === 'POST' && req.url === '/start') {
console.log('UI: starting Transmission container');
try {
await transmissionContainer.up();
await kvDataStorage.set({ isServiceRunning: true });
} catch (e) {
console.error('UI: failed to start Transmission', e);
}
res.writeHead(302, { Location: '/' });
res.end();
return;
}
res.writeHead(404);
res.end();
}
export function startUiServer(port: number) {
createServer(handleRequest).listen(port, () => {
console.log(`UI listening on http://localhost:${port}`);
});
}