refactor: enhance ping and tcp monitoring logic, improve error handling, and introduce shared types

This commit is contained in:
Raj Nandan Sharma
2026-02-13 12:02:13 +05:30
parent 09afbbd17d
commit 552bdc09ad
9 changed files with 249 additions and 53 deletions
+51 -3
View File
@@ -1,4 +1,5 @@
import net from "net"; // Use import instead of require
import dns from "node:dns/promises";
import ping from "ping";
interface TCPResult {
@@ -59,6 +60,30 @@ interface PingResult {
type: string;
}
function ToLatencyNumber(value: string | number | null | undefined): number {
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
if (typeof value === "string") {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
return 0;
}
function IsSpawnRestrictionError(error: unknown): error is NodeJS.ErrnoException {
if (!(error instanceof Error)) return false;
const err = error as NodeJS.ErrnoException;
const message = (err.message || "").toLowerCase();
const code = err.code || "";
return (
err.syscall === "spawn" ||
code === "EPERM" ||
code === "EACCES" ||
code === "ENOENT" ||
message.includes("spawn eperm") ||
message.includes("operation not permitted")
);
}
const Ping = async function (type: string, host: string, timeout: number, count: number): Promise<PingResult> {
let output: PingResult = {
alive: false,
@@ -82,9 +107,32 @@ const Ping = async function (type: string, host: string, timeout: number, count:
output.max = res.max;
output.avg = res.avg;
output.latencies = (res as unknown as { times?: string[] }).times ?? []; //sv5-verify
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
output.latency = ToLatencyNumber(res.time as string | number | null | undefined);
} catch (error: unknown) {
if (IsSpawnRestrictionError(error)) {
const start = process.hrtime.bigint();
try {
await dns.lookup(host, { family: type === "IP6" ? 6 : 4 });
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6;
const latencyStr = latency.toFixed(3);
output.alive = true;
output.min = latencyStr;
output.max = latencyStr;
output.avg = latencyStr;
output.latencies = [latencyStr];
output.latency = latency;
console.warn(
`[Ping] ICMP unavailable for ${host} (${(error as Error).message}). Falling back to DNS lookup reachability.`,
);
} catch (lookupError) {
console.log(`Error in ping fallback DNS for ${host}`, lookupError);
}
} else {
console.log(`Error in pingCall ${type} for ${host}`, error);
}
}
return output;
};
+12 -6
View File
@@ -15,22 +15,25 @@ class PingCall {
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : DefaultPingEval;
let tag = this.monitor.tag;
if (hosts === undefined) {
console.log(
"Hosts is undefined. The ping monitor has changed in version 3.0.10. Please update your monitor with tag",
tag,
);
return {
status: GC.DOWN,
latency: 0,
type: GC.ERROR,
error_message:
"Hosts is undefined. The ping monitor has changed in version 3.0.10. Please update your monitor with tag " +
tag,
};
}
let arrayOfPings = [];
let errorMessages: string[] = [];
for (let i = 0; i < hosts.length; i++) {
const host = hosts[i];
arrayOfPings.push(await Ping(host.type, host.host, host.timeout, host.count));
const result = await Ping(host.type, host.host, host.timeout, host.count);
if (!result.alive) {
errorMessages.push(`Host ${host.host} is unreachable.`);
}
arrayOfPings.push(result);
}
let evalResp: EvalResponse | undefined = undefined;
try {
@@ -42,13 +45,16 @@ class PingCall {
status: GC.DOWN,
latency: 0,
type: GC.ERROR,
error_message: `Error in pingEval: ${(error as Error).message}`,
};
}
//reduce to get the status
return {
status: evalResp?.status || GC.DOWN,
latency: evalResp?.latency || 0,
type: GC.REALTIME,
error_message: errorMessages.length > 0 ? errorMessages.join("; ") : undefined,
};
}
}
+24 -3
View File
@@ -16,20 +16,28 @@ class TcpCall {
let tag = this.monitor.tag;
if (hosts === undefined) {
const message =
"Hosts is undefined. The TCP monitor has changed in version 3.0.10. Please update your monitor with tag " + tag;
console.log(
"Hosts is undefined. The ping monitor has changed in version 3.0.10. Please update your monitor with tag",
"Hosts is undefined. The TCP monitor has changed in version 3.0.10. Please update your monitor with tag",
tag,
);
return {
status: GC.DOWN,
latency: 0,
type: GC.ERROR,
error_message: message,
};
}
let arrayOfPings = [];
let errorMessages: string[] = [];
for (let i = 0; i < hosts.length; i++) {
const host = hosts[i];
arrayOfPings.push(await TCP(host.type, host.host, host.port, host.timeout));
const result = await TCP(host.type, host.host, host.port, host.timeout);
if (result.status !== "open") {
errorMessages.push(`Host ${host.host}:${host.port} is ${result.status}`);
}
arrayOfPings.push(result);
}
let evalResp: EvalResponse | undefined = undefined;
@@ -38,11 +46,23 @@ class TcpCall {
const evalFunction = new Function("arrayOfPings", `return (${tcpEval})(arrayOfPings);`);
evalResp = await evalFunction(arrayOfPings);
} catch (error: unknown) {
console.log(`Error in tcpEval for ${tag}`, (error as Error).message);
const message = error instanceof Error ? error.message : String(error);
console.log(`Error in tcpEval for ${tag}`, message);
return {
status: GC.DOWN,
latency: 0,
type: GC.ERROR,
error_message: `Error in tcpEval: ${message}`,
};
}
if (!!!evalResp) {
const message = "tcpEval did not return a valid response.";
console.log(`Error in tcpEval for ${tag}:`, message);
return {
status: GC.DOWN,
latency: 0,
type: GC.ERROR,
error_message: `Error in tcpEval: ${message}`,
};
}
//reduce to get the status
@@ -50,6 +70,7 @@ class TcpCall {
status: evalResp?.status || GC.DOWN,
latency: evalResp?.latency || 0,
type: GC.REALTIME,
error_message: errorMessages.length > 0 ? errorMessages.join("; ") : undefined,
};
}
}
+5 -23
View File
@@ -1,5 +1,8 @@
// Server-only monitor types (internal representation with all fields).
import type { PingHost, PingMonitorTypeData } from "$lib/types/ping.js";
import type { TcpHost, TcpMonitorTypeData } from "$lib/types/tcp.js";
export interface MonitoringResult {
status: string;
latency: number;
@@ -30,29 +33,8 @@ export interface DnsMonitorTypeData {
values: string[];
}
export interface PingHost {
type: string;
host: string;
timeout: number;
count: number;
}
export interface PingMonitorTypeData {
hosts: PingHost[];
pingEval?: string;
}
export interface TcpHost {
type: string;
host: string;
port: number;
timeout: number;
}
export interface TcpMonitorTypeData {
hosts: TcpHost[];
tcpEval?: string;
}
export type { PingHost, PingMonitorTypeData };
export type { TcpHost, TcpMonitorTypeData };
export interface SslMonitorTypeData {
host: string;
+17
View File
@@ -0,0 +1,17 @@
// Shared ping monitor types: safe to import from both server and client.
export const PING_HOST_TYPES = ["IP4", "IP6", "DOMAIN"] as const;
export type PingHostType = (typeof PING_HOST_TYPES)[number];
export interface PingHost {
type: PingHostType;
host: string;
timeout: number;
count: number;
}
export interface PingMonitorTypeData {
hosts: PingHost[];
pingEval?: string;
}
+15
View File
@@ -0,0 +1,15 @@
// Shared TCP monitor types: safe to import from both server and client.
import type { PingHostType } from "$lib/types/ping.js";
export interface TcpHost {
type: PingHostType;
host: string;
port: number;
timeout: number;
}
export interface TcpMonitorTypeData {
hosts: TcpHost[];
tcpEval?: string;
}
@@ -190,7 +190,7 @@
fetchPages();
});
let activeAccordionItem = $state<string>("");
let activeAccordionItem = $derived<string>(isNew ? "general" : "configuration");
let cloneDialogOpen = $state(false);
let cloneTag = $state("");
let cloneName = $state("");
@@ -1,9 +1,9 @@
<script lang="ts">
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Textarea } from "$lib/components/ui/textarea/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import { ValidateIpAddress } from "$lib/clientTools";
import Plus from "@lucide/svelte/icons/plus";
import X from "@lucide/svelte/icons/x";
import { DefaultPingEval } from "$lib/anywhere.js";
@@ -11,21 +11,55 @@
import { javascript } from "@codemirror/lang-javascript";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
import type { PingHost, PingHostType, PingMonitorTypeData } from "$lib/types/ping.js";
import { PING_HOST_TYPES } from "$lib/types/ping.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let { data = $bindable() }: { data: any } = $props();
let { data = $bindable({ hosts: [], pingEval: DefaultPingEval }) }: { data: PingMonitorTypeData } = $props();
function normalizeHostType(value: unknown): PingHostType {
return typeof value === "string" && PING_HOST_TYPES.includes(value as PingHostType)
? (value as PingHostType)
: "IP4";
}
function inferHostType(host: string): PingHostType | null {
const inferred = ValidateIpAddress((host || "").trim());
return inferred === "Invalid" ? null : inferred;
}
// Initialize defaults if not set
if (!data.hosts) data.hosts = [];
if (!Array.isArray(data.hosts) || data.hosts.length === 0)
data.hosts = [
{
type: "IP4",
host: "",
timeout: 1000,
count: 3
}
];
else {
data.hosts = data.hosts.map((host) => ({
...host,
type: normalizeHostType(host?.type)
}));
}
if (!data.pingEval) data.pingEval = DefaultPingEval;
function addHost() {
data.hosts = [...data.hosts, { type: "cmd", host: "", timeout: 1000, count: 3 }];
data.hosts = [...data.hosts, { type: "IP4", host: "", timeout: 1000, count: 3 }];
}
function removeHost(index: number) {
data.hosts = data.hosts.filter((_: unknown, i: number) => i !== index);
}
function onHostChange(host: PingHost) {
const inferredType = inferHostType(host.host);
if (inferredType) {
host.type = inferredType;
}
}
</script>
<div class="space-y-4">
@@ -39,7 +73,7 @@
</div>
{#if data.hosts.length > 0}
<div class="space-y-3">
{#each data.hosts as host, index}
{#each data.hosts as host, index (index)}
<div class="bg-muted/50 rounded-lg border p-3">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">Host {index + 1}</span>
@@ -47,10 +81,32 @@
<X class="size-4" />
</Button>
</div>
<div class="grid grid-cols-4 gap-2">
<div class="grid grid-cols-5 gap-2">
<div class="flex flex-col gap-2">
<Label for="ping-type-{index}">Type</Label>
<Select.Root
type="single"
value={normalizeHostType(host.type)}
onValueChange={(v) => (host.type = normalizeHostType(v))}
>
<Select.Trigger id="ping-type-{index}" class="w-full">
{normalizeHostType(host.type)}
</Select.Trigger>
<Select.Content>
{#each PING_HOST_TYPES as typeOption (typeOption)}
<Select.Item value={typeOption}>{typeOption}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-2 flex flex-col gap-2">
<Label for="ping-host-{index}">Host</Label>
<Input id="ping-host-{index}" bind:value={host.host} placeholder="8.8.8.8 or example.com" />
<Input
id="ping-host-{index}"
bind:value={host.host}
oninput={() => onHostChange(host)}
placeholder="8.8.8.8, 2001:db8::1, or example.com"
/>
</div>
<div class="flex flex-col gap-2">
<Label for="ping-timeout-{index}">Timeout (ms)</Label>
@@ -1,8 +1,9 @@
<script lang="ts">
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Textarea } from "$lib/components/ui/textarea/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import { ValidateIpAddress } from "$lib/clientTools";
import Plus from "@lucide/svelte/icons/plus";
import X from "@lucide/svelte/icons/x";
import { DefaultTCPEval } from "$lib/anywhere.js";
@@ -10,21 +11,49 @@
import { javascript } from "@codemirror/lang-javascript";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
import type { PingHostType } from "$lib/types/ping.js";
import { PING_HOST_TYPES } from "$lib/types/ping.js";
import type { TcpHost, TcpMonitorTypeData } from "$lib/types/tcp.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let { data = $bindable() }: { data: any } = $props();
let { data = $bindable({ hosts: [], tcpEval: DefaultTCPEval }) }: { data: TcpMonitorTypeData } = $props();
function normalizeHostType(value: unknown): PingHostType {
return typeof value === "string" && PING_HOST_TYPES.includes(value as PingHostType)
? (value as PingHostType)
: "IP4";
}
function inferHostType(host: string): PingHostType | null {
const inferred = ValidateIpAddress((host || "").trim());
return inferred === "Invalid" ? null : inferred;
}
// Initialize defaults if not set
if (!data.hosts) data.hosts = [];
if (!Array.isArray(data.hosts) || data.hosts.length === 0) {
data.hosts = [{ type: "IP4", host: "", port: 80, timeout: 1000 }];
} else {
data.hosts = data.hosts.map((host) => ({
...host,
type: normalizeHostType(host?.type)
}));
}
if (!data.tcpEval) data.tcpEval = DefaultTCPEval;
function addHost() {
data.hosts = [...data.hosts, { type: "tcp", host: "", port: 80, timeout: 1000 }];
data.hosts = [...data.hosts, { type: "IP4", host: "", port: 80, timeout: 1000 }];
}
function removeHost(index: number) {
data.hosts = data.hosts.filter((_: unknown, i: number) => i !== index);
}
function onHostChange(host: TcpHost) {
const inferredType = inferHostType(host.host);
if (inferredType) {
host.type = inferredType;
}
}
</script>
<div class="space-y-4">
@@ -38,7 +67,7 @@
</div>
{#if data.hosts.length > 0}
<div class="space-y-3">
{#each data.hosts as host, index}
{#each data.hosts as host, index (index)}
<div class="bg-muted/50 rounded-lg border p-3">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">Host {index + 1}</span>
@@ -46,10 +75,32 @@
<X class="size-4" />
</Button>
</div>
<div class="grid grid-cols-3 gap-2">
<div class="col-span-1 flex flex-col gap-2">
<div class="grid grid-cols-5 gap-2">
<div class="flex flex-col gap-2">
<Label for="tcp-type-{index}">Type</Label>
<Select.Root
type="single"
value={normalizeHostType(host.type)}
onValueChange={(v) => (host.type = normalizeHostType(v))}
>
<Select.Trigger id="tcp-type-{index}" class="w-full">
{normalizeHostType(host.type)}
</Select.Trigger>
<Select.Content>
{#each PING_HOST_TYPES as typeOption (typeOption)}
<Select.Item value={typeOption}>{typeOption}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-2 flex flex-col gap-2">
<Label for="tcp-host-{index}">Host</Label>
<Input id="tcp-host-{index}" bind:value={host.host} placeholder="example.com" />
<Input
id="tcp-host-{index}"
bind:value={host.host}
oninput={() => onHostChange(host)}
placeholder="example.com"
/>
</div>
<div class="flex flex-col gap-2">
<Label for="tcp-port-{index}">Port</Label>