monitor/core/boot/startReadyWatcher.ts
2025-04-16 22:30:27 +07:00

194 lines
6.5 KiB
TypeScript

import boxen, { type Options as BoxenOptions } from 'boxen';
import chalk from 'chalk';
import open from 'open';
import { shuffle } from 'd3-array';
import { z } from 'zod';
import got from '@lib/got';
import getOsDistro from '@lib/host/getOsDistro.js';
import { txDevEnv, txEnv, txHostConfig } from '@core/globalData';
import consoleFactory from '@lib/console';
import { addLocalIpAddress } from '@lib/host/isIpAddressLocal';
import { chalkInversePad } from '@lib/misc';
const console = consoleFactory();
const getPublicIp = async () => {
const zIpValidator = z.string().ip();
const reqOptions = {
timeout: { request: 2000 },
};
const httpGetter = async (url: string, jsonPath: string) => {
const res = await got(url, reqOptions).json();
return zIpValidator.parse((res as any)[jsonPath]);
};
const allApis = shuffle([
['https://api.ipify.org?format=json', 'ip'],
['https://api.myip.com', 'ip'],
['https://ipv4.jsonip.com/', 'ip'],
['https://api.my-ip.io/v2/ip.json', 'ip'],
['https://www.l2.io/ip.json', 'ip'],
]);
for await (const [url, jsonPath] of allApis) {
try {
return await httpGetter(url, jsonPath);
} catch (error) { }
}
return false;
};
const getOSMessage = async () => {
const serverMessage = [
`To be able to access txAdmin from the internet open port ${txHostConfig.txaPort}`,
'on your OS Firewall as well as in the hosting company.',
];
const winWorkstationMessage = [
'[!] Home-hosting fxserver is not recommended [!]',
'You need to open the fxserver port (usually 30120) on Windows Firewall',
'and set up port forwarding on your router so other players can access it.',
];
if (txEnv.displayAds) {
winWorkstationMessage.push('We recommend renting a server from ' + chalk.inverse(' https://zap-hosting.com/txAdmin ') + '.');
}
//FIXME: use si.osInfo() instead
const distro = await getOsDistro();
return (distro && distro.includes('Linux') || distro.includes('Server'))
? serverMessage
: winWorkstationMessage;
};
const awaitHttp = new Promise((resolve, reject) => {
const tickLimit = 100; //if over 15 seconds
let counter = 0;
let interval: NodeJS.Timeout;
const check = () => {
counter++;
if (txCore.webServer && txCore.webServer.isListening && txCore.webServer.isServing) {
clearInterval(interval);
resolve(true);
} else if (counter == tickLimit) {
clearInterval(interval);
interval = setInterval(check, 2500);
} else if (counter > tickLimit) {
console.warn('The WebServer is taking too long to start:', {
module: !!txCore.webServer,
listening: txCore?.webServer?.isListening,
serving: txCore?.webServer?.isServing,
});
}
};
interval = setInterval(check, 150);
});
const awaitMasterPin = new Promise((resolve, reject) => {
const tickLimit = 100; //if over 15 seconds
let counter = 0;
let interval: NodeJS.Timeout;
const check = () => {
counter++;
if (txCore.adminStore && txCore.adminStore.admins !== null) {
clearInterval(interval);
const pin = (txCore.adminStore.admins === false) ? txCore.adminStore.addMasterPin : false;
resolve(pin);
} else if (counter == tickLimit) {
clearInterval(interval);
interval = setInterval(check, 2500);
} else if (counter > tickLimit) {
console.warn('The AdminStore is taking too long to start:', {
module: !!txCore.adminStore,
admins: txCore?.adminStore?.admins === null ? 'null' : 'not null',
});
}
};
interval = setInterval(check, 150);
});
const awaitDatabase = new Promise((resolve, reject) => {
const tickLimit = 100; //if over 15 seconds
let counter = 0;
let interval: NodeJS.Timeout;
const check = () => {
counter++;
if (txCore.database && txCore.database.isReady) {
clearInterval(interval);
resolve(true);
} else if (counter == tickLimit) {
clearInterval(interval);
interval = setInterval(check, 2500);
} else if (counter > tickLimit) {
console.warn('The Database is taking too long to start:', {
module: !!txCore.database,
ready: !!txCore?.database?.isReady,
});
}
};
interval = setInterval(check, 150);
});
export const startReadyWatcher = async (cb: () => void) => {
const [publicIpResp, msgRes, adminPinRes] = await Promise.allSettled([
getPublicIp(),
getOSMessage(),
awaitMasterPin as Promise<undefined | string | false>,
awaitHttp,
awaitDatabase,
]);
//Addresses
let detectedUrls;
if (txHostConfig.netInterface && txHostConfig.netInterface !== '0.0.0.0') {
detectedUrls = [txHostConfig.netInterface];
} else {
detectedUrls = [
(txEnv.isWindows) ? 'localhost' : 'your-public-ip',
];
if ('value' in publicIpResp && publicIpResp.value) {
detectedUrls.push(publicIpResp.value);
addLocalIpAddress(publicIpResp.value);
}
}
const bannerUrls = txHostConfig.txaUrl
? [txHostConfig.txaUrl]
: detectedUrls.map((addr) => `http://${addr}:${txHostConfig.txaPort}/`);
//Admin PIN
const adminMasterPin = 'value' in adminPinRes && adminPinRes.value ? adminPinRes.value : false;
const adminPinLines = !adminMasterPin ? [] : [
'',
'Use the PIN below to register:',
chalk.inverse(` ${adminMasterPin} `),
];
//Printing stuff
const boxOptions = {
padding: 1,
margin: 1,
align: 'center',
borderStyle: 'bold',
borderColor: 'cyan',
} satisfies BoxenOptions;
const boxLines = [
'All ready! Please access:',
...bannerUrls.map(chalkInversePad),
...adminPinLines,
];
console.multiline(boxen(boxLines.join('\n'), boxOptions), chalk.bgGreen);
if (!txDevEnv.ENABLED && !txHostConfig.netInterface && 'value' in msgRes && msgRes.value) {
console.multiline(msgRes.value, chalk.bgBlue);
}
//Opening page
if (txEnv.isWindows && adminMasterPin && bannerUrls[0]) {
const linkUrl = new URL(bannerUrls[0]);
linkUrl.pathname = '/addMaster/pin';
linkUrl.hash = adminMasterPin;
open(linkUrl.href);
}
//Callback
cb();
};