Reported from a Windows Server 2019 test: a "Windows Installer" dialog listing every msiexec /Option appeared, then the wizard reported that the bundled MySQL database could not be installed. That dialog is msiexec's usage help - it prints it when the command line does not parse - so the install never started. Cause: $MysqlRoot defaulted to 'C:\Program Files\MySQL\MySQL Server 8.4', which contains spaces. Invoke-Native wraps any argument containing whitespace in quotes, producing "INSTALLDIR=C:\Program Files\...". msiexec takes public properties as PROPERTY=value and expects the VALUE quoted - INSTALLDIR="C:\Program Files\..." - so it rejected the line, printed usage, and exited non-zero. This file already carried the rule, next to the Python target: "Never put a space in a path this installer controls." I broke it setting the 8.4 path. Two fixes, because one of them alone leaves the trap in place: - $MysqlRoot is now C:\MySQL84, space-free like C:\Python314. The MySQL client search paths in the installer, the preflight and the operator console all look there first, keeping backups working against the bundled server. - Invoke-Native now quotes PROPERTY=value correctly, so passing a spaced path explicitly no longer produces an unparseable command line. tests/test_installer_defaults.py fails if an installer-controlled path default ever contains a space again.
557 lines
25 KiB
PowerShell
557 lines
25 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
ShopDB-Flask installer - Stage 1: read-only preflight.
|
|
|
|
.DESCRIPTION
|
|
Discovers everything the installer needs to know about this box and reports
|
|
it. Makes NO changes: no installs, no config edits, no service restarts.
|
|
Safe to run on a production server.
|
|
|
|
Written for stock Windows PowerShell 5.1 (Windows Server ships it). No
|
|
pwsh-only syntax, no external modules, no network access.
|
|
|
|
.PARAMETER SitePort
|
|
The port the ShopDB site will listen on. Default 8090 (the runbook's example;
|
|
the classic ASP site keeps 8080).
|
|
|
|
.PARAMETER AppRoot
|
|
Intended install directory. Default C:\shopdb-flask.
|
|
|
|
.PARAMETER Json
|
|
Emit machine-readable JSON instead of the human report. Later installer
|
|
stages consume this.
|
|
|
|
.EXAMPLE
|
|
powershell -ExecutionPolicy Bypass -File .\shopdb-preflight.ps1
|
|
powershell -ExecutionPolicy Bypass -File .\shopdb-preflight.ps1 -Json > preflight.json
|
|
|
|
.NOTES
|
|
Exit 0 = no blocking problems. Exit 1 = at least one FAIL.
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[int] $SitePort = 8090,
|
|
[string] $AppRoot = 'C:\shopdb-flask',
|
|
# Needed so the port check can tell OUR site apart from a stranger's.
|
|
[string] $SiteName = 'shopdb-flask',
|
|
[switch] $Json,
|
|
# Machine-readable output for a GUI caller: one record per line,
|
|
# STATUS|AREA|CHECK|DETAIL|FIX
|
|
# The console rendering below aligns columns with padding spaces, which only
|
|
# works in a fixed-width font at console width. A GUI must do its own layout,
|
|
# so give it DATA and let it decide - do not make it parse a formatted table.
|
|
# (-Json exists too, but Inno's Pascal Script has no JSON parser.)
|
|
[switch] $Delimited
|
|
)
|
|
|
|
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# --- result collection -------------------------------------------------------
|
|
# Every check appends one record. Status is PASS / WARN / FAIL / INFO.
|
|
$script:Results = New-Object System.Collections.ArrayList
|
|
$script:IisPresent = $false
|
|
|
|
function Add-Result {
|
|
param(
|
|
[string] $Area,
|
|
[string] $Check,
|
|
[ValidateSet('PASS','WARN','FAIL','INFO','SKIP')] [string] $Status,
|
|
[string] $Detail,
|
|
[string] $Fix = ''
|
|
)
|
|
$null = $script:Results.Add([PSCustomObject]@{
|
|
Area = $Area
|
|
Check = $Check
|
|
Status = $Status
|
|
Detail = $Detail
|
|
Fix = $Fix
|
|
})
|
|
}
|
|
|
|
# Wrap a check so one failure cannot abort the whole run. On an unfamiliar box
|
|
# an unexpected exception is itself a finding, not a crash.
|
|
function Invoke-Check {
|
|
param([string] $Area, [string] $Check, [scriptblock] $Body)
|
|
try { & $Body }
|
|
catch {
|
|
Add-Result $Area $Check 'WARN' "check could not run: $($_.Exception.Message)" `
|
|
'Report this output; the installer needs to handle this box shape.'
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# 1. Operator context
|
|
# =============================================================================
|
|
|
|
Invoke-Check 'System' 'Elevation' {
|
|
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$adm = (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole(
|
|
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
if ($adm) { Add-Result 'System' 'Elevation' 'PASS' 'running as Administrator' }
|
|
else {
|
|
Add-Result 'System' 'Elevation' 'FAIL' 'not elevated' `
|
|
'Re-run PowerShell as Administrator. IIS and service changes require it.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'System' 'Windows version' {
|
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
$name = $os.Caption
|
|
$ver = $os.Version
|
|
# ProductType: 1 = workstation, 2 = domain controller, 3 = server
|
|
$isServer = ($os.ProductType -ne 1)
|
|
$detail = "$name (build $ver), $(if ($isServer) {'Server'} else {'Client'})"
|
|
|
|
$supported = $false
|
|
if ($isServer -and [version]$ver -ge [version]'10.0.17763') { $supported = $true } # 2019+
|
|
if (-not $isServer -and [version]$ver -ge [version]'10.0.19045') { $supported = $true } # Win10 22H2+
|
|
|
|
if ($supported) { Add-Result 'System' 'Windows version' 'PASS' $detail }
|
|
else {
|
|
Add-Result 'System' 'Windows version' 'FAIL' $detail `
|
|
'Supported: Windows Server 2019/2022+, or Windows 10 22H2 / 11 Pro+.'
|
|
}
|
|
|
|
# Client SKUs must be Pro/Enterprise/Education for IIS.
|
|
if (-not $isServer -and $name -match 'Home') {
|
|
Add-Result 'System' 'Windows edition' 'FAIL' 'Windows Home edition' `
|
|
'IIS is not available on Home editions. Pro or higher is required.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'System' 'Architecture' {
|
|
if ([Environment]::Is64BitOperatingSystem) {
|
|
Add-Result 'System' 'Architecture' 'PASS' '64-bit'
|
|
} else {
|
|
Add-Result 'System' 'Architecture' 'FAIL' '32-bit' `
|
|
'The bundled Python and wheels are 64-bit (win_amd64) only.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'System' 'PowerShell version' {
|
|
$v = $PSVersionTable.PSVersion
|
|
Add-Result 'System' 'PowerShell version' 'INFO' "$v"
|
|
if ($v.Major -lt 5) {
|
|
Add-Result 'System' 'PowerShell version' 'FAIL' "$v" `
|
|
'PowerShell 5.1 or later is required.'
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# 2. Disk and ports
|
|
# =============================================================================
|
|
|
|
Invoke-Check 'Disk' 'Free space' {
|
|
$drive = (Split-Path -Qualifier $AppRoot)
|
|
$d = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$drive'"
|
|
if ($null -eq $d) {
|
|
Add-Result 'Disk' 'Free space' 'FAIL' "drive $drive not found" `
|
|
"Choose an -AppRoot on an existing volume."
|
|
return
|
|
}
|
|
$freeGB = [math]::Round($d.FreeSpace / 1GB, 1)
|
|
$detail = "$freeGB GB free on $drive"
|
|
if ($freeGB -ge 5) { Add-Result 'Disk' 'Free space' 'PASS' $detail }
|
|
else { Add-Result 'Disk' 'Free space' 'FAIL' $detail 'At least 5 GB is required.' }
|
|
}
|
|
|
|
Invoke-Check 'Disk' 'AppRoot' {
|
|
if (Test-Path $AppRoot) {
|
|
$existing = @(Get-ChildItem $AppRoot -Force -ErrorAction SilentlyContinue)
|
|
if ($existing.Count -gt 0) {
|
|
$hasEnv = Test-Path (Join-Path $AppRoot '.env')
|
|
if ($hasEnv) {
|
|
# An existing install is the NORMAL state for an upgrade. Reporting
|
|
# it as a warning makes a routine update look like a problem.
|
|
$ver = ''
|
|
$vf = Join-Path $AppRoot '.installed-version'
|
|
if (Test-Path $vf) { $ver = ' version ' + (Get-Content $vf -TotalCount 1).Trim() }
|
|
Add-Result 'Disk' 'AppRoot' 'INFO' `
|
|
"existing ShopDB-Flask install found$ver - it will be upgraded in place, and your settings and database are kept"
|
|
} else {
|
|
Add-Result 'Disk' 'AppRoot' 'WARN' "$AppRoot exists and is not empty" `
|
|
'Confirm this directory is safe to install into.'
|
|
}
|
|
} else { Add-Result 'Disk' 'AppRoot' 'PASS' "$AppRoot exists and is empty" }
|
|
} else { Add-Result 'Disk' 'AppRoot' 'PASS' "$AppRoot does not exist yet" }
|
|
}
|
|
|
|
function Test-PortFree {
|
|
param([int] $Port)
|
|
# Get-NetTCPConnection is the reliable listener check on Server 2012R2+.
|
|
try {
|
|
$listening = @(Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue)
|
|
return ($listening.Count -eq 0)
|
|
} catch {
|
|
# Fall back to a bind attempt if the cmdlet is unavailable.
|
|
try {
|
|
$l = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Any, $Port)
|
|
$l.Start(); $l.Stop(); return $true
|
|
} catch { return $false }
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'Network' 'Site port' {
|
|
if (Test-PortFree $SitePort) {
|
|
Add-Result 'Network' 'Site port' 'PASS' "TCP $SitePort is free"
|
|
} else {
|
|
$owner = ''
|
|
try {
|
|
$c = Get-NetTCPConnection -State Listen -LocalPort $SitePort -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($c) { $owner = " (pid $($c.OwningProcess): $((Get-Process -Id $c.OwningProcess -ErrorAction SilentlyContinue).ProcessName))" }
|
|
} catch { }
|
|
# Is the listener OUR OWN site? On a reinstall or upgrade the port is held
|
|
# by the very application being upgraded, and blocking on that makes the
|
|
# installer refuse to update anything it previously installed.
|
|
$ours = $false
|
|
try {
|
|
Import-Module WebAdministration -ErrorAction SilentlyContinue
|
|
$site = Get-Website -Name $SiteName -ErrorAction SilentlyContinue
|
|
if ($site) {
|
|
foreach ($b in $site.bindings.Collection) {
|
|
# ${} is required: "$SitePort:" parses as a DRIVE-qualified variable.
|
|
if ($b.bindingInformation -match ":${SitePort}:") { $ours = $true }
|
|
}
|
|
}
|
|
} catch { }
|
|
|
|
if ($ours) {
|
|
Add-Result 'Network' 'Site port' 'INFO' `
|
|
"TCP $SitePort is used by the existing $SiteName site - this will be upgraded in place"
|
|
} else {
|
|
# WARN, not FAIL. This check runs before the operator has reached the
|
|
# Address page, so it is testing the DEFAULT port, not necessarily
|
|
# the one they intend to use. Blocking here would refuse an install
|
|
# over a conflict the very next page lets them resolve.
|
|
Add-Result 'Network' 'Site port' 'WARN' "TCP $SitePort is in use$owner" `
|
|
"Pick a different port on the Address page later in this wizard, or stop whatever is holding it."
|
|
}
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# 3. IIS
|
|
# =============================================================================
|
|
|
|
Invoke-Check 'IIS' 'Installed' {
|
|
$svc = Get-Service -Name W3SVC -ErrorAction SilentlyContinue
|
|
$script:IisPresent = ($null -ne $svc)
|
|
if ($null -eq $svc) {
|
|
Add-Result 'IIS' 'Installed' 'FAIL' 'W3SVC service not found' `
|
|
'Install IIS. Server: Install-WindowsFeature Web-Server -IncludeManagementTools. Client: enable Internet Information Services in Windows Features.'
|
|
return
|
|
}
|
|
Add-Result 'IIS' 'Installed' 'PASS' "W3SVC present, status $($svc.Status)"
|
|
if ($svc.Status -ne 'Running') {
|
|
Add-Result 'IIS' 'Running' 'WARN' "W3SVC is $($svc.Status)" 'Start-Service W3SVC'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'IIS' 'WebAdministration module' {
|
|
$m = Get-Module -ListAvailable -Name WebAdministration
|
|
if ($m) { Add-Result 'IIS' 'WebAdministration module' 'PASS' 'available' }
|
|
else {
|
|
Add-Result 'IIS' 'WebAdministration module' 'FAIL' 'not available' `
|
|
'Install the IIS management tools (Web-Mgmt-Console / IIS Management Scripts and Tools).'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'IIS' 'HttpPlatformHandler' {
|
|
# The handler registers itself as a global module. Check the module list.
|
|
$appcmd = Join-Path $env:windir 'system32\inetsrv\appcmd.exe'
|
|
if (-not (Test-Path $appcmd)) {
|
|
Add-Result 'IIS' 'HttpPlatformHandler' 'SKIP' 'appcmd.exe not present (IIS not installed)' `
|
|
'Re-run this preflight after installing IIS.'
|
|
return
|
|
}
|
|
$modules = & $appcmd list module 2>$null
|
|
if ($modules -match 'httpPlatformHandler') {
|
|
Add-Result 'IIS' 'HttpPlatformHandler' 'PASS' 'installed'
|
|
} else {
|
|
# NOT a blocker: the MSI is in the bundle and stage 4 installs it. This
|
|
# was a FAIL, which - once the preflight page started blocking on any
|
|
# failure - stopped the wizard dead over something the installer was
|
|
# about to do by itself, with no way forward but to go and install it by
|
|
# hand. Nothing the installer SUPPLIES may be a blocker.
|
|
Add-Result 'IIS' 'HttpPlatformHandler' 'INFO' 'not installed yet' `
|
|
'The installer installs it from the bundle. No action needed.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'IIS' 'Locked config sections' {
|
|
# The authoritative source is applicationHost.config. `appcmd list config
|
|
# /section:X` prints the section CONTENTS, not its lock state, so grepping
|
|
# that output silently reports every section as unlocked.
|
|
$cfg = Join-Path $env:windir 'system32\inetsrv\config\applicationHost.config'
|
|
if (-not (Test-Path $cfg)) {
|
|
Add-Result 'IIS' 'Locked config sections' 'SKIP' 'applicationHost.config not found'
|
|
return
|
|
}
|
|
foreach ($name in @('handlers','httpPlatform')) {
|
|
$line = Select-String -Path $cfg -Pattern ('<section name="' + $name + '"') |
|
|
Select-Object -First 1
|
|
if ($null -eq $line) {
|
|
# httpPlatform is registered by the HttpPlatformHandler MSI. Before
|
|
# that, `appcmd unlock config /section:system.webServer/httpPlatform`
|
|
# fails with "Unknown config section".
|
|
Add-Result 'IIS' "Section $name" 'SKIP' 'section not registered yet' `
|
|
'Install HttpPlatformHandler FIRST; only then can this section be unlocked.'
|
|
} elseif ($line.Line -match 'overrideModeDefault="Deny"') {
|
|
Add-Result 'IIS' "Section $name" 'WARN' 'locked (overrideModeDefault="Deny")' `
|
|
"Installer must run: appcmd unlock config /section:system.webServer/$name (else IIS 500.19)"
|
|
} else {
|
|
Add-Result 'IIS' "Section $name" 'PASS' 'not locked'
|
|
}
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'IIS' 'URL Rewrite module' {
|
|
$appcmd = Join-Path $env:windir 'system32\inetsrv\appcmd.exe'
|
|
if (-not (Test-Path $appcmd)) {
|
|
Add-Result 'IIS' 'URL Rewrite module' 'SKIP' 'IIS not installed; cannot check'
|
|
return
|
|
}
|
|
$modules = & $appcmd list module 2>$null
|
|
if ($modules -match 'RewriteModule') {
|
|
Add-Result 'IIS' 'URL Rewrite module' 'PASS' 'installed (the X-Forwarded-For rule can be enabled)'
|
|
} else {
|
|
# Not a FAIL: it is only needed for -ClientIpSource direct, and the
|
|
# installer carries the MSI and installs it itself. Worth reporting
|
|
# because without the rule IIS sends no X-Forwarded-For at all, so every
|
|
# client reads as 127.0.0.1 and the GE-Enforce IP allowlist, the
|
|
# visitor-location lookup and per-host login rate limiting go quiet.
|
|
Add-Result 'IIS' 'URL Rewrite module' 'INFO' 'not installed' `
|
|
'The installer installs it from the bundle when -ClientIpSource is direct. Behind a reverse proxy that already sets X-Forwarded-For, use -ClientIpSource proxy and leave it out.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'IIS' 'Existing sites' {
|
|
if (-not $script:IisPresent) {
|
|
Add-Result 'IIS' 'Existing sites' 'SKIP' 'IIS not installed'
|
|
return
|
|
}
|
|
try {
|
|
Import-Module WebAdministration -ErrorAction Stop
|
|
$sites = @(Get-Website)
|
|
if ($sites.Count -eq 0) { Add-Result 'IIS' 'Existing sites' 'INFO' 'none' ; return }
|
|
$desc = ($sites | ForEach-Object {
|
|
$b = ($_.bindings.Collection | ForEach-Object { $_.bindingInformation }) -join ','
|
|
"$($_.Name) [$($_.State)] $b"
|
|
}) -join '; '
|
|
Add-Result 'IIS' 'Existing sites' 'INFO' $desc
|
|
# Adoption sites typically run the classic ASP shopdb here already.
|
|
if ($desc -match '8080') {
|
|
Add-Result 'IIS' 'Classic ASP site' 'INFO' 'a site is bound on 8080 (likely the classic ASP shopdb)' `
|
|
'Install ShopDB as a separate site on its own port; do not disturb this one.'
|
|
}
|
|
} catch {
|
|
Add-Result 'IIS' 'Existing sites' 'WARN' "could not enumerate: $($_.Exception.Message)" ''
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# 4. MySQL (detect BEFORE offering bundled vs existing)
|
|
# =============================================================================
|
|
|
|
Invoke-Check 'MySQL' 'Service' {
|
|
$svcs = @(Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^MySQL' -or $_.DisplayName -match 'MySQL' })
|
|
if ($svcs.Count -eq 0) {
|
|
Add-Result 'MySQL' 'Service' 'INFO' 'no MySQL service found' `
|
|
'Bundled MySQL 8.4 LTS is the appropriate choice on this box.'
|
|
return
|
|
}
|
|
foreach ($s in $svcs) {
|
|
Add-Result 'MySQL' 'Service' 'WARN' "$($s.Name) ($($s.DisplayName)) is $($s.Status)" `
|
|
'MySQL already present. Default to the EXISTING-server option; installing bundled MySQL will collide on port 3306.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'MySQL' 'Port 3306' {
|
|
if (Test-PortFree 3306) {
|
|
Add-Result 'MySQL' 'Port 3306' 'INFO' 'nothing listening on 3306'
|
|
} else {
|
|
Add-Result 'MySQL' 'Port 3306' 'WARN' 'something is listening on 3306' `
|
|
'Bundled MySQL cannot use the default port. Use the existing server, or pick another port.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'MySQL' 'Backup client' {
|
|
# mysqldump is what takes the mandatory pre-upgrade backup. Without it every
|
|
# upgrade skips the backup - and skips it AFTER the application pool has been
|
|
# stopped and the tree replaced, so the site is down and there is nothing to
|
|
# restore from. A site whose database is on another server typically has no
|
|
# MySQL client installed here at all, which is exactly the case that needs it.
|
|
$names = @('mysqldump.exe')
|
|
$found = ''
|
|
foreach ($root in @((Join-Path $PSScriptRoot 'mysqlclient'),
|
|
(Join-Path $AppRoot 'mysqlclient'),
|
|
'C:\MySQL84', 'C:\Program Files\MySQL', 'C:\mysql56\bin',
|
|
'C:\Program Files (x86)\MySQL')) {
|
|
if (-not (Test-Path $root)) { continue }
|
|
$hit = Get-ChildItem $root -Filter $names[0] -Recurse -ErrorAction SilentlyContinue |
|
|
Select-Object -First 1
|
|
if ($hit) { $found = $hit.FullName; break }
|
|
}
|
|
if ($found) {
|
|
Add-Result 'MySQL' 'Backup client' 'PASS' "mysqldump found ($found)"
|
|
} else {
|
|
Add-Result 'MySQL' 'Backup client' 'WARN' 'mysqldump not found on this server' `
|
|
'Needed for the automatic pre-upgrade backup and for "shopdb-admin.ps1 backup". A first install works without it; upgrades will not be protected. Add mysqlclient\ to the installer bundle, or install the MySQL client on this server.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'MySQL' 'Version and config' {
|
|
# Find mysqld.exe via the service binary path; read the version and locate my.ini.
|
|
$svc = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.PathName -match 'mysqld' } | Select-Object -First 1
|
|
if ($null -eq $svc) { return }
|
|
|
|
$path = $svc.PathName
|
|
$exe = ''
|
|
if ($path -match '"([^"]+mysqld[^"]*)"') { $exe = $matches[1] }
|
|
elseif ($path -match '(\S+mysqld\S*)') { $exe = $matches[1] }
|
|
|
|
$ver = ''
|
|
if ($exe -and (Test-Path $exe)) {
|
|
try { $ver = (& $exe --version 2>$null | Out-String).Trim() } catch { }
|
|
}
|
|
if ($ver) { Add-Result 'MySQL' 'Version' 'INFO' $ver }
|
|
|
|
# my.ini path is passed as --defaults-file in the service command line.
|
|
$ini = ''
|
|
if ($path -match '--defaults-file="?([^"]+\.ini)"?') { $ini = $matches[1] }
|
|
if ($ini -and (Test-Path $ini)) {
|
|
Add-Result 'MySQL' 'Config file' 'INFO' $ini
|
|
|
|
# MySQL 5.6 needs three flags or `flask db upgrade` dies with error 1071.
|
|
$is56 = ($ver -match '\b5\.6\.')
|
|
if ($is56) {
|
|
$content = Get-Content $ini -Raw
|
|
$need = @('innodb_file_per_table','innodb_file_format','innodb_large_prefix')
|
|
$missing = @()
|
|
foreach ($k in $need) { if ($content -notmatch $k) { $missing += $k } }
|
|
if ($missing.Count -eq 0) {
|
|
# Present in the FILE is not the same as ACTIVE. MySQL must be
|
|
# restarted for them to take effect, and the app's own
|
|
# `flask db-utils preflight` queries the live server - trust that.
|
|
Add-Result 'MySQL' '5.6 index flags' 'WARN' 'all three present in my.ini' `
|
|
'Present in the file only. They take effect after a MySQL RESTART, which interrupts the classic ASP app. Confirm with SHOW VARIABLES or flask db-utils preflight.'
|
|
} else {
|
|
Add-Result 'MySQL' '5.6 index flags' 'FAIL' ("missing: " + ($missing -join ', ')) `
|
|
"Add to [mysqld] in $ini and restart MySQL, or 'flask db upgrade' fails with error 1071. NOTE: restarting interrupts the classic ASP app."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# 5. Python (detect, but the installer uses its OWN bundled interpreter)
|
|
# =============================================================================
|
|
|
|
Invoke-Check 'Python' 'On PATH' {
|
|
$cmd = Get-Command python -ErrorAction SilentlyContinue
|
|
if ($null -eq $cmd) {
|
|
Add-Result 'Python' 'On PATH' 'INFO' 'no python on PATH' `
|
|
'Expected. The installer supplies its own interpreter.'
|
|
return
|
|
}
|
|
$v = ''
|
|
try { $v = (& $cmd.Source --version 2>&1 | Out-String).Trim() } catch { }
|
|
$detail = "$v at $($cmd.Source)"
|
|
|
|
# A per-user install under %LOCALAPPDATA% is unreadable by the IIS app-pool
|
|
# identity. That produces a 500 with an empty HttpPlatform log.
|
|
if ($cmd.Source -like "$env:LOCALAPPDATA*") {
|
|
Add-Result 'Python' 'On PATH' 'WARN' "$detail (PER-USER install)" `
|
|
'The IIS app-pool identity cannot read %LOCALAPPDATA%. The installer must install Python for ALL USERS and use absolute paths.'
|
|
} elseif ($cmd.Source -like '*WindowsApps*') {
|
|
Add-Result 'Python' 'On PATH' 'WARN' "$detail (Microsoft Store)" `
|
|
'Store Python misbehaves under service identities. The installer will not use it.'
|
|
} else {
|
|
Add-Result 'Python' 'On PATH' 'INFO' $detail `
|
|
'Not used by the installer, but a manual `flask` command later would resolve to this interpreter.'
|
|
}
|
|
}
|
|
|
|
Invoke-Check 'Python' 'Registered installs' {
|
|
$found = @()
|
|
foreach ($hive in @('HKLM:\SOFTWARE\Python\PythonCore','HKCU:\SOFTWARE\Python\PythonCore')) {
|
|
if (Test-Path $hive) {
|
|
foreach ($k in Get-ChildItem $hive -ErrorAction SilentlyContinue) {
|
|
$ip = Join-Path $k.PSPath 'InstallPath'
|
|
if (Test-Path $ip) {
|
|
$loc = (Get-ItemProperty $ip -ErrorAction SilentlyContinue).'(default)'
|
|
$scope = if ($hive -like 'HKLM*') { 'all-users' } else { 'per-user' }
|
|
$found += "$($k.PSChildName) ($scope) $loc"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($found.Count -eq 0) { Add-Result 'Python' 'Registered installs' 'INFO' 'none' }
|
|
else { Add-Result 'Python' 'Registered installs' 'INFO' ($found -join '; ') }
|
|
}
|
|
|
|
# =============================================================================
|
|
# Report
|
|
# =============================================================================
|
|
|
|
$fails = @($script:Results | Where-Object { $_.Status -eq 'FAIL' })
|
|
$warns = @($script:Results | Where-Object { $_.Status -eq 'WARN' })
|
|
$skips = @($script:Results | Where-Object { $_.Status -eq 'SKIP' })
|
|
|
|
if ($Delimited) {
|
|
# Data only. No padding, no colour, no alignment - the caller lays it out.
|
|
# Pipes are stripped from field values so the record can be split naively.
|
|
foreach ($r in $script:Results) {
|
|
$fix = ''
|
|
if ($r.Fix) { $fix = $r.Fix }
|
|
$fields = @($r.Status, $r.Area, $r.Check, $r.Detail, $fix) | ForEach-Object {
|
|
([string]$_) -replace '\|', '/' -replace '\s*\r?\n\s*', ' '
|
|
}
|
|
Write-Output ($fields -join '|')
|
|
}
|
|
} elseif ($Json) {
|
|
[PSCustomObject]@{
|
|
Timestamp = (Get-Date).ToString('s')
|
|
Computer = $env:COMPUTERNAME
|
|
SitePort = $SitePort
|
|
AppRoot = $AppRoot
|
|
Failures = $fails.Count
|
|
Warnings = $warns.Count
|
|
Skipped = $skips.Count
|
|
Results = $script:Results
|
|
} | ConvertTo-Json -Depth 5
|
|
} else {
|
|
Write-Host ''
|
|
Write-Host 'ShopDB-Flask preflight' -ForegroundColor Cyan
|
|
Write-Host (" host {0} site port {1} approot {2}" -f $env:COMPUTERNAME, $SitePort, $AppRoot)
|
|
Write-Host ''
|
|
$area = ''
|
|
foreach ($r in $script:Results) {
|
|
if ($r.Area -ne $area) { $area = $r.Area; Write-Host "[$area]" -ForegroundColor White }
|
|
$colour = 'Gray'
|
|
if ($r.Status -eq 'PASS') { $colour = 'Green' }
|
|
if ($r.Status -eq 'WARN') { $colour = 'Yellow' }
|
|
if ($r.Status -eq 'FAIL') { $colour = 'Red' }
|
|
if ($r.Status -eq 'SKIP') { $colour = 'DarkGray' }
|
|
Write-Host (" {0,-5} {1,-28} {2}" -f $r.Status, $r.Check, $r.Detail) -ForegroundColor $colour
|
|
if ($r.Fix -and $r.Status -ne 'PASS' -and $r.Status -ne 'INFO' -and $r.Status -ne 'SKIP') {
|
|
Write-Host (" -> {0}" -f $r.Fix) -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
Write-Host ''
|
|
if ($fails.Count -eq 0) {
|
|
Write-Host "No blocking problems. $($warns.Count) warning(s), $($skips.Count) skipped." -ForegroundColor Green
|
|
} else {
|
|
Write-Host "$($fails.Count) blocking problem(s), $($warns.Count) warning(s), $($skips.Count) skipped." -ForegroundColor Red
|
|
}
|
|
if ($skips.Count -gt 0) {
|
|
Write-Host " Skipped checks were NOT verified. Re-run once their prerequisite is installed." -ForegroundColor DarkGray
|
|
}
|
|
Write-Host ''
|
|
}
|
|
|
|
if ($fails.Count -gt 0) { exit 1 } else { exit 0 }
|