feat(deploy): add the air-gapped Windows installer
Roughly 2500 lines of tested installer had been living in ~/Downloads and an untracked folder - nothing was under version control. It goes here rather than in a repo of its own because it depends on application internals: the `flask plugin` verbs, site-profile.json, MOUNT_PATH, and the plugin registry. Versioned separately it would drift out of step with the thing it installs. Contents: the read-only preflight, the staged installer (bundled MySQL, runtime, schema, IIS, verify, uninstall), the operator console, the Inno Setup wizard, the bundle builder and the artwork generator. bundle/ and Output/ are ignored - regenerable, and ~220MB. plugins.iss is ignored because build-installer.sh generates it from the staged payload. The artwork IS committed so a Windows build box does not need Python and cairosvg. Verified end to end on Windows Server 2025 against a bundled MySQL 8.0 and an existing MySQL 5.6: fresh install, upgrade with backup and rollback, re-run idempotency, uninstall, and both deployment methods including switching between them. Not yet verified: a hypervisor-level air-gapped run, and any load from a real browser (every HTTP check so far used curl, which sends no Origin header).
This commit is contained in:
529
deploy/windows/installer/shopdb-admin.ps1
Normal file
529
deploy/windows/installer/shopdb-admin.ps1
Normal file
@@ -0,0 +1,529 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Day-to-day control of a ShopDB-Flask installation, on the server itself.
|
||||
|
||||
.DESCRIPTION
|
||||
Installed alongside the application so an operator never has to open IIS
|
||||
Manager, hunt for a log, or remember an appcmd incantation.
|
||||
|
||||
Run it with no arguments for a menu, or pass a command directly:
|
||||
|
||||
.\shopdb-admin.ps1 status
|
||||
.\shopdb-admin.ps1 restart
|
||||
.\shopdb-admin.ps1 backup D:\backups
|
||||
|
||||
Everything here is safe to run at any time EXCEPT backup/restore, which are
|
||||
called out explicitly.
|
||||
|
||||
.NOTES
|
||||
Written for stock Windows PowerShell 5.1. No modules to install.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('menu','status','start','stop','restart','logs','open','backup',
|
||||
'check','sessions','plugins','add-plugin','uninstall')]
|
||||
[string] $Command = 'menu',
|
||||
[string] $Path = '',
|
||||
[string] $AppRoot = 'C:\shopdb-flask',
|
||||
[string] $SiteName = 'shopdb-flask',
|
||||
[string] $AppPool = 'shopdbflask',
|
||||
[int] $SitePort = 8090
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$AppCmd = Join-Path $env:windir 'System32\inetsrv\appcmd.exe'
|
||||
|
||||
# --- bitness ---------------------------------------------------------------
|
||||
# IIS's management COM objects are 64-BIT ONLY. Under the 32-bit PowerShell,
|
||||
# Import-Module WebAdministration SUCCEEDS but Get-Website then fails with
|
||||
# Retrieving the COM class factory ... REGDB_E_CLASSNOTREG
|
||||
# which this script caught and reported as "cannot read IIS" - indistinguishable
|
||||
# from IIS being absent.
|
||||
#
|
||||
# A 32-bit launcher is easy to end up with: Inno Setup is a 32-bit process, so
|
||||
# anything it starts gets SysWOW64 powershell through WOW64 redirection. Rather
|
||||
# than fix every launcher, relaunch under the native PowerShell. 'Sysnative' is
|
||||
# the alias that lets a 32-bit process reach the real System32, and it exists
|
||||
# ONLY for 32-bit processes - hence the guard.
|
||||
if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess) {
|
||||
$native = Join-Path $env:windir 'Sysnative\WindowsPowerShell\v1.0\powershell.exe'
|
||||
if (Test-Path $native) {
|
||||
$relaunch = @('-NoProfile', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', ('"' + $PSCommandPath + '"'), $Command)
|
||||
if ($Path) { $relaunch += @('-Path', ('"' + $Path + '"')) }
|
||||
Start-Process -FilePath $native -ArgumentList $relaunch -Wait -NoNewWindow
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# --- elevation -------------------------------------------------------------
|
||||
# Reading IIS state needs Administrator: the WebAdministration module and the
|
||||
# IIS: drive both fail without it. Unelevated, this tool used to report
|
||||
# "IIS not available / application pool: not installed", which reads as "your
|
||||
# install is broken" when it actually means "I cannot see it". Relaunch elevated
|
||||
# instead of reporting a false state.
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
Write-Host ''
|
||||
Write-Host ' Administrator rights are needed to read IIS state.' -ForegroundColor Yellow
|
||||
Write-Host ' Re-launching elevated - approve the prompt.' -ForegroundColor Yellow
|
||||
$argList = @('-NoExit', '-NoProfile', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', ('"' + $PSCommandPath + '"'), $Command)
|
||||
if ($Path) { $argList += @('-Path', ('"' + $Path + '"')) }
|
||||
try {
|
||||
Start-Process -FilePath 'powershell.exe' -ArgumentList $argList -Verb RunAs | Out-Null
|
||||
} catch {
|
||||
Write-Host ''
|
||||
Write-Host ' Elevation was declined.' -ForegroundColor Red
|
||||
Write-Host ' Right-click the shortcut and choose "Run as administrator".' -ForegroundColor Red
|
||||
Read-Host ' Press Enter to close' | Out-Null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
# --- brand header ----------------------------------------------------------
|
||||
# Deliberately typographic, NOT ASCII art. The GE monogram is fine cursive
|
||||
# linework; rendered as block characters at console resolution it reads as noise,
|
||||
# which looks worse than no mark at all. A clean rule and correct wordmark says
|
||||
# "considered"; mushy art says the opposite.
|
||||
#
|
||||
# Box-drawing characters used here are all in code page 437 (the Windows console
|
||||
# default), and this file is saved UTF-8 WITH BOM so PowerShell 5.1 reads them
|
||||
# correctly rather than assuming the ANSI code page.
|
||||
function Show-Banner {
|
||||
$rule = ([string][char]0x2500) * 58
|
||||
Write-Host ''
|
||||
Write-Host ' GE AEROSPACE' -ForegroundColor Blue
|
||||
Write-Host (' ' + $rule) -ForegroundColor DarkGray
|
||||
Write-Host ' ShopDB-Flask' -ForegroundColor White
|
||||
Write-Host ' Asset management for the shop floor' -ForegroundColor DarkGray
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Say { param($m, $c = 'Gray') Write-Host $m -ForegroundColor $c }
|
||||
function Head {
|
||||
param($m)
|
||||
Write-Host ''
|
||||
Write-Host (' ' + $m) -ForegroundColor Cyan
|
||||
Write-Host (' ' + (([string][char]0x2500) * $m.Length)) -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
function Get-EnvValue {
|
||||
param([string] $Key)
|
||||
$envFile = Join-Path $AppRoot '.env'
|
||||
if (-not (Test-Path $envFile)) { return '' }
|
||||
$line = Get-Content $envFile | Where-Object { $_ -like "$Key=*" } | Select-Object -First 1
|
||||
if ($line) { return $line.Substring($Key.Length + 1) }
|
||||
return ''
|
||||
}
|
||||
|
||||
function Get-Deployment {
|
||||
<#
|
||||
Which way was this installed?
|
||||
|
||||
Method A: its own IIS site on $SitePort.
|
||||
Method B: an IIS Application under an existing site, at /<alias>, reached
|
||||
on that site's port. MOUNT_PATH in .env is what distinguishes
|
||||
them - it is the same value wsgi.py uses to mount the app.
|
||||
|
||||
Without this the console looked for a SITE that method B never creates and
|
||||
reported "web site: not installed" on a perfectly healthy server, then
|
||||
probed the wrong port and said it was not responding.
|
||||
#>
|
||||
$mount = (Get-EnvValue 'MOUNT_PATH').Trim()
|
||||
if (-not $mount) {
|
||||
return @{ Subpath = $false; Alias = ''; BaseUrl = "http://localhost:$SitePort"; Port = $SitePort }
|
||||
}
|
||||
$alias = $mount.Trim('/')
|
||||
$port = 80
|
||||
$parent = 'Default Web Site'
|
||||
try {
|
||||
Import-Module WebAdministration -ErrorAction Stop
|
||||
foreach ($site in (Get-Website)) {
|
||||
$app = Get-WebApplication -Site $site.Name -Name $alias -ErrorAction SilentlyContinue
|
||||
if ($app) {
|
||||
$parent = $site.Name
|
||||
$b = $site.bindings.Collection | Where-Object { $_.protocol -eq 'http' } | Select-Object -First 1
|
||||
if ($b -and ($b.bindingInformation -match '^[^:]*:(\d+):')) { $port = [int]$Matches[1] }
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
$base = "http://localhost:$port/$alias"
|
||||
if ($port -eq 80) { $base = "http://localhost/$alias" }
|
||||
return @{ Subpath = $true; Alias = $alias; Parent = $parent; BaseUrl = $base; Port = $port }
|
||||
}
|
||||
|
||||
function Get-DbParts {
|
||||
# Pull host/port/name/user out of DATABASE_URL without printing the password.
|
||||
$url = Get-EnvValue 'DATABASE_URL'
|
||||
if ($url -match '://([^:]+):([^@]*)@([^:/]+):(\d+)/([^?]+)') {
|
||||
# The password is PERCENT-ENCODED in DATABASE_URL (the installer applies
|
||||
# [uri]::EscapeDataString). SQLAlchemy unescapes it; so must we. Without
|
||||
# this, every command here fails to authenticate whenever the password
|
||||
# contains a space, @, %, ! or /, and the console reports the database as
|
||||
# unreachable on a server where the application is running perfectly.
|
||||
return @{ User = [uri]::UnescapeDataString($Matches[1])
|
||||
Pass = [uri]::UnescapeDataString($Matches[2])
|
||||
Host = $Matches[3]; Port = $Matches[4]; Name = $Matches[5] }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-MysqlClient {
|
||||
$candidates = @(
|
||||
'C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe',
|
||||
'C:\mysql56\bin\mysql.exe'
|
||||
) + @(Get-ChildItem 'C:\Program Files\MySQL' -Filter mysql.exe -Recurse -EA SilentlyContinue |
|
||||
Select-Object -ExpandProperty FullName)
|
||||
foreach ($c in $candidates) { if ($c -and (Test-Path $c)) { return $c } }
|
||||
return ''
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
function Show-Status {
|
||||
Head 'Status'
|
||||
|
||||
$deploy = Get-Deployment
|
||||
$siteState = 'not installed'
|
||||
$poolState = 'not installed'
|
||||
try {
|
||||
Import-Module WebAdministration -ErrorAction Stop
|
||||
if ($deploy.Subpath) {
|
||||
# Method B: there is no site of our own - we are an Application.
|
||||
$app = Get-WebApplication -Site $deploy.Parent -Name $deploy.Alias -ErrorAction SilentlyContinue
|
||||
if ($app) {
|
||||
$parentSite = Get-Website -Name $deploy.Parent -ErrorAction SilentlyContinue
|
||||
$siteState = ("/{0} under '{1}' ({2})" -f $deploy.Alias, $deploy.Parent,
|
||||
$(if ($parentSite) { $parentSite.State } else { 'unknown' }))
|
||||
}
|
||||
} else {
|
||||
$site = Get-Website -Name $SiteName -ErrorAction SilentlyContinue
|
||||
if ($site) { $siteState = $site.State }
|
||||
}
|
||||
if (Test-Path "IIS:\AppPools\$AppPool") { $poolState = (Get-Item "IIS:\AppPools\$AppPool").State }
|
||||
} catch {
|
||||
# Should be unreachable now that the script self-elevates, but if IIS is
|
||||
# genuinely absent say THAT, rather than implying ShopDB is missing.
|
||||
# Name the real cause instead of implying ShopDB is missing.
|
||||
if (-not (Get-Service W3SVC -ErrorAction SilentlyContinue)) {
|
||||
$siteState = 'IIS is not installed on this server'
|
||||
} else {
|
||||
$siteState = 'could not read IIS - ' + $_.Exception.Message
|
||||
}
|
||||
$poolState = $siteState
|
||||
}
|
||||
|
||||
$colour = if (($siteState -eq 'Started') -or ($siteState -like '*Started*')) { 'Green' } else { 'Yellow' }
|
||||
Say (" published as : {0}" -f $siteState) $colour
|
||||
Say (" application pool: {0}" -f $poolState) $colour
|
||||
Say (" installed at : {0}" -f $(if (Test-Path $AppRoot) { $AppRoot } else { 'not found' }))
|
||||
|
||||
# Does it actually answer? A "Started" pool proves nothing. Under method B
|
||||
# this must include the mount path, or it tests a URL that never existed.
|
||||
$url = $deploy.BaseUrl + '/'
|
||||
try {
|
||||
$r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 20
|
||||
Say (" responding : yes (HTTP {0})" -f $r.StatusCode) 'Green'
|
||||
} catch {
|
||||
Say ' responding : NO' 'Red'
|
||||
Say (" {0}" -f $_.Exception.Message) 'DarkGray'
|
||||
}
|
||||
|
||||
$db = Get-DbParts
|
||||
if ($db) {
|
||||
Say (" database : {0} on {1}:{2} as {3}" -f $db.Name, $db.Host, $db.Port, $db.User)
|
||||
$mysql = Find-MysqlClient
|
||||
if ($mysql) {
|
||||
$q = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$($db.Name)';"
|
||||
$n = $q | & $mysql "-u$($db.User)" "-p$($db.Pass)" "-h$($db.Host)" "-P$($db.Port)" -N 2>$null
|
||||
if ($LASTEXITCODE -eq 0) { Say (" tables : {0}" -f $n) 'Green' }
|
||||
else { Say ' tables : could not connect' 'Red' }
|
||||
}
|
||||
} else { Say ' database : no .env found' 'Yellow' }
|
||||
|
||||
# Is anyone set up yet?
|
||||
try {
|
||||
$na = Invoke-WebRequest -Uri ($deploy.BaseUrl + '/api/setup/needs-admin') -UseBasicParsing -TimeoutSec 15
|
||||
if ($na.Content -match '"needsadmin"\s*:\s*true') {
|
||||
Say ' first run : NOT SET UP - open the site to create the first administrator' 'Yellow'
|
||||
} else { Say ' first run : complete' 'Green' }
|
||||
} catch { }
|
||||
|
||||
Say ''
|
||||
if ($deploy.Subpath) {
|
||||
$shown = "http://{0}/{1}/login" -f $env:COMPUTERNAME, $deploy.Alias
|
||||
if ($deploy.Port -ne 80) { $shown = "http://{0}:{1}/{2}/login" -f $env:COMPUTERNAME, $deploy.Port, $deploy.Alias }
|
||||
} else {
|
||||
$shown = "http://{0}:{1}/login" -f $env:COMPUTERNAME, $SitePort
|
||||
}
|
||||
Say (" open with : {0}" -f $shown) 'White'
|
||||
}
|
||||
|
||||
function Start-App {
|
||||
Head 'Starting'
|
||||
$deploy = Get-Deployment
|
||||
& $AppCmd start apppool /apppool.name:$AppPool 2>&1 | ForEach-Object { Say " $_" }
|
||||
if (-not $deploy.Subpath) {
|
||||
& $AppCmd start site /site.name:$SiteName 2>&1 | ForEach-Object { Say " $_" }
|
||||
} else {
|
||||
# Method B shares the parent site - starting or stopping THAT would take
|
||||
# every other application on this server with it.
|
||||
Say (" (published under '{0}' - only the application pool is ours to control)" -f $deploy.Parent) 'DarkGray'
|
||||
}
|
||||
Say ' started' 'Green'
|
||||
}
|
||||
|
||||
function Stop-App {
|
||||
Head 'Stopping'
|
||||
$deploy = Get-Deployment
|
||||
Say ' ShopDB-Flask will be unavailable until you start it again.' 'Yellow'
|
||||
if (-not $deploy.Subpath) {
|
||||
& $AppCmd stop site /site.name:$SiteName 2>&1 | ForEach-Object { Say " $_" }
|
||||
} else {
|
||||
Say (" (leaving site '{0}' running - stopping it would take down everything else on it)" -f $deploy.Parent) 'DarkGray'
|
||||
}
|
||||
& $AppCmd stop apppool /apppool.name:$AppPool 2>&1 | ForEach-Object { Say " $_" }
|
||||
Say ' stopped' 'Yellow'
|
||||
}
|
||||
|
||||
function Restart-App {
|
||||
Head 'Restarting'
|
||||
# Recycle rather than stop/start: it drains existing requests instead of
|
||||
# cutting them off, and it is what a config change actually needs.
|
||||
& $AppCmd recycle apppool /apppool.name:$AppPool 2>&1 | ForEach-Object { Say " $_" }
|
||||
Start-Sleep -Seconds 2
|
||||
try {
|
||||
$r = Invoke-WebRequest -Uri ((Get-Deployment).BaseUrl + '/') -UseBasicParsing -TimeoutSec 30
|
||||
Say (" back up (HTTP {0})" -f $r.StatusCode) 'Green'
|
||||
} catch { Say ' did not respond after restart - run: shopdb-admin.ps1 logs' 'Red' }
|
||||
}
|
||||
|
||||
function Show-Logs {
|
||||
Head 'Recent logs'
|
||||
$appLog = Join-Path $AppRoot 'logs'
|
||||
if (Test-Path $appLog) {
|
||||
$newest = Get-ChildItem "$appLog\*.log" -EA SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($newest) {
|
||||
Say (" application: {0}" -f $newest.FullName) 'White'
|
||||
Get-Content $newest.FullName -Tail 20 | ForEach-Object { Say " $_" }
|
||||
} else { Say ' no application log yet' }
|
||||
}
|
||||
$inst = 'C:\ProgramData\ShopDB-Flask\logs'
|
||||
if (Test-Path $inst) {
|
||||
$newest = Get-ChildItem "$inst\*.log" -EA SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($newest) {
|
||||
Say ''
|
||||
Say (" install: {0}" -f $newest.FullName) 'White'
|
||||
Get-Content $newest.FullName -Tail 12 | ForEach-Object { Say " $_" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Open-Site {
|
||||
$deploy = Get-Deployment
|
||||
if ($deploy.Subpath) {
|
||||
if ($deploy.Port -eq 80) { $u = "http://{0}/{1}/login" -f $env:COMPUTERNAME, $deploy.Alias }
|
||||
else { $u = "http://{0}:{1}/{2}/login" -f $env:COMPUTERNAME, $deploy.Port, $deploy.Alias }
|
||||
} else {
|
||||
$u = "http://{0}:{1}/login" -f $env:COMPUTERNAME, $SitePort
|
||||
}
|
||||
Start-Process $u
|
||||
Say ' opened in your browser' 'Green'
|
||||
}
|
||||
|
||||
function Backup-Db {
|
||||
param([string] $Dest)
|
||||
Head 'Database backup'
|
||||
$db = Get-DbParts
|
||||
if (-not $db) { Say ' no .env found - cannot determine the database' 'Red'; return }
|
||||
if (-not $Dest) { $Dest = 'C:\ProgramData\ShopDB-Flask\backups' }
|
||||
if (-not (Test-Path $Dest)) { New-Item -ItemType Directory -Path $Dest -Force | Out-Null }
|
||||
|
||||
$mysql = Find-MysqlClient
|
||||
if (-not $mysql) { Say ' mysql client not found' 'Red'; return }
|
||||
$dump = Join-Path (Split-Path $mysql -Parent) 'mysqldump.exe'
|
||||
if (-not (Test-Path $dump)) { Say ' mysqldump not found' 'Red'; return }
|
||||
|
||||
$file = Join-Path $Dest ("shopdb_flask-{0}.sql" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
Say (" writing {0}" -f $file)
|
||||
# Redirect rather than --result-file: keeps it working on 5.6 and 8.0 alike.
|
||||
# Do NOT pipe mysqldump through the PowerShell pipeline into Out-File.
|
||||
# PowerShell 5.1 writes a UTF-8 BOM and re-encodes the stream through the
|
||||
# console code page, producing a dump MySQL refuses to load and mangling any
|
||||
# non-ASCII data - discovered only when the backup is finally needed.
|
||||
# Redirect the process's stdout straight to the file instead.
|
||||
$args = @("-u$($db.User)", "-p$($db.Pass)", "-h$($db.Host)", "-P$($db.Port)",
|
||||
'--single-transaction', '--routines', '--triggers', $db.Name)
|
||||
$quoted = $args | ForEach-Object {
|
||||
if ($_ -match '\s' -and $_ -notmatch '^".*"$') { '"' + $_ + '"' } else { $_ }
|
||||
}
|
||||
$err = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
$proc = Start-Process -FilePath $dump -ArgumentList $quoted -Wait -PassThru -NoNewWindow `
|
||||
-RedirectStandardOutput $file -RedirectStandardError $err
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Say (" mysqldump failed (exit {0})" -f $proc.ExitCode) 'Red'
|
||||
Get-Content $err -Tail 3 -EA SilentlyContinue | ForEach-Object { Say (" " + $_) 'DarkGray' }
|
||||
return
|
||||
}
|
||||
} finally { Remove-Item $err -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
# Verify it is complete rather than merely present.
|
||||
$tail = @(Get-Content $file -Tail 5 -ErrorAction SilentlyContinue)
|
||||
if ((Test-Path $file) -and ((Get-Item $file).Length -gt 1024) -and ($tail -match 'Dump completed')) {
|
||||
Say (" done - {0:N1} MB, verified complete" -f ((Get-Item $file).Length / 1MB)) 'Green'
|
||||
Say ' Store this off the server. It contains all of your asset data.' 'Yellow'
|
||||
} else {
|
||||
Say ' backup is empty or truncated - do NOT rely on it' 'Red'
|
||||
Remove-Item $file -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Check {
|
||||
Head 'Health check'
|
||||
$flask = Join-Path $AppRoot 'venv\Scripts\flask.exe'
|
||||
if (-not (Test-Path $flask)) { Say ' application not installed' 'Red'; return }
|
||||
Push-Location $AppRoot
|
||||
$env:FLASK_APP = 'shopdb'
|
||||
try { & $flask db-utils preflight 2>&1 | ForEach-Object { Say " $_" } }
|
||||
finally { Pop-Location }
|
||||
}
|
||||
|
||||
function Show-Sessions {
|
||||
Head 'Worker processes'
|
||||
$w = Get-CimInstance Win32_Process -Filter "Name='w3wp.exe'" -EA SilentlyContinue
|
||||
if (-not $w) { Say ' no IIS worker running (the site starts one on first request)' }
|
||||
else {
|
||||
foreach ($p in $w) {
|
||||
Say (" pid {0} {1:N0} MB started {2}" -f $p.ProcessId,
|
||||
($p.WorkingSetSize/1MB), $p.CreationDate)
|
||||
}
|
||||
}
|
||||
$py = Get-CimInstance Win32_Process -Filter "Name='python.exe'" -EA SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -like "*$AppRoot*" }
|
||||
if ($py) { foreach ($p in $py) { Say (" python pid {0} {1:N0} MB" -f $p.ProcessId, ($p.WorkingSetSize/1MB)) } }
|
||||
}
|
||||
|
||||
|
||||
function Invoke-Flask {
|
||||
param([string[]] $Arguments)
|
||||
$flask = Join-Path $AppRoot 'venv\Scripts\flask.exe'
|
||||
if (-not (Test-Path $flask)) { Say ' application not installed' 'Red'; return $null }
|
||||
Push-Location $AppRoot
|
||||
$env:FLASK_APP = 'shopdb'
|
||||
# The app logs plugin startup to STDERR even on success; with EAP=Stop that
|
||||
# becomes a terminating error and a healthy command looks like a failure.
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try { & $flask @Arguments 2>&1 }
|
||||
finally { $ErrorActionPreference = $prev; Pop-Location }
|
||||
}
|
||||
|
||||
function Show-Plugins {
|
||||
Head 'Plugins'
|
||||
Invoke-Flask @('plugin','list') | ForEach-Object { Say " $_" }
|
||||
|
||||
# What is on disk but NOT installed can still be added here. What is absent
|
||||
# from disk cannot - see the note below.
|
||||
$dir = Join-Path $AppRoot 'plugins'
|
||||
if (Test-Path $dir) {
|
||||
$onDisk = (Get-ChildItem $dir -Directory -EA SilentlyContinue |
|
||||
Where-Object { Test-Path (Join-Path $_.FullName 'manifest.json') } |
|
||||
Select-Object -ExpandProperty Name) -join ', '
|
||||
Say ''
|
||||
Say (" shipped in this build : {0}" -f $onDisk) 'White'
|
||||
}
|
||||
Say ''
|
||||
Say ' Add one that is shipped : shopdb-admin.ps1 add-plugin -Path <name>' 'White'
|
||||
Say ''
|
||||
Say ' A plugin NOT listed above is not on this server at all. This build was' 'DarkGray'
|
||||
Say ' made for your site''s chosen plugin set, so its code was never shipped.' 'DarkGray'
|
||||
Say ' Adding one means a new installer built from an updated site profile.' 'DarkGray'
|
||||
}
|
||||
|
||||
function Add-Plugin {
|
||||
param([string] $Name)
|
||||
Head 'Add a plugin'
|
||||
if (-not $Name) { Say ' usage: shopdb-admin.ps1 add-plugin -Path <plugin-name>' 'Yellow'; return }
|
||||
|
||||
$dir = Join-Path $AppRoot ('plugins\' + $Name)
|
||||
if (-not (Test-Path (Join-Path $dir 'manifest.json'))) {
|
||||
Say (" '{0}' is not present on this server." -f $Name) 'Red'
|
||||
Say ''
|
||||
Say ' This build ships only the plugins your site chose. Adding a new one' 'Yellow'
|
||||
Say ' requires a new installer built from an updated site profile - the' 'Yellow'
|
||||
Say ' code is not here to install.' 'Yellow'
|
||||
Say ''
|
||||
Say ' Run "shopdb-admin.ps1 plugins" to see what IS available.' 'White'
|
||||
return
|
||||
}
|
||||
|
||||
Say (" installing {0}..." -f $Name)
|
||||
Invoke-Flask @('plugin','install',$Name) | ForEach-Object { Say " $_" }
|
||||
Say ' applying its database migrations...'
|
||||
Invoke-Flask @('plugin','upgrade-all') | ForEach-Object { Say " $_" }
|
||||
Say ' restarting so its routes register...'
|
||||
Restart-App
|
||||
Say (" {0} added" -f $Name) 'Green'
|
||||
}
|
||||
|
||||
function Show-Uninstall {
|
||||
Head 'Uninstall'
|
||||
Say ' Use Settings > Apps > ShopDB-Flask, or Add/Remove Programs.'
|
||||
Say ''
|
||||
Say ' That removes the web site, application pool, firewall rule and files.'
|
||||
Say ' It does NOT drop the database and does NOT uninstall MySQL.' 'Yellow'
|
||||
Say ' Take a backup first: shopdb-admin.ps1 backup' 'Yellow'
|
||||
}
|
||||
|
||||
function Show-Menu {
|
||||
$first = $true
|
||||
while ($true) {
|
||||
if ($first) { Show-Banner; $first = $false }
|
||||
Show-Status
|
||||
Write-Host ''
|
||||
Write-Host ' 1 Restart the application 6 Back up the database' -ForegroundColor White
|
||||
Write-Host ' 2 Stop the application 7 Worker processes' -ForegroundColor White
|
||||
Write-Host ' 3 Start the application 8 Open in browser' -ForegroundColor White
|
||||
Write-Host ' 4 View recent logs 9 Plugins' -ForegroundColor White
|
||||
Write-Host ' 5 Health check 0 Exit' -ForegroundColor White
|
||||
Write-Host ''
|
||||
$c = Read-Host ' Choose'
|
||||
switch ($c) {
|
||||
'1' { Restart-App } '2' { Stop-App } '3' { Start-App }
|
||||
'4' { Show-Logs } '5' { Invoke-Check } '6' { Backup-Db $Path }
|
||||
'7' { Show-Sessions } '8' { Open-Site }
|
||||
'9' { Show-Plugins
|
||||
$add = Read-Host ' Name of a shipped plugin to add (Enter to skip)'
|
||||
if ($add) { Add-Plugin $add } }
|
||||
'0' { return }
|
||||
default { Say ' not a choice' 'Yellow' }
|
||||
}
|
||||
Write-Host ''
|
||||
Read-Host ' Press Enter to continue' | Out-Null
|
||||
Clear-Host
|
||||
Show-Banner
|
||||
}
|
||||
}
|
||||
|
||||
switch ($Command) {
|
||||
'status' { Show-Banner; Show-Status }
|
||||
'start' { Start-App }
|
||||
'stop' { Stop-App }
|
||||
'restart' { Restart-App }
|
||||
'logs' { Show-Logs }
|
||||
'open' { Open-Site }
|
||||
'backup' { Backup-Db $Path }
|
||||
'check' { Invoke-Check }
|
||||
'sessions' { Show-Sessions }
|
||||
'plugins' { Show-Plugins }
|
||||
'add-plugin'{ Add-Plugin $Path }
|
||||
'uninstall' { Show-Uninstall }
|
||||
default { Show-Menu }
|
||||
}
|
||||
Reference in New Issue
Block a user