mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-10-30 19:43:49 +00:00 
			
		
		
		
	Fixed Agent checking upon entrypoint modified entrypoint to handle both binary files as well as the shell script
		
			
				
	
	
		
			744 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			744 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const axios = require("axios");
 | ||
| const fs = require("node:fs").promises;
 | ||
| const path = require("node:path");
 | ||
| const { exec, spawn } = require("node:child_process");
 | ||
| const { promisify } = require("node:util");
 | ||
| const _execAsync = promisify(exec);
 | ||
| 
 | ||
| // Simple semver comparison function
 | ||
| function compareVersions(version1, version2) {
 | ||
| 	const v1parts = version1.split(".").map(Number);
 | ||
| 	const v2parts = version2.split(".").map(Number);
 | ||
| 
 | ||
| 	// Ensure both arrays have the same length
 | ||
| 	while (v1parts.length < 3) v1parts.push(0);
 | ||
| 	while (v2parts.length < 3) v2parts.push(0);
 | ||
| 
 | ||
| 	for (let i = 0; i < 3; i++) {
 | ||
| 		if (v1parts[i] > v2parts[i]) return 1;
 | ||
| 		if (v1parts[i] < v2parts[i]) return -1;
 | ||
| 	}
 | ||
| 	return 0;
 | ||
| }
 | ||
| const crypto = require("node:crypto");
 | ||
| 
 | ||
| class AgentVersionService {
 | ||
| 	constructor() {
 | ||
| 		this.githubApiUrl =
 | ||
| 			"https://api.github.com/repos/PatchMon/PatchMon-agent/releases";
 | ||
| 		this.agentsDir = path.resolve(__dirname, "../../../agents");
 | ||
| 		this.supportedArchitectures = [
 | ||
| 			"linux-amd64",
 | ||
| 			"linux-arm64",
 | ||
| 			"linux-386",
 | ||
| 			"linux-arm",
 | ||
| 		];
 | ||
| 		this.currentVersion = null;
 | ||
| 		this.latestVersion = null;
 | ||
| 		this.lastChecked = null;
 | ||
| 		this.checkInterval = 30 * 60 * 1000; // 30 minutes
 | ||
| 	}
 | ||
| 
 | ||
| 	async initialize() {
 | ||
| 		try {
 | ||
| 			// Ensure agents directory exists
 | ||
| 			await fs.mkdir(this.agentsDir, { recursive: true });
 | ||
| 
 | ||
| 			console.log("🔍 Testing GitHub API connectivity...");
 | ||
| 			try {
 | ||
| 				const testResponse = await axios.get(
 | ||
| 					"https://api.github.com/repos/PatchMon/PatchMon-agent/releases",
 | ||
| 					{
 | ||
| 						timeout: 5000,
 | ||
| 						headers: {
 | ||
| 							"User-Agent": "PatchMon-Server/1.0",
 | ||
| 							Accept: "application/vnd.github.v3+json",
 | ||
| 						},
 | ||
| 					},
 | ||
| 				);
 | ||
| 				console.log(
 | ||
| 					`✅ GitHub API accessible - found ${testResponse.data.length} releases`,
 | ||
| 				);
 | ||
| 			} catch (testError) {
 | ||
| 				console.error("❌ GitHub API not accessible:", testError.message);
 | ||
| 				if (testError.response) {
 | ||
| 					console.error(
 | ||
| 						"❌ Status:",
 | ||
| 						testError.response.status,
 | ||
| 						testError.response.statusText,
 | ||
| 					);
 | ||
| 					if (testError.response.status === 403) {
 | ||
| 						console.log("⚠️ GitHub API rate limit exceeded - will retry later");
 | ||
| 					}
 | ||
| 				}
 | ||
| 			}
 | ||
| 
 | ||
| 			// Get current agent version by executing the binary
 | ||
| 			await this.getCurrentAgentVersion();
 | ||
| 
 | ||
| 			// Try to check for updates, but don't fail initialization if GitHub API is unavailable
 | ||
| 			try {
 | ||
| 				await this.checkForUpdates();
 | ||
| 			} catch (updateError) {
 | ||
| 				console.log(
 | ||
| 					"⚠️ Failed to check for updates on startup, will retry later:",
 | ||
| 					updateError.message,
 | ||
| 				);
 | ||
| 			}
 | ||
| 
 | ||
| 			// Set up periodic checking
 | ||
| 			setInterval(() => {
 | ||
| 				this.checkForUpdates().catch((error) => {
 | ||
| 					console.log("⚠️ Periodic update check failed:", error.message);
 | ||
| 				});
 | ||
| 			}, this.checkInterval);
 | ||
| 
 | ||
| 			console.log("✅ Agent Version Service initialized");
 | ||
| 		} catch (error) {
 | ||
| 			console.error(
 | ||
| 				"❌ Failed to initialize Agent Version Service:",
 | ||
| 				error.message,
 | ||
| 			);
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async getCurrentAgentVersion() {
 | ||
| 		try {
 | ||
| 			console.log("🔍 Getting current agent version...");
 | ||
| 
 | ||
| 			// Try to find the agent binary in agents/ folder only (what gets distributed)
 | ||
| 			const possiblePaths = [
 | ||
| 				path.join(this.agentsDir, "patchmon-agent-linux-amd64"),
 | ||
| 				path.join(this.agentsDir, "patchmon-agent"),
 | ||
| 			];
 | ||
| 
 | ||
| 			let agentPath = null;
 | ||
| 			for (const testPath of possiblePaths) {
 | ||
| 				try {
 | ||
| 					await fs.access(testPath);
 | ||
| 					agentPath = testPath;
 | ||
| 					console.log(`✅ Found agent binary at: ${testPath}`);
 | ||
| 					break;
 | ||
| 				} catch {
 | ||
| 					// Path doesn't exist, continue to next
 | ||
| 				}
 | ||
| 			}
 | ||
| 
 | ||
| 			if (!agentPath) {
 | ||
| 				console.log(
 | ||
| 					"⚠️ No agent binary found in agents/ folder, current version will be unknown",
 | ||
| 				);
 | ||
| 				console.log("💡 Use the Download Updates button to get agent binaries");
 | ||
| 				this.currentVersion = null;
 | ||
| 				return;
 | ||
| 			}
 | ||
| 
 | ||
| 			// Execute the agent binary with help flag to get version info
 | ||
| 			try {
 | ||
| 				const child = spawn(agentPath, ["--help"], {
 | ||
| 					timeout: 10000,
 | ||
| 				});
 | ||
| 
 | ||
| 				let stdout = "";
 | ||
| 				let stderr = "";
 | ||
| 
 | ||
| 				child.stdout.on("data", (data) => {
 | ||
| 					stdout += data.toString();
 | ||
| 				});
 | ||
| 
 | ||
| 				child.stderr.on("data", (data) => {
 | ||
| 					stderr += data.toString();
 | ||
| 				});
 | ||
| 
 | ||
| 				const result = await new Promise((resolve, reject) => {
 | ||
| 					child.on("close", (code) => {
 | ||
| 						resolve({ stdout, stderr, code });
 | ||
| 					});
 | ||
| 					child.on("error", reject);
 | ||
| 				});
 | ||
| 
 | ||
| 				if (result.stderr) {
 | ||
| 					console.log("⚠️ Agent help stderr:", result.stderr);
 | ||
| 				}
 | ||
| 
 | ||
| 				// Parse version from help output (e.g., "PatchMon Agent v1.3.0")
 | ||
| 				const versionMatch = result.stdout.match(
 | ||
| 					/PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i,
 | ||
| 				);
 | ||
| 				if (versionMatch) {
 | ||
| 					this.currentVersion = versionMatch[1];
 | ||
| 					console.log(`✅ Current agent version: ${this.currentVersion}`);
 | ||
| 				} else {
 | ||
| 					console.log(
 | ||
| 						"⚠️ Could not parse version from agent help output:",
 | ||
| 						result.stdout,
 | ||
| 					);
 | ||
| 					this.currentVersion = null;
 | ||
| 				}
 | ||
| 			} catch (execError) {
 | ||
| 				console.error("❌ Failed to execute agent binary:", execError.message);
 | ||
| 				this.currentVersion = null;
 | ||
| 			}
 | ||
| 		} catch (error) {
 | ||
| 			console.error("❌ Failed to get current agent version:", error.message);
 | ||
| 			this.currentVersion = null;
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async checkForUpdates() {
 | ||
| 		try {
 | ||
| 			console.log("🔍 Checking for agent updates...");
 | ||
| 
 | ||
| 			const response = await axios.get(this.githubApiUrl, {
 | ||
| 				timeout: 10000,
 | ||
| 				headers: {
 | ||
| 					"User-Agent": "PatchMon-Server/1.0",
 | ||
| 					Accept: "application/vnd.github.v3+json",
 | ||
| 				},
 | ||
| 			});
 | ||
| 
 | ||
| 			console.log(`📡 GitHub API response status: ${response.status}`);
 | ||
| 			console.log(`📦 Found ${response.data.length} releases`);
 | ||
| 
 | ||
| 			const releases = response.data;
 | ||
| 			if (releases.length === 0) {
 | ||
| 				console.log("ℹ️ No releases found");
 | ||
| 				this.latestVersion = null;
 | ||
| 				this.lastChecked = new Date();
 | ||
| 				return {
 | ||
| 					latestVersion: null,
 | ||
| 					currentVersion: this.currentVersion,
 | ||
| 					hasUpdate: false,
 | ||
| 					lastChecked: this.lastChecked,
 | ||
| 				};
 | ||
| 			}
 | ||
| 
 | ||
| 			const latestRelease = releases[0];
 | ||
| 			this.latestVersion = latestRelease.tag_name.replace("v", ""); // Remove 'v' prefix
 | ||
| 			this.lastChecked = new Date();
 | ||
| 
 | ||
| 			console.log(`📦 Latest agent version: ${this.latestVersion}`);
 | ||
| 
 | ||
| 			// Don't download binaries automatically - only when explicitly requested
 | ||
| 			console.log(
 | ||
| 				"ℹ️ Skipping automatic binary download - binaries will be downloaded on demand",
 | ||
| 			);
 | ||
| 
 | ||
| 			return {
 | ||
| 				latestVersion: this.latestVersion,
 | ||
| 				currentVersion: this.currentVersion,
 | ||
| 				hasUpdate: this.currentVersion !== this.latestVersion,
 | ||
| 				lastChecked: this.lastChecked,
 | ||
| 			};
 | ||
| 		} catch (error) {
 | ||
| 			console.error("❌ Failed to check for updates:", error.message);
 | ||
| 			if (error.response) {
 | ||
| 				console.error(
 | ||
| 					"❌ GitHub API error:",
 | ||
| 					error.response.status,
 | ||
| 					error.response.statusText,
 | ||
| 				);
 | ||
| 				console.error(
 | ||
| 					"❌ Rate limit info:",
 | ||
| 					error.response.headers["x-ratelimit-remaining"],
 | ||
| 					"/",
 | ||
| 					error.response.headers["x-ratelimit-limit"],
 | ||
| 				);
 | ||
| 			}
 | ||
| 			throw error;
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async downloadBinariesToAgentsFolder(release) {
 | ||
| 		try {
 | ||
| 			console.log(
 | ||
| 				`⬇️ Downloading binaries for version ${release.tag_name} to agents folder...`,
 | ||
| 			);
 | ||
| 
 | ||
| 			for (const arch of this.supportedArchitectures) {
 | ||
| 				const assetName = `patchmon-agent-${arch}`;
 | ||
| 				const asset = release.assets.find((a) => a.name === assetName);
 | ||
| 
 | ||
| 				if (!asset) {
 | ||
| 					console.warn(`⚠️ Binary not found for architecture: ${arch}`);
 | ||
| 					continue;
 | ||
| 				}
 | ||
| 
 | ||
| 				const binaryPath = path.join(this.agentsDir, assetName);
 | ||
| 
 | ||
| 				console.log(`⬇️ Downloading ${assetName}...`);
 | ||
| 
 | ||
| 				const response = await axios.get(asset.browser_download_url, {
 | ||
| 					responseType: "stream",
 | ||
| 					timeout: 60000,
 | ||
| 				});
 | ||
| 
 | ||
| 				const writer = require("node:fs").createWriteStream(binaryPath);
 | ||
| 				response.data.pipe(writer);
 | ||
| 
 | ||
| 				await new Promise((resolve, reject) => {
 | ||
| 					writer.on("finish", resolve);
 | ||
| 					writer.on("error", reject);
 | ||
| 				});
 | ||
| 
 | ||
| 				// Make executable
 | ||
| 				await fs.chmod(binaryPath, "755");
 | ||
| 
 | ||
| 				console.log(`✅ Downloaded: ${assetName} to agents folder`);
 | ||
| 			}
 | ||
| 		} catch (error) {
 | ||
| 			console.error(
 | ||
| 				"❌ Failed to download binaries to agents folder:",
 | ||
| 				error.message,
 | ||
| 			);
 | ||
| 			throw error;
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async downloadBinaryForVersion(version, architecture) {
 | ||
| 		try {
 | ||
| 			console.log(
 | ||
| 				`⬇️ Downloading binary for version ${version} architecture ${architecture}...`,
 | ||
| 			);
 | ||
| 
 | ||
| 			// Get the release info from GitHub
 | ||
| 			const response = await axios.get(this.githubApiUrl, {
 | ||
| 				timeout: 10000,
 | ||
| 				headers: {
 | ||
| 					"User-Agent": "PatchMon-Server/1.0",
 | ||
| 					Accept: "application/vnd.github.v3+json",
 | ||
| 				},
 | ||
| 			});
 | ||
| 
 | ||
| 			const releases = response.data;
 | ||
| 			const release = releases.find(
 | ||
| 				(r) => r.tag_name.replace("v", "") === version,
 | ||
| 			);
 | ||
| 
 | ||
| 			if (!release) {
 | ||
| 				throw new Error(`Release ${version} not found`);
 | ||
| 			}
 | ||
| 
 | ||
| 			const assetName = `patchmon-agent-${architecture}`;
 | ||
| 			const asset = release.assets.find((a) => a.name === assetName);
 | ||
| 
 | ||
| 			if (!asset) {
 | ||
| 				throw new Error(`Binary not found for architecture: ${architecture}`);
 | ||
| 			}
 | ||
| 
 | ||
| 			const binaryPath = path.join(
 | ||
| 				this.agentBinariesDir,
 | ||
| 				`${release.tag_name}-${assetName}`,
 | ||
| 			);
 | ||
| 
 | ||
| 			console.log(`⬇️ Downloading ${assetName}...`);
 | ||
| 
 | ||
| 			const downloadResponse = await axios.get(asset.browser_download_url, {
 | ||
| 				responseType: "stream",
 | ||
| 				timeout: 60000,
 | ||
| 			});
 | ||
| 
 | ||
| 			const writer = require("node:fs").createWriteStream(binaryPath);
 | ||
| 			downloadResponse.data.pipe(writer);
 | ||
| 
 | ||
| 			await new Promise((resolve, reject) => {
 | ||
| 				writer.on("finish", resolve);
 | ||
| 				writer.on("error", reject);
 | ||
| 			});
 | ||
| 
 | ||
| 			// Make executable
 | ||
| 			await fs.chmod(binaryPath, "755");
 | ||
| 
 | ||
| 			console.log(`✅ Downloaded: ${assetName}`);
 | ||
| 			return binaryPath;
 | ||
| 		} catch (error) {
 | ||
| 			console.error(
 | ||
| 				`❌ Failed to download binary ${version}-${architecture}:`,
 | ||
| 				error.message,
 | ||
| 			);
 | ||
| 			throw error;
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async getBinaryPath(version, architecture) {
 | ||
| 		const binaryName = `patchmon-agent-${architecture}`;
 | ||
| 		const binaryPath = path.join(this.agentsDir, binaryName);
 | ||
| 
 | ||
| 		try {
 | ||
| 			await fs.access(binaryPath);
 | ||
| 			return binaryPath;
 | ||
| 		} catch {
 | ||
| 			throw new Error(`Binary not found: ${binaryName} version ${version}`);
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async serveBinary(version, architecture, res) {
 | ||
| 		try {
 | ||
| 			// Check if binary exists, if not download it
 | ||
| 			const binaryPath = await this.getBinaryPath(version, architecture);
 | ||
| 			const stats = await fs.stat(binaryPath);
 | ||
| 
 | ||
| 			res.setHeader("Content-Type", "application/octet-stream");
 | ||
| 			res.setHeader(
 | ||
| 				"Content-Disposition",
 | ||
| 				`attachment; filename="patchmon-agent-${architecture}"`,
 | ||
| 			);
 | ||
| 			res.setHeader("Content-Length", stats.size);
 | ||
| 
 | ||
| 			// Add cache headers
 | ||
| 			res.setHeader("Cache-Control", "public, max-age=3600");
 | ||
| 			res.setHeader("ETag", `"${version}-${architecture}"`);
 | ||
| 
 | ||
| 			const stream = require("node:fs").createReadStream(binaryPath);
 | ||
| 			stream.pipe(res);
 | ||
| 		} catch (_error) {
 | ||
| 			// Binary doesn't exist, try to download it
 | ||
| 			console.log(
 | ||
| 				`⬇️ Binary not found locally, attempting to download ${version}-${architecture}...`,
 | ||
| 			);
 | ||
| 			try {
 | ||
| 				await this.downloadBinaryForVersion(version, architecture);
 | ||
| 				// Retry serving the binary
 | ||
| 				const binaryPath = await this.getBinaryPath(version, architecture);
 | ||
| 				const stats = await fs.stat(binaryPath);
 | ||
| 
 | ||
| 				res.setHeader("Content-Type", "application/octet-stream");
 | ||
| 				res.setHeader(
 | ||
| 					"Content-Disposition",
 | ||
| 					`attachment; filename="patchmon-agent-${architecture}"`,
 | ||
| 				);
 | ||
| 				res.setHeader("Content-Length", stats.size);
 | ||
| 				res.setHeader("Cache-Control", "public, max-age=3600");
 | ||
| 				res.setHeader("ETag", `"${version}-${architecture}"`);
 | ||
| 
 | ||
| 				const stream = require("node:fs").createReadStream(binaryPath);
 | ||
| 				stream.pipe(res);
 | ||
| 			} catch (downloadError) {
 | ||
| 				console.error(
 | ||
| 					`❌ Failed to download binary ${version}-${architecture}:`,
 | ||
| 					downloadError.message,
 | ||
| 				);
 | ||
| 				res
 | ||
| 					.status(404)
 | ||
| 					.json({ error: "Binary not found and could not be downloaded" });
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async getVersionInfo() {
 | ||
| 		let hasUpdate = false;
 | ||
| 		let updateStatus = "unknown";
 | ||
| 		let effectiveLatestVersion = this.currentVersion; // Always use local version if available
 | ||
| 
 | ||
| 		// If we have a local version, use it as the latest regardless of GitHub
 | ||
| 		if (this.currentVersion) {
 | ||
| 			effectiveLatestVersion = this.currentVersion;
 | ||
| 			console.log(
 | ||
| 				`🔄 Using local agent version ${this.currentVersion} as latest`,
 | ||
| 			);
 | ||
| 		} else if (this.latestVersion) {
 | ||
| 			// Fallback to GitHub version only if no local version
 | ||
| 			effectiveLatestVersion = this.latestVersion;
 | ||
| 			console.log(
 | ||
| 				`🔄 No local version found, using GitHub version ${this.latestVersion}`,
 | ||
| 			);
 | ||
| 		}
 | ||
| 
 | ||
| 		if (this.currentVersion && effectiveLatestVersion) {
 | ||
| 			const comparison = compareVersions(
 | ||
| 				this.currentVersion,
 | ||
| 				effectiveLatestVersion,
 | ||
| 			);
 | ||
| 			if (comparison < 0) {
 | ||
| 				hasUpdate = true;
 | ||
| 				updateStatus = "update-available";
 | ||
| 			} else if (comparison > 0) {
 | ||
| 				hasUpdate = false;
 | ||
| 				updateStatus = "newer-version";
 | ||
| 			} else {
 | ||
| 				hasUpdate = false;
 | ||
| 				updateStatus = "up-to-date";
 | ||
| 			}
 | ||
| 		} else if (effectiveLatestVersion && !this.currentVersion) {
 | ||
| 			hasUpdate = true;
 | ||
| 			updateStatus = "no-agent";
 | ||
| 		} else if (this.currentVersion && !effectiveLatestVersion) {
 | ||
| 			// We have a current version but no latest version (GitHub API unavailable)
 | ||
| 			hasUpdate = false;
 | ||
| 			updateStatus = "github-unavailable";
 | ||
| 		} else if (!this.currentVersion && !effectiveLatestVersion) {
 | ||
| 			updateStatus = "no-data";
 | ||
| 		}
 | ||
| 
 | ||
| 		return {
 | ||
| 			currentVersion: this.currentVersion,
 | ||
| 			latestVersion: effectiveLatestVersion,
 | ||
| 			hasUpdate: hasUpdate,
 | ||
| 			updateStatus: updateStatus,
 | ||
| 			lastChecked: this.lastChecked,
 | ||
| 			supportedArchitectures: this.supportedArchitectures,
 | ||
| 			status: effectiveLatestVersion ? "ready" : "no-releases",
 | ||
| 		};
 | ||
| 	}
 | ||
| 
 | ||
| 	async refreshCurrentVersion() {
 | ||
| 		await this.getCurrentAgentVersion();
 | ||
| 		return this.currentVersion;
 | ||
| 	}
 | ||
| 
 | ||
| 	async downloadLatestUpdate() {
 | ||
| 		try {
 | ||
| 			console.log("⬇️ Downloading latest agent update...");
 | ||
| 
 | ||
| 			// First check for updates to get the latest release info
 | ||
| 			const _updateInfo = await this.checkForUpdates();
 | ||
| 
 | ||
| 			if (!this.latestVersion) {
 | ||
| 				throw new Error("No latest version available to download");
 | ||
| 			}
 | ||
| 
 | ||
| 			// Get the release info from GitHub
 | ||
| 			const response = await axios.get(this.githubApiUrl, {
 | ||
| 				timeout: 10000,
 | ||
| 				headers: {
 | ||
| 					"User-Agent": "PatchMon-Server/1.0",
 | ||
| 					Accept: "application/vnd.github.v3+json",
 | ||
| 				},
 | ||
| 			});
 | ||
| 
 | ||
| 			const releases = response.data;
 | ||
| 			const latestRelease = releases[0];
 | ||
| 
 | ||
| 			if (!latestRelease) {
 | ||
| 				throw new Error("No releases found");
 | ||
| 			}
 | ||
| 
 | ||
| 			console.log(
 | ||
| 				`⬇️ Downloading binaries for version ${latestRelease.tag_name}...`,
 | ||
| 			);
 | ||
| 
 | ||
| 			// Download binaries for all architectures directly to agents folder
 | ||
| 			await this.downloadBinariesToAgentsFolder(latestRelease);
 | ||
| 
 | ||
| 			console.log("✅ Latest update downloaded successfully");
 | ||
| 
 | ||
| 			return {
 | ||
| 				success: true,
 | ||
| 				version: this.latestVersion,
 | ||
| 				downloadedArchitectures: this.supportedArchitectures,
 | ||
| 				message: `Successfully downloaded version ${this.latestVersion}`,
 | ||
| 			};
 | ||
| 		} catch (error) {
 | ||
| 			console.error("❌ Failed to download latest update:", error.message);
 | ||
| 			throw error;
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	async getAvailableVersions() {
 | ||
| 		// No local caching - only return latest from GitHub
 | ||
| 		if (this.latestVersion) {
 | ||
| 			return [this.latestVersion];
 | ||
| 		}
 | ||
| 		return [];
 | ||
| 	}
 | ||
| 
 | ||
| 	async getBinaryInfo(version, architecture) {
 | ||
| 		try {
 | ||
| 			// Always use local version if it matches the requested version
 | ||
| 			if (version === this.currentVersion && this.currentVersion) {
 | ||
| 				const binaryPath = await this.getBinaryPath(
 | ||
| 					this.currentVersion,
 | ||
| 					architecture,
 | ||
| 				);
 | ||
| 				const stats = await fs.stat(binaryPath);
 | ||
| 
 | ||
| 				// Calculate file hash
 | ||
| 				const fileBuffer = await fs.readFile(binaryPath);
 | ||
| 				const hash = crypto
 | ||
| 					.createHash("sha256")
 | ||
| 					.update(fileBuffer)
 | ||
| 					.digest("hex");
 | ||
| 
 | ||
| 				return {
 | ||
| 					version: this.currentVersion,
 | ||
| 					architecture,
 | ||
| 					size: stats.size,
 | ||
| 					hash,
 | ||
| 					lastModified: stats.mtime,
 | ||
| 					path: binaryPath,
 | ||
| 				};
 | ||
| 			}
 | ||
| 
 | ||
| 			// For other versions, try to find them in the agents folder
 | ||
| 			const binaryPath = await this.getBinaryPath(version, architecture);
 | ||
| 			const stats = await fs.stat(binaryPath);
 | ||
| 
 | ||
| 			// Calculate file hash
 | ||
| 			const fileBuffer = await fs.readFile(binaryPath);
 | ||
| 			const hash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
 | ||
| 
 | ||
| 			return {
 | ||
| 				version,
 | ||
| 				architecture,
 | ||
| 				size: stats.size,
 | ||
| 				hash,
 | ||
| 				lastModified: stats.mtime,
 | ||
| 				path: binaryPath,
 | ||
| 			};
 | ||
| 		} catch (error) {
 | ||
| 			throw new Error(`Failed to get binary info: ${error.message}`);
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Check if an agent needs an update and push notification if needed
 | ||
| 	 * @param {string} agentApiId - The agent's API ID
 | ||
| 	 * @param {string} agentVersion - The agent's current version
 | ||
| 	 * @param {boolean} force - Force update regardless of version
 | ||
| 	 * @returns {Object} Update check result
 | ||
| 	 */
 | ||
| 	async checkAndPushAgentUpdate(agentApiId, agentVersion, force = false) {
 | ||
| 		try {
 | ||
| 			console.log(
 | ||
| 				`🔍 Checking update for agent ${agentApiId} (version: ${agentVersion})`,
 | ||
| 			);
 | ||
| 
 | ||
| 			// Get current server version info
 | ||
| 			const versionInfo = await this.getVersionInfo();
 | ||
| 
 | ||
| 			if (!versionInfo.latestVersion) {
 | ||
| 				console.log(`⚠️ No latest version available for agent ${agentApiId}`);
 | ||
| 				return {
 | ||
| 					needsUpdate: false,
 | ||
| 					reason: "no-latest-version",
 | ||
| 					message: "No latest version available on server",
 | ||
| 				};
 | ||
| 			}
 | ||
| 
 | ||
| 			// Compare versions
 | ||
| 			const comparison = compareVersions(
 | ||
| 				agentVersion,
 | ||
| 				versionInfo.latestVersion,
 | ||
| 			);
 | ||
| 			const needsUpdate = force || comparison < 0;
 | ||
| 
 | ||
| 			if (needsUpdate) {
 | ||
| 				console.log(
 | ||
| 					`📤 Agent ${agentApiId} needs update: ${agentVersion} → ${versionInfo.latestVersion}`,
 | ||
| 				);
 | ||
| 
 | ||
| 				// Import agentWs service to push notification
 | ||
| 				const { pushUpdateNotification } = require("./agentWs");
 | ||
| 
 | ||
| 				const updateInfo = {
 | ||
| 					version: versionInfo.latestVersion,
 | ||
| 					force: force,
 | ||
| 					downloadUrl: `/api/v1/agent/binary/${versionInfo.latestVersion}/linux-amd64`,
 | ||
| 					message: force
 | ||
| 						? "Force update requested"
 | ||
| 						: `Update available: ${versionInfo.latestVersion}`,
 | ||
| 				};
 | ||
| 
 | ||
| 				const pushed = pushUpdateNotification(agentApiId, updateInfo);
 | ||
| 
 | ||
| 				if (pushed) {
 | ||
| 					console.log(`✅ Update notification pushed to agent ${agentApiId}`);
 | ||
| 					return {
 | ||
| 						needsUpdate: true,
 | ||
| 						reason: force ? "force-update" : "version-outdated",
 | ||
| 						message: `Update notification sent: ${agentVersion} → ${versionInfo.latestVersion}`,
 | ||
| 						targetVersion: versionInfo.latestVersion,
 | ||
| 					};
 | ||
| 				} else {
 | ||
| 					console.log(
 | ||
| 						`⚠️ Failed to push update notification to agent ${agentApiId} (not connected)`,
 | ||
| 					);
 | ||
| 					return {
 | ||
| 						needsUpdate: true,
 | ||
| 						reason: "agent-offline",
 | ||
| 						message: "Agent needs update but is not connected",
 | ||
| 						targetVersion: versionInfo.latestVersion,
 | ||
| 					};
 | ||
| 				}
 | ||
| 			} else {
 | ||
| 				console.log(`✅ Agent ${agentApiId} is up to date: ${agentVersion}`);
 | ||
| 				return {
 | ||
| 					needsUpdate: false,
 | ||
| 					reason: "up-to-date",
 | ||
| 					message: `Agent is up to date: ${agentVersion}`,
 | ||
| 				};
 | ||
| 			}
 | ||
| 		} catch (error) {
 | ||
| 			console.error(
 | ||
| 				`❌ Failed to check update for agent ${agentApiId}:`,
 | ||
| 				error.message,
 | ||
| 			);
 | ||
| 			return {
 | ||
| 				needsUpdate: false,
 | ||
| 				reason: "error",
 | ||
| 				message: `Error checking update: ${error.message}`,
 | ||
| 			};
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Check and push updates to all connected agents
 | ||
| 	 * @param {boolean} force - Force update regardless of version
 | ||
| 	 * @returns {Object} Bulk update result
 | ||
| 	 */
 | ||
| 	async checkAndPushUpdatesToAll(force = false) {
 | ||
| 		try {
 | ||
| 			console.log(
 | ||
| 				`🔍 Checking updates for all connected agents (force: ${force})`,
 | ||
| 			);
 | ||
| 
 | ||
| 			// Import agentWs service to get connected agents
 | ||
| 			const { pushUpdateNotificationToAll } = require("./agentWs");
 | ||
| 
 | ||
| 			const versionInfo = await this.getVersionInfo();
 | ||
| 
 | ||
| 			if (!versionInfo.latestVersion) {
 | ||
| 				return {
 | ||
| 					success: false,
 | ||
| 					message: "No latest version available on server",
 | ||
| 					updatedAgents: 0,
 | ||
| 					totalAgents: 0,
 | ||
| 				};
 | ||
| 			}
 | ||
| 
 | ||
| 			const updateInfo = {
 | ||
| 				version: versionInfo.latestVersion,
 | ||
| 				force: force,
 | ||
| 				downloadUrl: `/api/v1/agent/binary/${versionInfo.latestVersion}/linux-amd64`,
 | ||
| 				message: force
 | ||
| 					? "Force update requested for all agents"
 | ||
| 					: `Update available: ${versionInfo.latestVersion}`,
 | ||
| 			};
 | ||
| 
 | ||
| 			const result = await pushUpdateNotificationToAll(updateInfo);
 | ||
| 
 | ||
| 			console.log(
 | ||
| 				`✅ Bulk update notification sent to ${result.notifiedCount} agents`,
 | ||
| 			);
 | ||
| 
 | ||
| 			return {
 | ||
| 				success: true,
 | ||
| 				message: `Update notifications sent to ${result.notifiedCount} agents`,
 | ||
| 				updatedAgents: result.notifiedCount,
 | ||
| 				totalAgents: result.totalAgents,
 | ||
| 				targetVersion: versionInfo.latestVersion,
 | ||
| 			};
 | ||
| 		} catch (error) {
 | ||
| 			console.error("❌ Failed to push updates to all agents:", error.message);
 | ||
| 			return {
 | ||
| 				success: false,
 | ||
| 				message: `Error pushing updates: ${error.message}`,
 | ||
| 				updatedAgents: 0,
 | ||
| 				totalAgents: 0,
 | ||
| 			};
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| module.exports = new AgentVersionService();
 |