# ============================================================ # FreeSWITCH Latest Installer (MSI-first) # - Downloads latest MSI/EXE from directory URL # - Saves to: C:\ProgramData\General Devices\Install Files # - If MSI: attempts to detect "Configuration Samples" feature # and disables it (equivalent to "Entire feature will be unavailable") # - Uses robust msiexec command string quoting to avoid 1639 # - Avoids Sort-Object parsing/paste issues # ============================================================ 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 # ------------------------- $BaseUrl = 'https://gdsupport.gdserver.net/CAREpoint3/CP3-Server/FreeSwitch/Latest/' $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 Get-LatestInstallerFromDirectory { param([Parameter(Mandatory=$true)][string]$DirectoryUrl) Log ('Reading directory 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 '^\#') { continue } if ($href -match '\.\./') { continue } if ($href -match '\.(msi|exe)$') { $abs = if ($href -match '^https?://') { $href } else { ($DirectoryUrl.TrimEnd('/') + '/' + $href.TrimStart('/')) } $links += $abs } } 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 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 } # ------------------------- # MSI table helpers # ------------------------- function Get-MsiTableRows { param( [Parameter(Mandatory=$true)][string]$MsiPath, [Parameter(Mandatory=$true)][string]$Sql, [Parameter(Mandatory=$true)][string[]]$Columns ) $installer = New-Object -ComObject WindowsInstaller.Installer $db = $installer.OpenDatabase($MsiPath, 0) $view = $db.OpenView($Sql) $view.Execute() $rows = @() while ($true) { $rec = $view.Fetch() if ($null -eq $rec) { break } $obj = [ordered]@{} for ($i=0; $i -lt $Columns.Count; $i++) { $obj[$Columns[$i]] = $rec.StringData($i+1) } $rows += [pscustomobject]$obj } $view.Close() return $rows } function Get-MsiFeaturesCore { param([Parameter(Mandatory=$true)][string]$MsiPath) Get-MsiTableRows -MsiPath $MsiPath ` -Sql 'SELECT `Feature`, `Title`, `Description` FROM `Feature`' ` -Columns @('Feature','Title','Description') } function Get-MsiFeatureComponents { param([Parameter(Mandatory=$true)][string]$MsiPath) Get-MsiTableRows -MsiPath $MsiPath ` -Sql 'SELECT `Feature_`, `Component_` FROM `FeatureComponents`' ` -Columns @('Feature','Component') } function Get-MsiComponents { param([Parameter(Mandatory=$true)][string]$MsiPath) Get-MsiTableRows -MsiPath $MsiPath ` -Sql 'SELECT `Component`, `Directory_`, `KeyPath` FROM `Component`' ` -Columns @('Component','Directory','KeyPath') } function Find-ConfigSamplesFeatureName { param([Parameter(Mandatory=$true)][string]$MsiPath) $features = Get-MsiFeaturesCore -MsiPath $MsiPath if (-not $features -or $features.Count -eq 0) { Log 'No MSI Feature rows found.' 'WARN' return $null } # 1) Fast path: Title/Description match $needles = @('Configuration Samples','Config Samples','Samples','Vanilla','Default configuration') foreach ($n in $needles) { $hit = $features | Where-Object { ($_.Title -and $_.Title -like ('*' + $n + '*')) -or ($_.Description -and $_.Description -like ('*' + $n + '*')) } | Select-Object -First 1 if ($hit) { Log ('Detected feature by title/desc match: Feature={0} Title={1}' -f $hit.Feature, $hit.Title) 'INFO' return $hit.Feature } } # 2) Deeper heuristic: FeatureComponents + Component metadata $fc = Get-MsiFeatureComponents -MsiPath $MsiPath $comps = Get-MsiComponents -MsiPath $MsiPath if (-not $fc -or -not $comps) { Log 'Could not read FeatureComponents/Component tables for deeper detection.' 'WARN' return $null } $keywords = @('sample','samples','vanilla','default','conf','configuration','example') $featureGroups = $fc | Group-Object Feature $scored = foreach ($g in $featureGroups) { $featureId = $g.Name $componentIds = @($g.Group.Component) $hits = 0 foreach ($cid in $componentIds) { $c = $comps | Where-Object { $_.Component -eq $cid } | Select-Object -First 1 if (-not $c) { continue } $blob = (($c.Directory + ' ' + $c.KeyPath) -as [string]).ToLowerInvariant() foreach ($kw in $keywords) { if ($blob -like ('*' + $kw + '*')) { $hits++ } } } [pscustomobject]@{ Feature = $featureId Score = $hits Components = $componentIds.Count } } # IMPORTANT: robust sort syntax (avoids "empty pipe element" / comma parsing issues) $scored = $scored | Sort-Object -Property @{Expression='Score';Descending=$true}, @{Expression='Components';Descending=$true} $top = $scored | Select-Object -First 5 foreach ($t in $top) { Log ('Feature candidate: {0} Score={1} Components={2}' -f $t.Feature, $t.Score, $t.Components) 'INFO' } $best = $scored | Select-Object -First 1 if ($best -and $best.Score -gt 0) { Log ('Auto-selected feature to disable by install-heuristics: {0} (Score={1})' -f $best.Feature, $best.Score) 'INFO' return $best.Feature } Log 'Could not auto-detect Configuration Samples feature (no matches found).' 'WARN' return $null } function Install-FreeSwitchLatest { param( [Parameter(Mandatory=$true)][string]$LatestUrl, [Parameter(Mandatory=$true)][string]$DownloadDir ) $fileName = [System.IO.Path]::GetFileName([Uri]$LatestUrl) $localPath = Join-Path $DownloadDir $fileName Invoke-Download -Url $LatestUrl -OutFile $localPath if ($localPath -match '\.msi$') { Log 'Installer is MSI. Installing silently and excluding configuration samples (if detected)...' 'INFO' $featureToDisable = Find-ConfigSamplesFeatureName -MsiPath $localPath $msiLogPath = Join-Path $DownloadDir ('FreeSwitchInstall-' + (Get-Date).ToString('yyyyMMdd-HHmmss') + '.log') # Quick sanity check that we can write in the folder (optional but helpful) try { 'log write test' | Out-File -FilePath $msiLogPath -Force Remove-Item -LiteralPath $msiLogPath -Force -ErrorAction SilentlyContinue } catch { Log ('Warning: unable to create log file in download dir: {0}' -f $_.Exception.Message) 'WARN' } # Build ONE quoted msiexec command string to avoid 1639 $cmd = '/i "{0}" /qn /norestart /l*v "{1}" ADDLOCAL=ALL' -f $localPath, $msiLogPath if ($featureToDisable) { $cmd = $cmd + (' REMOVE="{0}"' -f $featureToDisable) Log ('Will exclude MSI feature (Entire feature will be unavailable): REMOVE={0}' -f $featureToDisable) 'INFO' } else { Log 'Feature name not detected; proceeding without exclusion.' 'WARN' } 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 'FreeSWITCH installed successfully.' 'INFO' Log ('MSI log: {0}' -f $msiLogPath) 'INFO' } elseif ($p.ExitCode -eq 3010) { Log 'FreeSWITCH installed successfully (reboot required).' 'WARN' Log ('MSI log: {0}' -f $msiLogPath) 'INFO' } else { Log ('FreeSWITCH MSI install failed with exit code {0}. MSI log should be: {1}' -f $p.ExitCode, $msiLogPath) 'ERROR' Exit $p.ExitCode } } elseif ($localPath -match '\.exe$') { Log 'Installer is EXE. Feature-level selection may not be supported.' 'WARN' Log 'Attempting common silent switches...' 'INFO' $exeArgs = '/S /VERYSILENT /NORESTART' $p = Start-Process -FilePath $localPath -ArgumentList $exeArgs -Wait -PassThru if ($p.ExitCode -eq 0) { Log 'FreeSWITCH EXE install completed successfully.' 'INFO' } elseif ($p.ExitCode -eq 3010) { Log 'FreeSWITCH EXE install completed (reboot required).' 'WARN' } else { Log ('FreeSWITCH EXE install failed with exit code {0}.' -f $p.ExitCode) 'ERROR' Exit $p.ExitCode } } else { throw ('Downloaded file is not MSI or EXE: {0}' -f $localPath) } Log 'FreeSWITCH latest install workflow completed.' 'INFO' } # ------------------------- # MAIN # ------------------------- try { $latestInstallerUrl = Get-LatestInstallerFromDirectory -DirectoryUrl $BaseUrl Log ('Latest installer selected: {0}' -f $latestInstallerUrl) 'INFO' Install-FreeSwitchLatest -LatestUrl $latestInstallerUrl -DownloadDir $DownloadDir } catch { Log ('Fatal error: {0}' -f $_.Exception.Message) 'FATAL' throw }