diff --git a/VERSION b/VERSION index 17e51c3..6e8bf73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.1 +0.1.0 diff --git a/package-lock.json b/package-lock.json index f9d446c..dfc0d47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "better-sqlite3": "^9.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^0.545.0", "next": "^15.5.3", "node-pty": "^1.0.0", "react": "^19.0.0", @@ -7369,6 +7370,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.545.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", + "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index 45cfab9..930192b 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "better-sqlite3": "^9.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^0.545.0", "next": "^15.5.3", "node-pty": "^1.0.0", "react": "^19.0.0", diff --git a/src/app/_components/VersionDisplay.tsx b/src/app/_components/VersionDisplay.tsx new file mode 100644 index 0000000..68deef7 --- /dev/null +++ b/src/app/_components/VersionDisplay.tsx @@ -0,0 +1,233 @@ +'use client'; + +import { api } from "~/trpc/react"; +import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; +import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react"; +import { useState } from "react"; + +// Loading overlay component +function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }) { + return ( +
+
+
+
+ +
+
+
+

+ {isNetworkError ? 'Server Restarting' : 'Updating Application'} +

+

+ {isNetworkError + ? 'The server is restarting after the update...' + : 'Please stand by while we update your application...' + } +

+

+ {isNetworkError + ? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!.' + : 'The server will restart automatically when complete.' + } +

+
+
+
+
+
+
+
+
+
+ ); +} + +export function VersionDisplay() { + const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery(); + const [isUpdating, setIsUpdating] = useState(false); + const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null); + const [updateStartTime, setUpdateStartTime] = useState(null); + const [isNetworkError, setIsNetworkError] = useState(false); + + const executeUpdate = api.version.executeUpdate.useMutation({ + onSuccess: (result: any) => { + const now = Date.now(); + const elapsed = updateStartTime ? now - updateStartTime : 0; + + + setUpdateResult({ success: result.success, message: result.message }); + + if (result.success) { + // The script now runs independently, so we show a longer overlay + // and wait for the server to restart + setIsNetworkError(true); + setUpdateResult({ success: true, message: 'Update in progress... Server will restart automatically.' }); + + // Wait longer for the update to complete and server to restart + setTimeout(() => { + setIsUpdating(false); + setIsNetworkError(false); + // Try to reload after the update completes + setTimeout(() => { + window.location.reload(); + }, 10000); // 10 seconds to allow for update completion + }, 5000); // Show overlay for 5 seconds + } else { + // For errors, show for at least 1 second + const remainingTime = Math.max(0, 1000 - elapsed); + setTimeout(() => { + setIsUpdating(false); + }, remainingTime); + } + }, + onError: (error) => { + const now = Date.now(); + const elapsed = updateStartTime ? now - updateStartTime : 0; + + // Check if this is a network error (expected during server restart) + const isNetworkError = error.message.includes('Failed to fetch') || + error.message.includes('NetworkError') || + error.message.includes('fetch') || + error.message.includes('network'); + + if (isNetworkError && elapsed < 60000) { // If it's a network error within 30 seconds, treat as success + setIsNetworkError(true); + setUpdateResult({ success: true, message: 'Update in progress... Server is restarting.' }); + + // Wait longer for server to come back up + setTimeout(() => { + setIsUpdating(false); + setIsNetworkError(false); + // Try to reload after a longer delay + setTimeout(() => { + window.location.reload(); + }, 5000); + }, 3000); + } else { + // For real errors, show for at least 1 second + setUpdateResult({ success: false, message: error.message }); + const remainingTime = Math.max(0, 1000 - elapsed); + setTimeout(() => { + setIsUpdating(false); + }, remainingTime); + } + } + }); + + const handleUpdate = () => { + setIsUpdating(true); + setUpdateResult(null); + setIsNetworkError(false); + setUpdateStartTime(Date.now()); + executeUpdate.mutate(); + }; + + if (isLoading) { + return ( +
+ + Loading... + +
+ ); + } + + if (error || !versionStatus?.success) { + return ( +
+ + v{versionStatus?.currentVersion ?? 'Unknown'} + + + (Unable to check for updates) + +
+ ); + } + + const { currentVersion, isUpToDate, updateAvailable, releaseInfo } = versionStatus; + + return ( + <> + {/* Loading overlay */} + {isUpdating && } + +
+ + v{currentVersion} + + + {updateAvailable && releaseInfo && ( +
+
+ + Update Available + +
+
+
How to update:
+
Click the button to update
+
or update manually:
+
cd $PVESCRIPTLOCAL_DIR
+
git pull
+
npm install
+
npm run build
+
npm start
+
+
+
+
+ + + + + + + + {updateResult && ( +
+ {updateResult.message} +
+ )} +
+ )} + + {isUpToDate && ( + + ✓ Up to date + + )} +
+ + ); +} diff --git a/src/app/_components/ui/badge.tsx b/src/app/_components/ui/badge.tsx new file mode 100644 index 0000000..cae6814 --- /dev/null +++ b/src/app/_components/ui/badge.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import { cn } from "~/lib/utils" + +export interface BadgeProps extends React.HTMLAttributes { + variant?: "default" | "secondary" | "destructive" | "outline" +} + +function Badge({ className, variant = "default", ...props }: BadgeProps) { + const variantClasses = { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + } + + return ( +
+ ) +} + +export { Badge } diff --git a/src/app/page.tsx b/src/app/page.tsx index dbd8a17..b1298ef 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,6 +8,7 @@ import { InstalledScriptsTab } from './_components/InstalledScriptsTab'; import { ResyncButton } from './_components/ResyncButton'; import { Terminal } from './_components/Terminal'; import { SettingsButton } from './_components/SettingsButton'; +import { VersionDisplay } from './_components/VersionDisplay'; import { Button } from './_components/ui/button'; export default function Home() { @@ -30,9 +31,12 @@ export default function Home() {

🚀 PVE Scripts Management

-

+

Manage and execute Proxmox helper scripts locally with live output streaming

+
+ +
{/* Controls */} diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 833f334..2b3a3fc 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,6 +1,7 @@ import { scriptsRouter } from "~/server/api/routers/scripts"; import { installedScriptsRouter } from "~/server/api/routers/installedScripts"; import { serversRouter } from "~/server/api/routers/servers"; +import { versionRouter } from "~/server/api/routers/version"; import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; /** @@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({ scripts: scriptsRouter, installedScripts: installedScriptsRouter, servers: serversRouter, + version: versionRouter, }); // export type definition of API diff --git a/src/server/api/routers/version.ts b/src/server/api/routers/version.ts new file mode 100644 index 0000000..9aa0a70 --- /dev/null +++ b/src/server/api/routers/version.ts @@ -0,0 +1,148 @@ +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import { spawn } from "child_process"; + +interface GitHubRelease { + tag_name: string; + name: string; + published_at: string; + html_url: string; +} + +export const versionRouter = createTRPCRouter({ + // Get current local version + getCurrentVersion: publicProcedure + .query(async () => { + try { + const versionPath = join(process.cwd(), 'VERSION'); + const version = await readFile(versionPath, 'utf-8'); + return { + success: true, + version: version.trim() + }; + } catch (error) { + console.error('Error reading VERSION file:', error); + return { + success: false, + error: 'Failed to read VERSION file', + version: null + }; + } + }), + + getLatestRelease: publicProcedure + .query(async () => { + try { + const response = await fetch('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest'); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`); + } + + const release: GitHubRelease = await response.json(); + + return { + success: true, + release: { + tagName: release.tag_name, + name: release.name, + publishedAt: release.published_at, + htmlUrl: release.html_url + } + }; + } catch (error) { + console.error('Error fetching latest release:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch latest release', + release: null + }; + } + }), + + + getVersionStatus: publicProcedure + .query(async () => { + try { + + const versionPath = join(process.cwd(), 'VERSION'); + const currentVersion = (await readFile(versionPath, 'utf-8')).trim(); + + + const response = await fetch('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest'); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`); + } + + const release: GitHubRelease = await response.json(); + const latestVersion = release.tag_name.replace('v', ''); + + + const isUpToDate = currentVersion === latestVersion; + + return { + success: true, + currentVersion, + latestVersion, + isUpToDate, + updateAvailable: !isUpToDate, + releaseInfo: { + tagName: release.tag_name, + name: release.name, + publishedAt: release.published_at, + htmlUrl: release.html_url + } + }; + } catch (error) { + console.error('Error checking version status:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to check version status', + currentVersion: null, + latestVersion: null, + isUpToDate: false, + updateAvailable: false, + releaseInfo: null + }; + } + }), + + // Execute update script + executeUpdate: publicProcedure + .mutation(async () => { + try { + const updateScriptPath = join(process.cwd(), 'update.sh'); + + // Spawn the update script as a detached process using nohup + // This allows it to run independently and kill the parent Node.js process + const child = spawn('nohup', ['bash', updateScriptPath], { + cwd: process.cwd(), + stdio: ['ignore', 'ignore', 'ignore'], + shell: false, + detached: true + }); + + // Unref the child process so it doesn't keep the parent alive + child.unref(); + + // Immediately return success since we can't wait for completion + // The script will handle its own logging and restart + return { + success: true, + message: 'Update started in background. The server will restart automatically when complete.', + output: '', + error: '' + }; + } catch (error) { + console.error('Error executing update script:', error); + return { + success: false, + message: `Failed to execute update script: ${error instanceof Error ? error.message : 'Unknown error'}`, + output: '', + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + }) +}); diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..0020c3a --- /dev/null +++ b/update.sh @@ -0,0 +1,895 @@ +#!/bin/bash + +# Enhanced update script for ProxmoxVE-Local +# Fetches latest release from GitHub and backs up data directory + +set -euo pipefail # Exit on error, undefined vars, pipe failures + +# Add error trap for debugging +trap 'echo "Error occurred at line $LINENO, command: $BASH_COMMAND"' ERR + +# Configuration +REPO_OWNER="community-scripts" +REPO_NAME="ProxmoxVE-Local" +GITHUB_API="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}" +BACKUP_DIR="/tmp/pve-scripts-backup-$(date +%Y%m%d-%H%M%S)" +DATA_DIR="./data" +LOG_FILE="/tmp/update.log" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Initialize log file +init_log() { + # Clear/create log file + > "$LOG_FILE" + log "Starting ProxmoxVE-Local update process..." + log "Log file: $LOG_FILE" +} + +# Logging function +log() { + echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" >&2 +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" >&2 +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE" >&2 +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE" >&2 +} + +# Check if required tools are available +check_dependencies() { + log "Checking dependencies..." + + local missing_deps=() + + if ! command -v curl &> /dev/null; then + missing_deps+=("curl") + fi + + if ! command -v jq &> /dev/null; then + missing_deps+=("jq") + fi + + if ! command -v npm &> /dev/null; then + missing_deps+=("npm") + fi + + if ! command -v node &> /dev/null; then + missing_deps+=("node") + fi + + if [ ${#missing_deps[@]} -ne 0 ]; then + log_error "Missing dependencies: ${missing_deps[*]}" + log_error "Please install the missing dependencies and try again." + exit 1 + fi + + log_success "All dependencies are available" +} + +# Get latest release info from GitHub API +get_latest_release() { + log "Fetching latest release information from GitHub..." + + local release_info + if ! release_info=$(curl -s --connect-timeout 15 --max-time 60 --retry 2 --retry-delay 3 "$GITHUB_API/releases/latest"); then + log_error "Failed to fetch release information from GitHub API (timeout or network error)" + exit 1 + fi + + # Check if response is valid JSON + if ! echo "$release_info" | jq empty 2>/dev/null; then + log_error "Invalid JSON response from GitHub API" + log "Response: $release_info" + exit 1 + fi + + local tag_name + local download_url + local published_at + + tag_name=$(echo "$release_info" | jq -r '.tag_name') + download_url=$(echo "$release_info" | jq -r '.tarball_url') + published_at=$(echo "$release_info" | jq -r '.published_at') + + if [ "$tag_name" = "null" ] || [ "$download_url" = "null" ] || [ -z "$tag_name" ] || [ -z "$download_url" ]; then + log_error "Failed to parse release information from API response" + log "Tag name: $tag_name" + log "Download URL: $download_url" + exit 1 + fi + + log_success "Latest release: $tag_name (published: $published_at)" + echo "$tag_name|$download_url" +} + +# Backup data directory and .env file +backup_data() { + log "Creating backup directory at $BACKUP_DIR..." + + if ! mkdir -p "$BACKUP_DIR"; then + log_error "Failed to create backup directory" + exit 1 + fi + + # Backup data directory + if [ -d "$DATA_DIR" ]; then + log "Backing up data directory..." + + if ! cp -r "$DATA_DIR" "$BACKUP_DIR/data"; then + log_error "Failed to backup data directory" + exit 1 + else + log_success "Data directory backed up successfully" + fi + else + log_warning "Data directory not found, skipping backup" + fi + + # Backup .env file + if [ -f ".env" ]; then + log "Backing up .env file..." + if ! cp ".env" "$BACKUP_DIR/.env"; then + log_error "Failed to backup .env file" + exit 1 + else + log_success ".env file backed up successfully" + fi + else + log_warning ".env file not found, skipping backup" + fi +} + +# Download and extract latest release +download_release() { + local release_info="$1" + local tag_name="${release_info%|*}" + local download_url="${release_info#*|}" + + log "Downloading release $tag_name..." + + local temp_dir="/tmp/pve-update-$$" + local archive_file="$temp_dir/release.tar.gz" + + # Create temporary directory + if ! mkdir -p "$temp_dir"; then + log_error "Failed to create temporary directory" + exit 1 + fi + + # Download release with timeout and progress + log "Downloading from: $download_url" + log "Target file: $archive_file" + log "Starting curl download..." + + # Test if curl is working + log "Testing curl availability..." + if ! command -v curl >/dev/null 2>&1; then + log_error "curl command not found" + rm -rf "$temp_dir" + exit 1 + fi + + # Test basic connectivity + log "Testing basic connectivity..." + if ! curl -s --connect-timeout 10 --max-time 30 "https://api.github.com" >/dev/null 2>&1; then + log_error "Cannot reach GitHub API" + rm -rf "$temp_dir" + exit 1 + fi + log_success "Connectivity test passed" + + # Create a temporary file for curl output + local curl_log="/tmp/curl_log_$$.txt" + + # Run curl with verbose output + if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 5 -v -o "$archive_file" "$download_url" > "$curl_log" 2>&1; then + log_success "Curl command completed successfully" + # Show some of the curl output for debugging + log "Curl output (first 10 lines):" + head -10 "$curl_log" | while read -r line; do + log "CURL: $line" + done + else + local curl_exit_code=$? + log_error "Curl command failed with exit code: $curl_exit_code" + log_error "Curl output:" + cat "$curl_log" | while read -r line; do + log_error "CURL: $line" + done + rm -f "$curl_log" + rm -rf "$temp_dir" + exit 1 + fi + + # Clean up curl log + rm -f "$curl_log" + + # Verify download + if [ ! -f "$archive_file" ] || [ ! -s "$archive_file" ]; then + log_error "Downloaded file is empty or missing" + rm -rf "$temp_dir" + exit 1 + fi + + local file_size + file_size=$(stat -c%s "$archive_file" 2>/dev/null || echo "0") + log_success "Downloaded release ($file_size bytes)" + + # Extract release + log "Extracting release..." + if ! tar -xzf "$archive_file" -C "$temp_dir"; then + log_error "Failed to extract release" + rm -rf "$temp_dir" + exit 1 + fi + + # Debug: List contents after extraction + log "Contents after extraction:" + ls -la "$temp_dir" >&2 || true + + # Find the extracted directory (GitHub tarballs have a root directory) + log "Looking for extracted directory with pattern: ${REPO_NAME}-*" + local extracted_dir + extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "${REPO_NAME}-*" 2>/dev/null | head -1) + + # If not found with repo name, try alternative patterns + if [ -z "$extracted_dir" ]; then + log "Trying pattern: community-scripts-ProxmoxVE-Local-*" + extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "community-scripts-ProxmoxVE-Local-*" 2>/dev/null | head -1) + fi + + if [ -z "$extracted_dir" ]; then + log "Trying pattern: ProxmoxVE-Local-*" + extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "ProxmoxVE-Local-*" 2>/dev/null | head -1) + fi + + if [ -z "$extracted_dir" ]; then + log "Trying any directory in temp folder" + extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d ! -name "$temp_dir" 2>/dev/null | head -1) + fi + + # If still not found, error out + if [ -z "$extracted_dir" ]; then + log_error "Could not find extracted directory" + rm -rf "$temp_dir" + exit 1 + fi + + log_success "Found extracted directory: $extracted_dir" + log_success "Release downloaded and extracted successfully" + echo "$extracted_dir" +} + +# Clear the original directory before updating +clear_original_directory() { + log "Clearing original directory..." + + # List of files/directories to preserve (already backed up) + local preserve_patterns=( + "data" + ".env" + "*.log" + "update.log" + "*.backup" + "*.bak" + "node_modules" + ".git" + ) + + # Remove all files except preserved ones + while IFS= read -r file; do + local should_preserve=false + local filename=$(basename "$file") + + for pattern in "${preserve_patterns[@]}"; do + if [[ "$filename" == $pattern ]]; then + should_preserve=true + break + fi + done + + if [ "$should_preserve" = false ]; then + rm -f "$file" + fi + done < <(find . -maxdepth 1 -type f ! -name ".*") + + # Remove all directories except preserved ones + while IFS= read -r dir; do + local should_preserve=false + local dirname=$(basename "$dir") + + for pattern in "${preserve_patterns[@]}"; do + if [[ "$dirname" == $pattern ]]; then + should_preserve=true + break + fi + done + + if [ "$should_preserve" = false ]; then + rm -rf "$dir" + fi + done < <(find . -maxdepth 1 -type d ! -name "." ! -name "..") + + log_success "Original directory cleared" +} + +# Restore backup files before building +restore_backup_files() { + log "Restoring .env and data directory from backup..." + + if [ -d "$BACKUP_DIR" ]; then + # Restore .env file + if [ -f "$BACKUP_DIR/.env" ]; then + if [ -f ".env" ]; then + rm -f ".env" + fi + if mv "$BACKUP_DIR/.env" ".env"; then + log_success ".env file restored from backup" + else + log_error "Failed to restore .env file" + return 1 + fi + else + log_warning "No .env file backup found" + fi + + # Restore data directory + if [ -d "$BACKUP_DIR/data" ]; then + if [ -d "data" ]; then + rm -rf "data" + fi + if mv "$BACKUP_DIR/data" "data"; then + log_success "Data directory restored from backup" + else + log_error "Failed to restore data directory" + return 1 + fi + else + log_warning "No data directory backup found" + fi + else + log_error "No backup directory found for restoration" + return 1 + fi +} + +# Check if systemd service exists +check_service() { + if systemctl list-unit-files | grep -q "^pvescriptslocal.service"; then + return 0 + else + return 1 + fi +} + +# Kill application processes directly +kill_processes() { + # Try to find and stop the Node.js process + local pids + pids=$(pgrep -f "node server.js" 2>/dev/null || true) + + # Also check for npm start processes + local npm_pids + npm_pids=$(pgrep -f "npm start" 2>/dev/null || true) + + # Combine all PIDs + if [ -n "$npm_pids" ]; then + pids="$pids $npm_pids" + fi + + if [ -n "$pids" ]; then + log "Stopping application processes: $pids" + + # Send TERM signal to each PID individually + for pid in $pids; do + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + log "Sending TERM signal to PID: $pid" + kill -TERM "$pid" 2>/dev/null || true + fi + done + + # Wait for graceful shutdown with timeout + log "Waiting for graceful shutdown..." + local wait_count=0 + local max_wait=10 # Maximum 10 seconds + + while [ $wait_count -lt $max_wait ]; do + local still_running + still_running=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -z "$still_running" ]; then + log_success "Processes stopped gracefully" + break + fi + sleep 1 + wait_count=$((wait_count + 1)) + log "Waiting... ($wait_count/$max_wait)" + done + + # Force kill any remaining processes + local remaining_pids + remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -n "$remaining_pids" ]; then + log_warning "Force killing remaining processes: $remaining_pids" + pkill -9 -f "node server.js" 2>/dev/null || true + pkill -9 -f "npm start" 2>/dev/null || true + sleep 1 + fi + + # Final check + local final_check + final_check=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -n "$final_check" ]; then + log_warning "Some processes may still be running: $final_check" + else + log_success "All application processes stopped" + fi + else + log "No running application processes found" + fi +} + +# Kill application processes directly +kill_processes() { + # Try to find and stop the Node.js process + local pids + pids=$(pgrep -f "node server.js" 2>/dev/null || true) + + # Also check for npm start processes + local npm_pids + npm_pids=$(pgrep -f "npm start" 2>/dev/null || true) + + # Combine all PIDs + if [ -n "$npm_pids" ]; then + pids="$pids $npm_pids" + fi + + if [ -n "$pids" ]; then + log "Stopping application processes: $pids" + + # Send TERM signal to each PID individually + for pid in $pids; do + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + log "Sending TERM signal to PID: $pid" + kill -TERM "$pid" 2>/dev/null || true + fi + done + + # Wait for graceful shutdown with timeout + log "Waiting for graceful shutdown..." + local wait_count=0 + local max_wait=10 # Maximum 10 seconds + + while [ $wait_count -lt $max_wait ]; do + local still_running + still_running=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -z "$still_running" ]; then + log_success "Processes stopped gracefully" + break + fi + sleep 1 + wait_count=$((wait_count + 1)) + log "Waiting... ($wait_count/$max_wait)" + done + + # Force kill any remaining processes + local remaining_pids + remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -n "$remaining_pids" ]; then + log_warning "Force killing remaining processes: $remaining_pids" + pkill -9 -f "node server.js" 2>/dev/null || true + pkill -9 -f "npm start" 2>/dev/null || true + sleep 1 + fi + + # Final check + local final_check + final_check=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -n "$final_check" ]; then + log_warning "Some processes may still be running: $final_check" + else + log_success "All application processes stopped" + fi + else + log "No running application processes found" + fi +} + +# Stop the application before updating +stop_application() { + log "Stopping application..." + + # Change to the application directory if we're not already there + local app_dir + if [ -f "package.json" ] && [ -f "server.js" ]; then + app_dir="$(pwd)" + else + # Try to find the application directory + app_dir=$(find /root -name "package.json" -path "*/ProxmoxVE-Local*" -exec dirname {} \; 2>/dev/null | head -1) + if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then + cd "$app_dir" || { + log_error "Failed to change to application directory: $app_dir" + return 1 + } + else + log_error "Could not find application directory" + return 1 + fi + fi + + log "Working from application directory: $(pwd)" + + # Check if systemd service exists and is active + if check_service; then + if systemctl is-active --quiet pvescriptslocal.service; then + log "Stopping pvescriptslocal service..." + if systemctl stop pvescriptslocal.service; then + log_success "Service stopped successfully" + else + log_error "Failed to stop service, falling back to process kill" + kill_processes + fi + else + log "Service exists but is not active, checking for running processes..." + kill_processes + fi + else + log "No systemd service found, stopping processes directly..." + kill_processes + fi +} + +# Update application files +update_files() { + local source_dir="$1" + + log "Updating application files..." + + # List of files/directories to exclude from update + local exclude_patterns=( + "data" + "node_modules" + ".git" + ".env" + "*.log" + "update.log" + "*.backup" + "*.bak" + ) + + # Find the actual source directory (strip the top-level directory) + local actual_source_dir + actual_source_dir=$(find "$source_dir" -maxdepth 1 -type d -name "community-scripts-ProxmoxVE-Local-*" | head -1) + + if [ -z "$actual_source_dir" ]; then + log_error "Could not find the actual source directory in $source_dir" + return 1 + fi + + # Use process substitution instead of pipe to avoid subshell issues + local files_copied=0 + local files_excluded=0 + + log "Starting file copy process from: $actual_source_dir" + + # Create a temporary file list to avoid process substitution issues + local file_list="/tmp/file_list_$$.txt" + find "$actual_source_dir" -type f > "$file_list" + + local total_files + total_files=$(wc -l < "$file_list") + log "Found $total_files files to process" + + # Show first few files for debugging + log "First few files to process:" + head -5 "$file_list" | while read -r f; do + log " - $f" + done + + while IFS= read -r file; do + local rel_path="${file#$actual_source_dir/}" + local should_exclude=false + + for pattern in "${exclude_patterns[@]}"; do + if [[ "$rel_path" == $pattern ]]; then + should_exclude=true + break + fi + done + + if [ "$should_exclude" = false ]; then + local target_dir + target_dir=$(dirname "$rel_path") + if [ "$target_dir" != "." ]; then + mkdir -p "$target_dir" + fi + log "Copying: $file -> $rel_path" + if ! cp "$file" "$rel_path"; then + log_error "Failed to copy $rel_path" + rm -f "$file_list" + return 1 + else + files_copied=$((files_copied + 1)) + if [ $((files_copied % 10)) -eq 0 ]; then + log "Copied $files_copied files so far..." + fi + fi + else + files_excluded=$((files_excluded + 1)) + log "Excluded: $rel_path" + fi + done < "$file_list" + + # Clean up temporary file + rm -f "$file_list" + + log "Files processed: $files_copied copied, $files_excluded excluded" + + log_success "Application files updated successfully" +} + +# Install dependencies and build +install_and_build() { + log "Installing dependencies..." + + if ! npm install; then + log_error "Failed to install dependencies" + return 1 + fi + + # Ensure no processes are running before build + log "Ensuring no conflicting processes are running..." + local pids + pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -n "$pids" ]; then + log_warning "Found running processes, stopping them: $pids" + pkill -9 -f "node server.js" 2>/dev/null || true + pkill -9 -f "npm start" 2>/dev/null || true + sleep 2 + fi + + log "Building application..." + # Set NODE_ENV to production for build + export NODE_ENV=production + + if ! npm run build; then + log_error "Failed to build application" + return 1 + fi + + log_success "Dependencies installed and application built successfully" +} + +# Start the application after updating +start_application() { + log "Starting application..." + + # Check if systemd service exists + if check_service; then + log "Starting pvescriptslocal service..." + if systemctl start pvescriptslocal.service; then + log_success "Service started successfully" + # Wait a moment and check if it's running + sleep 2 + if systemctl is-active --quiet pvescriptslocal.service; then + log_success "Service is running" + else + log_warning "Service started but may not be running properly" + fi + else + log_error "Failed to start service, falling back to npm start" + start_with_npm + fi + else + log "No systemd service found, starting with npm..." + start_with_npm + fi +} + +# Start application with npm +start_with_npm() { + log "Starting application with npm start..." + + # Start in background + nohup npm start > server.log 2>&1 & + local npm_pid=$! + + # Wait a moment and check if it started + sleep 3 + if kill -0 $npm_pid 2>/dev/null; then + log_success "Application started with PID: $npm_pid" + else + log_error "Failed to start application with npm" + return 1 + fi +} + +# Rollback function +rollback() { + log_warning "Rolling back to previous version..." + + if [ -d "$BACKUP_DIR" ]; then + log "Restoring from backup directory: $BACKUP_DIR" + + # Restore data directory + if [ -d "$BACKUP_DIR/data" ]; then + log "Restoring data directory..." + if [ -d "$DATA_DIR" ]; then + rm -rf "$DATA_DIR" + fi + if mv "$BACKUP_DIR/data" "$DATA_DIR"; then + log_success "Data directory restored from backup" + else + log_error "Failed to restore data directory" + fi + else + log_warning "No data directory backup found" + fi + + # Restore .env file + if [ -f "$BACKUP_DIR/.env" ]; then + log "Restoring .env file..." + if [ -f ".env" ]; then + rm -f ".env" + fi + if mv "$BACKUP_DIR/.env" ".env"; then + log_success ".env file restored from backup" + else + log_error "Failed to restore .env file" + fi + else + log_warning "No .env file backup found" + fi + + # Clean up backup directory + log "Cleaning up backup directory..." + rm -rf "$BACKUP_DIR" + else + log_error "No backup directory found for rollback" + fi + + log_error "Update failed. Please check the logs and try again." + exit 1 +} + +# Main update process +main() { + init_log + + # Check if we're running from the application directory and not already relocated + if [ -z "${PVE_UPDATE_RELOCATED:-}" ] && [ -f "package.json" ] && [ -f "server.js" ]; then + log "Detected running from application directory" + log "Copying update script to temporary location for safe execution..." + + local temp_script="/tmp/pve-scripts-update-$$.sh" + if ! cp "$0" "$temp_script"; then + log_error "Failed to copy update script to temporary location" + exit 1 + fi + + chmod +x "$temp_script" + log "Executing update from temporary location: $temp_script" + + # Set flag to prevent infinite loop and execute from temporary location + export PVE_UPDATE_RELOCATED=1 + exec "$temp_script" "$@" + fi + + # Ensure we're in the application directory + local app_dir + + # First check if we're already in the right directory + if [ -f "package.json" ] && [ -f "server.js" ]; then + app_dir="$(pwd)" + log "Already in application directory: $app_dir" + else + # Try multiple common locations + for search_path in /opt /root /home /usr/local; do + if [ -d "$search_path" ]; then + app_dir=$(find "$search_path" -name "package.json" -path "*/ProxmoxVE-Local*" -exec dirname {} \; 2>/dev/null | head -1) + if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then + break + fi + fi + done + + if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then + cd "$app_dir" || { + log_error "Failed to change to application directory: $app_dir" + exit 1 + } + log "Changed to application directory: $(pwd)" + else + log_error "Could not find application directory" + log "Searched in: /opt, /root, /home, /usr/local" + exit 1 + fi + fi + + # Check dependencies + check_dependencies + + # Get latest release info + local release_info + release_info=$(get_latest_release) + + # Backup data directory + backup_data + + # Stop the application before updating (now running from /tmp/) + stop_application + + # Double-check that no processes are running + local remaining_pids + remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true) + if [ -n "$remaining_pids" ]; then + log_warning "Force killing remaining processes" + pkill -9 -f "node server.js" 2>/dev/null || true + pkill -9 -f "npm start" 2>/dev/null || true + sleep 2 + fi + + # Download and extract release + local source_dir + source_dir=$(download_release "$release_info") + log "Download completed, source_dir: $source_dir" + + # Clear the original directory before updating + log "Clearing original directory..." + clear_original_directory + log "Original directory cleared successfully" + + # Update files + log "Starting file update process..." + if ! update_files "$source_dir"; then + log_error "File update failed, rolling back..." + rollback + fi + log "File update completed successfully" + + # Restore .env and data directory before building + log "Restoring backup files..." + restore_backup_files + log "Backup files restored successfully" + + # Install dependencies and build + log "Starting install and build process..." + if ! install_and_build; then + log_error "Install and build failed, rolling back..." + rollback + fi + log "Install and build completed successfully" + + # Cleanup + log "Cleaning up temporary files..." + rm -rf "$source_dir" + rm -rf "/tmp/pve-update-$$" + + # Clean up temporary script if it exists + if [ -f "/tmp/pve-scripts-update-$$.sh" ]; then + rm -f "/tmp/pve-scripts-update-$$.sh" + fi + + # Start the application + start_application + + log_success "Update completed successfully!" +} + +# Run main function with error handling +if ! main "$@"; then + log_error "Update script failed with exit code $?" + exit 1 +fi \ No newline at end of file