# ============================================================ # FormViewer - Download + Install LATEST from version folders # - Reads https://gdsupport.gdserver.net/Forms/FormViewer/ # - Picks newest VERSION folder (by version number; fallback Last-Modified) # - Enters that folder, picks newest MSI/EXE (prefers MSI) # - Downloads to: C:\ProgramData\General Devices\Install Files # - Installs silently, writes MSI log if MSI # ============================================================ function Log { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$message, [Parameter(Mandatory=$False)] [ValidateSet('INFO','WARN','ERROR','FATAL','DEBUG')] [string]$level = 'INFO' ) switch ($level) { 'INFO' { $color = 'Green' } 'WARN' { $color = 'Yellow' } 'ERROR' { $color = 'Red' } 'FATAL' { $color = 'Magenta' } 'DEBUG' { $color = 'Cyan' } } $padding = '' if ($level.Length -eq 4) { $padding = ' ' } $ts = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') Write-Host ('{0}|{1}{2}|{3}' -f $ts, $level, $padding, $message) -ForegroundColor $color } # Re-launch into 64-bit PowerShell if running in 32-bit mode if ($PSHOME -like '*SysWOW64*') { & (Join-Path ($PSHOME -replace 'SysWOW64','SysNative') 'powershell.exe') ` -File (Join-Path $PSScriptRoot $MyInvocation.MyCommand) @args Exit $LastExitCode } Log ('Running {0} using {1}' -f $MyInvocation.MyCommand.Name, $PSHOME) 'INFO' # ------------------------- # Settings # ------------------------- $RootUrl = 'https://gdsupport.gdserver.net/Forms/FormViewer/' $DownloadDir = 'C:\ProgramData\General Devices\Install Files' # ------------------------- # Ensure download folder exists # ------------------------- try { if (-not (Test-Path -LiteralPath $DownloadDir)) { Log ('Creating download folder: {0}' -f $DownloadDir) 'INFO' New-Item -Path $DownloadDir -ItemType Directory -Force | Out-Null } else { Log ('Download folder exists: {0}' -f $DownloadDir) 'INFO' } } catch { Log ('Failed to create or access download folder: {0}' -f $_.Exception.Message) 'FATAL' throw } function Invoke-Download { param( [Parameter(Mandatory=$true)][string]$Url, [Parameter(Mandatory=$true)][string]$OutFile ) Log ('Downloading: {0}' -f $Url) 'INFO' Log ('Saving to: {0}' -f $OutFile) 'INFO' try { Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing } catch { Log ('Download failed: {0}' -f $_.Exception.Message) 'ERROR' throw } } function Normalize-AbsoluteUrl { param( [Parameter(Mandatory=$true)][string]$BaseUrl, [Parameter(Mandatory=$true)][string]$Href ) if ($Href -match '^https?://') { return $Href } return ($BaseUrl.TrimEnd('/') + '/' + $Href.TrimStart('/')) } function Get-VersionFolders { param([Parameter(Mandatory=$true)][string]$DirectoryUrl) Log ('Reading root directory listing: {0}' -f $DirectoryUrl) 'INFO' $resp = Invoke-WebRequest -Uri $DirectoryUrl -UseBasicParsing $folders = @() foreach ($l in $resp.Links) { if (-not $l.href) { continue } $href = [string]$l.href if ($href -match '^\s*$') { continue } if ($href -match 'Parent Directory') { continue } if ($href -match '^\#') { continue } if ($href -match '\.\./') { continue } # We only want folder links like "7.4.0.4/" etc. if ($href -match '/$') { $name = $href.TrimEnd('/').Trim() # Only accept dotted numeric versions (e.g., 7.4.0.4) if ($name -match '^\d+(\.\d+)+$') { $ver = $null try { $ver = [version]$name } catch { $ver = $null } $folders += [pscustomobject]@{ Name = $name Version = $ver Url = (Normalize-AbsoluteUrl -BaseUrl $DirectoryUrl -Href $href) } } } } return $folders } function Get-LatestVersionFolderUrl { param([Parameter(Mandatory=$true)][string]$RootDirectoryUrl) $folders = Get-VersionFolders -DirectoryUrl $RootDirectoryUrl if (-not $folders -or $folders.Count -eq 0) { throw ('No version folders found at {0}' -f $RootDirectoryUrl) } # Prefer semantic version sorting $withVer = $folders | Where-Object { $_.Version -ne $null } if ($withVer.Count -gt 0) { $pick = $withVer | Sort-Object Version -Descending | Select-Object -First 1 Log ('Latest version folder (by version): {0}' -f $pick.Name) 'INFO' return $pick.Url } # Fallback: try HEAD Last-Modified on folder URL $meta = foreach ($f in $folders) { $lm = $null try { $h = Invoke-WebRequest -Uri $f.Url -Method Head -UseBasicParsing $lmHeader = $h.Headers['Last-Modified'] if ($lmHeader) { $lm = [datetime]::Parse($lmHeader) } } catch { } [pscustomobject]@{ Name = $f.Name Url = $f.Url LastModified = $lm } } $withDates = $meta | Where-Object { $_.LastModified -ne $null } | Sort-Object LastModified -Descending if ($withDates.Count -gt 0) { $pick2 = $withDates[0] Log ('Latest version folder (by Last-Modified): {0} ({1})' -f $pick2.Name, $pick2.LastModified) 'INFO' return $pick2.Url } # Final fallback: string sort $fallback = $folders | Sort-Object Name -Descending | Select-Object -First 1 Log ('Could not parse versions or Last-Modified; using folder name fallback: {0}' -f $fallback.Name) 'WARN' return $fallback.Url } function Get-LatestInstallerFromDirectory { param([Parameter(Mandatory=$true)][string]$DirectoryUrl) Log ('Reading version folder listing: {0}' -f $DirectoryUrl) 'INFO' $resp = Invoke-WebRequest -Uri $DirectoryUrl -UseBasicParsing $links = @() foreach ($l in $resp.Links) { if (-not $l.href) { continue } $href = [string]$l.href if ($href -match '^\s*$') { continue } if ($href -match 'Parent Directory') { continue } if ($href -match '^\#') { continue } if ($href -match '\.\./') { continue } if ($href -match '\.(msi|exe)$') { $links += (Normalize-AbsoluteUrl -BaseUrl $DirectoryUrl -Href $href) } } if (-not $links -or $links.Count -eq 0) { throw ('No .msi or .exe installers found at {0}' -f $DirectoryUrl) } # Prefer MSI over EXE $msiLinks = $links | Where-Object { $_ -match '\.msi$' } $exeLinks = $links | Where-Object { $_ -match '\.exe$' } $candidates = if ($msiLinks.Count -gt 0) { $msiLinks } else { $exeLinks } Log ('Found {0} candidate installer(s). Determining newest...' -f $candidates.Count) 'INFO' # Prefer Last-Modified via HEAD $meta = foreach ($u in $candidates) { $lm = $null try { $h = Invoke-WebRequest -Uri $u -Method Head -UseBasicParsing $lmHeader = $h.Headers['Last-Modified'] if ($lmHeader) { $lm = [datetime]::Parse($lmHeader) } } catch { } [pscustomobject]@{ Url = $u FileName = [System.IO.Path]::GetFileName([Uri]$u) LastModified = $lm } } $withDates = $meta | Where-Object { $_.LastModified -ne $null } | Sort-Object LastModified -Descending if ($withDates.Count -gt 0) { $pick = $withDates[0] Log ('Newest installer by Last-Modified: {0} ({1})' -f $pick.FileName, $pick.LastModified) 'INFO' return $pick.Url } # fallback: filename $fallback = $meta | Sort-Object FileName -Descending | Select-Object -First 1 Log ('HEAD Last-Modified not available; using filename fallback: {0}' -f $fallback.FileName) 'WARN' return $fallback.Url } function Install-DownloadedInstaller { param([Parameter(Mandatory=$true)][string]$LocalPath) if ($LocalPath -match '\.msi$') { $msiLogPath = Join-Path $DownloadDir ('FormViewerInstall-' + (Get-Date).ToString('yyyyMMdd-HHmmss') + '.log') # One-string msiexec command (robust quoting) $cmd = '/i "{0}" /qn /norestart /l*v "{1}"' -f $LocalPath, $msiLogPath Log 'Starting MSI install...' 'INFO' Log ('msiexec.exe {0}' -f $cmd) 'DEBUG' $p = Start-Process -FilePath 'msiexec.exe' -ArgumentList $cmd -Wait -PassThru if ($p.ExitCode -eq 0) { Log 'FormViewer installed successfully.' 'INFO' Log ('MSI log: {0}' -f $msiLogPath) 'INFO' } elseif ($p.ExitCode -eq 3010) { Log 'FormViewer installed successfully (reboot required).' 'WARN' Log ('MSI log: {0}' -f $msiLogPath) 'INFO' } else { Log ('FormViewer MSI install failed with exit code {0}. MSI log: {1}' -f $p.ExitCode, $msiLogPath) 'ERROR' Exit $p.ExitCode } return } if ($LocalPath -match '\.exe$') { Log 'Installer is EXE. Attempting common silent switches...' 'INFO' $exeArgs = '/S /VERYSILENT /SUPPRESSMSGBOXES /NORESTART' Log ('Starting EXE install: "{0}" {1}' -f $LocalPath, $exeArgs) 'DEBUG' $p = Start-Process -FilePath $LocalPath -ArgumentList $exeArgs -Wait -PassThru if ($p.ExitCode -eq 0) { Log 'FormViewer EXE install completed successfully.' 'INFO' } elseif ($p.ExitCode -eq 3010) { Log 'FormViewer EXE install completed (reboot required).' 'WARN' } else { Log ('FormViewer EXE install failed with exit code {0}.' -f $p.ExitCode) 'ERROR' Exit $p.ExitCode } return } throw ('Downloaded file is not MSI or EXE: {0}' -f $LocalPath) } # ------------------------- # MAIN # ------------------------- try { $latestFolderUrl = Get-LatestVersionFolderUrl -RootDirectoryUrl $RootUrl Log ('Latest version folder URL: {0}' -f $latestFolderUrl) 'INFO' $latestInstallerUrl = Get-LatestInstallerFromDirectory -DirectoryUrl $latestFolderUrl Log ('Latest installer selected: {0}' -f $latestInstallerUrl) 'INFO' $fileName = [System.IO.Path]::GetFileName([Uri]$latestInstallerUrl) $localPath = Join-Path $DownloadDir $fileName Invoke-Download -Url $latestInstallerUrl -OutFile $localPath Install-DownloadedInstaller -LocalPath $localPath Log 'FormViewer latest install workflow completed.' 'INFO' } catch { Log ('Fatal error: {0}' -f $_.Exception.Message) 'FATAL' throw }