From e60465e4394cc5acf206603d61a3cc46a0b889f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 20:31:38 +0000 Subject: [PATCH 1/4] fix: remove Linux-only PowerShell nginx installer (#98) nginx_installer.ps1 was a PowerShell reimplementation of nginx_installer.sh that only ran on Linux (it hard-checked $IsLinux), duplicating the bash installer for no benefit and doubling the maintenance burden on every version bump. Drop it and the tooling built around syncing it (update-nginx-checksums.sh/.yml, CONTRIBUTING.md). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015JTpvkRWFpJNNhEdHMekLe --- .github/scripts/update-nginx-checksums.sh | 17 +- .github/workflows/update-nginx-checksums.yml | 3 +- CONTRIBUTING.md | 6 +- nginx/nginx_installer.ps1 | 892 ------------------- 4 files changed, 5 insertions(+), 913 deletions(-) delete mode 100644 nginx/nginx_installer.ps1 diff --git a/.github/scripts/update-nginx-checksums.sh b/.github/scripts/update-nginx-checksums.sh index ead1efd..191631f 100755 --- a/.github/scripts/update-nginx-checksums.sh +++ b/.github/scripts/update-nginx-checksums.sh @@ -97,7 +97,7 @@ Show-Usage() { Usage: update-nginx-checksums.sh [--apply] Options: - --apply Apply calculated checksums to nginx/nginx_installer.sh and nginx/nginx_installer.ps1 without prompting + --apply Apply calculated checksums to nginx/nginx_installer.sh without prompting -h, --help Show this help EOF } @@ -124,7 +124,6 @@ done REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" readonly REPO_ROOT readonly BASH_INSTALLER="$REPO_ROOT/nginx/nginx_installer.sh" -readonly PS_INSTALLER="$REPO_ROOT/nginx/nginx_installer.ps1" cd "$REPO_ROOT" @@ -139,12 +138,6 @@ Set-BashVar() { sed -i "s/^${key}=\"[^\"]*\"$/${key}=\"${value}\"/" "$BASH_INSTALLER" } -Set-PsVar() { - local key=$1 - local value=$2 - sed -i "s#^\\(\\\$Script:${key}[[:space:]]*=[[:space:]]*'\\)[^']*'#\\1${value}'#" "$PS_INSTALLER" -} - Get-UrlHash() { local url=$1 local file=$2 @@ -231,13 +224,5 @@ Set-BashVar HEADERS_MORE_SHA256 "$HEADERS_MORE_SHA256" Set-BashVar ZSTD_MODULE_SHA256 "$ZSTD_MODULE_SHA256" Set-BashVar ACME_MODULE_SHA256 "$ACME_MODULE_SHA256" -Set-PsVar NGINX_SHA256 "$NGINX_SHA256" -Set-PsVar PCRE2_SHA256 "$PCRE2_SHA256" -Set-PsVar ZLIB_SHA256 "$ZLIB_SHA256" -Set-PsVar HEADERS_MORE_SHA256 "$HEADERS_MORE_SHA256" -Set-PsVar ZSTD_MODULE_SHA256 "$ZSTD_MODULE_SHA256" -Set-PsVar ACME_MODULE_SHA256 "$ACME_MODULE_SHA256" - Write-Log SUCCESS "Updated checksums in:" echo " - nginx/nginx_installer.sh" -echo " - nginx/nginx_installer.ps1" diff --git a/.github/workflows/update-nginx-checksums.yml b/.github/workflows/update-nginx-checksums.yml index 105ac4f..c49e568 100644 --- a/.github/workflows/update-nginx-checksums.yml +++ b/.github/workflows/update-nginx-checksums.yml @@ -9,7 +9,6 @@ on: branches: [main] paths: - 'nginx/nginx_installer.sh' - - 'nginx/nginx_installer.ps1' permissions: contents: write @@ -35,7 +34,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add nginx/nginx_installer.sh nginx/nginx_installer.ps1 + git add nginx/nginx_installer.sh if git diff --staged --quiet; then echo "Checksums are already up to date, nothing to commit." else diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f5c099..4c4f3a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,11 +102,11 @@ Renovate normally handles version bumps. For manual updates: ### Example: Updating NGINX manually ```bash -# 1. Edit nginx/nginx_installer.sh AND nginx/nginx_installer.ps1 -# Update these lines (same format!): +# 1. Edit nginx/nginx_installer.sh +# Update this line: NGINX_VERSION="1.31.1" -# 2. Recalculate all checksums for both installers +# 2. Recalculate the checksum .github/scripts/update-nginx-checksums.sh --apply # 3. Test the script diff --git a/nginx/nginx_installer.ps1 b/nginx/nginx_installer.ps1 deleted file mode 100644 index 2c8237d..0000000 --- a/nginx/nginx_installer.ps1 +++ /dev/null @@ -1,892 +0,0 @@ -<# -.SYNOPSIS - NGINX Installer Script for Linux (PowerShell) - -.DESCRIPTION - Builds and installs NGINX with OpenSSL, HTTP/3, zstd compression, - and ACME support on Linux. - -.PARAMETER Command - install - Build and install NGINX - remove - Uninstall NGINX - -.EXAMPLE - ./nginx_installer.ps1 -Command install -#> - -#!/usr/bin/env pwsh -#Requires -Version 7.1 - -param( - [ValidateSet('install','remove')] - [string]$Command = 'install' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Linux only check -if (-not $IsLinux) { - Write-Host 'ERROR: This script is for Linux only.' -ForegroundColor Red - exit 1 -} - -# ============================================================================ -# Version Configuration -# ============================================================================ - -# NGINX -$Script:NGINX_VERSION = '1.31.5' -$Script:NGINX_SHA256 = 'e951607d534836624bd36b6b45a71dbfb055237deae3738da6bbf3270dada279' - -# PCRE2 -$Script:PCRE2_VERSION = '10.48' -$Script:PCRE2_SHA256 = 'ebcc25aadf2a51fa1fefa9b8bc9e7a79b3dae86870a0f1152a22e42befd46888' - -# Zlib -$Script:ZLIB_VERSION = '1.3.2' -$Script:ZLIB_SHA256 = 'bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16' - -# Headers-More Module -$Script:HEADERS_MORE_VERSION = '0.39' -$Script:HEADERS_MORE_SHA256 = 'dde68d3fa2a9fc7f52e436d2edc53c6d703dcd911283965d889102d3a877c778' - -# Zstd Module -$Script:ZSTD_MODULE_VERSION = '0.1.1' -$Script:ZSTD_MODULE_SHA256 = '707d534f8ca4263ff043066db15eac284632aea875f9fe98c96cea9529e15f41' - -# ACME Module -$Script:ACME_MODULE_VERSION = '0.4.1' -$Script:ACME_MODULE_SHA256 = 'b4f99f971bd0bebc89b2037f3afeaa3281004fe434de558df87d69cab2be1f22' - -# ============================================================================ -# Static Configuration -# ============================================================================ - -$Script:BUILD_DIR = "/var/tmp/nginx-build-$(Get-Date -Format 'yyyyMMdd-HHmmss')" -$Script:BACKUP_DIR = "/var/lib/nginx-backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')" -$Script:LOG_FILE = "/var/log/nginx-installer-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" - -# FHS-compliant install paths (matching what dnf/rpm would use) -$Script:NGINX_PREFIX = '/usr/share/nginx' -$Script:NGINX_LIBDIR = if ((& uname -m) -in @('x86_64', 'aarch64')) { '/usr/lib64' } else { '/usr/lib' } -$Script:NGINX_MODULES_PATH = "$Script:NGINX_LIBDIR/nginx/modules" - -# Download URLs -$Script:NGINX_URL = "https://github.com/nginx/nginx/releases/download/release-$($Script:NGINX_VERSION)/nginx-$($Script:NGINX_VERSION).tar.gz" -$Script:PCRE2_URL = "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-$($Script:PCRE2_VERSION)/pcre2-$($Script:PCRE2_VERSION).tar.gz" -$Script:ZLIB_URL = "https://github.com/madler/zlib/releases/download/v$($Script:ZLIB_VERSION)/zlib-$($Script:ZLIB_VERSION).tar.gz" -$Script:HEADERS_MORE_URL = "https://github.com/openresty/headers-more-nginx-module/archive/refs/tags/v$($Script:HEADERS_MORE_VERSION).tar.gz" -$Script:ZSTD_MODULE_URL = "https://github.com/tokers/zstd-nginx-module/archive/refs/tags/$($Script:ZSTD_MODULE_VERSION).tar.gz" -$Script:ACME_MODULE_URL = "https://github.com/nginx/nginx-acme/releases/download/v$($Script:ACME_MODULE_VERSION)/nginx-acme-$($Script:ACME_MODULE_VERSION).tar.gz" - -# Ensure directories exist -$null = New-Item -ItemType Directory -Path $Script:BUILD_DIR -Force -ErrorAction SilentlyContinue -$null = New-Item -ItemType Directory -Path (Split-Path $Script:LOG_FILE -Parent) -Force -ErrorAction SilentlyContinue - -# Start transcript logging (equivalent to Bash exec > >(tee -a ...)) -if (-not (Test-Path $Script:LOG_FILE)) { - Start-Transcript -Path $Script:LOG_FILE -Append | Out-Null -} - -# ============================================================================ -# Helper Functions -# ============================================================================ - -function Write-Log { - param( - [string]$Level, - [string]$Message - ) - $logMessage = "[$Level] $Message" - Write-Host $logMessage - Add-Content -Path $Script:LOG_FILE -Value $logMessage -ErrorAction SilentlyContinue -} - -function Stop-Script { - param([string]$Message) - Write-Log 'ERROR' $Message - exit 1 -} - -function Test-Hash { - param( - [string]$File, - [string]$Expected - ) - $actual = (Get-FileHash -Path $File -Algorithm SHA256).Hash.ToLower() - if ($actual -ne $Expected.ToLower()) { - Stop-Script "Checksum failed: $File" - } -} - -function Get-File { - param( - [string]$Url, - [string]$OutFile, - [string]$Hash - ) - $fullPath = Join-Path $Script:BUILD_DIR $OutFile - - if (Test-Path $fullPath) { - Test-Hash -File $fullPath -Expected $Hash - return - } - - Write-Log 'INFO' "Downloading $(Split-Path -Leaf $OutFile)..." - try { - Push-Location $Script:BUILD_DIR - & curl -fsSL $Url -o $OutFile - if ($LASTEXITCODE -ne 0) { throw 'Download failed' } - Pop-Location - } catch { - Pop-Location -ErrorAction SilentlyContinue - Stop-Script "Download failed: $Url" - } - Test-Hash -File $fullPath -Expected $Hash -} - -function Get-PkgMgr { - if (Get-Command apt-get -ErrorAction SilentlyContinue) { - return 'apt' - } elseif (Get-Command dnf -ErrorAction SilentlyContinue) { - return 'dnf' - } elseif (Get-Command pacman -ErrorAction SilentlyContinue) { - return 'pacman' - } else { - return 'unknown' - } -} - -# ============================================================================ -# System Dependencies -# ============================================================================ - -function Install-Dependencies { - try { - $uid = & id -u 2>$null - if ($uid -ne '0') { Stop-Script 'Run as root' } - } catch { - Stop-Script 'Cannot determine user ID; run as root' - } - - if (-not (Get-Command curl -ErrorAction SilentlyContinue)) { - Stop-Script 'curl required' - } - - Write-Log 'INFO' 'Installing build dependencies' - - $mgr = Get-PkgMgr - switch ($mgr) { - 'apt' { - $env:DEBIAN_FRONTEND = 'noninteractive' - & apt-get update -qq 2>&1 | Out-Null - & apt-get install -y build-essential libpcre2-dev zlib1g-dev libzstd-dev libssl-dev curl gcc make cargo pkg-config clang gawk cmake 2>&1 | Out-Null - } - 'dnf' { - & dnf install -y -q gcc gcc-c++ make pcre2-devel zlib-devel libzstd-devel openssl-devel curl perl cargo pkgconf-pkg-config clang gawk cmake 2>&1 | Out-Null - } - 'pacman' { - $null = & pacman -Sy --noconfirm --needed base-devel pcre2 zstd openssl curl clang gawk cmake pkgconf 2>&1 - $pacmanExit = $LASTEXITCODE - if ($pacmanExit -ne 0) { - Write-Log WARN "pacman install failed, will try rustup for cargo. Note: zlib is not required (zlib-ng-compat provides it)." - } - } - default { - Stop-Script 'Unsupported package manager. Only apt, dnf and pacman are supported.' - } - } - - # Verify cargo availability - if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { - Write-Log 'WARN' 'Cargo not found. Installing rustup...' - bash -lc "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" | Out-Null - } - - Write-Log 'INFO' 'Dependencies installed' -} - -function Update-SystemPackages { - try { - $uid = & id -u 2>$null - if ($uid -ne '0') { Stop-Script 'Run as root' } - } catch { - Stop-Script 'Cannot determine user ID; run as root' - } - - Write-Log 'INFO' 'Updating system packages' - - $mgr = Get-PkgMgr - switch ($mgr) { - 'apt' { - $env:DEBIAN_FRONTEND = 'noninteractive' - & apt-get update -qq 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { Write-Log 'WARN' 'apt-get update failed' } - & apt-get upgrade -y -q 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { Stop-Script 'apt-get upgrade failed' } - } - 'dnf' { - $dnfOutput = & dnf upgrade -y -q 2>&1 | Select-String -Pattern '(Complete|Error|Failed)' - if ($LASTEXITCODE -ne 0) { Stop-Script 'dnf upgrade failed' } - if ($dnfOutput) { $dnfOutput | Out-Host } - } - 'pacman' { - & pacman -Syu --noconfirm 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { Write-Log 'WARN' 'pacman upgrade failed' } - } - default { - Write-Log 'WARN' 'Unable to detect package manager' - } - } - - Write-Log 'INFO' 'System packages updated' -} - -# ============================================================================ -# Download Sources -# ============================================================================ - -function Get-Sources { - Push-Location $Script:BUILD_DIR - Write-Log 'INFO' 'Downloading sources' - - Get-File $Script:NGINX_URL 'nginx.tgz' $Script:NGINX_SHA256 - Get-File $Script:PCRE2_URL 'pcre2.tgz' $Script:PCRE2_SHA256 - Get-File $Script:ZLIB_URL 'zlib.tgz' $Script:ZLIB_SHA256 - Get-File $Script:HEADERS_MORE_URL 'headers.tgz' $Script:HEADERS_MORE_SHA256 - Get-File $Script:ZSTD_MODULE_URL 'zstd.tgz' $Script:ZSTD_MODULE_SHA256 - Get-File $Script:ACME_MODULE_URL 'acme.tgz' $Script:ACME_MODULE_SHA256 - - Write-Log 'INFO' 'Extracting archives' - - # Clean previous extractions - Remove-Item nginx, openssl, pcre2, zlib, headers-more, zstd-module, nginx-acme -Recurse -Force -ErrorAction SilentlyContinue - - & tar xzf nginx.tgz - Move-Item "nginx-$Script:NGINX_VERSION" nginx -Force - - & tar xzf pcre2.tgz - Move-Item "pcre2-$Script:PCRE2_VERSION" pcre2 -Force - - & tar xzf zlib.tgz - Move-Item "zlib-$Script:ZLIB_VERSION" zlib -Force - - & tar xzf headers.tgz - Move-Item "headers-more-nginx-module-$Script:HEADERS_MORE_VERSION" headers-more -Force - - & tar xzf zstd.tgz - Move-Item "zstd-nginx-module-$Script:ZSTD_MODULE_VERSION" zstd-module -Force - - & tar xzf acme.tgz - Move-Item "nginx-acme-$Script:ACME_MODULE_VERSION" nginx-acme -Force - - Pop-Location - Write-Log 'INFO' 'Sources ready' -} - -# ============================================================================ -# Build Functions -# ============================================================================ - -function Build-Nginx { - # Clean compiler temp files (not the build dir itself — managed by finally block) - Get-ChildItem /tmp -Filter 'cc*' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue - Get-ChildItem /tmp -Filter 'tmp.*' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue - - # Check disk space in /var/tmp - $tmpSpace = (& df /var/tmp | Select-Object -Skip 1 | ForEach-Object { - $_.Split([char[]]@(' ', "`t"), [System.StringSplitOptions]::RemoveEmptyEntries)[3] - }) - if ($tmpSpace -and [int]$tmpSpace -lt 1048576) { - Write-Log 'WARN' 'Low disk space in /var/tmp, using build directory' - $env:TMPDIR = $Script:BUILD_DIR - } - - # Ensure cc symlink exists - if (-not (Get-Command cc -ErrorAction SilentlyContinue)) { - $gccPath = Get-Command gcc -ErrorAction SilentlyContinue - if ($gccPath -and (Test-Path '/usr/local/bin')) { - New-Item -ItemType SymbolicLink -Path '/usr/local/bin/cc' -Target $gccPath.Source -Force -ErrorAction SilentlyContinue | Out-Null - $env:PATH = "/usr/local/bin:$($env:PATH)" - } - } - - # Build NGINX - Write-Log 'INFO' "Building Nginx $Script:NGINX_VERSION" - $nginxSrc = Join-Path $Script:BUILD_DIR 'nginx' - if (-not (Test-Path $nginxSrc)) { Stop-Script 'Nginx source missing' } - Push-Location $nginxSrc - - # Verify libzstd availability - $ldconfigAvail = bash -c 'command -v ldconfig >/dev/null 2>&1 && echo yes || echo no' - if ($ldconfigAvail.Trim() -eq 'yes') { - $ldconfigResult = bash -c 'ldconfig -p 2>/dev/null | grep -q "libzstd.so" && echo found || echo missing' - if ($ldconfigResult.Trim() -eq 'missing') { - Stop-Script 'Shared libzstd not found. Install libzstd-dev/devel' - } - } else { - $zstdPaths = @('/usr/lib/libzstd.so', '/usr/lib/libzstd.so.1', '/usr/lib64/libzstd.so', '/usr/lib64/libzstd.so.1', '/usr/local/lib/libzstd.so') - $found = $zstdPaths | Where-Object { Test-Path $_ } - if (-not $found) { - Stop-Script 'Shared libzstd not found. Install libzstd-dev/devel' - } - } - - $pcre2Path = Join-Path $Script:BUILD_DIR 'pcre2' - $zlibPath = Join-Path $Script:BUILD_DIR 'zlib' - $headersPath = Join-Path $Script:BUILD_DIR 'headers-more' - $zstdPath = Join-Path $Script:BUILD_DIR 'zstd-module' - - $configCmd = @" -export TMPDIR='$Script:BUILD_DIR' -export CC=gcc -export LDFLAGS='-lzstd' -./configure \ - --with-compat \ - --prefix=$Script:NGINX_PREFIX \ - --sbin-path=/usr/sbin/nginx \ - --conf-path=/etc/nginx/nginx.conf \ - --http-log-path=/var/log/nginx/access.log \ - --error-log-path=/var/log/nginx/error.log \ - --pid-path=/run/nginx.pid \ - --lock-path=/run/lock/nginx.lock \ - --http-client-body-temp-path=/var/lib/nginx/tmp/client_body \ - --http-proxy-temp-path=/var/lib/nginx/tmp/proxy \ - --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi \ - --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi \ - --http-scgi-temp-path=/var/lib/nginx/tmp/scgi \ - --with-pcre=$pcre2Path \ - --with-zlib=$zlibPath \ - --with-pcre-jit \ - --with-http_ssl_module \ - --with-http_v2_module \ - --with-http_v3_module \ - --with-http_gzip_static_module \ - --with-http_stub_status_module \ - --with-http_realip_module \ - --with-http_sub_module \ - --with-http_secure_link_module \ - --with-stream \ - --with-stream_ssl_module \ - --with-stream_ssl_preread_module \ - --with-stream_realip_module \ - --with-file-aio \ - --with-threads \ - --modules-path=$Script:NGINX_MODULES_PATH \ - --add-dynamic-module=$headersPath \ - --add-dynamic-module=$zstdPath -"@ - - $configOutput = bash -c "$configCmd 2>&1" - if ($LASTEXITCODE -ne 0) { - $lastLines = ($configOutput -split "`n" | Select-Object -Last 20) -join "`n" - Write-Log 'ERROR' "Configure output: $lastLines" - Stop-Script 'Nginx configure failed' - } - - # Patch Makefile for shared libzstd - if (Test-Path 'objs/Makefile') { - Write-Log 'INFO' 'Patching nginx Makefile for shared libzstd' - bash -c "sed -i 's/-l:libzstd\.a/-lzstd/g' objs/Makefile" | Out-Null - } - - $nproc = (bash -c 'nproc').Trim() - $makeOutput = bash -c "export TMPDIR='$Script:BUILD_DIR' && make -j$nproc 2>&1" - if ($LASTEXITCODE -ne 0) { - $lastLines = ($makeOutput -split "`n" | Select-Object -Last 20) -join "`n" - Write-Log 'ERROR' "Make output: $lastLines" - Stop-Script 'Nginx build failed' - } - - # Build ACME Module - Write-Log 'INFO' "Building ACME module $Script:ACME_MODULE_VERSION" - $acmeSrc = Join-Path $Script:BUILD_DIR 'nginx-acme' - if (-not (Test-Path $acmeSrc)) { Stop-Script 'ACME source missing' } - Push-Location $acmeSrc - - $env:NGINX_BUILD_DIR = Join-Path $Script:BUILD_DIR 'nginx/objs' - $env:NGX_ACME_STATE_PREFIX = '/var/cache/nginx' - - $sourceCargo = 'if [ -f "$HOME/.cargo/env" ]; then source "$HOME/.cargo/env"; fi' - - # Verify Rust toolchain - $rustcVer = bash -lc "$sourceCargo && rustc --version 2>/dev/null" - if (-not $rustcVer) { - Write-Log 'WARN' 'rustc not found, installing rustup...' - bash -lc "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" | Out-Null - } - - $acmeOutput = bash -lc "$sourceCargo && cargo build --release 2>&1" - if ($LASTEXITCODE -ne 0) { - $lastLines = ($acmeOutput -split "`n" | Select-Object -Last 20) -join "`n" - Write-Log 'ERROR' "ACME build failed: $lastLines" - Stop-Script 'ACME module build failed' - } - - New-Item -ItemType Directory -Path "$Script:BUILD_DIR/nginx-acme/objs" -Force | Out-Null - $acmeSo = 'target/release/libnginx_acme.so' - if (-not (Test-Path $acmeSo)) { - Stop-Script "ACME module not built: $acmeSo missing (cargo build may have failed)" - } - try { - Copy-Item $acmeSo -Destination "$Script:BUILD_DIR/nginx-acme/objs/ngx_http_acme_module.so" -Force -ErrorAction Stop - } catch { - Stop-Script "Failed to stage ACME module: $($_.Exception.Message)" - } - - Pop-Location # nginx-acme - Pop-Location # nginx - Write-Log 'INFO' 'ACME module built successfully' - Write-Log 'INFO' 'Build complete' -} - -# ============================================================================ -# Configuration Functions -# ============================================================================ - -function Install-HtmlFiles { - Write-Log 'INFO' 'Installing HTML files' - $htmlDir = '/usr/share/nginx/html' - New-Item -ItemType Directory -Path $htmlDir -Force | Out-Null - - $indexContent = @' - - - - - -Welcome to nginx! - - - -

Welcome to nginx!

-

If you see this page, the nginx web server is successfully installed and -working. Further configuration is required.

- - -'@ - - $styleContent = @' -body { - width: 35em; - margin: 0 auto; - font-family: Tahoma, Verdana, Arial, sans-serif; -} -'@ - - $indexContent | Out-File (Join-Path $htmlDir 'index.html') -Encoding utf8 -Force - $styleContent | Out-File (Join-Path $htmlDir 'style.css') -Encoding utf8 -Force - bash -c "chmod 0644 $htmlDir/*.html $htmlDir/*.css 2>/dev/null || true" | Out-Null -} - -function New-NginxSelfSignedCertificate { - Write-Log 'INFO' 'Generating self-signed TLS certificate' - $sslDir = '/etc/nginx/ssl' - New-Item -ItemType Directory -Path $sslDir -Force | Out-Null - - if ((Test-Path "$sslDir/nginx.key") -and (Test-Path "$sslDir/nginx.crt")) { - Write-Log 'INFO' 'Existing SSL certificate preserved' - return - } - - $opensslBin = (Get-Command openssl -ErrorAction SilentlyContinue)?.Source - if (-not $opensslBin) { Stop-Script 'openssl not found' } - - $keyPath = "$sslDir/nginx.key" - $crtPath = "$sslDir/nginx.crt" - - $cmd = "OPENSSL_CONF=/dev/null '$opensslBin' req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 365 -nodes -keyout '$keyPath' -out '$crtPath' -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' 2>&1" - $output = bash -c $cmd - if ($LASTEXITCODE -ne 0) { - Write-Log 'ERROR' "OpenSSL output: $output" - Stop-Script 'Certificate generation failed' - } - - bash -c "chmod 600 '$keyPath' && chmod 644 '$crtPath'" | Out-Null -} - -function New-NginxConfig { - Write-Log 'INFO' 'Creating nginx configuration' - $conf = @' -load_module /etc/nginx/modules/ngx_http_zstd_filter_module.so; -load_module /etc/nginx/modules/ngx_http_zstd_static_module.so; -load_module /etc/nginx/modules/ngx_http_headers_more_filter_module.so; -load_module /etc/nginx/modules/ngx_http_acme_module.so; - -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /run/nginx.pid; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - server_tokens off; - more_set_headers 'Server: nginx'; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 4096; - types_hash_bucket_size 128; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; - - # Zstd compression - zstd on; - zstd_comp_level 6; - zstd_min_length 1024; - zstd_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; - - # SSL/TLS configuration - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers off; - # TLS 1.2 ciphers — ECDSA-only (matches the ECDSA certificate generated below). - # TLS 1.3 ciphers are built-in and always secure; no need to list them. - ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256; - ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 1d; - ssl_session_tickets off; - ssl_buffer_size 4k; - - # QUIC configuration - quic_retry on; - # 0-RTT disabled: no replay attack protection configured at application layer - ssl_early_data off; - - server { - listen 80; - listen [::]:80; - server_name _; - return 301 https://$host$request_uri; - } - - server { - listen 443 ssl; - listen [::]:443 ssl; - listen 443 quic reuseport; - listen [::]:443 quic reuseport; - - http2 on; - http3 on; - - server_name localhost; - - ssl_certificate /etc/nginx/ssl/nginx.crt; - ssl_certificate_key /etc/nginx/ssl/nginx.key; - - add_header Alt-Svc 'h3=":443"; ma=86400' always; - add_header X-Protocol $server_protocol always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "DENY" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';" always; - add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } -} -'@ - $conf | Out-File '/etc/nginx/nginx.conf' -Encoding utf8 -Force -} - -# ============================================================================ -# Install/Remove Functions -# ============================================================================ - -function Install-Nginx { - Write-Log 'INFO' 'Installing Nginx' - - $hadExistingNginx = Test-Path '/etc/nginx' - $hadExistingHtml = Test-Path '/usr/share/nginx/html/index.html' - - # Backup existing configuration - if (Test-Path '/etc/nginx') { - New-Item -ItemType Directory -Path $Script:BACKUP_DIR -Force | Out-Null - Copy-Item '/etc/nginx' "$Script:BACKUP_DIR/" -Recurse -Force - } - - # Install binaries - Push-Location (Join-Path $Script:BUILD_DIR 'nginx') - $installOut = bash -c 'make install 2>&1' - if ($LASTEXITCODE -ne 0) { - $lastLines = ($installOut -split "`n" | Select-Object -Last 10) -join "`n" - Write-Log 'ERROR' "Install output: $lastLines" - Stop-Script 'Nginx install failed' - } - Pop-Location - - # Create directories - $dirs = @( - '/etc/nginx/conf.d', - '/etc/nginx/sites-available', - '/etc/nginx/sites-enabled', - $Script:NGINX_MODULES_PATH, - '/var/log/nginx', - '/var/cache/nginx', - "$Script:NGINX_PREFIX/html", - '/var/lib/nginx/tmp/client_body', - '/var/lib/nginx/tmp/proxy', - '/var/lib/nginx/tmp/fastcgi', - '/var/lib/nginx/tmp/uwsgi', - '/var/lib/nginx/tmp/scgi' - ) - foreach ($d in $dirs) { - New-Item -ItemType Directory -Path $d -Force | Out-Null - } - - # Symlink /etc/nginx/modules -> real modules dir (matches Fedora/RHEL convention) - $modulesLink = '/etc/nginx/modules' - $existingItem = Get-Item $modulesLink -Force -ErrorAction SilentlyContinue - if (-not $existingItem -or $existingItem.LinkType -ne 'SymbolicLink') { - Remove-Item $modulesLink -Recurse -Force -ErrorAction SilentlyContinue - New-Item -ItemType SymbolicLink -Path $modulesLink -Target $Script:NGINX_MODULES_PATH -Force | Out-Null - } - - # Install dynamic modules - $nginxModules = Get-ChildItem -Path "$Script:BUILD_DIR/nginx/objs" -Filter '*.so' -File -ErrorAction SilentlyContinue - if (-not $nginxModules) { - Stop-Script "No NGINX dynamic modules found in $Script:BUILD_DIR/nginx/objs" - } - try { - Copy-Item -Path $nginxModules.FullName -Destination $Script:NGINX_MODULES_PATH -Force -ErrorAction Stop - } catch { - Stop-Script "Failed to copy NGINX modules: $($_.Exception.Message)" - } - - $acmeModule = "$Script:BUILD_DIR/nginx-acme/objs/ngx_http_acme_module.so" - if (-not (Test-Path $acmeModule)) { - Stop-Script "ACME module not found: $acmeModule" - } - try { - Copy-Item -Path $acmeModule -Destination $Script:NGINX_MODULES_PATH -Force -ErrorAction Stop - } catch { - Stop-Script "Failed to copy ACME module: $($_.Exception.Message)" - } - - $requiredModules = @( - 'ngx_http_zstd_filter_module.so', - 'ngx_http_zstd_static_module.so', - 'ngx_http_headers_more_filter_module.so', - 'ngx_http_acme_module.so' - ) - foreach ($module in $requiredModules) { - if (-not (Test-Path (Join-Path $Script:NGINX_MODULES_PATH $module))) { - Stop-Script "Required module missing after install: $module" - } - } - - # Install configuration files - if (-not $hadExistingHtml) { - Install-HtmlFiles - } else { - Write-Log 'INFO' 'Existing HTML files preserved' - } - New-NginxSelfSignedCertificate - if (-not $hadExistingNginx) { - New-NginxConfig - } else { - Write-Log 'INFO' 'Existing nginx.conf preserved' - } - - # Create nginx user - bash -c 'id nginx 2>/dev/null || useradd -r -s /sbin/nologin nginx' | Out-Null - - bash -c "chown -R nginx:nginx /var/log/nginx /var/cache/nginx /var/lib/nginx" | Out-Null - bash -c "chown root:root /etc/nginx/ssl" | Out-Null - bash -c "chmod 600 /etc/nginx/ssl/nginx.key" | Out-Null - bash -c "chmod 644 /etc/nginx/ssl/nginx.crt" | Out-Null - bash -c "chmod 755 /etc/nginx/conf.d '$Script:NGINX_MODULES_PATH'" | Out-Null - - # Create systemd service - $svc = @' -[Unit] -Description=Nginx HTTP Server -After=network.target - -[Service] -Type=forking -PIDFile=/run/nginx.pid -ExecStartPre=/usr/sbin/nginx -t -ExecStart=/usr/sbin/nginx -ExecReload=/usr/sbin/nginx -s reload -ExecStop=/bin/kill -s QUIT $MAINPID -Restart=on-failure - -[Install] -WantedBy=multi-user.target -'@ - $svc | Out-File '/etc/systemd/system/nginx.service' -Encoding utf8 -Force - - bash -c 'systemctl daemon-reload' | Out-Null - bash -c 'systemctl enable nginx' 2>&1 | Out-Null - bash -c '/usr/sbin/nginx -t && systemctl start nginx' | Out-Null - if ($LASTEXITCODE -ne 0) { Stop-Script 'Failed to start nginx' } - - $opensslVer = (bash -c 'openssl version 2>/dev/null').Trim() - if (-not $opensslVer) { $opensslVer = 'OpenSSL unknown' } - Write-Log 'INFO' "Nginx $Script:NGINX_VERSION with $opensslVer (system) installed" - Write-Log 'INFO' 'Access: https://localhost' - Write-Log 'INFO' 'Manage nginx with: systemctl {start|stop|reload|restart|status} nginx' - bash -c '/usr/sbin/nginx -V 2>&1 | head -n1' - - $testResult = Test-NginxInstallation - if (-not $testResult) { - Write-Log 'WARN' 'Post-install checks detected issues' - } -} - -function Test-NginxInstallation { - Write-Log 'INFO' 'Running post-install checks' - $ok = $true - - if (-not (Test-Path '/etc/nginx/ssl/nginx.crt') -or -not (Test-Path '/etc/nginx/ssl/nginx.key')) { - Write-Log 'ERROR' 'SSL certificates missing' - $ok = $false - } - - if (-not (Test-Path "$Script:NGINX_MODULES_PATH/ngx_http_acme_module.so")) { - Write-Log 'WARN' 'ACME module not found' - } else { - Write-Log 'INFO' 'ACME module present' - } - - $nginxTest = bash -c '/usr/sbin/nginx -t 2>&1' - if ($LASTEXITCODE -ne 0) { - Write-Log 'ERROR' "nginx -t failed: $nginxTest" - $ok = $false - } - - bash -c 'systemctl is-active --quiet nginx' | Out-Null - if ($LASTEXITCODE -ne 0) { - Write-Log 'WARN' 'Nginx service not active' - } else { - Write-Log 'INFO' 'Nginx service is active' - } - - bash -c 'curl -k https://localhost -I >/dev/null 2>&1 || true' | Out-Null - - return $ok -} - -function Remove-Nginx { - Write-Log 'INFO' 'Removing Nginx' - - bash -c 'systemctl stop nginx 2>/dev/null || true' | Out-Null - bash -c 'systemctl disable nginx 2>/dev/null || true' | Out-Null - Remove-Item '/etc/systemd/system/nginx.service' -Force -ErrorAction SilentlyContinue - bash -c 'systemctl daemon-reload 2>/dev/null || true' | Out-Null - - $toRemove = @( - '/usr/sbin/nginx', - '/etc/nginx', - '/var/log/nginx', - '/var/cache/nginx', - '/var/lib/nginx', - $Script:NGINX_PREFIX, - "$Script:NGINX_LIBDIR/nginx" - ) - foreach ($path in $toRemove) { - Remove-Item $path -Recurse -Force -ErrorAction SilentlyContinue - } - - bash -c 'userdel nginx 2>/dev/null || true' | Out-Null - Write-Log 'INFO' 'Nginx removed' -} - -function Test-RunningWebServers { - $portsInUse = [System.Collections.Generic.List[string]]::new() - - $portTool = (bash -c '(command -v lsof >/dev/null 2>&1 && echo lsof) || (command -v ss >/dev/null 2>&1 && echo ss) || echo none').Trim() - if ($portTool -eq 'none') { - Write-Log 'WARN' 'Neither lsof nor ss available; skipping port conflict check' - return - } - - foreach ($port in @(80, 443)) { - $detectPid = @' -tool=$1; port=$2 -if [ "$tool" = "lsof" ]; then - lsof -ti ":$port" 2>/dev/null | head -n1 -else - ss -tlnp 2>/dev/null | awk -v p="$port" ' - $0 ~ ":"p"[[:space:]]" { - if (match($0, /pid=[0-9]+/)) { print substr($0, RSTART+4, RLENGTH-4); exit } - }' -fi -'@ - $procId = (bash -c $detectPid 'detect-port' $portTool $port 2>$null)?.Trim() - if ($procId) { - $proc = (bash -c "ps -p $procId -o comm= 2>/dev/null || echo unknown").Trim() - $portsInUse.Add("$port ($proc)") - Write-Log 'WARN' "Port $port in use by: $proc" - } - } - - if ($portsInUse.Count -gt 0) { - $response = Read-Host 'Stop conflicting services? [y/N]' - if ($response -match '^[Yy]') { - bash -c 'systemctl stop apache2 2>/dev/null || true' | Out-Null - bash -c 'systemctl stop httpd 2>/dev/null || true' | Out-Null - bash -c 'systemctl stop nginx 2>/dev/null || true' | Out-Null - Write-Log 'INFO' 'Services stopped' - } else { - Stop-Script "Cannot proceed with ports in use: $($portsInUse -join ', ')" - } - } -} - -# ============================================================================ -# Main Entry Point -# ============================================================================ - -try { - switch ($Command) { - 'install' { - Update-SystemPackages - Test-RunningWebServers - Install-Dependencies - Get-Sources - Build-Nginx - Install-Nginx - Write-Host "`nInstallation log: $Script:LOG_FILE" - } - 'remove' { - Remove-Nginx - Write-Host "`nRemoval log: $Script:LOG_FILE" - } - } -} finally { - Stop-Transcript -ErrorAction SilentlyContinue - Remove-Item $Script:BUILD_DIR -Recurse -Force -ErrorAction SilentlyContinue -} From fcb53f98714b695e8491be86cce59ef512d7f600 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 20:31:48 +0000 Subject: [PATCH 2/4] feat: allow skipping/disabling optional nginx modules (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zstd, headers-more and ACME are now optional: --skip-acme, --skip-zstd, --skip-headers-more and --skip-modules=a,b,c disable them up front, and any of them is disabled automatically (with a warning) if its download or build fails instead of aborting the whole install — the ACME module in particular used to take the entire script down with it. The generated nginx.conf, required-module checks and dependency install step (cargo/rustup, only needed for ACME) all adapt accordingly. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015JTpvkRWFpJNNhEdHMekLe --- nginx/nginx_installer.sh | 425 +++++++++++++++++++++++++++++---------- 1 file changed, 321 insertions(+), 104 deletions(-) diff --git a/nginx/nginx_installer.sh b/nginx/nginx_installer.sh index 33afd4f..20b7598 100755 --- a/nginx/nginx_installer.sh +++ b/nginx/nginx_installer.sh @@ -10,8 +10,17 @@ set -euo pipefail # and ACME support on Linux. # # Usage: -# ./nginx_installer.sh install - Build and install NGINX -# ./nginx_installer.sh remove - Uninstall NGINX +# ./nginx_installer.sh install [options] - Build and install NGINX +# ./nginx_installer.sh remove - Uninstall NGINX +# +# Options: +# --skip-acme Do not build/install the ACME module +# --skip-zstd Do not build/install the zstd module +# --skip-headers-more Do not build/install the headers-more module +# --skip-modules=a,b,c Comma-separated list of: acme, zstd, headers-more +# +# A module that fails to download or build is skipped automatically +# (with a warning) instead of aborting the whole install. # # ============================================================================ @@ -126,6 +135,22 @@ ZSTD_MODULE_SHA256="707d534f8ca4263ff043066db15eac284632aea875f9fe98c96cea9529e1 ACME_MODULE_VERSION="0.4.1" ACME_MODULE_SHA256="b4f99f971bd0bebc89b2037f3afeaa3281004fe434de558df87d69cab2be1f22" +# ============================================================================ +# Optional Module Configuration +# ============================================================================ +# zstd, headers-more and ACME are optional dynamic modules. Any of them can +# be disabled up front with a --skip-* flag, and any of them is disabled +# automatically (with a warning) if its download or build fails, instead of +# aborting the whole install. Env vars let CI/automation set the same thing. + +SKIP_ACME="${NGINX_SKIP_ACME:-false}" +SKIP_ZSTD="${NGINX_SKIP_ZSTD:-false}" +SKIP_HEADERS_MORE="${NGINX_SKIP_HEADERS_MORE:-false}" + +# Set once the ACME module has actually been built; used to decide whether +# to install/require it later, since it can be skipped mid-build on failure. +ACME_MODULE_BUILT=false + # ============================================================================ # Configuration # ============================================================================ @@ -169,19 +194,54 @@ Get-File() { local url=$1 local file=$2 local sha=$3 - + if [[ -f "$file" ]]; then Test-Hash "$file" "$sha" return 0 fi - + Write-Log INFO "Downloading $(basename "$file")..." curl -fsSL "$url" -o "$file" || Stop-Script "Download failed: $url" Test-Hash "$file" "$sha" } +# Like Get-File, but for optional modules: returns 1 instead of aborting the +# script when the download fails or the checksum doesn't match. +Get-OptionalFile() { + local url=$1 + local file=$2 + local sha=$3 + local label=$4 + local actual + + if [[ -f "$file" ]]; then + actual=$(sha256sum "$file" | awk '{print $1}') + if [[ "$actual" == "$sha" ]]; then + return 0 + fi + Write-Log WARN "Checksum mismatch for existing $label archive, re-downloading" + rm -f "$file" + fi + + Write-Log INFO "Downloading $label..." + if ! curl -fsSL "$url" -o "$file"; then + Write-Log WARN "Download failed for $label module: $url" + return 1 + fi + + actual=$(sha256sum "$file" | awk '{print $1}') + if [[ "$actual" != "$sha" ]]; then + Write-Log WARN "Checksum verification failed for $label module, skipping it" + rm -f "$file" + return 1 + fi + return 0 +} + # Downloads rustup-init, verifies its published SHA256, then installs the # Rust toolchain. Replaces the old unverified `curl | sh` pattern. +# Returns 1 instead of aborting the script on failure, so callers can skip +# whichever module needed it (currently: ACME). Install-Rustup() { Write-Log INFO "Installing Rust toolchain via rustup-init" local rustup_arch @@ -190,19 +250,36 @@ Install-Rustup() { local tmp_dir tmp_dir=$(mktemp -d) - curl -fsSL "$rustup_url" -o "$tmp_dir/rustup-init" || Stop-Script "Failed to download rustup-init" - curl -fsSL "${rustup_url}.sha256" -o "$tmp_dir/rustup-init.sha256" || Stop-Script "Failed to download rustup-init checksum" + if ! curl -fsSL "$rustup_url" -o "$tmp_dir/rustup-init"; then + Write-Log WARN "Failed to download rustup-init" + rm -rf "$tmp_dir" + return 1 + fi + if ! curl -fsSL "${rustup_url}.sha256" -o "$tmp_dir/rustup-init.sha256"; then + Write-Log WARN "Failed to download rustup-init checksum" + rm -rf "$tmp_dir" + return 1 + fi local expected actual expected=$(cut -d' ' -f1 "$tmp_dir/rustup-init.sha256") actual=$(sha256sum "$tmp_dir/rustup-init" | awk '{print $1}') - [[ -n "$expected" && "$actual" == "$expected" ]] || Stop-Script "rustup-init checksum verification failed" + if [[ -z "$expected" || "$actual" != "$expected" ]]; then + Write-Log WARN "rustup-init checksum verification failed" + rm -rf "$tmp_dir" + return 1 + fi chmod +x "$tmp_dir/rustup-init" - "$tmp_dir/rustup-init" -y >/dev/null 2>&1 || Stop-Script "rustup installation failed" + if ! "$tmp_dir/rustup-init" -y >/dev/null 2>&1; then + Write-Log WARN "rustup installation failed" + rm -rf "$tmp_dir" + return 1 + fi rm -rf "$tmp_dir" # shellcheck disable=SC1091 source "$HOME/.cargo/env" + return 0 } # ============================================================================ @@ -237,12 +314,15 @@ Install-Dependencies() { ;; esac - # Verify cargo availability - if ! command -v cargo >/dev/null 2>&1; then + # Verify cargo availability (only needed to build the ACME module) + if [[ $SKIP_ACME != true ]] && ! command -v cargo >/dev/null 2>&1; then Write-Log WARN "Cargo not found. Installing rustup..." - Install-Rustup + if ! Install-Rustup; then + Write-Log WARN "Rust toolchain unavailable, disabling ACME module for this run" + SKIP_ACME=true + fi fi - + Write-Log INFO "Dependencies installed" } @@ -288,22 +368,32 @@ Get-Sources() { Get-File "$NGINX_URL" "nginx.tgz" "$NGINX_SHA256" Get-File "$PCRE2_URL" "pcre2.tgz" "$PCRE2_SHA256" Get-File "$ZLIB_URL" "zlib.tgz" "$ZLIB_SHA256" - Get-File "$HEADERS_MORE_URL" "headers.tgz" "$HEADERS_MORE_SHA256" - Get-File "$ZSTD_MODULE_URL" "zstd.tgz" "$ZSTD_MODULE_SHA256" - Get-File "$ACME_MODULE_URL" "acme.tgz" "$ACME_MODULE_SHA256" - + + if [[ $SKIP_HEADERS_MORE != true ]] && ! Get-OptionalFile "$HEADERS_MORE_URL" "headers.tgz" "$HEADERS_MORE_SHA256" "headers-more"; then + Write-Log WARN "Disabling headers-more module for this run" + SKIP_HEADERS_MORE=true + fi + if [[ $SKIP_ZSTD != true ]] && ! Get-OptionalFile "$ZSTD_MODULE_URL" "zstd.tgz" "$ZSTD_MODULE_SHA256" "zstd"; then + Write-Log WARN "Disabling zstd module for this run" + SKIP_ZSTD=true + fi + if [[ $SKIP_ACME != true ]] && ! Get-OptionalFile "$ACME_MODULE_URL" "acme.tgz" "$ACME_MODULE_SHA256" "acme"; then + Write-Log WARN "Disabling ACME module for this run" + SKIP_ACME=true + fi + Write-Log INFO "Extracting archives" - + # Clean previous extractions rm -rf nginx openssl pcre2 zlib headers-more zstd-module nginx-acme 2>/dev/null || true - + tar xzf nginx.tgz && mv "nginx-${NGINX_VERSION}" nginx tar xzf pcre2.tgz && mv "pcre2-${PCRE2_VERSION}" pcre2 tar xzf zlib.tgz && mv "zlib-${ZLIB_VERSION}" zlib - tar xzf headers.tgz && mv "headers-more-nginx-module-${HEADERS_MORE_VERSION}" headers-more - tar xzf zstd.tgz && mv "zstd-nginx-module-${ZSTD_MODULE_VERSION}" zstd-module - tar xzf acme.tgz && mv "nginx-acme-${ACME_MODULE_VERSION}" nginx-acme - + [[ $SKIP_HEADERS_MORE == true ]] || { tar xzf headers.tgz && mv "headers-more-nginx-module-${HEADERS_MORE_VERSION}" headers-more; } + [[ $SKIP_ZSTD == true ]] || { tar xzf zstd.tgz && mv "zstd-nginx-module-${ZSTD_MODULE_VERSION}" zstd-module; } + [[ $SKIP_ACME == true ]] || { tar xzf acme.tgz && mv "nginx-acme-${ACME_MODULE_VERSION}" nginx-acme; } + Write-Log INFO "Sources ready" } @@ -335,105 +425,131 @@ Build-Nginx() { export TMPDIR="$BUILD_DIR" export CC=gcc - - # Verify libzstd availability - if command -v ldconfig >/dev/null 2>&1; then - if ! ldconfig -p 2>/dev/null | grep -q "libzstd.so"; then - Stop-Script "Shared libzstd not found. Install libzstd-dev/devel" + + if [[ $SKIP_ZSTD != true ]]; then + # Verify libzstd availability + if command -v ldconfig >/dev/null 2>&1; then + if ! ldconfig -p 2>/dev/null | grep -q "libzstd.so"; then + Write-Log WARN "Shared libzstd not found. Install libzstd-dev/devel; disabling zstd module for this run" + SKIP_ZSTD=true + fi + elif [[ ! -f /usr/lib/libzstd.so && ! -f /usr/lib/libzstd.so.1 && + ! -f /usr/lib64/libzstd.so && ! -f /usr/lib64/libzstd.so.1 && + ! -f /usr/local/lib/libzstd.so ]]; then + Write-Log WARN "Shared libzstd not found. Install libzstd-dev/devel; disabling zstd module for this run" + SKIP_ZSTD=true fi - elif [[ ! -f /usr/lib/libzstd.so && ! -f /usr/lib/libzstd.so.1 && - ! -f /usr/lib64/libzstd.so && ! -f /usr/lib64/libzstd.so.1 && - ! -f /usr/local/lib/libzstd.so ]]; then - Stop-Script "Shared libzstd not found. Install libzstd-dev/devel" fi - - export LDFLAGS="-lzstd" - + [[ $SKIP_ZSTD == true ]] || export LDFLAGS="-lzstd" + + local configure_args=( + --with-compat + --prefix="${NGINX_PREFIX}" + --sbin-path=/usr/sbin/nginx + --conf-path=/etc/nginx/nginx.conf + --http-log-path=/var/log/nginx/access.log + --error-log-path=/var/log/nginx/error.log + --pid-path=/run/nginx.pid + --lock-path=/run/lock/nginx.lock + --http-client-body-temp-path=/var/lib/nginx/tmp/client_body + --http-proxy-temp-path=/var/lib/nginx/tmp/proxy + --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi + --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi + --http-scgi-temp-path=/var/lib/nginx/tmp/scgi + --with-pcre="$BUILD_DIR/pcre2" + --with-zlib="$BUILD_DIR/zlib" + --with-pcre-jit + --with-http_ssl_module + --with-http_v2_module + --with-http_v3_module + --with-http_gzip_static_module + --with-http_stub_status_module + --with-http_realip_module + --with-http_sub_module + --with-http_secure_link_module + --with-stream + --with-stream_ssl_module + --with-stream_ssl_preread_module + --with-stream_realip_module + --with-file-aio + --with-threads + --modules-path="${NGINX_MODULES_PATH}" + ) + [[ $SKIP_HEADERS_MORE == true ]] || configure_args+=(--add-dynamic-module="$BUILD_DIR/headers-more") + [[ $SKIP_ZSTD == true ]] || configure_args+=(--add-dynamic-module="$BUILD_DIR/zstd-module") + local output - if ! output=$(./configure \ - --with-compat \ - --prefix="${NGINX_PREFIX}" \ - --sbin-path=/usr/sbin/nginx \ - --conf-path=/etc/nginx/nginx.conf \ - --http-log-path=/var/log/nginx/access.log \ - --error-log-path=/var/log/nginx/error.log \ - --pid-path=/run/nginx.pid \ - --lock-path=/run/lock/nginx.lock \ - --http-client-body-temp-path=/var/lib/nginx/tmp/client_body \ - --http-proxy-temp-path=/var/lib/nginx/tmp/proxy \ - --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi \ - --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi \ - --http-scgi-temp-path=/var/lib/nginx/tmp/scgi \ - --with-pcre="$BUILD_DIR/pcre2" \ - --with-zlib="$BUILD_DIR/zlib" \ - --with-pcre-jit \ - --with-http_ssl_module \ - --with-http_v2_module \ - --with-http_v3_module \ - --with-http_gzip_static_module \ - --with-http_stub_status_module \ - --with-http_realip_module \ - --with-http_sub_module \ - --with-http_secure_link_module \ - --with-stream \ - --with-stream_ssl_module \ - --with-stream_ssl_preread_module \ - --with-stream_realip_module \ - --with-file-aio \ - --with-threads \ - --modules-path="${NGINX_MODULES_PATH}" \ - --add-dynamic-module="$BUILD_DIR/headers-more" \ - --add-dynamic-module="$BUILD_DIR/zstd-module" \ - 2>&1); then + if ! output=$(./configure "${configure_args[@]}" 2>&1); then printf '%s\n' "$output" >> "$LOG_FILE" Write-Log ERROR "Configure output: $(echo "$output" | tail -20)" Stop-Script "Configure failed" fi - + # Patch Makefile for shared libzstd - if [[ -f "objs/Makefile" ]]; then + if [[ $SKIP_ZSTD != true && -f "objs/Makefile" ]]; then Write-Log INFO "Patching nginx Makefile for shared libzstd" sed -i 's/-l:libzstd\.a/-lzstd/g' "objs/Makefile" fi - + if ! output=$(make -j"$(nproc)" 2>&1); then printf '%s\n' "$output" >> "$LOG_FILE" Write-Log ERROR "Make output: $(echo "$output" | tail -20)" Stop-Script "Build failed" fi - - # Build ACME Module + + # Build ACME Module (optional — skipped automatically on failure) + if [[ $SKIP_ACME == true ]]; then + Write-Log INFO "Skipping ACME module" + elif Build-AcmeModule; then + ACME_MODULE_BUILT=true + else + Write-Log WARN "Continuing without the ACME module" + SKIP_ACME=true + fi + + Write-Log INFO "Build complete" +} + +# Builds the ACME dynamic module. Returns 1 on any failure instead of +# aborting the script, so the caller can skip the module and carry on. +Build-AcmeModule() { Write-Log INFO "Building ACME module ${ACME_MODULE_VERSION}" - cd "$BUILD_DIR/nginx-acme" || Stop-Script "ACME source missing" - + cd "$BUILD_DIR/nginx-acme" || { Write-Log WARN "ACME source missing"; return 1; } + export NGINX_BUILD_DIR="$BUILD_DIR/nginx/objs" export NGX_ACME_STATE_PREFIX="/var/cache/nginx" - + if [[ -f "$HOME/.cargo/env" ]]; then + # shellcheck disable=SC1091 source "$HOME/.cargo/env" fi - + # Verify Rust toolchain if ! command -v rustc >/dev/null 2>&1; then Write-Log WARN "rustc not found, installing rustup" - Install-Rustup + Install-Rustup || { Write-Log WARN "Rust toolchain unavailable"; return 1; } fi - + local cargo_output if ! cargo_output=$(cargo build --release 2>&1); then printf '%s\n' "$cargo_output" >> "$LOG_FILE" - Write-Log ERROR "ACME build failed: $(echo "$cargo_output" | tail -20)" - Stop-Script "ACME module build failed" + Write-Log WARN "ACME build failed: $(echo "$cargo_output" | tail -20)" + return 1 fi - + mkdir -p "$BUILD_DIR/nginx-acme/objs" local acme_so="target/release/libnginx_acme.so" - [[ -f "$acme_so" ]] || Stop-Script "ACME module not built: $acme_so missing (cargo build may have failed)" - cp "$acme_so" "$BUILD_DIR/nginx-acme/objs/ngx_http_acme_module.so" \ - || Stop-Script "Failed to stage ACME module: cp failed (check disk space or permissions)" - + if [[ ! -f "$acme_so" ]]; then + Write-Log WARN "ACME module not built: $acme_so missing (cargo build may have failed)" + return 1 + fi + if ! cp "$acme_so" "$BUILD_DIR/nginx-acme/objs/ngx_http_acme_module.so"; then + Write-Log WARN "Failed to stage ACME module: cp failed (check disk space or permissions)" + return 1 + fi + Write-Log INFO "ACME module built successfully" - Write-Log INFO "Build complete" + return 0 } # ============================================================================ @@ -503,12 +619,27 @@ New-SelfSignedCertificate() { New-NginxConfig() { Write-Log INFO "Creating nginx configuration" - - cat > /etc/nginx/nginx.conf <<'EOF' + + : > /etc/nginx/nginx.conf + + if [[ $SKIP_ZSTD != true ]]; then + cat >> /etc/nginx/nginx.conf <<'EOF' load_module /etc/nginx/modules/ngx_http_zstd_filter_module.so; load_module /etc/nginx/modules/ngx_http_zstd_static_module.so; +EOF + fi + if [[ $SKIP_HEADERS_MORE != true ]]; then + cat >> /etc/nginx/nginx.conf <<'EOF' load_module /etc/nginx/modules/ngx_http_headers_more_filter_module.so; +EOF + fi + if [[ $ACME_MODULE_BUILT == true ]]; then + cat >> /etc/nginx/nginx.conf <<'EOF' load_module /etc/nginx/modules/ngx_http_acme_module.so; +EOF + fi + + cat >> /etc/nginx/nginx.conf <<'EOF' user nginx; worker_processes auto; @@ -524,7 +655,15 @@ http { default_type application/octet-stream; server_tokens off; +EOF + + if [[ $SKIP_HEADERS_MORE != true ]]; then + cat >> /etc/nginx/nginx.conf <<'EOF' more_set_headers 'Server: nginx'; +EOF + fi + + cat >> /etc/nginx/nginx.conf <<'EOF' log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' @@ -546,12 +685,20 @@ http { gzip_comp_level 6; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; +EOF + + if [[ $SKIP_ZSTD != true ]]; then + cat >> /etc/nginx/nginx.conf <<'EOF' # Zstd compression zstd on; zstd_comp_level 6; zstd_min_length 1024; zstd_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; +EOF + fi + + cat >> /etc/nginx/nginx.conf <<'EOF' # SSL/TLS configuration ssl_protocols TLSv1.2 TLSv1.3; @@ -664,20 +811,29 @@ Install-Nginx() { done < <(compgen -G 'objs/*.so' || true) if [[ ${#nginx_module_files[@]} -eq 0 ]]; then - Stop-Script "No NGINX dynamic modules found in $BUILD_DIR/nginx/objs" + if [[ $SKIP_ZSTD == true && $SKIP_HEADERS_MORE == true ]]; then + Write-Log INFO "No optional dynamic modules to install (zstd and headers-more both skipped)" + else + Stop-Script "No NGINX dynamic modules found in $BUILD_DIR/nginx/objs" + fi + else + cp "${nginx_module_files[@]}" "${NGINX_MODULES_PATH}/" || Stop-Script "Failed to copy NGINX modules" fi - cp "${nginx_module_files[@]}" "${NGINX_MODULES_PATH}/" || Stop-Script "Failed to copy NGINX modules" - local acme_module="$BUILD_DIR/nginx-acme/objs/ngx_http_acme_module.so" - [[ -f "$acme_module" ]] || Stop-Script "ACME module not found: $acme_module" - cp "$acme_module" "${NGINX_MODULES_PATH}/" || Stop-Script "Failed to copy ACME module" + if [[ $ACME_MODULE_BUILT == true ]]; then + local acme_module="$BUILD_DIR/nginx-acme/objs/ngx_http_acme_module.so" + if [[ -f "$acme_module" ]]; then + cp "$acme_module" "${NGINX_MODULES_PATH}/" || Stop-Script "Failed to copy ACME module" + else + Write-Log WARN "ACME module artifact missing, skipping" + ACME_MODULE_BUILT=false + fi + fi - local required_modules=( - ngx_http_zstd_filter_module.so - ngx_http_zstd_static_module.so - ngx_http_headers_more_filter_module.so - ngx_http_acme_module.so - ) + local required_modules=() + [[ $SKIP_ZSTD == true ]] || required_modules+=(ngx_http_zstd_filter_module.so ngx_http_zstd_static_module.so) + [[ $SKIP_HEADERS_MORE == true ]] || required_modules+=(ngx_http_headers_more_filter_module.so) + [[ $ACME_MODULE_BUILT == true ]] && required_modules+=(ngx_http_acme_module.so) local module for module in "${required_modules[@]}"; do [[ -f "${NGINX_MODULES_PATH}/${module}" ]] || Stop-Script "Required module missing after install: ${module}" @@ -752,7 +908,11 @@ Test-NginxInstallation() { } if [[ ! -f /etc/nginx/modules/ngx_http_acme_module.so ]]; then - Write-Log WARN "ACME module not found" + if [[ $ACME_MODULE_BUILT == true ]]; then + Write-Log WARN "ACME module not found" + else + Write-Log INFO "ACME module skipped" + fi else Write-Log INFO "ACME module present" fi @@ -844,9 +1004,66 @@ Test-RunningWebServers() { # Main Entry Point # ============================================================================ +Show-Usage() { + cat <<'EOF' +Usage: nginx_installer.sh {install|remove} [options] + +Options: + --skip-acme Do not build/install the ACME module + --skip-zstd Do not build/install the zstd module + --skip-headers-more Do not build/install the headers-more module + --skip-modules=a,b,c Comma-separated list of: acme, zstd, headers-more + -h, --help Show this help + +A module that fails to download or build is skipped automatically (with a +warning) instead of aborting the whole install. +EOF +} + +COMMAND="install" +if [[ $# -gt 0 && "$1" != -* ]]; then + COMMAND="$1" + shift +fi + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-acme) + SKIP_ACME=true + ;; + --skip-zstd) + SKIP_ZSTD=true + ;; + --skip-headers-more) + SKIP_HEADERS_MORE=true + ;; + --skip-modules=*) + IFS=',' read -ra _skip_list <<< "${1#*=}" + for _module in "${_skip_list[@]}"; do + case "$_module" in + acme) SKIP_ACME=true ;; + zstd) SKIP_ZSTD=true ;; + headers-more) SKIP_HEADERS_MORE=true ;; + *) Stop-Script "Unknown module: $_module (expected acme, zstd, headers-more)" ;; + esac + done + ;; + -h|--help) + Show-Usage + exit 0 + ;; + *) + Write-Log ERROR "Unknown argument: $1" + Show-Usage + exit 1 + ;; + esac + shift +done + trap 'rm -rf "$BUILD_DIR"' EXIT -case "${1:-install}" in +case "$COMMAND" in install) Update-SystemPackages Test-RunningWebServers @@ -863,7 +1080,7 @@ case "${1:-install}" in echo "Removal log: $LOG_FILE" ;; *) - echo "Usage: $0 {install|remove}" + Show-Usage exit 1 ;; esac From 6b6ce8d9ad9db60bd49c81953d1520847bb032f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 20:31:56 +0000 Subject: [PATCH 3/4] feat: add planned shutdown/reboot scripts (#102) Adds system/planned_shutdown.sh (Linux, wraps the systemd shutdown command) and windows/Set-PlannedShutdown.ps1 (Windows, wraps shutdown.exe with its own state file since shutdown.exe has no query verb). Both support schedule (--at HH:MM or --delay/-Delay, optional reboot and broadcast message), cancel, and status. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015JTpvkRWFpJNNhEdHMekLe --- system/planned_shutdown.sh | 216 ++++++++++++++++++++++++++++++++ windows/Set-PlannedShutdown.ps1 | 164 ++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100755 system/planned_shutdown.sh create mode 100644 windows/Set-PlannedShutdown.ps1 diff --git a/system/planned_shutdown.sh b/system/planned_shutdown.sh new file mode 100755 index 0000000..22d3ddf --- /dev/null +++ b/system/planned_shutdown.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# +# Planned Shutdown Script (standalone) +# +# Schedules or cancels a system shutdown/reboot via the systemd `shutdown` +# command, and reports whether one is currently pending. +# Run as root. +# + +set -euo pipefail + +# ============================================================================ +# Common Helper Functions +# The same helpers are used in every bash script in this repo, so the +# scripts stay consistent while remaining standalone single-file downloads. +# Function names follow the PowerShell Verb-Noun convention. +# ============================================================================ + +# shellcheck disable=SC2034 # not every script uses every color +readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' \ + BLUE='\033[0;34m' PURPLE='\033[0;35m' BOLD='\033[1m' NC='\033[0m' + +# Optional plain-text logfile; set LOG_FILE after this block to enable. +LOG_FILE="${LOG_FILE:-}" + +# Usage: Write-Log "message" +Write-Log() { + local level=$1; shift + local color=$NC + case $level in + INFO) color=$BLUE ;; + SUCCESS) color=$GREEN ;; + WARN) color=$YELLOW ;; + ERROR) color=$RED ;; + STEP) color=$PURPLE ;; + esac + if [[ $level == ERROR ]]; then + echo -e "${color}[$level]${NC} $*" >&2 + else + echo -e "${color}[$level]${NC} $*" + fi + if [[ -n "$LOG_FILE" ]]; then + echo "[$level] $*" >> "$LOG_FILE" + fi +} + +# Usage: Stop-Script "fatal message" +Stop-Script() { + Write-Log ERROR "$1" + exit 1 +} + +# Usage: Test-Root (exits unless running as root) +Test-Root() { + [[ $EUID -eq 0 ]] || Stop-Script "Run as root (sudo)." +} + +# ============================================================================ +# Usage +# ============================================================================ + +Show-Usage() { + cat <<'EOF' +Usage: planned_shutdown.sh [options] + +Commands: + schedule Schedule a shutdown or reboot + cancel Cancel a previously scheduled shutdown/reboot + status Show whether a shutdown/reboot is currently pending + +Options (schedule): + --at HH:MM Shut down at a specific time (24h clock, today or tomorrow) + --delay N[smh] Shut down after a delay, e.g. 45, 45m, 2h, 90s (default unit: minutes) + --reboot Reboot instead of powering off + --message TEXT Broadcast message shown to logged-in users + -y, --yes Skip the confirmation prompt + +Exactly one of --at or --delay is required for "schedule". + +Examples: + sudo ./planned_shutdown.sh schedule --delay 30m --message "Maintenance starting soon" + sudo ./planned_shutdown.sh schedule --at 23:30 --reboot + sudo ./planned_shutdown.sh cancel + sudo ./planned_shutdown.sh status +EOF +} + +# ============================================================================ +# Helpers +# ============================================================================ + +# Usage: minutes=$(Convert-DelayToMinutes "30m") -> rounds up to whole minutes +Convert-DelayToMinutes() { + local delay=$1 + [[ $delay =~ ^([0-9]+)([smh]?)$ ]] || Stop-Script "Invalid --delay value: $delay (expected e.g. 45, 45m, 2h, 90s)" + local amount=${BASH_REMATCH[1]} + local unit=${BASH_REMATCH[2]:-m} + case $unit in + s) echo $(( (amount + 59) / 60 )) ;; + m) echo "$amount" ;; + h) echo $(( amount * 60 )) ;; + esac +} + +Test-ShutdownAvailable() { + command -v shutdown >/dev/null 2>&1 || Stop-Script "shutdown command not found" +} + +# ============================================================================ +# Commands +# ============================================================================ + +Set-PlannedShutdown() { + local at="" delay="" reboot=false message="" assume_yes=false + + while [[ $# -gt 0 ]]; do + case "$1" in + --at) + at=${2:-}; [[ -n "$at" ]] || Stop-Script "--at requires a value"; shift 2 ;; + --delay) + delay=${2:-}; [[ -n "$delay" ]] || Stop-Script "--delay requires a value"; shift 2 ;; + --reboot) + reboot=true; shift ;; + --message) + message=${2:-}; [[ -n "$message" ]] || Stop-Script "--message requires a value"; shift 2 ;; + -y|--yes) + assume_yes=true; shift ;; + *) + Stop-Script "Unknown option for 'schedule': $1" ;; + esac + done + + [[ -n "$at" || -n "$delay" ]] || Stop-Script "Specify --at HH:MM or --delay N[smh]" + [[ -z "$at" || -z "$delay" ]] || Stop-Script "Use either --at or --delay, not both" + + local when + if [[ -n "$at" ]]; then + [[ $at =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] || Stop-Script "Invalid --at value: $at (expected HH:MM, 24h clock)" + when="$at" + else + when="+$(Convert-DelayToMinutes "$delay")" + fi + + local action="power off" + [[ $reboot == true ]] && action="reboot" + + if [[ $assume_yes != true ]]; then + read -r -p "Schedule a $action for $when? [y/N]: " response + [[ "$response" =~ ^[Yy]$ ]] || Stop-Script "Aborted" + fi + + local shutdown_args=() + [[ $reboot == true ]] && shutdown_args+=(-r) || shutdown_args+=(-h) + shutdown_args+=("$when") + [[ -n "$message" ]] && shutdown_args+=("$message") + + shutdown "${shutdown_args[@]}" || Stop-Script "Failed to schedule shutdown" + Write-Log SUCCESS "Scheduled $action for $when" +} + +Stop-PlannedShutdown() { + if shutdown -c 2>/dev/null; then + Write-Log SUCCESS "Scheduled shutdown/reboot cancelled" + else + Write-Log WARN "No shutdown/reboot was pending" + fi +} + +Get-PlannedShutdown() { + local scheduled_file="/run/systemd/shutdown/scheduled" + + if [[ ! -f "$scheduled_file" ]]; then + Write-Log INFO "No shutdown/reboot is currently scheduled" + return 0 + fi + + local usec mode + usec=$(sed -n 's/^USEC=//p' "$scheduled_file") + mode=$(sed -n 's/^MODE=//p' "$scheduled_file") + + if [[ -n "$usec" ]]; then + local when + when=$(date -d "@$(( usec / 1000000 ))" 2>/dev/null || echo "unknown time") + Write-Log INFO "A ${mode:-shutdown} is scheduled for: $when" + else + Write-Log INFO "A shutdown/reboot appears to be scheduled, but details could not be read" + fi +} + +# ============================================================================ +# Main Entry Point +# ============================================================================ + +Test-Root +Test-ShutdownAvailable + +case "${1:-}" in + schedule) + shift + Set-PlannedShutdown "$@" + ;; + cancel) + Stop-PlannedShutdown + ;; + status) + Get-PlannedShutdown + ;; + -h|--help|"") + Show-Usage + ;; + *) + Write-Log ERROR "Unknown command: $1" + Show-Usage + exit 1 + ;; +esac diff --git a/windows/Set-PlannedShutdown.ps1 b/windows/Set-PlannedShutdown.ps1 new file mode 100644 index 0000000..d2cdc45 --- /dev/null +++ b/windows/Set-PlannedShutdown.ps1 @@ -0,0 +1,164 @@ +<# +.SYNOPSIS + Schedules, cancels, or reports a planned shutdown/reboot. +.DESCRIPTION + Thin wrapper around shutdown.exe: schedules a power-off or restart after + a delay or at a specific time, with an optional broadcast message, and + can cancel a pending one or report whether one is active. + + shutdown.exe has no built-in query verb, so this script keeps a small + state file (ProgramData\PlannedShutdown\state.json) to answer 'status' + without side effects. Only shutdowns scheduled through this script are + reflected in 'status'. +.PARAMETER Command + schedule - Schedule a shutdown or reboot + cancel - Cancel a previously scheduled shutdown/reboot + status - Show whether a shutdown/reboot is currently pending +.PARAMETER At + Shut down at a specific time today (24h clock, e.g. "23:30"). +.PARAMETER Delay + Shut down after a delay in minutes (e.g. 30). +.PARAMETER Reboot + Reboot instead of powering off. +.PARAMETER Message + Message shown to logged-in users before shutdown. +.PARAMETER Force + Force running applications to close without warning. +.PARAMETER Yes + Skip the confirmation prompt. +.EXAMPLE + ./Set-PlannedShutdown.ps1 -Command schedule -Delay 30 -Message "Maintenance starting soon" +.EXAMPLE + ./Set-PlannedShutdown.ps1 -Command schedule -At 23:30 -Reboot +.EXAMPLE + ./Set-PlannedShutdown.ps1 -Command cancel +.EXAMPLE + ./Set-PlannedShutdown.ps1 -Command status +.NOTES + Run as Administrator (the script self-elevates if needed). +#> + +param( + [Parameter(Position = 0)] + [ValidateSet('schedule', 'cancel', 'status')] + [string]$Command = 'status', + + [string]$At, + [int]$Delay, + [switch]$Reboot, + [string]$Message, + [switch]$Force, + [switch]$Yes +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Self-elevate when not running as Administrator +$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) +if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Host "Restarting the script with administrator rights..." -ForegroundColor Yellow + $argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-Command', $Command) + if ($At) { $argList += @('-At', $At) } + if ($Delay) { $argList += @('-Delay', $Delay) } + if ($Reboot) { $argList += '-Reboot' } + if ($Message) { $argList += @('-Message', "`"$Message`"") } + if ($Force) { $argList += '-Force' } + if ($Yes) { $argList += '-Yes' } + Start-Process -FilePath "PowerShell" -ArgumentList ($argList -join ' ') -Verb RunAs + Exit +} +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + +$StateDir = Join-Path $env:ProgramData 'PlannedShutdown' +$StateFile = Join-Path $StateDir 'state.json' + +function Convert-AtTimeToTarget { + param([string]$Time) + + if ($Time -notmatch '^([01][0-9]|2[0-3]):[0-5][0-9]$') { + throw "Invalid -At value: $Time (expected HH:MM, 24h clock)" + } + + $target = Get-Date -Hour ([int]$Time.Split(':')[0]) -Minute ([int]$Time.Split(':')[1]) -Second 0 + if ($target -le (Get-Date)) { + $target = $target.AddDays(1) + } + return $target +} + +function Save-ShutdownState { + param([datetime]$Target, [string]$Action) + + New-Item -ItemType Directory -Path $StateDir -Force | Out-Null + [pscustomobject]@{ + TargetTime = $Target.ToString('o') + Action = $Action + } | ConvertTo-Json | Set-Content -Path $StateFile -Encoding UTF8 +} + +function Clear-ShutdownState { + Remove-Item -Path $StateFile -Force -ErrorAction SilentlyContinue +} + +switch ($Command) { + 'schedule' { + if (-not $At -and -not $Delay) { + throw "Specify -At HH:MM or -Delay " + } + if ($At -and $Delay) { + throw "Use either -At or -Delay, not both" + } + + $target = if ($At) { Convert-AtTimeToTarget -Time $At } else { (Get-Date).AddMinutes($Delay) } + $seconds = [int]([TimeSpan]($target - (Get-Date))).TotalSeconds + $action = if ($Reboot) { "reboot" } else { "power off" } + + if (-not $Yes) { + $response = Read-Host "Schedule a $action for $($target.ToString('yyyy-MM-dd HH:mm'))? [y/N]" + if ($response -notmatch '^[Yy]$') { + Write-Host "Aborted" -ForegroundColor Yellow + Exit 1 + } + } + + $shutdownArgs = @(if ($Reboot) { '/r' } else { '/s' }) + $shutdownArgs += @('/t', $seconds) + if ($Force) { $shutdownArgs += '/f' } + if ($Message) { $shutdownArgs += @('/c', $Message) } + + & shutdown.exe @shutdownArgs + if ($LASTEXITCODE -ne 0) { + throw "Failed to schedule shutdown (shutdown.exe exit code $LASTEXITCODE)" + } + Save-ShutdownState -Target $target -Action $action + Write-Host "Scheduled $action for $($target.ToString('yyyy-MM-dd HH:mm'))" -ForegroundColor Green + } + + 'cancel' { + & shutdown.exe /a + $cancelled = ($LASTEXITCODE -eq 0) + Clear-ShutdownState + if ($cancelled) { + Write-Host "Scheduled shutdown/reboot cancelled" -ForegroundColor Green + } else { + Write-Host "No shutdown/reboot was pending" -ForegroundColor Yellow + } + } + + 'status' { + if (-not (Test-Path $StateFile)) { + Write-Host "No shutdown/reboot is currently scheduled" -ForegroundColor Cyan + } else { + $state = Get-Content -Path $StateFile -Raw | ConvertFrom-Json + $target = [datetime]$state.TargetTime + + if ($target -le (Get-Date)) { + Write-Host "No shutdown/reboot is currently scheduled" -ForegroundColor Cyan + Clear-ShutdownState + } else { + Write-Host "A $($state.Action) is scheduled for: $($target.ToString('yyyy-MM-dd HH:mm'))" -ForegroundColor Cyan + } + } + } +} From 495d5432da0ab03c494e326e002dabca972c2e14 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 20:32:01 +0000 Subject: [PATCH 4/4] docs: update script tables for #97, #98, #102 Reflect the removed nginx PowerShell installer, the new nginx module skip flags, and the new planned-shutdown scripts in README.md and nginx/README.md. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015JTpvkRWFpJNNhEdHMekLe --- README.md | 4 +++- nginx/README.md | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 671f860..00bc861 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,10 @@ pwsh ./_installer.ps1 | `ansible/` | `ansible_installer.sh` | Linux | Installs Ansible via pip in a venv | | `docker/` | `docker_installer.sh` | Linux | Official Docker repositories | | `kubernetes/` | `kubernetes_installer.sh` | Linux | kubectl + optional Minikube | -| `nginx/` | `nginx_installer.sh`, `nginx_installer.ps1` | Linux | Custom build: OpenSSL 3.x, HTTP/2, HTTP/3, zstd, headers-more, ACME | +| `nginx/` | `nginx_installer.sh` | Linux | Custom build: OpenSSL 3.x, HTTP/2, HTTP/3, zstd, headers-more, ACME | | `openssh/` | `openssh_installer.sh` | Linux | Hardened config, Ed25519-only, post-quantum KEX (ML-KEM) | | `podman/` | `podman_installer.sh` | Linux | Distribution repositories | +| `system/` | `planned_shutdown.sh` | Linux | Schedule/cancel/check a planned shutdown or reboot | | `terraform/` | `terraform_installer.sh` | Linux | HashiCorp repositories | | `TLS-tools/` | `TLS-checker.ps1` | Cross-platform | Tests TLS versions, HTTP versions, QUIC, HSTS, compression | | `TLS-tools/` | `testssl.sh` (submodule) | Linux | Comprehensive TLS/SSL scanner by Dirk Wetter — pinned at a specific version | @@ -38,6 +39,7 @@ pwsh ./_installer.ps1 | `windows/` | `Install-VagrantVMware.ps1` | Windows | Installs Vagrant + VMware Workstation | | `windows/` | `Install-DellCommandUpdate.ps1` | Windows | Installs Dell Command Update via winget | | `windows/` | `Install-HPImageAssistant.ps1` | Windows | Installs HP Image Assistant via winget | +| `windows/` | `Set-PlannedShutdown.ps1` | Windows | Schedule/cancel/check a planned shutdown or reboot | ## Linux distro support diff --git a/nginx/README.md b/nginx/README.md index 8c4d3d8..735eeb3 100644 --- a/nginx/README.md +++ b/nginx/README.md @@ -7,7 +7,23 @@ sudo ./nginx_installer.sh install sudo ./nginx_installer.sh remove ``` -PowerShell variant also available: `nginx_installer.ps1` +## Optional modules + +zstd, headers-more and ACME are optional dynamic modules. Skip any of them +up front, or let the installer skip them automatically (with a warning) +when a download or build fails, instead of aborting the whole install: + +```bash +sudo ./nginx_installer.sh install --skip-acme +sudo ./nginx_installer.sh install --skip-modules=acme,zstd,headers-more +``` + +| Flag | Skips | +|------|-------| +| `--skip-acme` | ACME module | +| `--skip-zstd` | zstd compression module | +| `--skip-headers-more` | headers-more module | +| `--skip-modules=a,b,c` | Any combination of `acme`, `zstd`, `headers-more` | ## Installed paths