containers update + docker file build
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
This commit is contained in:
parent
4a2a9eba30
commit
dac64ac41a
@ -4,7 +4,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js"
|
"start": "node dist/index.js",
|
||||||
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.14.3",
|
"@hono/node-server": "^1.14.3",
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import Docker from 'dockerode';
|
|
||||||
|
|
||||||
export function createDockerClient() {
|
|
||||||
return new Docker();
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { createDockerClient } from './client.js';
|
import createDockerClient from "../utils/client.js";
|
||||||
import type { DockerContainer, Networks } from './types.js';
|
import type { DockerContainer, Networks } from "./types.js";
|
||||||
|
|
||||||
const docker = createDockerClient();
|
const docker = createDockerClient();
|
||||||
|
|
||||||
@ -21,8 +21,8 @@ export async function listContainers(): Promise<DockerContainer[]> {
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error listing containers:', error);
|
console.error("Error listing containers:", error);
|
||||||
throw new Error('Failed to list Docker containers');
|
throw new Error("Failed to list Docker containers");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,10 +35,13 @@ export async function getContainerStats(containerId: string) {
|
|||||||
const currSys = stats.cpu_stats.system_cpu_usage;
|
const currSys = stats.cpu_stats.system_cpu_usage;
|
||||||
const cpuDelta = currCpu - prevCpu;
|
const cpuDelta = currCpu - prevCpu;
|
||||||
const sysDelta = currSys - prevSys;
|
const sysDelta = currSys - prevSys;
|
||||||
|
const numCpus =
|
||||||
|
stats.cpu_stats.online_cpus ||
|
||||||
|
(stats.cpu_stats.cpu_usage.percpu_usage
|
||||||
|
? stats.cpu_stats.cpu_usage.percpu_usage.length
|
||||||
|
: 1);
|
||||||
const cpuPercent =
|
const cpuPercent =
|
||||||
sysDelta > 0
|
sysDelta > 0 ? (cpuDelta / sysDelta) * numCpus * 100.0 : 0.0;
|
||||||
? (cpuDelta / sysDelta) * stats.cpu_stats.cpu_usage.percpu_usage.length * 100.0
|
|
||||||
: 0.0;
|
|
||||||
const memUsage = stats.memory_stats.usage;
|
const memUsage = stats.memory_stats.usage;
|
||||||
const memLimit = stats.memory_stats.limit;
|
const memLimit = stats.memory_stats.limit;
|
||||||
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
|
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
|
||||||
@ -58,8 +61,8 @@ export async function getContainerStats(containerId: string) {
|
|||||||
Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
|
Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
|
||||||
) {
|
) {
|
||||||
for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
|
for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
|
||||||
if (entry.op === 'Read') blkRead += entry.value;
|
if (entry.op === "Read") blkRead += entry.value;
|
||||||
if (entry.op === 'Write') blkWrite += entry.value;
|
if (entry.op === "Write") blkWrite += entry.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -82,19 +85,26 @@ export async function streamContainerStats(
|
|||||||
): Promise<() => void> {
|
): Promise<() => void> {
|
||||||
const container = docker.getContainer(containerId);
|
const container = docker.getContainer(containerId);
|
||||||
try {
|
try {
|
||||||
const stream = (await container.stats({ stream: true })) as import('stream').Readable;
|
const stream = (await container.stats({
|
||||||
stream.on('data', (chunk: Buffer) => {
|
stream: true,
|
||||||
|
})) as import("stream").Readable;
|
||||||
|
stream.on("data", (chunk: Buffer) => {
|
||||||
const stats = JSON.parse(chunk.toString());
|
const stats = JSON.parse(chunk.toString());
|
||||||
|
|
||||||
const prevCpu = stats.precpu_stats.cpu_usage.total_usage;
|
const prevCpu = stats.precpu_stats.cpu_usage.total_usage;
|
||||||
const prevSys = stats.precpu_stats.system_cpu_usage;
|
const prevSys = stats.precpu_stats.system_cpu_usage;
|
||||||
const currCpu = stats.cpu_stats.cpu_usage.total_usage;
|
const currCpu = stats.cpu_stats.cpu_usage.total_usage;
|
||||||
const currSys = stats.cpu_stats.system_cpu_usage;
|
const currSys = stats.cpu_stats.system_cpu_usage;
|
||||||
const cpuDelta = currCpu - prevCpu;
|
const cpuDelta = currCpu - prevCpu;
|
||||||
const sysDelta = currSys - prevSys;
|
const sysDelta = currSys - prevSys;
|
||||||
|
|
||||||
|
const numCpus =
|
||||||
|
stats.cpu_stats.online_cpus ||
|
||||||
|
(stats.cpu_stats.cpu_usage.percpu_usage
|
||||||
|
? stats.cpu_stats.cpu_usage.percpu_usage.length
|
||||||
|
: 1);
|
||||||
const cpuPercent =
|
const cpuPercent =
|
||||||
sysDelta > 0
|
sysDelta > 0 ? (cpuDelta / sysDelta) * numCpus * 100.0 : 0.0;
|
||||||
? (cpuDelta / sysDelta) * stats.cpu_stats.cpu_usage.percpu_usage.length * 100.0
|
|
||||||
: 0.0;
|
|
||||||
const memUsage = stats.memory_stats.usage;
|
const memUsage = stats.memory_stats.usage;
|
||||||
const memLimit = stats.memory_stats.limit;
|
const memLimit = stats.memory_stats.limit;
|
||||||
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
|
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
|
||||||
@ -114,8 +124,8 @@ export async function streamContainerStats(
|
|||||||
Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
|
Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
|
||||||
) {
|
) {
|
||||||
for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
|
for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
|
||||||
if (entry.op === 'Read') blkRead += entry.value;
|
if (entry.op === "Read") blkRead += entry.value;
|
||||||
if (entry.op === 'Write') blkWrite += entry.value;
|
if (entry.op === "Write") blkWrite += entry.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback({
|
callback({
|
||||||
@ -131,14 +141,17 @@ export async function streamContainerStats(
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
stream.on('error', (err: Error) => {
|
stream.on("error", (err: Error) => {
|
||||||
console.error(`Error streaming stats for container ${containerId}:`, err);
|
console.error(`Error streaming stats for container ${containerId}:`, err);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
stream.destroy();
|
stream.destroy();
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting stats stream for container ${containerId}:`, error);
|
console.error(
|
||||||
|
`Error getting stats stream for container ${containerId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
throw new Error(`Failed to get stats stream for container ${containerId}`);
|
throw new Error(`Failed to get stats stream for container ${containerId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,112 +1,133 @@
|
|||||||
import { serve } from '@hono/node-server'
|
import { serve } from "@hono/node-server";
|
||||||
import { Hono } from 'hono'
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { createNodeWebSocket } from '@hono/node-ws'
|
import { createNodeWebSocket } from "@hono/node-ws";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
import {
|
import {
|
||||||
registerContainerClient,
|
|
||||||
registerStatsClient,
|
|
||||||
registerCombinedClient,
|
registerCombinedClient,
|
||||||
|
registerContainerClient,
|
||||||
registerSingleContainerClient,
|
registerSingleContainerClient,
|
||||||
removeContainerClient,
|
registerStatsClient,
|
||||||
removeStatsClient,
|
|
||||||
removeCombinedClient,
|
removeCombinedClient,
|
||||||
|
removeContainerClient,
|
||||||
removeSingleContainerClient,
|
removeSingleContainerClient,
|
||||||
stopAllStreams
|
removeStatsClient,
|
||||||
} from './ws/manager.js';
|
stopAllStreams,
|
||||||
|
} from "./ws/manager.js";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
||||||
|
|
||||||
app.get('/', (c) => c.text('WS Docker Stats server (ESM) is running.'))
|
app.use("/*", serveStatic({ root: "./public" }));
|
||||||
|
|
||||||
|
app.get("/", (c) => c.text("WS Docker Stats server (ESM) is running."));
|
||||||
|
|
||||||
// Containers-only endpoint
|
app.get(
|
||||||
app.get('/ws/containers', upgradeWebSocket(c => ({
|
"/ws/containers",
|
||||||
onOpen(_, ws) {
|
upgradeWebSocket((c) => ({
|
||||||
console.log('WebSocket containers connection opened');
|
|
||||||
registerContainerClient(ws);
|
|
||||||
},
|
|
||||||
onClose(_, ws) {
|
|
||||||
console.log('❌ WS containers client disconnected');
|
|
||||||
removeContainerClient(ws);
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
console.error('❌ WS containers error:', err);
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
|
|
||||||
// Stats-only endpoint
|
|
||||||
app.get('/ws/stats', upgradeWebSocket(c => ({
|
|
||||||
onOpen(_, ws) {
|
|
||||||
console.log('WebSocket stats connection opened');
|
|
||||||
registerStatsClient(ws);
|
|
||||||
},
|
|
||||||
onClose(_, ws) {
|
|
||||||
console.log('❌ WS stats client disconnected');
|
|
||||||
removeStatsClient(ws);
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
console.error('❌ WS stats error:', err);
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
|
|
||||||
// Combined endpoint (explicit)
|
|
||||||
app.get('/ws/combined', upgradeWebSocket(c => ({
|
|
||||||
onOpen(_, ws) {
|
|
||||||
console.log('WebSocket combined connection opened');
|
|
||||||
registerCombinedClient(ws);
|
|
||||||
},
|
|
||||||
onClose(_, ws) {
|
|
||||||
console.log('❌ WS combined client disconnected');
|
|
||||||
removeCombinedClient(ws);
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
console.error('❌ WS combined error:', err);
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
|
|
||||||
// Single container endpoint
|
|
||||||
app.get('/ws/container', upgradeWebSocket(c => {
|
|
||||||
const containerId = c.req.query('id');
|
|
||||||
if (!containerId) {
|
|
||||||
throw new Error('Container ID is required');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
onOpen(_, ws) {
|
onOpen(_, ws) {
|
||||||
console.log(`WebSocket connection opened for container ${containerId}`);
|
console.log("WebSocket containers connection opened");
|
||||||
registerSingleContainerClient(ws, containerId);
|
registerContainerClient(ws);
|
||||||
},
|
},
|
||||||
onClose(_, ws) {
|
onClose(_, ws) {
|
||||||
console.log(`❌ WS client for container ${containerId} disconnected`);
|
console.log("❌ WS containers client disconnected");
|
||||||
removeSingleContainerClient(ws, containerId);
|
removeContainerClient(ws);
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
console.error(`❌ WS error for container ${containerId}:`, err);
|
console.error("❌ WS containers error:", err);
|
||||||
}
|
},
|
||||||
};
|
}))
|
||||||
}))
|
);
|
||||||
|
|
||||||
const server = serve({
|
// Stats-only endpoint
|
||||||
fetch: app.fetch,
|
app.get(
|
||||||
port: 3000
|
"/ws/stats",
|
||||||
}, (info) => {
|
upgradeWebSocket((c) => ({
|
||||||
console.log(`Server is running on http://localhost:${info.port}`)
|
onOpen(_, ws) {
|
||||||
})
|
console.log("WebSocket stats connection opened");
|
||||||
|
registerStatsClient(ws);
|
||||||
|
},
|
||||||
|
onClose(_, ws) {
|
||||||
|
console.log("❌ WS stats client disconnected");
|
||||||
|
removeStatsClient(ws);
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error("❌ WS stats error:", err);
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combined endpoint (explicit)
|
||||||
|
app.get(
|
||||||
|
"/ws/combined",
|
||||||
|
upgradeWebSocket((c) => ({
|
||||||
|
onOpen(_, ws) {
|
||||||
|
console.log("WebSocket combined connection opened");
|
||||||
|
registerCombinedClient(ws);
|
||||||
|
},
|
||||||
|
onClose(_, ws) {
|
||||||
|
console.log("❌ WS combined client disconnected");
|
||||||
|
removeCombinedClient(ws);
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error("❌ WS combined error:", err);
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Single container endpoint
|
||||||
|
app.get(
|
||||||
|
"/ws/container",
|
||||||
|
upgradeWebSocket((c) => {
|
||||||
|
const containerId = c.req.query("id");
|
||||||
|
if (!containerId) {
|
||||||
|
throw new Error("Container ID is required");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
onOpen(_, ws) {
|
||||||
|
console.log(`WebSocket connection opened for container ${containerId}`);
|
||||||
|
registerSingleContainerClient(ws, containerId);
|
||||||
|
},
|
||||||
|
onClose(_, ws) {
|
||||||
|
console.log(`❌ WS client for container ${containerId} disconnected`);
|
||||||
|
removeSingleContainerClient(ws, containerId);
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error(`❌ WS error for container ${containerId}:`, err);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get("*", (c) => {
|
||||||
|
return c.html(readFileSync("./public/index.html", "utf-8"));
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = serve(
|
||||||
|
{
|
||||||
|
fetch: app.fetch,
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
(info) => {
|
||||||
|
console.log(`Server is running on http://localhost:${info.port}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
injectWebSocket(server);
|
injectWebSocket(server);
|
||||||
|
|
||||||
process.on('SIGTERM', () => cleanupAndExit());
|
process.on("SIGTERM", () => cleanupAndExit());
|
||||||
process.on('SIGINT', () => cleanupAndExit());
|
process.on("SIGINT", () => cleanupAndExit());
|
||||||
|
|
||||||
function cleanupAndExit() {
|
function cleanupAndExit() {
|
||||||
console.log('Shutting down gracefully...');
|
console.log("Shutting down gracefully...");
|
||||||
stopAllStreams();
|
stopAllStreams();
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
console.log('Server closed');
|
console.log("Server closed");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('Forcing exit after timeout');
|
console.log("Forcing exit after timeout");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import type { WSContext } from 'hono/ws';
|
import type { WSContext } from "hono/ws";
|
||||||
import { listContainers, streamContainerStats } from '../docker/containers.js';
|
import { listContainers, streamContainerStats } from "../docker/containers.js";
|
||||||
import { sendContainerInfo, sendStats, sendCombined, sendToSingleContainer } from './sender.js';
|
import type { DockerContainer } from "../docker/types.js";
|
||||||
import type { ContainerInfo, ContainerStats } from './types.js';
|
import {
|
||||||
import type { DockerContainer } from '../docker/types.js';
|
sendCombined,
|
||||||
|
sendContainerInfo,
|
||||||
|
sendStats,
|
||||||
|
sendToSingleContainer,
|
||||||
|
} from "./sender.js";
|
||||||
|
import type { ContainerInfo, ContainerStats } from "./types.js";
|
||||||
|
|
||||||
const containerClients = new Set<WSContext<WebSocket>>();
|
const containerClients = new Set<WSContext<WebSocket>>();
|
||||||
const statsClients = new Set<WSContext<WebSocket>>();
|
const statsClients = new Set<WSContext<WebSocket>>();
|
||||||
@ -11,18 +15,29 @@ const combinedClients = new Set<WSContext<WebSocket>>();
|
|||||||
const singleContainerClients = new Map<string, Set<WSContext<WebSocket>>>();
|
const singleContainerClients = new Map<string, Set<WSContext<WebSocket>>>();
|
||||||
const activeStreams = new Map<string, () => void>();
|
const activeStreams = new Map<string, () => void>();
|
||||||
|
|
||||||
|
const containerCache = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
data: DockerContainer | null;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
const CACHE_TTL = 2000;
|
||||||
|
|
||||||
const containerCache = new Map<string, {
|
// Add a map to collect all container info
|
||||||
data: DockerContainer | null;
|
const allContainerInfo = new Map<string, ContainerInfo>();
|
||||||
lastUpdated: number;
|
let containerUpdateTimer: NodeJS.Timeout | null = null;
|
||||||
}>();
|
|
||||||
const CACHE_TTL = 2000;
|
|
||||||
|
|
||||||
|
|
||||||
export function registerContainerClient(ws: WSContext<WebSocket>) {
|
export function registerContainerClient(ws: WSContext<WebSocket>) {
|
||||||
const wasEmpty = allClientsEmpty();
|
const wasEmpty = allClientsEmpty();
|
||||||
containerClients.add(ws);
|
containerClients.add(ws);
|
||||||
if (wasEmpty) setupStreams();
|
if (wasEmpty) setupStreams();
|
||||||
|
|
||||||
|
// Send current container data immediately if available
|
||||||
|
if (allContainerInfo.size > 0) {
|
||||||
|
const containers = Array.from(allContainerInfo.values());
|
||||||
|
sendContainerInfo(new Set([ws]), containers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerStatsClient(ws: WSContext<WebSocket>) {
|
export function registerStatsClient(ws: WSContext<WebSocket>) {
|
||||||
@ -37,7 +52,10 @@ export function registerCombinedClient(ws: WSContext<WebSocket>) {
|
|||||||
if (wasEmpty) setupStreams();
|
if (wasEmpty) setupStreams();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerSingleContainerClient(ws: WSContext<WebSocket>, containerId: string) {
|
export function registerSingleContainerClient(
|
||||||
|
ws: WSContext<WebSocket>,
|
||||||
|
containerId: string
|
||||||
|
) {
|
||||||
if (!singleContainerClients.has(containerId)) {
|
if (!singleContainerClients.has(containerId)) {
|
||||||
singleContainerClients.set(containerId, new Set());
|
singleContainerClients.set(containerId, new Set());
|
||||||
}
|
}
|
||||||
@ -48,7 +66,6 @@ export function registerSingleContainerClient(ws: WSContext<WebSocket>, containe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function removeContainerClient(ws: WSContext<WebSocket>) {
|
export function removeContainerClient(ws: WSContext<WebSocket>) {
|
||||||
containerClients.delete(ws);
|
containerClients.delete(ws);
|
||||||
checkAndCleanup();
|
checkAndCleanup();
|
||||||
@ -64,7 +81,10 @@ export function removeCombinedClient(ws: WSContext<WebSocket>) {
|
|||||||
checkAndCleanup();
|
checkAndCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeSingleContainerClient(ws: WSContext<WebSocket>, containerId: string) {
|
export function removeSingleContainerClient(
|
||||||
|
ws: WSContext<WebSocket>,
|
||||||
|
containerId: string
|
||||||
|
) {
|
||||||
const clients = singleContainerClients.get(containerId);
|
const clients = singleContainerClients.get(containerId);
|
||||||
if (clients) {
|
if (clients) {
|
||||||
clients.delete(ws);
|
clients.delete(ws);
|
||||||
@ -76,10 +96,12 @@ export function removeSingleContainerClient(ws: WSContext<WebSocket>, containerI
|
|||||||
}
|
}
|
||||||
|
|
||||||
function allClientsEmpty() {
|
function allClientsEmpty() {
|
||||||
return containerClients.size === 0 &&
|
return (
|
||||||
|
containerClients.size === 0 &&
|
||||||
statsClients.size === 0 &&
|
statsClients.size === 0 &&
|
||||||
combinedClients.size === 0 &&
|
combinedClients.size === 0 &&
|
||||||
singleContainerClients.size === 0;
|
singleContainerClients.size === 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkAndCleanup() {
|
function checkAndCleanup() {
|
||||||
@ -92,108 +114,193 @@ async function setupStreams() {
|
|||||||
if (allClientsEmpty()) return;
|
if (allClientsEmpty()) return;
|
||||||
try {
|
try {
|
||||||
const containers = await listContainers();
|
const containers = await listContainers();
|
||||||
|
|
||||||
for (const container of containers) {
|
for (const container of containers) {
|
||||||
|
const containerInfo: ContainerInfo = {
|
||||||
|
id: container.id,
|
||||||
|
name:
|
||||||
|
container.names?.[0]?.replace("/", "") ?? container.id.slice(0, 12),
|
||||||
|
image: container.image,
|
||||||
|
state: container.state,
|
||||||
|
status: container.status,
|
||||||
|
};
|
||||||
|
allContainerInfo.set(container.id, containerInfo);
|
||||||
|
|
||||||
containerCache.set(container.id, {
|
containerCache.set(container.id, {
|
||||||
data: container,
|
data: container,
|
||||||
lastUpdated: Date.now()
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send all containers to clients
|
||||||
|
if (containerClients.size > 0) {
|
||||||
|
sendContainerInfo(
|
||||||
|
containerClients,
|
||||||
|
Array.from(allContainerInfo.values())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup periodic container list updates
|
||||||
|
if (!containerUpdateTimer) {
|
||||||
|
containerUpdateTimer = setInterval(updateAllContainers, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
for (const container of containers) {
|
for (const container of containers) {
|
||||||
if (activeStreams.has(container.id)) continue;
|
if (activeStreams.has(container.id)) continue;
|
||||||
setupStreamForContainer(container.id);
|
setupStreamForContainer(container.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up streams:', error);
|
console.error("Error setting up streams:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getContainerData(containerId: string): Promise<DockerContainer | null> {
|
async function updateAllContainers() {
|
||||||
const cached = containerCache.get(containerId);
|
if (allClientsEmpty()) {
|
||||||
const now = Date.now();
|
if (containerUpdateTimer) {
|
||||||
|
clearInterval(containerUpdateTimer);
|
||||||
if (cached && (now - cached.lastUpdated < CACHE_TTL)) {
|
containerUpdateTimer = null;
|
||||||
return cached.data;
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const containers = await listContainers();
|
const containers = await listContainers();
|
||||||
const containerData = containers.find(c => c.id === containerId) || null;
|
|
||||||
|
// Update container cache and allContainerInfo
|
||||||
|
const now = Date.now();
|
||||||
for (const container of containers) {
|
for (const container of containers) {
|
||||||
containerCache.set(container.id, {
|
containerCache.set(container.id, {
|
||||||
data: container,
|
data: container,
|
||||||
lastUpdated: now
|
lastUpdated: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerInfo: ContainerInfo = {
|
||||||
|
id: container.id,
|
||||||
|
name:
|
||||||
|
container.names?.[0]?.replace("/", "") ?? container.id.slice(0, 12),
|
||||||
|
image: container.image,
|
||||||
|
state: container.state,
|
||||||
|
status: container.status,
|
||||||
|
};
|
||||||
|
allContainerInfo.set(container.id, containerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all containers to clients
|
||||||
|
if (containerClients.size > 0) {
|
||||||
|
sendContainerInfo(
|
||||||
|
containerClients,
|
||||||
|
Array.from(allContainerInfo.values())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating containers:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getContainerData(
|
||||||
|
containerId: string
|
||||||
|
): Promise<DockerContainer | null> {
|
||||||
|
const cached = containerCache.get(containerId);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (cached && now - cached.lastUpdated < CACHE_TTL) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const containers = await listContainers();
|
||||||
|
const containerData = containers.find((c) => c.id === containerId) || null;
|
||||||
|
|
||||||
|
for (const container of containers) {
|
||||||
|
containerCache.set(container.id, {
|
||||||
|
data: container,
|
||||||
|
lastUpdated: now,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return containerData;
|
return containerData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching container data for ${containerId}:`, error);
|
console.error(`Error fetching container data for ${containerId}:`, error);
|
||||||
|
|
||||||
return cached?.data || null;
|
return cached?.data || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupStreamForContainer(containerId: string) {
|
async function setupStreamForContainer(containerId: string) {
|
||||||
if (activeStreams.has(containerId)) return;
|
if (activeStreams.has(containerId)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const containerData = await getContainerData(containerId);
|
const containerData = await getContainerData(containerId);
|
||||||
if (!containerData) {
|
if (!containerData) {
|
||||||
console.warn(`Container ${containerId} not found`);
|
console.warn(`Container ${containerId} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopStream = await streamContainerStats(containerId, async (stats: ContainerStats) => {
|
|
||||||
if (allClientsEmpty()) {
|
|
||||||
stopAllStreams();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const stopStream = await streamContainerStats(
|
||||||
const freshContainerData = await getContainerData(containerId);
|
containerId,
|
||||||
|
async (stats: ContainerStats) => {
|
||||||
const containerInfo: ContainerInfo = freshContainerData ? {
|
if (allClientsEmpty()) {
|
||||||
id: freshContainerData.id,
|
stopAllStreams();
|
||||||
name: freshContainerData.names?.[0]?.replace('/', '') ?? freshContainerData.id.slice(0, 12),
|
return;
|
||||||
image: freshContainerData.image,
|
}
|
||||||
state: freshContainerData.state,
|
|
||||||
status: freshContainerData.status,
|
|
||||||
} : {
|
|
||||||
id: containerData.id,
|
|
||||||
name: containerData.names?.[0]?.replace('/', '') ?? containerData.id.slice(0, 12),
|
|
||||||
image: containerData.image,
|
|
||||||
state: 'exited',
|
|
||||||
status: 'Container stopped',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const freshContainerData = await getContainerData(containerId);
|
||||||
if (!freshContainerData && !singleContainerClients.has(containerId)) {
|
|
||||||
stopStreamForContainer(containerId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containerClients.size > 0) {
|
const containerInfo: ContainerInfo = freshContainerData
|
||||||
sendContainerInfo(containerClients, containerInfo);
|
? {
|
||||||
|
id: freshContainerData.id,
|
||||||
|
name:
|
||||||
|
freshContainerData.names?.[0]?.replace("/", "") ??
|
||||||
|
freshContainerData.id.slice(0, 12),
|
||||||
|
image: freshContainerData.image,
|
||||||
|
state: freshContainerData.state,
|
||||||
|
status: freshContainerData.status,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: containerData.id,
|
||||||
|
name:
|
||||||
|
containerData.names?.[0]?.replace("/", "") ??
|
||||||
|
containerData.id.slice(0, 12),
|
||||||
|
image: containerData.image,
|
||||||
|
state: "exited",
|
||||||
|
status: "Container stopped",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the containerInfo in our map
|
||||||
|
allContainerInfo.set(containerId, containerInfo);
|
||||||
|
|
||||||
|
if (!freshContainerData && !singleContainerClients.has(containerId)) {
|
||||||
|
stopStreamForContainer(containerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't send containerInfo here anymore, it's sent by updateAllContainers
|
||||||
|
|
||||||
|
if (statsClients.size > 0) {
|
||||||
|
sendStats(statsClients, stats);
|
||||||
|
}
|
||||||
|
if (combinedClients.size > 0) {
|
||||||
|
sendCombined(combinedClients, containerInfo, stats);
|
||||||
|
}
|
||||||
|
const singleClients = singleContainerClients.get(containerId);
|
||||||
|
if (singleClients && singleClients.size > 0) {
|
||||||
|
sendToSingleContainer(
|
||||||
|
singleContainerClients,
|
||||||
|
containerId,
|
||||||
|
containerInfo,
|
||||||
|
stats
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (statsClients.size > 0) {
|
);
|
||||||
sendStats(statsClients, stats);
|
|
||||||
}
|
|
||||||
if (combinedClients.size > 0) {
|
|
||||||
sendCombined(combinedClients, containerInfo, stats);
|
|
||||||
}
|
|
||||||
const singleClients = singleContainerClients.get(containerId);
|
|
||||||
if (singleClients && singleClients.size > 0) {
|
|
||||||
sendToSingleContainer(singleContainerClients, containerId, containerInfo, stats);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
activeStreams.set(containerId, stopStream);
|
activeStreams.set(containerId, stopStream);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to setup stream for container ${containerId}:`, error);
|
console.error(
|
||||||
|
`Failed to setup stream for container ${containerId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
containerCache.delete(containerId);
|
containerCache.delete(containerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,8 +310,9 @@ function stopStreamForContainer(containerId: string) {
|
|||||||
if (stopStream) {
|
if (stopStream) {
|
||||||
stopStream();
|
stopStream();
|
||||||
activeStreams.delete(containerId);
|
activeStreams.delete(containerId);
|
||||||
|
|
||||||
containerCache.delete(containerId);
|
containerCache.delete(containerId);
|
||||||
|
allContainerInfo.delete(containerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,4 +322,10 @@ export function stopAllStreams() {
|
|||||||
}
|
}
|
||||||
activeStreams.clear();
|
activeStreams.clear();
|
||||||
containerCache.clear();
|
containerCache.clear();
|
||||||
|
allContainerInfo.clear();
|
||||||
|
|
||||||
|
if (containerUpdateTimer) {
|
||||||
|
clearInterval(containerUpdateTimer);
|
||||||
|
containerUpdateTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +1,46 @@
|
|||||||
import type { WSContext } from 'hono/ws';
|
import type { WSContext } from "hono/ws";
|
||||||
import type { ContainerInfo, ContainerStats } from './types.js';
|
import type { ContainerInfo, ContainerStats } from "./types.js";
|
||||||
|
|
||||||
export function sendContainerInfo(containerClients: Set<WSContext<WebSocket>>, containerInfo: ContainerInfo) {
|
export function sendContainerInfo(
|
||||||
const payload = JSON.stringify({ containers: [containerInfo] });
|
containerClients: Set<WSContext<WebSocket>>,
|
||||||
|
containerInfo: ContainerInfo | ContainerInfo[]
|
||||||
|
) {
|
||||||
|
const containers = Array.isArray(containerInfo)
|
||||||
|
? containerInfo
|
||||||
|
: [containerInfo];
|
||||||
|
const payload = JSON.stringify({ containers });
|
||||||
sendToClientSet(containerClients, payload);
|
sendToClientSet(containerClients, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendStats(statsClients: Set<WSContext<WebSocket>>, stats: ContainerStats) {
|
export function sendStats(
|
||||||
|
statsClients: Set<WSContext<WebSocket>>,
|
||||||
|
stats: ContainerStats
|
||||||
|
) {
|
||||||
const payload = JSON.stringify({ stats: [stats] });
|
const payload = JSON.stringify({ stats: [stats] });
|
||||||
sendToClientSet(statsClients, payload);
|
sendToClientSet(statsClients, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendCombined(
|
export function sendCombined(
|
||||||
combinedClients: Set<WSContext<WebSocket>>,
|
combinedClients: Set<WSContext<WebSocket>>,
|
||||||
containerInfo: ContainerInfo,
|
containerInfo: ContainerInfo,
|
||||||
stats: ContainerStats
|
stats: ContainerStats
|
||||||
) {
|
) {
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
containers: [containerInfo],
|
containers: [containerInfo],
|
||||||
stats: [stats]
|
stats: [stats],
|
||||||
});
|
});
|
||||||
sendToClientSet(combinedClients, payload);
|
sendToClientSet(combinedClients, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendToSingleContainer(
|
export function sendToSingleContainer(
|
||||||
singleContainerClients: Map<string, Set<WSContext<WebSocket>>>,
|
singleContainerClients: Map<string, Set<WSContext<WebSocket>>>,
|
||||||
containerId: string,
|
containerId: string,
|
||||||
containerInfo: ContainerInfo,
|
containerInfo: ContainerInfo,
|
||||||
stats: ContainerStats
|
stats: ContainerStats
|
||||||
) {
|
) {
|
||||||
const combinedData = {
|
const combinedData = {
|
||||||
...containerInfo,
|
...containerInfo,
|
||||||
...stats
|
...stats,
|
||||||
};
|
};
|
||||||
const payload = JSON.stringify({ container: combinedData });
|
const payload = JSON.stringify({ container: combinedData });
|
||||||
const clients = singleContainerClients.get(containerId);
|
const clients = singleContainerClients.get(containerId);
|
||||||
@ -40,13 +49,16 @@ export function sendToSingleContainer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendToClientSet(clients: Set<WSContext<WebSocket>>, payload: string) {
|
export function sendToClientSet(
|
||||||
|
clients: Set<WSContext<WebSocket>>,
|
||||||
|
payload: string
|
||||||
|
) {
|
||||||
for (const ws of clients) {
|
for (const ws of clients) {
|
||||||
if (ws.readyState === 1) {
|
if (ws.readyState === 1) {
|
||||||
try {
|
try {
|
||||||
ws.send(payload);
|
ws.send(payload);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error sending to client:', err);
|
console.error("Error sending to client:", err);
|
||||||
clients.delete(ws);
|
clients.delete(ws);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user