Eleven findings, grouped by the root cause each belongs to. Wizard input reaching a command line unchecked (ShopDBFlask.iss). Port fields were spliced in bare and arrive as [int] parameters, so a blank or mistyped port shifted every argument after it; both port fields are now validated as 1-65535 digits. A path ending in a backslash, which is what a drive root looks like, ended its argument with \" and CommandLineToArgvW read that as an escaped quote, so paths are now quoted through a helper that doubles the trailing backslash. A drive root is refused outright as well: uninstall deletes the application directory recursively, so installing to D:\ would have wiped the drive on removal. The password handoff was written with SaveStringToFile, which writes an AnsiString, and read back as UTF-8, so a correct non-ASCII password was reported as wrong; it now goes out as UTF-8 without a BOM. Launching without checking the result. Plugin deregistration invoked "flask plugin uninstall" without --yes, and the command carries a click confirmation_option that aborts with exit 1 when nothing can answer the prompt, so it could never once have succeeded; the bare 2>&1 under EAP Stop then turned that into a terminating error which the catch downgraded to a warning while the plugin directory was deleted regardless. It now passes --yes, brackets the error preference, restores the location in a finally, and keeps the code on disk unless deregistration actually succeeded. MarkShortcutRunAs had four quotes where it needed three, which kept the whole command inside one Pascal literal so LnkPath was never interpolated and no shortcut ever got the elevation flag; its exit code is now logged too. Comparing IIS physical paths as raw strings. IIS stores the path as typed, so it may carry environment variables or a trailing backslash. A Test-SamePath helper now normalises both sides. That closes a real hazard in uninstall, which matched applications on alias alone and would remove an unrelated application of the same name under another site, unattended, since -OnFailure never suppresses the confirmation. Accepting existing IIS state without reconciling it. "Site already exists" took the site however it was, so re-running with a different port left the old binding while CORS_ORIGINS, the firewall rule and the smoke test all used the new one, failing a working server. It now refuses with both ports named rather than silently re-binding, and refuses a site of that name serving a different directory. Preflight rows drawn past the panel. The failures loop had no cap at all and the warnings loop capped at 6, a number unrelated to the panel, which holds about three rows. The cap is now measured from the panel height, applies to both loops, and the footer counts what was actually left out instead of inferring it. Also: a failed upgrade now says the application pool is still stopped and how to start it, rather than only "part-configured", since stage 2 stops a pool that was serving. It is deliberately not restarted automatically, because after a stage 3 failure the deployed code and the schema may disagree. shopdb-admin Restart-App starts a stopped pool or site instead of recycling, which is a no-op on a stopped pool and then reported the application as unresponsive. A dead Write-Log line that parsed as three arguments is gone, and a preflight warning no longer tells the operator to add a directory to a compiled exe.
563 lines
25 KiB
PowerShell
563 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. It is not on this server yet - the installer stages its own copy, so this normally resolves itself during installation.'
|
|
}
|
|
}
|
|
|
|
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 {
|
|
# WARN, not FAIL. This inspects the LOCAL MySQL, which may not be the
|
|
# database the operator is about to install against - a bundled 8.4,
|
|
# or a remote server. Blocking the wizard here refused an install
|
|
# over a server that had nothing to do with it. Stage 3 runs
|
|
# 'flask db-utils preflight' against the database actually chosen,
|
|
# which is the check that can genuinely block.
|
|
Add-Result 'MySQL' '5.6 index flags' 'WARN' ("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 }
|