Five defects from the Windows-defect review, each confirmed against the code
before changing it. Four of the five only fire on a RE-RUN - and after eight
attempts a re-run is the normal case, not an edge case, which is exactly why
they survived.
Invoke-Native, three defects in one function:
- Any non-zero exit was failure. 3010 and 1641 mean "done, reboot required",
and the VC++ redistributable returns 3010 on a server with a pending file
rename - an ordinary state on a freshly patched box. It is now an accepted
outcome for the installers that can report it, logged as a warning so the
operator knows a reboot is owed.
- -Wait blocks inside Start-Process until the child exits, so the -TimeoutSec
block below it could never run. Every timeout on every MSI was decorative.
The wait is now bounded here, followed by a parameterless WaitForExit so the
redirected output is flushed before it is read.
- The Python bootstrapper ran /quiet with no /norestart, free to reboot the
server mid-install.
Stage 0 refused to run when a MySQL service existed - including the MySQL84 it
had registered itself. Every bundled-database retry dead-ended while the wizard
promised that re-running was safe. A foreign MySQL still blocks; ours is started
if stopped, and the create-the-server block is skipped. It also no longer tries
to bootstrap through a root account whose password it set on the previous run:
with the handoff present there is nothing to do, and without it there is no safe
automatic recovery, so it says what to do instead of guessing.
Stage 4's appcmd unlock used '2>&1' under $ErrorActionPreference = 'Stop', which
turns any appcmd stderr into a terminating error - so the exit-code test and the
server-wide fallback, the whole reason the block exists, were unreachable, and
the stage aborted after Python, the venv, the schema and the ACLs had been
changed.
Stage 3 ran prune-schema and treated its refusal as a failure. Refusing is the
designed outcome when a table holds rows, signalled with SystemExit(1), so
Invoke-Native killed the stage and the reporting written to explain the refusal
was unreachable. Core migration 7d05 seeds access protocols owned by the
computers plugin, so any profile omitting computers hit this on every retry.
The preflight's MySQL 5.6 index-flag check is a warning, not a blocker. It
inspects the LOCAL MySQL, which may not be the database being installed against;
stage 3 checks the one actually chosen. Same class as the HttpPlatformHandler
blocker fixed earlier.
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. 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 {
|
|
# 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 }
|