SqlServer-Version-Management.psm1

# Include Directive: [ src\Synopsis.ps1 ]
# Include Directive: [ ..\Includes\*.ps1 ]
# Include File: [\Includes\$Full7zLinksMetadata.ps1]
$Full7zLinksMetadata_onWindows = @(
  @{ Ver = 2301; 
     X64Links = @(
       "https://www.7-zip.org/a/7z2301-x64.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x64-2301.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x64-2301.7z?viasf=1"
     );
     ARM64Links = @(
       "https://www.7-zip.org/a/7z2301-arm64.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-arm64-2301.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-arm64-2301.7z?viasf=1"
     );
     X86Links = @(
       "https://www.7-zip.org/a/7z2301.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x86-2301.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x86-2301.7z?viasf=1"
     )
  },
  @{ Ver = 2201; 
     X64Links = @(
       "https://www.7-zip.org/a/7z2201-x64.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x64-2201.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x64-2201.7z?viasf=1"
     );
     ARM64Links = @(
       "https://www.7-zip.org/a/7z2201-arm64.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-arm64-2201.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-arm64-2201.7z?viasf=1"
     );
     X86Links = @(
       "https://www.7-zip.org/a/7z2201.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x86-2201.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x86-2201.7z?viasf=1"
     )
  },
  @{ Ver = 1900; 
     X64Links = @(
       "https://www.7-zip.org/a/7z1900-x64.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x64-1900.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x64-1900.7z?viasf=1"
     );
     X86Links = @(
       "https://www.7-zip.org/a/7z1900.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x86-1900.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x86-1900.7z?viasf=1"
     )
  },
  @{ Ver = 1604;
     X64Links = @(
       "https://www.7-zip.org/a/7z1604-x64.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x64-1604.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x64-1604.7z?viasf=1"
     );
     X86Links = @(
       "https://www.7-zip.org/a/7z1604.exe",
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x86-1604.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x86-1604.7z?viasf=1"
     )
  },
  @{ Ver = 920;
     X86Links = @(
       "https://sourceforge.net/projects/p7zz-repack/files/windows/7z-full-x86-920.7z/download",
       "https://master.dl.sourceforge.net/project/p7zz-repack/windows/7z-full-x86-920.7z?viasf=1"
     )
  }
);

<#
  https://www.7-zip.org/a/7z2301-arm64.exe
  https://www.7-zip.org/a/7z2301-x64.exe
  https://www.7-zip.org/a/7z2301.exe
 
  https://www.7-zip.org/a/7z2201-arm64.exe
  https://www.7-zip.org/a/7z2201-x64.exe
  https://www.7-zip.org/a/7z2201.exe
 
  https://www.7-zip.org/a/7z1900-x64.exe
  https://www.7-zip.org/a/7z1900.exe
   
  https://www.7-zip.org/a/7z1604-x64.exe
  https://www.7-zip.org/a/7z1604.exe
   
  https://www.7-zip.org/a/7z920.exe
  https://www.7-zip.org/a/7z920-arm.exe
 
  https://www.7-zip.org/a/7zr.exe
#>


# Include File: [\Includes\$VcRuntimeLinksMetadata.ps1]
$VcRuntimeLinksMetadata = @(
  @{ Ver=14; 
     Args="/install /passive /norestart"; 
     X64Link="https://aka.ms/vs/17/release/vc_redist.x64.exe"; 
     X86Link="https://aka.ms/vs/17/release/vc_redist.x86.exe"; 
     ARM64Link="https://aka.ms/vs/17/release/vc_redist.arm64.exe" 
  },
  @{ Ver=12; 
     Args="/install /passive /norestart"; 
     X64Link="https://aka.ms/highdpimfc2013x64enu"; 
     X86Link="https://aka.ms/highdpimfc2013x86enu"; 
  },
  @{ Ver=11; 
     Args="/install /passive /norestart"; 
     X64Link="https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe"; 
     X86Link="https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe"; 
  },
  @{ Ver=10; 
     Args="/q /norestart";
     X64Link="https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe";       
     X86Link="https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe";
  },
  @{ Ver=9;  
     Args="/q /norestart";                
     X64Link="https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe";       
     X86Link="https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe"; 
  },
  @{ Ver=8;  
     Args="/q:a";   
     # FULLY SILENT X64 on x86: /q /c:"msiexec /i vcredist.msi IACCEPTSQLLOCALDBLICENSETERMS=YES /qn /L*v c:\vc8b-x64.log"
     X64Link="https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE";       
     X86Link="https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE"; 
  }
);

# Include File: [\Includes\Append-All-Text.ps1]
function Append-All-Text( [string]$file, [string]$text ) {
  Create-Directory-for-File $file
  $utf8=new-object System.Text.UTF8Encoding($false); 
  [System.IO.File]::AppendAllText($file, $text, $utf8);
}

function Write-All-Text( [string]$file, [string]$text ) {
  Create-Directory-for-File $file
  $utf8=new-object System.Text.UTF8Encoding($false); 
  [System.IO.File]::WriteAllText($file, $text, $utf8);
}

# Include File: [\Includes\Bootstrap-Aria2-If-Required.ps1]
# on windows 7 api.gitgub.com, etc are not available
function Bootstrap-Aria2-If-Required(
  [bool] $force = $false,
  [string] $deployMode = "copy-to", # copy-to | modify-path
  [string] $copyToFolder = $ENV:SystemRoot
) 
{
  if ((Get-Os-Platform) -ne "Windows") { return; }
  $major = [System.Environment]::OSVersion.Version.Major;
  $minor = [System.Environment]::OSVersion.Version.Minor;
  $canWebClient = ($major -gt 6) -or ($major -eq 6 -and $minor -ge 2);
  $okAria=$false; try { & aria2c.exe -h *| out-null; $okAria=$? } catch {}
  if (-not $force) { 
    if ($canWebClient -or $okAria) { return; }
  }
  $ariaExe = Get-Aria2c-Exe-FullPath-for-Windows
  if ($deployMode -eq "copy-to") {
    Copy-Item $ariaExe $copyToFolder -Force -EA Continue
    Write-Host "Provisioning aria2.exe for Windows $major.$minor. Copied to $copyToFolder"
  } elseif ($deployMode -eq "modify-path") {
    $dir=[System.IO.Path]::GetDirectoryName($ariaExe)
    $ENV:PATH="$($ENV:PATH);$($dir)"
    Write-Host "Provisioning aria2.exe for Windows $major.$minor. Added $dir to PATH"
  }
}

# Include File: [\Includes\Combine-Path.ps1]
function Combine-Path($start) { foreach($a in $args) { $start=[System.IO.Path]::Combine($start, $a); }; $start }

# Include File: [\Includes\Create-Directory.ps1]
function Create-Directory($dirName) { 
  if ($dirName) { 
    $err = $null;
    try { 
      $_ = [System.IO.Directory]::CreateDirectory($dirName); return; 
    } catch {
      $err = "Create-Directory failed for `"$dirName`". $($_.Exception.GetType().ToString()) $($_.Exception.Message)"
      Write-Host "Warning! $err";
      throw $err;
    }
  }
}

function Create-Directory-for-File($fileFullName) { 
  $dirName=[System.IO.Path]::GetDirectoryName($fileFullName)
  Create-Directory "$dirName";
}

# Include File: [\Includes\Demo-Test-of-Is-Vc-Runtime-Installed.ps1]
function Demo-Test-of-Is-Vc-Runtime-Installed() {
  foreach($arch in @("X86", "X64", "ARM64")) {
    Write-Host -NoNewline "$("{0,5}" -f $arch)| "
    foreach($ver in $VcRuntimeLinksMetadata | % {$_.Ver}) {
      $isInstalled = Is-Vc-Runtime-Installed $ver $arch
      $color="Red"; if ($isInstalled) { $color="Green"; }
      Write-Host -NoNewline "v$($ver)=$("{0,-8}" -f $isInstalled) " -ForegroundColor $color
    }
    Write-Host ""
  }
}

# Include File: [\Includes\Demo-Test-of-Platform-Info.ps1]
function Demo-Test-of-Platform-Info() {
  echo "Memory $((Get-Memory-Info).Description)"
  echo "OS Platform: '$(Get-Os-Platform)'"
  if ("$(Get-Os-Platform)" -ne "Windows") { echo "UName System: '$(Get-Nix-Uname-Value "-s")'" }
  echo "CPU: '$(Get-Cpu-Name)'"
  Measure-Action "The Greeting Test Action" {echo "Hello World"}
  Measure-Action "The Fail Test Action" {$x=0; echo "Cant devide by zero $(42/$x)"; }
  Measure-Action "The CPU Name" {echo "CPU: '$(Get-Cpu-Name)'"}
}; # test

# Include File: [\Includes\Download-And-Install-Specific-VC-Runtime.ps1]
function Download-And-Install-Specific-VC-Runtime([string] $arch, [int] $version, [bool] $wait = $true) {
  $fullPath = Download-Specific-VC-Runtime $arch $version
  $commandLine=$VcRuntimeLinksMetadata | where { "$($_.Ver)" -eq "$version" } | % { $_.Args }
  # & "$fullPath" $commandLine.Split([char]32)
  # $isOk = $?;
  # return $isOk;
  $isOk = $false
  try { 
    Start-Process -FilePath "$fullPath" -ArgumentList ($commandLine.Split([char]32)) -Wait:$wait -NoNewWindow 
    $isOk = $true
  } catch {}
  return $isOk
}

# Include File: [\Includes\Download-File-FailFree-and-Cached.ps1]
function Download-File-FailFree-and-Cached([string] $fullName, [string[]] $urlList, [string] $algorithm="SHA512") {
  
  if ((Is-File-Not-Empty "$fullName") -and (Is-File-Not-Empty "$fullName.$algorithm")) { 
    $hashActual = Get-Smarty-FileHash "$fullName" $algorithm
    $hashExpected = Get-Content -Path "$fullName.$algorithm"
    if ($hashActual -eq $hashExpected -and "$hashActual" -ne "") {
      Troubleshoot-Info "File already downloaded: '" -Highlight "$fullName" "'"
      return $true;
    }
  }
  $isOk = [bool] ((Download-File-FailFree $fullName $urlList) | Select -Last 1)
  if ($isOk) { 
    $hashActual = Get-Smarty-FileHash "$fullName" $algorithm
    echo "$hashActual" > "$($fullName).$algorithm"
    return $true; 
  }

  return $false;
}

# Include File: [\Includes\Download-File-Managed.ps1]
function Download-File-Managed([string] $url, [string]$outfile) {
  $dirName=[System.IO.Path]::GetDirectoryName($outfile)
  Create-Directory "$dirName";
  $okAria=$false; try { & aria2c.exe -h *| out-null; $okAria=$? } catch {}
  if ($okAria) {
    Troubleshoot-Info "Starting download `"" -Highlight "$url" "`" using aria2c as `"" -Highlight "$outfile" "`""
    # "-k", "2M",
    $startAt = [System.Diagnostics.Stopwatch]::StartNew()
    & aria2c.exe @("--allow-overwrite=true", "--check-certificate=false", "-x", "16", "-j", "16", "-d", "$($dirName)", "-o", "$([System.IO.Path]::GetFileName($outfile))", "$url");
    if ($?) { 
      <# Write-Host "aria2 rocks ($([System.IO.Path]::GetFileName($outfile)))"; #> 
      try { $length = (new-object System.IO.FileInfo($outfile)).Length; } catch {}; $milliSeconds = $startAt.ElapsedMilliseconds;
      $size=""; if ($length -gt 0) { $size=" ($($length.ToString("n0")) bytes)"; }
      $speed=""; if ($length -gt 0 -and $milliSeconds -gt 0) { $speed=" Speed is $(($length*1000/1024/$milliSeconds).ToString("n0")) Kb/s."; }
      $duration=""; if ($milliSeconds -gt 0) {$duration=" It took $(($milliSeconds/1000.).ToString("n1")) seconds."; }
      $downloadReport="Download of '$outfile'$($size) completed.$($duration)$($speed)";
      Write-Host $downloadReport;
      if ("$($ENV:SYSTEM_ARTIFACTSDIRECTORY)") { Append-All-Text (Combine-Path "$($ENV:SYSTEM_ARTIFACTSDIRECTORY)" "Download Speed Report.log") "$downloadReport$([Environment]::NewLine)"; }
      return $true; 
    }
  }
  elseif (([System.Environment]::OSVersion.Version.Major) -eq 5 -and (Get-Os-Platform) -eq "Windows" ) {
    Write-Host "Warning! Windows XP and Server 2003 requires aria2c.exe in the PATH for downloading." -ForegroundColor Red; 
  }
  [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;
  if ($PSVersionTable.PSEdition -ne "Core") {
    [System.Net.ServicePointManager]::ServerCertificateValidationCallback={$true};
  }
  for ($i=1; $i -le 3; $i++) {
    Troubleshoot-Info "Starting download attempt #$i `"" -Highlight "$url" "`" using built-in http client as `"" -Highlight "$outfile" "`""
    $d=new-object System.Net.WebClient;
    # $d.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36");
    try {
      $startAt = [System.Diagnostics.Stopwatch]::StartNew()
      $d.DownloadFile("$url","$outfile"); 
      try { $length = (new-object System.IO.FileInfo($outfile)).Length; } catch {}; $milliSeconds = $startAt.ElapsedMilliseconds;
      $size=""; if ($length -gt 0) { $size=" ($($length.ToString("n0")) bytes)"; }
      $speed=""; if ($length -gt 0 -and $milliSeconds -gt 0) { $speed=" Speed is $(($length*1000/1024/$milliSeconds).ToString("n0")) Kb/s."; }
      $duration=""; if ($milliSeconds -gt 0) {$duration=" It took $(($milliSeconds/1000.).ToString("n1")) seconds."; }
      $downloadReport="Download of '$outfile'$($size) completed.$($duration)$($speed)";
      Write-Host $downloadReport;
      if ("$($ENV:SYSTEM_ARTIFACTSDIRECTORY)") { Append-All-Text (Combine-Path "$($ENV:SYSTEM_ARTIFACTSDIRECTORY)" "Download Speed Report.log") "$downloadReport$([Environment]::NewLine)"; }
      return $true
    } 
    catch { 
      $fileExists = (Test-Path $outfile)
      if ($fileExists) { Remove-Item $outfile -force }
      # Write-Host $_.Exception -ForegroundColor DarkRed;
      if ($i -lt 3) {
        Write-Host "The download of the '$url' url failed.$([System.Environment]::NewLine)Retrying, $($i+1) of 3. $($_.Exception.Message)" -ForegroundColor Red;
        sleep 1;
      } else {
        Write-Host "Unable to download of the '$url' url.$([System.Environment]::NewLine)$($_.Exception.Message)" -ForegroundColor Red;
      }
    } 
  } 
  return $false
}

function Download-File-FailFree([string] $outFile, [string[]] $urlList) {
  foreach($url in $urlList) {
    $isOk = Download-File-Managed $url $outFile | Select -Last 1;
    if ($isOk) { return $true; }
  }
  return $fasle;
}


# Include File: [\Includes\Download-Specific-VC-Runtime.ps1]
function Download-Specific-VC-Runtime([string] $arch, [int] $version) {
  $algorithm="SHA512"
  $downloadFolder = Combine-Path "$(Get-PS1-Repo-Downloads-Folder)" "VC-Runtime"
  $link = $VcRuntimeLinksMetadata | where {"$($_.Ver)" -eq "$version"} | % { $_."$($arch)Link"}
  if (-not "$link")  {
    Write-Host "Warning! Undefined link for Visual C++ Runtime v$($version) for $arch architecture" -ForegroundColor Red
  } else {
    $fullPath = Combine-Path $downloadFolder "$arch-v$version" "$([System.IO.Path]::GetFilename($link))"
    if (-not ($fullPath.ToLower().EndsWith(".exe"))) { $fullPath = "$($fullPath).exe"; }
    if ((Is-File-Not-Empty "$fullPath") -and (Is-File-Not-Empty "$fullPath.$algorithm")) { 
      $hashActual = Get-Smarty-FileHash "$fullPath" $algorithm
      $hashExpected = Get-Content -Path "$fullPath.$algorithm"
      if ($hashActual -eq $hashExpected -and "$hashActual" -ne "") {
        Troubleshoot-Info "Already downloaded " -Highlight "$arch" - "v" -Highlight "$($version)" ": '$fullPath'"
        return $fullPath;
      }
    }
    $isOk = [bool] (Download-File-Managed $link $fullPath)
    if ($isOk) { 
      $hashActual = Get-Smarty-FileHash "$fullPath" $algorithm
      echo "$hashActual" > "$($fullPath).$algorithm"
      return $fullPath; 
    }
  }
  return "";
}

# Include File: [\Includes\Execute-Process-Smarty.ps1]
function Execute-Process-Smarty {
  Param(
    [string] $title, 
    [string] $launcher,
    [string[]] $arguments,
    [string] $workingDirectory = $null,
    [int] $waitTimeout = 3600,
    [switch] $Hidden = [switch] $false
  )
  $arguments = @($arguments | ? { "$_".Trim() -ne "" })
  Troubleshoot-Info "[$title] `"$launcher`" $arguments";
  $startAt = [System.Diagnostics.Stopwatch]::StartNew()

  $ret = @{};
  $windowStyle = if ($Hidden) { "Hidden" } else { "Normal" };
  try { 
    if ($workingDirectory) { 
      $app = Start-Process "$launcher" -ArgumentList $arguments -WorkingDirectory $workingDirectory -PassThru -WindowStyle $windowStyle;
    } else {
      $app = Start-Process "$launcher" -ArgumentList $arguments -PassThru -WindowStyle $windowStyle;
    }
  } catch {
    $err = "$($_.Exception.GetType()): '$($_.Exception.Message)'";
    $ret = @{Error = "$title failed. $err"; };
  }
  
  $exitCode = $null;
  $okExitCode = $false;
  if (-not $ret.Error) {
    if ($app -and $app.Id) {
      $isExited = $app.WaitForExit(1000*$waitTimeout);
      if (-not $isExited) { 
        $ret = @{ Error = "$title timed out." };
      }
      if (-not $ret.Error) {
        sleep 0.01 # background tasks
        $exitCode = [int] $app.ExitCode;
        $isLegacy = ([System.Environment]::OSVersion.Version.Major) -eq 5;
        if ($isExited -and $isLegacy -and "$exitCode" -eq "") { $exitCode = 0; }
        $okExitCode = $exitCode -eq 0;
        # Write-Host "Exit Code = [$exitCode], okExitCode = [$okExitCode]"
        $ret = @{ExitCode = $exitCode};
        if (-not $okExitCode) {
          $err = "$title failed."; if ($app.ExitCode) { $err += " Exit code $($app.ExitCode)."; }
          $ret = @{ Error = $err };
        }
      }
    } else {
      if (-not "$($ret.Error)") { $ret["Error"] = "$title failed."; }
    }
  }

  $isOk = ((-not $ret.Error) -and $okExitCode);
  $status = IIF $isOk "Successfully completed" $ret.Error;
  
  if ($isOk) { 
    Write-Host "$title $status. It took $($startAt.ElapsedMilliseconds.ToString("n0")) ms";
  } else {
    Write-Host "$title $status. It took $($startAt.ElapsedMilliseconds.ToString("n0")) ms" -ForegroundColor DarkRed;
  }
  
  if (!$isOk -and ($app.Id)) {
    # TODO: Windows Only
    & taskkill.exe @("/t", "/f", "/pid", "$($app.Id)") | out-null;
  }
  return $ret;
}

# Include File: [\Includes\Extract-Archive-by-Default-Full-7z.ps1]
function Extract-Archive-by-Default-Full-7z([string] $fromArchive, [string] $toDirectory, $extractCommand = "x") {
  New-Item -Path "$($toDirectory)" -ItemType Directory -Force -EA SilentlyContinue | Out-Null
  $full7zExe = "$(Get-Full7z-Exe-FullPath-for-Windows)"
  try { $fileOnly = [System.IO.Path]::GetFileName($fromArchive); } catch { $fileOnly = $fromArchive; }
  $execResult = Execute-Process-Smarty "'$fileOnly' Extractor" $full7zExe @($extractCommand, "-y", "-o`"$toDirectory`"", "$fromArchive") -Hidden;
  $ret = $true;
  if ($execResult -and $execResult.Error) { $ret = $fasle; }
  return $ret;
}

# Include File: [\Includes\Format-Table-Smarty.ps1]
function Format-Table-Smarty
{ 
   $arr = (@($Input) | % { [PSCustomObject]$_} | Format-Table -AutoSize | Out-String -Width 2048).Split(@([char]13, [char]10)) | ? { "$_".Length -gt 0 };
   if (-not $arr) { return; }
   [string]::Join([Environment]::NewLine, $arr);
}

# Include File: [\Includes\Get-7z-Exe-FullPath-for-Windows.ps1]
function Get-Mini7z-Exe-FullPath-for-Windows() {
  $algorithm="SHA512"
  $ret = Combine-Path "$(Get-PS1-Repo-Downloads-Folder)" "7z-mini-x86" "7zr.exe";
  $isOk = Download-File-FailFree-and-Cached $ret @("https://www.7-zip.org/a/7zr.exe", "https://sourceforge.net/projects/p7zz-repack/files/windows/7zr.exe/download")
  return (IIF $isOk $ret $null);
}




function Get-Mini7z-Exe-FullPath-for-Windows-Prev() {
  $algorithm="SHA512"

  $ret = Combine-Path "$(Get-PS1-Repo-Downloads-Folder)" "7z-mini-x86" "7zr.exe";
  if ((Is-File-Not-Empty "$ret") -and (Is-File-Not-Empty "$ret.$algorithm")) {
    $hashActual = Get-Smarty-FileHash "$ret" $algorithm
    $hashExpected = Get-Content -Path "$ret.$algorithm"
    if ($hashActual -eq $hashExpected -and "$hashActual" -ne "") {
      return $ret
    }
  } else {
    $isOk = Download-File-Managed "https://www.7-zip.org/a/7zr.exe" $ret
    if ($isOk) {
      $hashActual = Get-Smarty-FileHash "$ret" $algorithm
      echo "$hashActual" > "$($ret).$algorithm"
      return $ret;
    }
  }
    
  return $null
}



# Include File: [\Includes\Get-Aria2c-Exe-FullPath-for-Windows.ps1]
# arch: x86|x64|arm64|Xp
function Get-Aria2c-Exe-FullPath-for-Windows([string] $arch) {
  $linkXp="https://github.com/q3aql/aria2-static-builds/releases/download/v1.19.2/aria2-1.19.2-win-xp-build1.7z"
  $linkX86="https://github.com/aria2/aria2/releases/download/release-1.36.0/aria2-1.36.0-win-32bit-build1.zip"
  $linkX64="https://github.com/aria2/aria2/releases/download/release-1.36.0/aria2-1.36.0-win-64bit-build1.zip"

  if (-not $arch) { 
    $currentArch = Get-CPU-Architecture-Suffix-for-Windows;
    if ($currentArch -eq "arm64") {
      if (Is-Intel-Emulation-Available 32) { $arch="x86"; }
      if (Is-Intel-Emulation-Available 64) { $arch="x64"; }
    } else {
      $arch=$currentArch;
    }
    if (([System.Environment]::OSVersion.Version.Major) -eq 5) {
      $arch="Xp";
    }
  }

  $link = Get-Variable -Name "Link$arch" -Scope Local -ValueOnly
  # $link="$($"Link$arch")"
  # return "Not Implemented";

  $archiveFileOnly="aria2c-$arch.$([System.IO.Path]::GetExtension($link).Trim([char]46))"
  $downloadFolder = Combine-Path "$(Get-PS1-Repo-Downloads-Folder)" "aria2-setup"
  $archiveFullName = Combine-Path $downloadFolder $archiveFileOnly
  $plainFolder = Combine-Path "$(Get-PS1-Repo-Downloads-Folder)" "aria2-$arch"
  $ret = Combine-Path "$plainFolder" "aria2c.exe"

  Troubleshoot-Info "Download Link for [$arch] : $link"
  Troubleshoot-Info "archiveFullName for [$arch] : $archiveFullName"
  Troubleshoot-Info "plainFolder for [$arch] : $plainFolder"

  $algorithm="SHA512"
  if ((Is-File-Not-Empty "$ret") -and (Is-File-Not-Empty "$ret.$algorithm")) {
    $hashActual = Get-Smarty-FileHash "$ret" $algorithm
    $hashExpected = Get-Content -Path "$ret.$algorithm"
    if ($hashActual -eq $hashExpected -and "$hashActual" -ne "") {
      return $ret;
    }
  } else {
    $isDownloadOk = Download-File-Managed "$link" "$archiveFullName" | Select -Last 1
    if (-not $isDownloadOk) {
      Write-Host "Error downloading $link" -ForeGroundColor Red;
    }
    else {
      Troubleshoot-Info "Starting extract of '$archiveFullName'"
      $isExtractOk = ExtractArchiveByDefault7zFull "$archiveFullName" "$plainFolder" "e" | Select -Last 1
      Troubleshoot-Info "isExtractOk: $isExtractOk ($archiveFullName)"
      if (-not $isExtractOk) { 
        Write-Host "Error extracting $archiveFullName" -ForeGroundColor Red;
      } else {
        $hashActual = Get-Smarty-FileHash "$ret" $algorithm
        echo "$hashActual" > "$($ret).$algorithm"
        return $ret;
      }
    }
  }

  return $null;
}
<#
  https://github.com/q3aql/aria2-static-builds/releases/download/v1.19.2/aria2-1.19.2-win-xp-build1.7z
  https://github.com/aria2/aria2/releases/download/release-1.36.0/aria2-1.36.0-win-32bit-build1.zip
  https://github.com/aria2/aria2/releases/download/release-1.36.0/aria2-1.36.0-win-64bit-build1.zip
#>


# Include File: [\Includes\Get-CPU-Architecture-Suffix-for-Windows.ps1]

# x86 (0), MIPS (1), Alpha (2), PowerPC (3), ARM (5), ia64 (6) Itanium-based systems, x64 (9), ARM64 (12)
function Get-CPU-Architecture-Suffix-for-Windows-Implementation() {
    # on multiple sockets x64
    $proc = Select-WMI-Objects "Win32_Processor";
    $a = ($proc | Select -First 1).Architecture
    if ($a -eq 0)  { return "x86" };
    if ($a -eq 1)  { return "mips" };
    if ($a -eq 2)  { return "alpha" };
    if ($a -eq 3)  { return "powerpc" };
    if ($a -eq 5)  { return "arm" };
    if ($a -eq 6)  { return "ia64" };
    if ($a -eq 9)  { 
      # Is 32-bit system on 64-bit CPU?
      # OSArchitecture: "ARM 64-bit Processor", "32-bit", "64-bit"
      $os = Select-WMI-Objects "Win32_OperatingSystem";
      $osArchitecture = ($os | Select -First 1).OSArchitecture
      if ($osArchitecture -like "*32-bit*") { return "x86"; }
      return "x64" 
    };
    if ($a -eq 12) { return "arm64" };
    return "";
}

function Get-CPU-Architecture-Suffix-for-Windows() {
  if ($Global:CPUArchitectureSuffixforWindows -eq $null) { $Global:CPUArchitectureSuffixforWindows = Get-CPU-Architecture-Suffix-for-Windows-Implementation; }
  return $Global:CPUArchitectureSuffixforWindows
}

# Include File: [\Includes\Get-Cpu-Name.ps1]
function Get-Cpu-Name-Implementation {
  $platform = Get-Os-Platform
  if ($platform -eq "Windows") {
    $proc = Select-WMI-Objects "Win32_Processor" | Select -First 1;
    return "$($proc.Name)".Trim()
  }

  if ($platform -eq "MacOS") {
    return (& sysctl "-n" "machdep.cpu.brand_string" | Out-String-And-TrimEnd)
  }

  if ($platform -eq "Linux") {
    # TODO: Replace grep, awk, sed by NET
    $shell="cat /proc/cpuinfo | grep -E '^(model name|Hardware)' | awk -F':' 'NR==1 {print `$2}' | sed -e 's/^[[:space:]]*//'"
    $ret = "$(& bash -c "$shell" | Out-String-And-TrimEnd)"
    if (-not $ret) {
      $parts = @(
        (Get-Nix-Uname-Value "-m"), 
        "$(& bash -c "getconf LONG_BIT" | Out-String-And-TrimEnd) bit"
      );
      $ret = ($parts | where { "$_" }) -join ", "
    }
    return $ret
  }

  $ret = $null;
  try { $ret = Get-Nix-Uname-Value "-m"; } catch {}
  if ($ret) { return "$ret"; }

  return "Unknown"
}

function Get-Cpu-Name {
  [OutputType([string])] param()

  if (-not $Global:_Cpu_Name) { $Global:_Cpu_Name = "$(Get-Cpu-Name-Implementation)"; }
  return $Global:_Cpu_Name;
}

# Include File: [\Includes\Get-Folder-Size.ps1]
function Get-Folder-Size([string] $folder) {
  if (Test-Path "$folder" -PathType Container) {
     $subFolderItems = Get-ChildItem "$folder" -recurse -force | Where-Object {$_.PSIsContainer -eq $false} | Measure-Object -property Length -sum | Select-Object Sum
     return $subFolderItems.sum;
  }
}


# Include File: [\Includes\Get-Full7z-Exe-FullPath-for-Windows.ps1]
# arch: x86|x64|arm64
# version: 1604|2301
function Get-Full7z-Exe-FullPath-for-Windows([string] $arch, [string] $version = "2301") {
  if (-not $arch) { 
    $currentArch = Get-CPU-Architecture-Suffix-for-Windows;
    $arch = "x86"
    if ($currentArch -eq "arm64") { $arch="arm64"; }
    if ($currentArch -eq "x64") { $arch="x64"; }

    # arm64 below 2201 is not supported
    if ($arch -eq "arm64" -and (([int] $version) -lt 2201)) {
      if (Is-Intel-Emulation-Available 32) { $arch="x86"; }
      if (Is-Intel-Emulation-Available 64) { $arch="x64"; }
    }
    # v9.2 available as x86 only
    if ((([int] $version) -eq 920)) {
      $arch="x86"; 
    }
  }
  # $suffix="-$arch"; if ($suffix -eq "-x86") { $suffix=""; }
  # $link="https://www.7-zip.org/a/7z$($version)$($suffix).exe"
  $versionLinks = $Full7zLinksMetadata_onWindows | where { "$($_.Ver)" -eq "$version" } | Select -First 1
  $archLinks = $versionLinks."$($arch)Links"
  if (-not $archLinks) { 
     TroubleShoot-Info "ERROR. Unknown links for full 7z v$($version) arch $arch"
  }

  $archiveFileOnly="7z-$version-$arch.exe"
  $downloadFolder = Combine-Path "$(Get-PS1-Repo-Downloads-Folder)" "7z-Full-Setup"
  $archiveFullName = Combine-Path $downloadFolder $archiveFileOnly
  $plainFolder = Combine-Path "$(Get-PS1-Repo-Downloads-Folder)" "7z-Full-$arch-$version"
  $ret = Combine-Path "$plainFolder" "7z.exe"

  $algorithm="SHA512"
  if ((Is-File-Not-Empty "$ret") -and (Is-File-Not-Empty "$ret.$algorithm")) {
    $hashActual = Get-Smarty-FileHash "$ret" $algorithm
    $hashExpected = Get-Content -Path "$ret.$algorithm"
    if ($hashActual -eq $hashExpected -and "$hashActual" -ne "") {
      return $ret;
    }
  } else {
    $isDownloadOk = Download-File-FailFree $archiveFullName $archLinks | Select -Last 1
    if (-not $isDownloadOk) {
      Write-Host "Error downloading any link of [$archLinks]" -ForeGroundColor Red;
      return $null;
    }

    $isExtractOk = ExtractArchiveBy7zMini "$archiveFullName" "$plainFolder" | Select -Last 1
    popd
    if (-not $isExtractOk) {
      Write-Host "Error extracting $archiveFullName" -ForeGroundColor Red;
    } else {
      $hashActual = Get-Smarty-FileHash "$ret" $algorithm
      echo "$hashActual" > "$($ret).$algorithm"
      return $ret;
    }
  }

  return $null;
}

function ExtractArchiveBy7zMini([string] $fromArchive, [string] $toDirectory) {
  New-Item -Path "$($toDirectory)" -ItemType Directory -Force -EA SilentlyContinue | Out-Null
  pushd "$($toDirectory)"
  $mini7z = "$(Get-Mini7z-Exe-FullPath-for-Windows)"
  # "-o`"$plainFolder`""
  $commandLine=@("x", "-y", "$fromArchive")
  Troubleshoot-Info "fromArchive: '$fromArchive'; commandLine: '$commandLine'"
  # ok on pwsh and powersheel
  & "$mini7z" @commandLine
  $isExtractOk = $?;
  return $isExtractOk;
}

function ExtractArchiveByDefault7zFull([string] $fromArchive, [string] $toDirectory, $extractCommand = "x") {
  New-Item -Path "$($toDirectory)" -ItemType Directory -Force -EA SilentlyContinue | Out-Null
  # pushd "$($toDirectory)"
  $full7zExe = "$(Get-Full7z-Exe-FullPath-for-Windows)"
  Troubleshoot-Info "`"$fromArchive`" $([char]8594) " -Highlight "`"$($toDirectory)`"" " by `"$full7zExe`""
  & "$full7zExe" @("$extractCommand", "-y", "-o`"$($toDirectory)`"", "$fromArchive")
  $isExtractOk = $?;
  return $isExtractOk;
}


<#
  https://www.7-zip.org/a/7z2301-arm64.exe
  https://www.7-zip.org/a/7z2301-x64.exe
  https://www.7-zip.org/a/7z2301.exe
 
  https://www.7-zip.org/a/7z2201-arm64.exe
  https://www.7-zip.org/a/7z2201-x64.exe
  https://www.7-zip.org/a/7z2201.exe
 
  https://www.7-zip.org/a/7z1900-x64.exe
  https://www.7-zip.org/a/7z1900.exe
   
  https://www.7-zip.org/a/7z1604-x64.exe
  https://www.7-zip.org/a/7z1604.exe
   
  https://www.7-zip.org/a/7z920.exe
  https://www.7-zip.org/a/7z920-arm.exe
 
  https://www.7-zip.org/a/7zr.exe
#>


# Include File: [\Includes\Get-Github-Latest-Release.ps1]
function Get-Github-Latest-Release([string] $owner, [string] $repo) {
  $queryLatest="https://api.github.com/repos/$owner/$repo/releases/latest" # "tag_name": "v3.227.2",
  $qyeryResultFullName = Combine-Path (Get-PS1-Repo-Downloads-Folder) "Queries" "Github Latest Release" "$(([System.Guid]::NewGuid()).ToString("N")).json"
  $isOk = Download-File-FailFree $qyeryResultFullName @($queryLatest)
  if (-not $isOk) {
    Write-Host "Error query latest version for '$owner/$repo'" -ForegroundColor Red
  }
  $jsonResult = Get-Content $qyeryResultFullName | ConvertFrom-Json
  $ret = $jsonResult.tag_name;
  if (-not $ret) {
    Write-Host "Maflormed query latest version for '$owner/$repo'. Missing property 'tag_name'" -ForegroundColor Red
  } else {
    Remove-Item $qyeryResultFullName -Force
  }
  return $ret;
}


# Include File: [\Includes\Get-Github-Releases.ps1]
function Get-Github-Releases([string] $owner, [string] $repo) {
  $url="https://api.github.com/repos/$owner/$repo/releases?per_page=128"
  # https://api.github.com/repos/microsoft/azure-pipelines-agent/releases?per_page=128
  $qyeryResultFullName = Combine-Path (Get-PS1-Repo-Downloads-Folder) "Queries" "Github Releases" "$(([System.Guid]::NewGuid()).ToString("N")).json"
  $isOk = Download-File-FailFree $qyeryResultFullName @($url)
  if (-not $isOk) {
    Write-Host "Error query release list for '$owner/$repo'" -ForegroundColor Red
  }
  $jsonResult = Get-Content $qyeryResultFullName | ConvertFrom-Json

<#
    "tag_name":"v3.230.0",
    "target_commitish":"6ee2a6be8f5e0cccac6079e4fb42b5fe9f8de04e",
    "name":"v3.230.0",
    "draft":false,
    "prerelease":true,
    "created_at":"2023-11-03T02:33:23Z",
    "published_at":"2023-11-07T11:17:01Z",
    "tarball_url":"https://api.github.com/repos/microsoft/azure-pipelines-agent/tarball/v3.230.0",
    "zipball_url":"https://api.github.com/repos/microsoft/azure-pipelines-agent/zipball/v3.230.0",
    "body":"## Features\r\n - Add `AllowWorkDirectoryRepositories` knob (#4423)\r\n - Update process handler (#4425)\r\n - Check task deprecation (#4458)\r\n - Enable Domains for Pipeline Artifact (#4460)\r\n - dedupStoreHttpClient honors redirect timeout from client settings and update ADO lib to 0.5.227-262a3469 (#4504)\r\n\r\n## Bugs\r\n - Detect the OS and switch node runner if not supported for Node20 (#4470)\r\n - Revert \"Enable Domains for Pipeline Artifact\" (#4477)\r\n - Add capability to publish/download pipeline artifact in a different domain. (#4482)\r\n - Mount Workspace (#4483)\r\n\r\n## Misc\r\n\r\n\r\n\r\n## Agent Downloads\r\n\r\n| | Package | SHA-256 |\r\n| -------------- | ------- | ------- |\r\n| Windows x64 | [vsts-agent-win-x64-3.230.0.zip](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-win-x64-3.230.0.zip) | cbb21ea2ec0b64663c35d13f204e215cfe41cf2e3c8efff7c228fdab344d00de |\r\n| Windows x86 | [vsts-agent-win-x86-3.230.0.zip](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-win-x86-3.230.0.zip) | 7182a054b1f58c5d104f7b581fe00765c32f1bd544dc2bcc423d0159929f4692 |\r\n| macOS x64 | [vsts-agent-osx-x64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-osx-x64-3.230.0.tar.gz) | 988234fe3a1bbc6f79c3f6d94d70ea1908f2395ce6b685118d1dae983f03479e |\r\n| macOS ARM64 | [vsts-agent-osx-arm64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-osx-arm64-3.230.0.tar.gz) | 82f670482ffb45de2e533687c5eefa9506cbe0686edaa6a3c02487887729101c |\r\n| Linux x64 | [vsts-agent-linux-x64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-linux-x64-3.230.0.tar.gz) | bc222ec99ff675c1035efd0a086cea02adb5847ae7df8ee36e89db14aee8673d |\r\n| Linux ARM | [vsts-agent-linux-arm-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-linux-arm-3.230.0.tar.gz) | f399e0ddceb10f09cd768c29e31fa51eb05c51c092e2392282e63795729f6a39 |\r\n| Linux ARM64 | [vsts-agent-linux-arm64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-linux-arm64-3.230.0.tar.gz) | 3c6fa98e26c7d8b19e8a35ca5b45a32122088a3bc12e817e7ccdead303893789 |\r\n| Linux musl x64 | [vsts-agent-linux-musl-x64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-linux-musl-x64-3.230.0.tar.gz) | 3461bef5756e452b920779b1f163cd194fa1971267acd582c2ad4870b1f611c2 |\r\n\r\nAfter Download:\r\n\r\n## Windows x64\r\n\r\n``` bash\r\nC:\\> mkdir myagent && cd myagent\r\nC:\\myagent> Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory(\"$HOME\\Downloads\\vsts-agent-win-x64-3.230.0.zip\", \"$PWD\")\r\n```\r\n\r\n## Windows x86\r\n\r\n``` bash\r\nC:\\> mkdir myagent && cd myagent\r\nC:\\myagent> Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory(\"$HOME\\Downloads\\vsts-agent-win-x86-3.230.0.zip\", \"$PWD\")\r\n```\r\n\r\n## macOS x64\r\n\r\n``` bash\r\n~/$ mkdir myagent && cd myagent\r\n~/myagent$ tar xzf ~/Downloads/vsts-agent-osx-x64-3.230.0.tar.gz\r\n```\r\n\r\n## macOS ARM64\r\n\r\n``` bash\r\n~/$ mkdir myagent && cd myagent\r\n~/myagent$ tar xzf ~/Downloads/vsts-agent-osx-arm64-3.230.0.tar.gz\r\n```\r\n\r\n## Linux x64\r\n\r\n``` bash\r\n~/$ mkdir myagent && cd myagent\r\n~/myagent$ tar xzf ~/Downloads/vsts-agent-linux-x64-3.230.0.tar.gz\r\n```\r\n\r\n## Linux ARM\r\n\r\n``` bash\r\n~/$ mkdir myagent && cd myagent\r\n~/myagent$ tar xzf ~/Downloads/vsts-agent-linux-arm-3.230.0.tar.gz\r\n```\r\n\r\n## Linux ARM64\r\n\r\n``` bash\r\n~/$ mkdir myagent && cd myagent\r\n~/myagent$ tar xzf ~/Downloads/vsts-agent-linux-arm64-3.230.0.tar.gz\r\n```\r\n\r\n## Alpine x64\r\n\r\n``` bash\r\n~/$ mkdir myagent && cd myagent\r\n~/myagent$ tar xzf ~/Downloads/vsts-agent-linux-musl-x64-3.230.0.tar.gz\r\n```\r\n\r\n***Note:*** Node 6 does not exist for Alpine.\r\n\r\n## Alternate Agent Downloads\r\n\r\nAlternate packages below do not include Node 6 and are only suitable for users who do not use Node 6 dependent tasks. \r\nSee [notes](docs/node6.md) on Node version support for more details.\r\n\r\n| | Package | SHA-256 |\r\n| ----------- | ------- | ------- |\r\n| Windows x64 | [pipelines-agent-win-x64-3.230.0.zip](https://vstsagentpackage.azureedge.net/agent/3.230.0/pipelines-agent-win-x64-3.230.0.zip) | f5bbae6dad8c39ea809db9b04abbcf3add37962d67ef9c67245a09fb536d38ca |\r\n| Windows x86 | [pipelines-agent-win-x86-3.230.0.zip](https://vstsagentpackage.azureedge.net/agent/3.230.0/pipelines-agent-win-x86-3.230.0.zip) | 00d5f1776767ead3e70036f63cdbd38a007b7d971c287a4d24d7346f4d3715a6 |\r\n| macOS x64 | [pipelines-agent-osx-x64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/pipelines-agent-osx-x64-3.230.0.tar.gz) | e6e602c6664414b8a9b27a2df73511156d32a6bc76f8b4bb69aa960767aa9684 |\r\n| macOS ARM64 | [pipelines-agent-osx-arm64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/pipelines-agent-osx-x64-3.230.0.tar.gz) | a00182572b1be649fe6836336bde3d4d3f79ceee42822fe44707afa9950b2232 |\r\n| Linux x64 | [pipelines-agent-linux-x64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/pipelines-agent-linux-x64-3.230.0.tar.gz) | d46581abbf0eb5c3aef534825b51f92ade9d86a5b089b9489e84387070366d1b |\r\n| Linux ARM | [pipelines-agent-linux-arm-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/pipelines-agent-linux-arm-3.230.0.tar.gz) | 1dc871562bd6266567f7accced8d4a9ec3b4b85139891cf563a8fa305968ad40 |\r\n| Linux ARM64 | [pipelines-agent-linux-arm64-3.230.0.tar.gz](https://vstsagentpackage.azureedge.net/agent/3.230.0/pipelines-agent-linux-arm64-3.230.0.tar.gz) | 4ce243af0a09d5be2a6194b94d98c11fc323607b083ec5dcf3893bf67abb2dda |\r\n"
#>

  $ret=@()
  foreach($release in $jsonResult) { 
    $ret += New-Object PSObject -Property @{
        Tag          = $release.tag_name
        Commit       = $release.target_commitish
        Name         = $release.name
        IsDraft      = [bool] $release.draft
        IsPrerelease = [bool] $release.prerelease
        CreatedAt    = $release.created_at
        PublishedAt  = $release.published_at
        TarballUrl   = $release.tarball_url
        ZipballUrl   = $release.zipball_url
    }
  }
  
  if (-not ($ret | Select -First 1)) {
    Write-Host "Empty release list for '$owner/$repo'" -ForegroundColor Red
  } else {
    Remove-Item $qyeryResultFullName -Force
  }
  return $ret | where { -not ($_.IsDraft) };
}

# Include File: [\Includes\Get-Installed-VC-Runtimes.ps1]
function Get-Installed-VC-Runtimes() {
  $softwareFilter = { $_.name -like "*Visual C++*" -and $_.vendor -like "*Microsoft*" -and ($_.name -like "*Runtime*" -or $_.name -like "*Redistributable*")}
  return Get-Speedy-Software-Product-List | where $softwareFilter
}

# Include File: [\Includes\Get-Memory-Info.ps1]
function Get-Memory-Info {
  [OutputType([object])] param()

  $platform = Get-Os-Platform
  if ($platform -eq "Windows") {
    $os = Select-WMI-Objects "Win32_OperatingSystem";
    $mem=($os | Where { $_.FreePhysicalMemory } | Select FreePhysicalMemory,TotalVisibleMemorySize -First 1);
    $total=[int] ($mem.TotalVisibleMemorySize / 1024);
    $free=[int] ($mem.FreePhysicalMemory / 1024);
    
    $wmiSwap = @(Select-WMI-Objects "Win32_PageFileUsage")
    $swapCurrent   = $wmiSwap | Measure-Object -property CurrentUsage -sum      | % { $_.Sum } | Select -First 1
    $swapPeak      = $wmiSwap | Measure-Object -property PeakUsage -sum         | % { $_.Sum } | Select -First 1
    $swapAllocated = $wmiSwap | Measure-Object -property AllocatedBaseSize -sum | % { $_.Sum } | Select -First 1
    if ($swapAllocated) {
      $customDescription = ". Swap Usage: $(FormatNullableNumeric $swapCurrent) (peak $(FormatNullableNumeric $swapPeak)) of $(FormatNullableNumeric $swapAllocated) Mb"
    }
  }

  if ($platform -eq "MacOS") {
    $total=[long] (& sysctl -n hw.memsize | Out-String).TrimEnd(@([char]13,[char]10))
    $total=[int] ($total/1024/1024)
    $free=[long] (& vm_stat | grep "Pages free" | awk -v OFMT="%.0f" '{print (4 * $NF / 1024)}' | Out-String-And-TrimEnd)
    $inactive=[long] (& vm_stat | grep "Pages inactive" | awk -v OFMT="%.0f" '{print (4 * $NF / 1024)}' | Out-String-And-TrimEnd)
    $free = [int]$free + [int]$inactive;
    # Write-Host "Mem Total: $total, Free: $free"
  }

  if ($platform -eq "Linux") {
    # total: $2, $used: $3, shared: $5. free = total-(used+shared)
    $total=[int] (& free -m | awk 'NR==2 {print $2}' | Out-String-And-TrimEnd)
    $used =[int] (& free -m | awk 'NR==2 {print $3 + $5}' | Out-String-And-TrimEnd)
    $free=$total-$used

    $swapAllocated = [int] (& free -m | awk '$1 ~ /^[S|s]wap/ {print $2}' | Out-String-And-TrimEnd)
    $swapCurrent   = [int] (& free -m | awk '$1 ~ /^[S|s]wap/ {print $3}' | Out-String-And-TrimEnd)
    if ($swapAllocated) {
      $customDescription = ". Swap Usage: $(FormatNullableNumeric $swapCurrent) of $(FormatNullableNumeric $swapAllocated) Mb"
    }
  }

  if ($total) {
    $info="Total RAM: $($total.ToString("n0")) MB. Free: $($free.ToString("n0")) MB ($([Math]::Round($free * 100 / $total, 1))%)$customDescription";
    $ret = @{
        Total=$total;
        Free=$free;
        Description=$info;
    }
    if ($swapAllocated) {
      $ret["SwapAllocated"] = $swapAllocated;
      $ret["SwapCurrent"]   = $swapCurrent;
    }
    return $ret;
  }

  <#
     .OUTPUTS
     Object with 3 properties: [int] Total, [int] Free, [string] Description
  #>


}

function FormatNullableNumeric($n, $fractionalDigits = 0) {
  try { $num = [int] $n } catch { $num = 0 }
  return $num.ToString("n$fractionalDigits");
}

# Include File: [\Includes\Get-Nix-Uname-Value.ps1]

# Linux/Darwin/FreeBSD, Error on Windows
function Get-Nix-Uname-Value {
  param([string] $arg)
  return (& uname "$arg" | Out-String-And-TrimEnd)
}

# Include File: [\Includes\Get-Os-Platform.ps1]
# Returns Linux/Windows/Mac/FreeBSD
function Get-Os-Platform {
  [OutputType([string])] param()

  $platform = [System.Environment]::OSVersion.Platform;
  if ($platform -like "Win*") { return "Windows"; }

  $nixUnameSystem = Get-Nix-Uname-Value "-s"
  if ($nixUnameSystem -eq "Linux") { return "Linux"; }
  if ($nixUnameSystem -eq "Darwin") { return "MacOS"; }
  if ($nixUnameSystem -eq "FreeBSD") { return "FreeBSD"; }

  return "Unknown"

  <#
     .OUTPUTS
     One of the following values: "Linux", "Windows", "MacOS", "FreeBSD", "Unknown"
  #>

}

# Include File: [\Includes\Get-PS1-Repo-Downloads-Folder.ps1]
function Get-PS1-Repo-Downloads-Folder() {
  return (GetPersistentTempFolder "PS1_REPO_DOWNLOAD_FOLDER" "PS1 Repo Downloads");
}

function GetPersistentTempFolder([string] $envPrefix, [string] $pathSuffix) {
  
  foreach($pair in (gci env:$($envPrefix)* | sort-object name)) {
      $explicitRet = "$($pair.Value)";
      if ($explicitRet) {
        New-Item -Path $explicitRet -ItemType Directory -Force -EA SilentlyContinue | Out-null
        $isExplicit = Test-Path -Path $explicitRet -PathType Container -EA SilentlyContinue;
        if ($isExplicit) { return "$explicitRet"; }
      }
  }

  If (Get-Os-Platform -eq "Windows") { $ret = "$($ENV:TEMP)" } else { $ret = "$($ENV:TMPDIR)"; if (-not $ret) { $ret = "/tmp" }};
  $is1 = Test-Path -Path $ret -PathType Container -EA SilentlyContinue
  if (-not $is1) {
    try { New-Item -Path $ret -ItemType Directory -Force -EA SilentlyContinue | Out-null } catch { }
    $is2 = Test-Path -Path $ret -PathType Container -EA SilentlyContinue
    if (-not $is2) { 
      $ret=""
    }
  }

  if (-not $ret) {
    $ret="$($ENV:LOCALAPPDATA)";
    if ("$ret" -eq "") { $ret="$($ENV:APPDATA)"; }; 
    if ("$ret" -eq "") { $ret="$($ENV:HOME)/.cache"; }; 
  }
  
  $separator="$([System.IO.Path]::DirectorySeparatorChar)"
  if (-not ($ret -like "*\Temp" -or $ret -like "*/.cache")) { $ret += "$($separator)Temp"; }
  $ret += "$($separator)$pathSuffix";
  return $ret;
}


# Include File: [\Includes\Get-Random-Free-Port.ps1]
function Get-Random-Free-Port() {
  $tcpListener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback <# ::Any? #>, 0)
  $tcpListener.Start()
  $port = ([System.Net.IPEndPoint] $tcpListener.LocalEndpoint).Port;
  $tcpListener.Stop()
  return $port;
}

# Get-Random-Free-Port

# Include File: [\Includes\Get-Smarty-FileHash.ps1]
# $algorithm: MD5|SHA1|SHA256|SHA384|SHA512
function Get-Smarty-FileHash([string] $fileName, [string] $algorithm = "MD5") {
  $fileExists = (Test-Path $fileName -PathType Leaf)
  if (-not $fileExists) { return $null; }
  $hashAlg = [System.Security.Cryptography.HashAlgorithm]::Create($algorithm)
  try {
    $fileStream = new-object System.IO.FileStream($fileName, "Open", "Read", "ReadWrite")
    $bytes = $hashAlg.ComputeHash($fileStream);
    # $ret="";
    # foreach($b in $bytes) { $ret = "$($ret)$($b.ToString("X2"))"; }
    $ret = "$($bytes | % { $_.ToString("X2") })".Replace(" ","")
    return $ret;
  }
  finally {
    if ($fileStream) { $fileStream.Dispose(); }
  }
  return $null;
}


# Include File: [\Includes\Get-Smarty-FolderHash.ps1]
# Get-Smarty-FileHash([string] $fileName, [string] $algorithm = "MD5") {
function Get-Smarty-FolderHash([string] $rootFolder, [string] $algorithm = "MD5", [bool] $includeDates = $false) {
  $startAt = [System.Diagnostics.Stopwatch]::StartNew()
  $folderExists = (Test-Path $rootFolder -PathType Container)
  if (-not $folderExists) { return $null; }
  $colItems = Get-ChildItem $rootFolder -recurse -force | Where-Object {$_.PSIsContainer -eq $false} | Sort-Object -Property FullName
  $summary=New-Object System.Collections.ArrayList;
  $filesCount = 0;
  foreach ($i in $colItems)
  {
      $filesCount++;
      $fileHash = Get-Smarty-FileHash $i.FullName $algorithm;
      if ($includeDates) { 
        foreach($date in $i.CreationTimeUtc, $i.LastWriteTimeUtc) {
          $fileHash += $date.ToString("s")
        }
      }
      $_=$summary.Add($fileHash)
  }
  $utf8 = new-object System.Text.UTF8Encoding($false)
  $summaryBytes = $utf8.GetBytes("$summary")
  $hashAlg = [System.Security.Cryptography.HashAlgorithm]::Create($algorithm)
  $retBytes = $hashAlg.ComputeHash($summaryBytes);
  $ret = "$($retBytes | % { $_.ToString("X2") })".Replace(" ","")
  TroubleShoot-Info "Hash $algorithm for folder " -Highlight $rootFolder " (" -Highlight $filesCount " files) took " -Highlight "$($startAt.ElapsedMilliseconds.ToString("n0"))" " ms"
  return $ret;
}

# Include File: [\Includes\Get-Speedy-Software-Product-List.ps1]
function Get-Speedy-Software-Product-List() {
  $ret=@();
  $origins=@(
    @{ Path="HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall"; Origin="" },
    @{ Path="HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall"; Origin="" },
    @{ Path="HKCU:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"; Origin="X86" },
    @{ Path="HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"; Origin="X86" },
    @{ Path="HKCU:\Software\WowAA32Node\Microsoft\Windows\CurrentVersion\Uninstall"; Origin="ARM32" },
    @{ Path="HKLM:\Software\WowAA32Node\Microsoft\Windows\CurrentVersion\Uninstall"; Origin="ARM32" }
  );
  foreach($origin in $origins) {
    $keys = Get-ChildItem $origin.Path -EA SilentlyContinue
    if ($keys) {
      foreach($key in $keys) {
        # Write-Host "$($key.Name): $key"
        $ret += New-Object PSObject -Property @{
            Name    = "$($key.GetValue('DisplayName'))".Trim()
            Vendor  = "$($key.GetValue('Publisher'))".Trim()
            Version = "$($key.GetValue('DisplayVersion'))".Trim()
            IdentifyingNumber = [System.IO.Path]::GetFileName("$($key.Name)".Trim())
            Origin  = $origin.Origin
        }
      }
    }
  }
  return $ret | where { "$($_.Name)" -ne "" -and "$($_.Vendor)" -ne "" } | Sort-Object Vendor, Name, Version, Origin -Unique
}

# Get-Speedy-Software-Product-List | ft
# $localDbList = Get-Speedy-Software-Product-List | ? { $_.Name -match "LocalDB" -and $_.Vendor -match "Microsoft" }

# Include File: [\Includes\Get-System-Drive.ps1]
function Get-System-Drive() { $ret = "$($Env:SystemDrive)"; $c = "$([System.IO.Path]::DirectorySeparatorChar)"; if (-not ($ret.EndsWith($c))) { $ret += $c; }; return $ret; }
  

# Include File: [\Includes\Get-Windows-Power-Plans.ps1]
function Get-Windows-Power-Plans() {
  if ((Get-Os-Platform) -ne "Windows") { return @(); }
  $rawOutput = (& powercfg.exe "-l") | Out-String
  # Write-Host $rawOutput
  $lines = "$rawOutput".Split([char]13,[char]10) | ? { "$_" -match ":" } | % { @(@("$_".Split(":")) | Select -Skip 1) -join " " } | % { "$_".Trim() }
  foreach($line in $lines) {
    $guid = "$line".Split(" ") | % { "$_".Trim() } | ? { "$_".Length -gt 0 } | Select -First 1
    $pSpace = "$line".IndexOf(" ");
    if ($pSpace -gt 1) {
      $name = "$line".SubString($pSpace).Trim()
      $isActive = "$line".EndsWith("*")
      if ($isActive) { $name = "$name".TrimEnd("*"); }
      $name = "$name".Trim().TrimStart("(").TrimEnd(")")
      [PsCustomObject] @{ "Id" = $guid; Name = $name; IsActive = $isActive };
    }
  }
}

function Get-Windows-Active-Power-Plan() {
  Get-Windows-Power-Plans | ? { $_.IsActive } | Select -First 1
}

function Get-Windows-Active-Power-Plan-Name() {
  (Get-Windows-Active-Power-Plan).Name
}

function Set-Windows-Power-Plan-by-Name([string] $newPowerPlanName) {
  $allPlans = @(Get-Windows-Power-Plans)
  $newPlan = $allPlans | ? { $_.Name -eq $newPowerPlanName } | Select -First 1
  if ($newPlan) {
    & powercfg.exe @("/s", "$($newPlan.Id)") | Out-Host
  } else {
    Write-Host "Warning! Unable to set power plan '$newPowerPlanName'. Not Found." -ForeGroundColor DarkRed
  }
}

# Get-Windows-Active-Power-Plan-Name;
# Get-Windows-Active-Power-Plan
# Get-Windows-Power-Plans | ft *

# Set-Windows-Power-Plan-by-Name "High Performance"
# Set-Windows-Power-Plan-by-Name "ZXC PPlan"
# Set-Windows-Power-Plan-by-Name "Balanced"




# Include File: [\Includes\Has-Cmd.ps1]
function Has-Cmd {
  param([string] $arg)
  if ("$arg" -eq "") { return $false; }
  [bool] (Get-Command "$arg" -ErrorAction SilentlyContinue)
}

function Select-WMI-Objects([string] $class) {
  if (Has-Cmd "Get-CIMInstance")     { $ret = Get-CIMInstance $class; } 
  elseif (Has-Cmd "Get-WmiObject")   { $ret = Get-WmiObject   $class; } 
  if (-not $ret) { Write-Host "Warning ! Missing neither Get-CIMInstance nor Get-WmiObject" -ForegroundColor DarkRed; }
  return $ret;
}
# Include File: [\Includes\IIf.ps1]
# function IIf([bool] $flag, $trueResult, $falseResult) {
# if ($flag) { return $trueResult; } else { return $falseResult; }
# }

# https://stackoverflow.com/a/54702474
Function IIf($If, $Then, $Else) {
  If ($If) { 
    If ($Then -is [scriptblock]) { ForEach-Object -InputObject $If -Process $Then } 
    Else { $Then } 
  } Else {
    If ($Else -is [scriptblock]) { ForEach-Object -InputObject $If -Process $Else }
    Else { $Else }
  }
}

# Include File: [\Includes\Is-BuildServer.ps1]
function Is-BuildServer() {
  return "$(Try-BuildServerType)" -ne "";
}

function Try-BuildServerType() {
  $simpleKeys = @(
     "APPVEYOR",
     "bamboo_planKey",
     "BITBUCKET_COMMIT",
     "BITRISE_IO",
     "BUDDY_WORKSPACE_ID",
     "BUILDKITE",
     "CIRCLECI",
     "CIRRUS_CI",
     "CODEBUILD_BUILD_ARN"
     "DRONE",
     "DSARI",
     "GITHUB_ACTIONS",
     "GITLAB_CI",
     "GO_PIPELINE_LABEL",
     "HUDSON_URL",
     "MAGNUM",
     "SAILCI",
     "SEMAPHORE",
     "SHIPPABLE",
     "TDDIUM",
     "STRIDER",
     "TDDIUM",
     "TEAMCITY_VERSION",
     "TF_BUILD",
     "TRAVIS");
  
  foreach($varName in $simpleKeys) {
    $val=[Environment]::GetEnvironmentVariable($varName);
    if ("$val" -ne "" -and $val -ne "False") { return $varName; }
  }

  return $null;
}
# Write-Host "Try-BuildServerType: [$(Try-BuildServerType)], Is-BuildServer: $(Is-BuildServer)"

# Include File: [\Includes\Is-File-Not-Empty.ps1]
function Is-File-Not-Empty([string] $fileName) {
  try { $fi = new-object System.IO.FileInfo($fileName); return $fi.Length -gt 0; } catch {}; return $fasle; 
}


# Include File: [\Includes\Is-Intel-Emulation-Available.ps1]
# On non-arm returns $false
function Is-Intel-Emulation-Available([int] $bitCount <# 32|64 #> = 64) {
  $systemRoot="$($ENV:SystemRoot)"
  $fileOnly = if ($bitCount -eq 64) { "xtajit64.dll" } else { "xtajit.dll" }; 
  $fullName=Combine-Path $systemRoot "System32" $fileOnly;
  return [bool] (Is-File-Not-Empty $fullName)
}



# Include File: [\Includes\Is-Vc-Runtime-Installed.ps1]
function Is-Vc-Runtime-Installed([int] $major, [string] $arch) {
  $vcList = Get-Installed-VC-Runtimes
  # Does not support x86 v8 (2005) on x86 Windows
  $found = $vcList | where { ($_.Version.StartsWith($major.ToString("0")+".")) -and ($_.Name.ToLower().IndexOf($arch.ToLower()) -ge 0 -or $_.Origin -eq $arch) }
  # return $found.Length -gt 0; v6+
  # return @($found).Length -gt 0; v5+
  return "$($found)" -ne ""; # v2+
}

# Include File: [\Includes\Lazy-Aggregator.ps1]
function Update-Lazy-Aggregator([string] $storageFileName, [string] $keyName, [HashTable] $properties) {
    # TODO:
}
# Include File: [\Includes\Measure-Action.ps1]
function Measure-Action {
  Param(
    [string] $Title,
    [ScriptBlock] $Action
  )

  $startAt = [System.Diagnostics.Stopwatch]::StartNew()
  try { Invoke-Command -ScriptBlock $action; $err=$null; } catch { $err=$_.Exception; }
  $msec = $startAt.ElapsedMilliseconds;
  $ea = $ErrorActionPreference
  $ErrorActionPreference = "SilentlyContinue"
  if (-not $err) {
    Write-Host "Success. " -ForeGroundColor Green -NoNewLine;
    Write-Host "'$title' took $($msec.ToString("n0")) ms"
  } else {
    # Write-Host $err.GetType()
    Write-Host "Fail. $($err.Message)" -ForeGroundColor Red -NoNewLine;
    Write-Host " '$title' took $($msec.ToString("n0")) ms"
  }
  $ErrorActionPreference=$ea
}

# Include File: [\Includes\Out-String-And-TrimEnd.ps1]
Function Out-String-And-TrimEnd
{
  Param ([int] $Skip=0, [int] $Take=2000000000)
  Begin { $n=0; $list = New-Object System.Collections.Generic.List[System.Object]}
  Process { $n++; if (-not ($n -le $Skip -or $n -gt ($Skip+$Take))) { $list.Add("$_"); } }
  End { return [string]::join([System.Environment]::NewLine, $list.ToArray()).TrimEnd(@([char]13,[char]10)) }
}

# Include File: [\Includes\Remove-Windows-Service-If-Exists.ps1]
function Remove-Windows-Service-If-Exists([string] $serviceName, [string] $humanName) {
  # Delete Existing?
  $serviceStatus = [string](Get-Service -Name $serviceName -EA SilentlyContinue).Status
  # & sc.exe query "$serviceName" | out-null; $?
  if ($serviceStatus) { 
    if ($serviceStatus -ne "Stopped") {
      Say "Stopping existing $humanName"
      & net.exe stop $serviceName
    }
    Say "Deleting existing $humanName"
    & sc.exe delete $serviceName
  }
}

# Remove-Windows-Service-If-Exists "PG$9_26_X86" "Postgres SQL Windows Service"

# Include File: [\Includes\Reverse-Pipe.ps1]
function Reverse-Pipe() { $copy=@($input); for($i = $copy.Length - 1; $i -ge 0; $i--) { $copy[$i] } }

# $null | Reverse-Pipe
# $() | Reverse-Pipe
# @(42) | Reverse-Pipe
# @(1,2,3,4,"42") | Reverse-Pipe

# Include File: [\Includes\Say.ps1]
function Say { # param( [string] $message )
    if ($Global:_Say_Stopwatch -eq $null) { $Global:_Say_Stopwatch = [System.Diagnostics.Stopwatch]::StartNew(); }
    $milliSeconds=$Global:_Say_Stopwatch.ElapsedMilliseconds
    if ($milliSeconds -ge 3600000) { $format="HH:mm:ss"; } else { $format="mm:ss"; }
    $elapsed="[$((new-object System.DateTime(0)).AddMilliseconds($milliSeconds).ToString($format))]"
    if (-not (Is-Ansi-Supported)) {
      Write-Host "$($elapsed) " -NoNewline -ForegroundColor Magenta
      Write-Host "$args" -ForegroundColor Yellow
    } else {
      $esc = [char] 27; 
      Write-Host "$esc[95;1m$($elapsed) " -NoNewline -ForegroundColor Magenta
      Write-Host "$esc[93;1m$esc[1m$($args)$($esc)[m" -ForegroundColor Yellow
    }
}
$Global:_Say_Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# https://ss64.com/nt/syntax-ansi.html
function Is-Ansi-Supported() {
  if ((Get-Os-Platform) -ne "Windows") { return $true; }
  $buildServerType = Try-BuildServerType;
  if ("$buildServerType" -eq "GITHUB_ACTIONS" -or "$buildServerType" -eq "TF_BUILD") { return $true; }
  $rawReleaseId = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ReleaseId -EA SilentlyContinue | % {$_.ReleaseId}
  if ($rawReleaseId) { 
    $releaseId = [int] $rawReleaseId;
    return ($releaseId -ge 1809); # 1909
  }
  return $false;
}

function Get-Windows-Release-Id() {
  if ((Get-Os-Platform) -ne "Windows") { return $null; }
  $rawReleaseId = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ReleaseId -EA SilentlyContinue | % {$_.ReleaseId}
  if ($rawReleaseId) { 
    $releaseId = [int] $rawReleaseId;
    return $releaseId;
  }
  return $null;
}

# Include File: [\Includes\Start-Stopwatch.ps1]
function Start-Stopwatch() {
  $ret = [PSCustomObject] @{
    StartAt = [System.Diagnostics.Stopwatch]::StartNew();
  };
  $ret | Add-Member -Force -MemberType ScriptMethod -name GetElapsed -value {
      $milliSeconds = $this.StartAt.ElapsedMilliseconds
      if ($milliSeconds -lt 9000) { return "{0:f2}" -f ($milliSeconds / [double] 1000); }
      if ($milliSeconds -lt 60000) { return "{0:f1}" -f ($milliSeconds / [double] 1000); }
      if ($milliSeconds -ge 3600000) { $format="HH:mm:ss"; } else { $format="mm:ss.f"; }
      return "$((new-object System.DateTime(0)).AddMilliseconds($milliSeconds).ToString($format))"
  }
  # legacy powershell does not override ToString properly
  $ret | Add-Member -Force -MemberType ScriptMethod -name ToString -value { $this.GetElapsed(); }
  return $ret;
}

<#
  $x = Start-Stopwatch; Sleep -Milliseconds 123; "[$($x.GetElapsed()) seconds]"
#>


# Include File: [\Includes\To-Boolean.ps1]
function To-Boolean() { param([string] $name, [string] $value)
  if (($value -eq "True") -Or ($value -eq "On") -Or ($value -eq "1") -Or ("$value".ToLower().StartsWith("enable"))) { return $true; }
  if (("$value" -eq "") -Or ($value -eq "False") -Or ($value -eq "Off") -Or ($value -eq "0") -Or ("$value".ToLower().StartsWith("disable"))) { return $false; }
  Write-Host "Validation Error! Invalid $name parameter '$value'. Boolean parameter accept only True|False|On|Off|Enable|Disable|1|0" -ForegroundColor Red
  return $false;
}

# Include File: [\Includes\To-Sortable-Version-String.ps1]
function To-Sortable-Version-String([string] $arg) {
  $ret = New-Object System.Text.StringBuilder;
  $numberBuffer = New-Object System.Text.StringBuilder;
  for($i=0; $i -lt $arg.Length; $i++) {
    $c = $arg.Substring($i, 1);
    if ($c -ge "0" -and $c -le "9") {
      $__ = $numberBuffer.Append($c)
    }
    else {
      if ($numberBuffer.Length -gt 0) { $__ = $ret.Append($numberBuffer.ToString().PadLeft(42,"0")); $numberBuffer.Length = 0; }
      $__ = $ret.Append($c)
    }
  }
  # same
  if ($numberBuffer.Length -gt 0) { $__ = $ret.Append($numberBuffer.ToString().PadLeft(42,"0")) }
  return $ret.ToString();
}

function Test-Version-Sort() {
  @("PG-9.6.24", "PG-10.1", "PG-11.3", "PG-11.12", "PG-16.4") | % { To-Sortable-Version-String $_ }

  $objects = @(
    @{Version = "PG-9.6.24"; InstalledDate = [DateTime] "2020-01-01"}, 
    @{Version = "PG-10.1";   InstalledDate = [DateTime] "2021-02-02"}, 
    @{Version = "PG-11.3";   InstalledDate = [DateTime] "2022-03-03"}, 
    @{Version = "PG-11.12";  InstalledDate = [DateTime] "2023-04-04"}, 
    @{Version = "PG-16.4";   InstalledDate = [DateTime] "2024-05-05"}
  );
  $objects |
    % { [pscustomobject] $_ } |
    Sort-Object -Property @{ Expression = { To-Sortable-Version-String $_.Version }; Descending = $true }, @{ Expression = "InstalleDate"; Descending = $false } |
    Format-Table * -AutoSize
}

# Test-Version-Sort

# Include File: [\Includes\Troubleshoot-Info.ps1]
function Troubleshoot-Info() {
  $enableTroubleShoot = To-Boolean "PS1_TROUBLE_SHOOT" "$($ENV:PS1_TROUBLE_SHOOT)"
  if (-not $enableTroubleShoot) { return; }
  $c = (Get-PSCallStack)[1]
  $cmd = $c.Command;
  # Write-Host -NoNewLine "[$cmd" -ForegroundColor DarkCyan
  $toWrite = @("-TextDarkCyan", "[$cmd");
  $line=$null; 
  if ($c.Location) { 
    # $line = ":$($c.Location.Split(32) | Select -Last 1)";
    $lineRaw = "$($c.Location.Split(32) | Select -Last 1)"; 
    try { $line = [int]::Parse($lineRaw); $line=":$line" } catch { $line="" }
  }
  if ($line) {
    # Write-Host -NoNewLine "$line" -ForegroundColor DarkCyan;
    $toWrite += "$line"
  }
  # Write-Host -NoNewLine "] " -ForegroundColor DarkCyan
  $toWrite += @("] ", "-Reset");
  $color="Gray";
  $args | % {
    if ($_ -eq "-Highlight") { 
      $color = "Cyan";
    } else { 
      # if ($color) { Write-Host -NoNewLine "$_" -ForegroundColor $color; } else { Write-Host -NoNewLine "$_"; }
      if ($color) { 
        # Write-Host -NoNewLine "$_" -ForegroundColor $color;
        $toWrite += $("-Text$color", "$_");
      } else { 
        # Write-Host -NoNewLine "$_";
        $toWrite += $("-Reset", "$_");
      }
      $color = "Gray"
      $toWrite += "-Reset"
    }
  }
  Write-Line -DirectArgs $toWrite;
  # $toWrite
}

<#
function Troubleshoot-Info-Prev([string] $message) {
  $enableTroubleShoot = To-Boolean "PS1_TROUBLE_SHOOT" "$($ENV:PS1_TROUBLE_SHOOT)"
  if (-not $enableTroubleShoot) { return; }
  $c = (Get-PSCallStack)[1]
  $cmd = $c.Command;
  Write-Host -NoNewLine "[$cmd" -ForegroundColor DarkGreen
  $line=$null; if ($c.Location) { $line = ":$($c.Location.Split(32) | Select -Last 1)"; }
  if ($line) {
    Write-Host -NoNewLine "$line" -ForegroundColor Green;
  }
  Write-Host -NoNewLine "] " -ForegroundColor DarkGreen
  Write-Host "$message"
}
#>


# Black DarkBlue DarkGreen DarkCyan DarkRed DarkMagenta DarkYellow Gray DarkGray Blue Green Cyan Red Magenta Yellow White
# Include File: [\Includes\Try-And-Retry.ps1]
function Try-And-Retry([string] $title, [ScriptBlock] $action, [int] $retryCount = 3, [int] $pauseMilliseconds = 1000) {
  for($retry=1; $retry -le $retryCount; $retry++) {
    $exitCode = 0;
    $err=$null;
    try { 
       $Global:LASTEXITCODE = 0;
       $ret = Invoke-Command -ScriptBlock $action; 
       $exitCode = $Global:LASTEXITCODE; 
       $err = $null; 
       if ($exitCode) { $err = new-object Exception("Command Failed. Exit Code $exitCode"); }
    } catch { $err=$_.Exception; }
    if ($err -eq $null) {
      return $ret;
    }
    if ($retry -eq $retryCount) { $msg = "The action `"$title`" failed $retry times. $($err.Message)"; Write-Host $msg -ForeGroundColor Red; throw new-object Exception($msg); return; }
    Write-Host "The action `"$($title)`" failed. Retrying, $($retry+1) of $retryCount. Error reason is $($err.Message)" -ForeGroundColor Red
    [System.Threading.Thread]::Sleep($pauseMilliseconds)
  }
}
# Try-And-Retry "Success 42" { 42; }
# Try-And-Retry "Download httttt://xxxx" { & curl.exe httttt://xxxx }
# Try-And-Retry "Download https://google.com" { & curl.exe "-I" https://google.com }

# Include File: [\Includes\Write-Line.ps1]
function Get-ANSI-Colors() {
  $isAzurePipeline = (Try-BuildServerType) -eq "TF_BUILD";
  $Esc=[char]27;
  $ANSI_COLORS = @{ 
    Reset     = "$($Esc)[0m"
    Bold      = "$($Esc)[1m"
    Underline = "$($Esc)[4m"
    Inverse   = "$($Esc)[7m"
    
    TextBlack       = "$($Esc)[30m"
    TextDarkBlue    = "$($Esc)[34m"
    TextDarkGreen   = "$($Esc)[32m"
    TextDarkCyan    = "$($Esc)[36m"
    TextDarkRed     = "$($Esc)[31m"
    TextDarkMagenta = "$($Esc)[35m"
    TextDarkYellow  = "$($Esc)[33m"
    # 98 is incorrent on azure pipeline
    TextGray        = IIF $isAzurePipeline "$($Esc)[90m$($Esc)[98m" "$($Esc)[98m" #?
    TextDarkGray    = "$($Esc)[90m" #?
    TextBlue        = "$($Esc)[94m"
    TextGreen       = "$($Esc)[92m"
    TextCyan        = "$($Esc)[96m"
    TextRed         = "$($Esc)[91m"
    TextMagenta     = "$($Esc)[95m"
    TextYellow      = "$($Esc)[93m"
    TextWhite       = "$($Esc)[97m"

    BackBlack       = "$($Esc)[40m"
    BackDarkBlue    = "$($Esc)[44m"
    BackDarkGreen   = "$($Esc)[42m"
    BackDarkCyan    = "$($Esc)[46m"
    BackDarkRed     = "$($Esc)[41m"
    BackDarkMagenta = "$($Esc)[45m"
    BackDarkYellow  = "$($Esc)[43m"
    BackGray        = "$($Esc)[100m" #?
    BackDarkGray    = "$($Esc)[108m" #?
    BackBlue        = "$($Esc)[104m"
    BackGreen       = "$($Esc)[102m"
    BackCyan        = "$($Esc)[106m"
    BackRed         = "$($Esc)[101m"
    BackMagenta     = "$($Esc)[105m"
    BackYellow      = "$($Esc)[103m"
    BackWhite       = "$($Esc)[107m"
  }
  $ANSI_COLORS
}

# Write-Line "Hello " -TextRed -Bold "World"
Function Write-Line([string[]] $directArgs = @()) {
  $ansiColors = Get-ANSI-Colors;
  $isAnsiSupported = Is-Ansi-Supported;
  $directArgs += @($args);
  $arguments = @($directArgs);
  $text="Gray";
  $back="Black";
  $ansi="";
  foreach($arg in $arguments) {
    $isControl = $false;
    $isReset = $false;
    if ($arg.StartsWith("-")) {
      if ($arg.Length -gt 1) {
        $key = $arg.SubString(1);
        if ($ansiColors -and $ansiColors[$key]) {
          $ansiValue = $ansiColors[$key];
          $ansi += $ansiValue;
          $isReset = ($key -eq "Reset");
          if ($isReset) { $text="Gray"; $back="Black"; }
          if ($key -like "Text*") { $text = $key.SubString(4) }
          if ($key -like "Back*") { $back = $key.SubString(4) }
          $isControl = $true;
        }
      }
    }
    if (-not $isControl) {
      if ($isAnsiSupported) { Write-Host "$($ansi)$($arg)" -NoNewLine -ForegroundColor $text -BackgroundColor $back }
      # if ($isAnsiSupported) { Write-Host "$($ansi)$($arg)" -NoNewLine }
      else { Write-Host "$($arg)" -NoNewLine -ForegroundColor $text -BackgroundColor $back }
    }
    # if ($isReset) { $ansi = ""; } TODO: After Text
  }
  Write-Host "";
}

# Include Directive: [ ..\Includes.SqlServer\*.ps1 ]
# Include File: [\Includes.SqlServer\$SqlServer2010DownloadLinks.ps1]
$SqlServer2010DownloadLinks = @(
  @{ 
    Version="2014-x64"; #SP3
    Core     ="https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe" 
    Advanced ="https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPRADV_x64_ENU.exe" #SP3,
    # SP1 does not work on Pipeline
    # Developer="https://archive.org/download/sql-server-2014-enterprise-sp-1-x-64/SQL_Server_2014_Enterprise_SP1_x64.rar" #SP1
    # DeveloperFormat="ISO-In-Archive"
    Developer=@("https://archive.org/download/sql_server_2014_sp3_developer_edition_x64.7z/sql_server_2014_sp3_developer_edition_x64.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2014_sp3_developer_edition_x64.7z/download") #SP3, 12.0.6024
    DeveloperFormat="Archive"
    LocalDB ="https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/ENU/x64/SqlLocalDB.msi"
    CU=@(
    )
  };
  @{ 
    Version="2014-x86"; #SP3
    Core     ="https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x86_ENU.exe" 
    Advanced ="https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPRADV_x86_ENU.exe"
    # Developer="https://archive.org/download/microsoft-sql-server-2014-enterprise-sp-3-32-bit/Microsoft%20SQL%20Server%202014%20Enterprise%20SP3%20%2832Bit%29.zip"
    # DeveloperFormat="ISO-In-Archive"
    Developer=("https://archive.org/download/sql_server_2014_sp3_developer_edition_x86.7z/sql_server_2014_sp3_developer_edition_x86.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2014_sp3_developer_edition_x86.7z/download")
    DeveloperFormat="Archive"
    LocalDB  ="https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/ENU/x86/SqlLocalDB.msi"
    CU=@(
    )
  };
  @{ 
    Version="2012-x64"; #SP4
    Core ="https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe" 
    Advanced="https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPRADV_x64_ENU.exe" 
    Developer=@("https://archive.org/download/sql_server_2012_sp4_developer_x86_x64/sql_server_2012_sp4_developer_x64.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2012_sp4_developer_edition_x64.7z/download") # 11.0.7001.0 SP4
    DeveloperFormat="Archive"
    LocalDB ="https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/ENU/x64/SqlLocalDB.msi"
    CU=@(
    )
  };
  @{ 
    Version="2012-x86"; #SP4
    Core ="https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x86_ENU.exe" 
    Advanced="https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPRADV_x86_ENU.exe"
    Developer=@("https://archive.org/download/sql_server_2012_sp4_developer_x86_x64/sql_server_2012_sp4_developer_x86.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2012_sp4_developer_edition_x86.7z/download") # 11.0.7001.0 SP4
    DeveloperFormat="Archive"
    LocalDB ="https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/ENU/x86/SqlLocalDB.msi"
    CU=@(
    )
  };

  @{ 
    Version="2008R2-x64"; #SP2
    Core    ="https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe" 
    Advanced="https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPRADV_x64_ENU.exe" 
    Developer=@("https://archive.org/download/sql_server_2008r2_sp3_developer_edition_x86_x64_ia64/sql_server_2008r2_sp3_developer_edition_x64.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2008r2_sp3_developer_edition_x64.7z/download")
    DeveloperFormat="Archive"
    CU=@(
      @{ Id="SP3"; Url="https://download.microsoft.com/download/D/7/A/D7A28B6C-FCFE-4F70-A902-B109388E01E9/ENU/SQLServer2008R2SP3-KB2979597-x64-ENU.exe" }
    )
  };
  @{ 
    Version="2008R2-x86"; #SP2
    Core    ="https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x86_ENU.exe" 
    Advanced="https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPRADV_x86_ENU.exe"
    Developer=@("https://archive.org/download/sql_server_2008r2_sp3_developer_edition_x86_x64_ia64/sql_server_2008r2_sp3_developer_edition_x86.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2008r2_sp3_developer_edition_x86.7z/download")
    DeveloperFormat="Archive"
    CU=@(
      @{ Id="SP3"; Url="https://download.microsoft.com/download/D/7/A/D7A28B6C-FCFE-4F70-A902-B109388E01E9/ENU/SQLServer2008R2SP3-KB2979597-x86-ENU.exe" }
    )
  };
  # https://archive.org/download/en_sql_server_2008_r2_developer_x86_x64_ia64_dvd_522665

  # 2008 SP2
  @{ 
    Version="2008-x64"; 
    # DONE: Updated for the direct publication
    # Core ="https://web.archive.org/web/20160617214727/https://download.microsoft.com/download/0/F/D/0FD88169-F86F-46E1-8B3B-56C44F6E9505/SQLEXPR_x64_ENU.exe" #SP3
    Core    ="https://archive.org/download/sql-server-express-2008-x86-x64-sp4/SQL-Core-2008-x64-ENU.exe"
    Advanced="https://download.microsoft.com/download/e/9/b/e9bcf5d7-2421-464f-94dc-0c694ba1b5a4/SQLEXPRADV_x64_ENU.exe" #RTM
    Developer=@("https://archive.org/download/sql_server_2008_developer_edition_x86_x64/sql_server_2008_rtm_developer_edition_x64.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2008_rtm_developer_edition_x64.7z/download") # RTM
    DeveloperFormat="Archive"
    CU=@(
      # x64 is alive
      @{ Id="SP4"; Url="https://download.microsoft.com/download/5/E/7/5E7A89F7-C013-4090-901E-1A0F86B6A94C/ENU/SQLServer2008SP4-KB2979596-x64-ENU.exe" }
      # https://web.archive.org/web/20200812071912/https://download.microsoft.com/download/5/E/7/5E7A89F7-C013-4090-901E-1A0F86B6A94C/ENU/SQLServer2008SP4-KB2979596-x64-ENU.exe
    )
    
  };
  @{ 
    Version ="2008-x86"; #SP2
    # DONE: Updated for the direct publication
    # Core ="https://web.archive.org/web/20160617214727/https://download.microsoft.com/download/0/F/D/0FD88169-F86F-46E1-8B3B-56C44F6E9505/SQLEXPR_x86_ENU.exe" #SP3
    Core    ="https://archive.org/download/sql-server-express-2008-x86-x64-sp4/SQL-Core-2008-x86-ENU.exe"
    Advanced="https://download.microsoft.com/download/e/9/b/e9bcf5d7-2421-464f-94dc-0c694ba1b5a4/SQLEXPRADV_x86_ENU.exe" #RTM
    Developer=@("https://archive.org/download/sql_server_2008_developer_edition_x86_x64/sql_server_2008_rtm_developer_edition_x86.7z", "https://sourceforge.net/projects/archived-sql-servers/files/sql_server_2008_rtm_developer_edition_x86.7z/download") # RTM
    DeveloperFormat="Archive"
    CU=@(
      # x86 is removed
      # @{ Id="SP4"; Url="https://download.microsoft.com/download/5/E/7/5E7A89F7-C013-4090-901E-1A0F86B6A94C/ENU/SQLServer2008SP4-KB2979596-x86-ENU.exe" }
      # It is not reliable
      # @{ Id="SP4"; Url="https://web.archive.org/web/20200804042408/https://download.microsoft.com/download/5/E/7/5E7A89F7-C013-4090-901E-1A0F86B6A94C/ENU/SQLServer2008SP4-KB2979596-x86-ENU.exe" }
      # Explicit web archive publication
      @{ Id="SP4"; Url="https://archive.org/download/sql-server-express-2008-x86-x64-sp4/SQLServer2008SP4-KB2979596-x86-ENU.exe" }
    )
  };
  @{ 
    Version="2005-x86"; 
    Core    ="https://sourceforge.net/projects/db-engine/files/database-engine-x86-9.0.5000.exe/download"  #SP4
    # Advanced="https://ia601402.us.archive.org/34/items/Microsoft_SQL_Server_2005/en_sql_2005_express_adv.exe" #SP1
    # Advanced="https://archive.org/download/Microsoft_SQL_Server_2005/en_sql_2005_express_adv.exe" #SP1
    Advanced="https://archive.org/download/SQLEXPR_ADV_2005_SP2/SQLEXPR_ADV.EXE"
    CU=@(
      # Core already SP4
      @{ Id="SP4"; Url="https://catalog.s.download.windowsupdate.com/msdownload/update/software/svpk/2011/01/sqlserver2005expressadvancedsp4-kb2463332-x86-enu_b8640fde879a23a2372b27f158d54abb5079033e.exe" }
    )
  };
)
<#
              (sp2) https://archive.org/download/SQLEXPR_ADV_2005_SP2/SQLEXPR_ADV.EXE
was: 9.0.2047 (sp1) https://ia601402.us.archive.org/34/items/Microsoft_SQL_Server_2005/en_sql_2005_express_adv.exe
now: 9.0.5000 (sp4) https://catalog.s.download.windowsupdate.com/msdownload/update/software/svpk/2011/01/sqlserver2005expressadvancedsp4-kb2463332-x86-enu_b8640fde879a23a2372b27f158d54abb5079033e.exe
#>


# Include File: [\Includes.SqlServer\$SqlServerAlreadyUpdatedList.ps1]
$SqlServerAlreadyUpdatedList = @(
  @{ Version = "2008R2-x64"; MediaType = "Developer"; },
  @{ Version = "2008R2-x86"; MediaType = "Developer"; },
  @{ Version = "2005-x86";   MediaType = "Core"; }
);

# Include File: [\Includes.SqlServer\$SqlServerDownloadLinks.ps1]
$SqlServerDownloadLinks = @(
   @{
      Version="2022"
      Advanced="https://download.microsoft.com/download/3/8/d/38de7036-2433-4207-8eae-06e247e17b25/SQLEXPRADV_x64_ENU.exe"
      Core="https://download.microsoft.com/download/3/8/d/38de7036-2433-4207-8eae-06e247e17b25/SQLEXPR_x64_ENU.exe"
      LocalDB="https://download.microsoft.com/download/3/8/d/38de7036-2433-4207-8eae-06e247e17b25/SqlLocalDB.msi"
      Developer=@(
         "https://download.microsoft.com/download/3/8/d/38de7036-2433-4207-8eae-06e247e17b25/SQLServer2022-DEV-x64-ENU.box",
         "https://download.microsoft.com/download/3/8/d/38de7036-2433-4207-8eae-06e247e17b25/SQLServer2022-DEV-x64-ENU.exe")
      CU=@(
        # @{ Id="CU14"; Url="https://download.microsoft.com/download/9/6/8/96819b0c-c8fb-4b44-91b5-c97015bbda9f/SQLServer2022-KB5038325-x64.exe"; }
        # @{ Id="CU15";
        # Url=@(
        # "https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/updt/2024/09/sqlserver2022-kb5041321-x64_1b40129fb51df67f28feb2a1ea139044c611b93f.exe",
        # "https://archive.org/download/sql_server_2016_2017_2019_2022_comulative_updates/sqlserver2022-kb5041321-x64_1b40129fb51df67f28feb2a1ea139044c611b93f.exe"
        # );
        # }
        # CU17
        @{ Id="CU17"; 
           Url=@(
            "https://download.microsoft.com/download/9/6/8/96819b0c-c8fb-4b44-91b5-c97015bbda9f/SQLServer2022-KB5048038-x64.exe",
            "https://archive.org/download/sql_server_2016_2017_2019_2022_comulative_updates/SQLServer2022-KB5048038-x64.exe"
           );
         }
      )
   },
   @{
      Version="2019"
      Advanced="https://download.microsoft.com/download/8/4/c/84c6c430-e0f5-476d-bf43-eaaa222a72e0/SQLEXPRADV_x64_ENU.exe"
      Core="https://download.microsoft.com/download/8/4/c/84c6c430-e0f5-476d-bf43-eaaa222a72e0/SQLEXPR_x64_ENU.exe"
      LocalDB="https://download.microsoft.com/download/8/4/c/84c6c430-e0f5-476d-bf43-eaaa222a72e0/SqlLocalDB.msi"
      Developer=@(
         "https://download.microsoft.com/download/8/4/c/84c6c430-e0f5-476d-bf43-eaaa222a72e0/SQLServer2019-DEV-x64-ENU.box",
         "https://download.microsoft.com/download/8/4/c/84c6c430-e0f5-476d-bf43-eaaa222a72e0/SQLServer2019-DEV-x64-ENU.exe")
      CU=@(
        # @{ Id="CU30";
        # Url=@(
        # "https://download.microsoft.com/download/6/e/7/6e72dddf-dfa4-4889-bc3d-e5d3a0fd11ce/SQLServer2019-KB5049235-x64.exe",
        # "https://archive.org/download/sql_server_2016_2017_2019_2022_comulative_updates/SQLServer2019-KB5049235-x64.exe"
        # );
        # }
        # @{ Id="CU28"; Url="https://download.microsoft.com/download/6/e/7/6e72dddf-dfa4-4889-bc3d-e5d3a0fd11ce/SQLServer2019-KB5039747-x64.exe"; }
        # CU31
        # https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/updt/2025/02/sqlserver2019-kb5049296-x64_df5a52a752a86ff79331698949a99e8a53c97d97.exe
        # https://archive.org/download/sql_server_2016_2017_2019_2022_comulative_updates/sqlserver2019-kb5049296-x64_df5a52a752a86ff79331698949a99e8a53c97d97.exe
        @{ Id="CU31"; 
           Url=@(
            "https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/updt/2025/02/sqlserver2019-kb5049296-x64_df5a52a752a86ff79331698949a99e8a53c97d97.exe",
            "https://archive.org/download/sql_server_2016_2017_2019_2022_comulative_updates/sqlserver2019-kb5049296-x64_df5a52a752a86ff79331698949a99e8a53c97d97.exe"
           );
         }
      )

   },
   @{
      Version="2017"
      Advanced="https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SQLEXPRADV_x64_ENU.exe"
      Core="https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SQLEXPR_x64_ENU.exe"
      LocalDB="https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SqlLocalDB.msi"
      Developer=@(
         "https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SQLServer2017-DEV-x64-ENU.box",
         "https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SQLServer2017-DEV-x64-ENU.exe")
      CU=@(
        @{ Id="CU31"; 
           Url=@(
             "https://download.microsoft.com/download/C/4/F/C4F908C9-98ED-4E5F-88D5-7D6A5004AEBD/SQLServer2017-KB5016884-x64.exe",
             "https://archive.org/download/sql_server_2016_2017_2019_2022_comulative_updates/SQLServer2017-KB5016884-x64.exe"
           );
         }
      )
   },
   @{
      Version="2016"
      Advanced="https://download.microsoft.com/download/f/9/8/f982347c-fee3-4b3e-a8dc-c95383aa3020/sql16_sp3_dlc/en-us/SQLEXPRADV_x64_ENU.exe"
      Core="https://download.microsoft.com/download/f/9/8/f982347c-fee3-4b3e-a8dc-c95383aa3020/sql16_sp3_dlc/en-us/SQLEXPR_x64_ENU.exe"
      LocalDB="https://download.microsoft.com/download/f/9/8/f982347c-fee3-4b3e-a8dc-c95383aa3020/sql16_sp3_dlc/en-us/SqlLocalDB.msi"
      Developer=@(
        "https://download.microsoft.com/download/f/9/8/f982347c-fee3-4b3e-a8dc-c95383aa3020/sql16_sp3_dlc/en-us/SQLServer2016SP3-FullSlipstream-DEV-x64-ENU.box",
        "https://download.microsoft.com/download/f/9/8/f982347c-fee3-4b3e-a8dc-c95383aa3020/sql16_sp3_dlc/en-us/SQLServer2016SP3-FullSlipstream-DEV-x64-ENU.exe")
      CU=@(
        @{ Id="1.KB5040946"; 
           Url=@(
             "https://download.microsoft.com/download/d/a/1/da18aac1-2cd0-4c52-b30d-39c3172cd156/SQLServer2016-KB5040946-x64.exe",
             "https://archive.org/download/sql_server_2016_2017_2019_2022_comulative_updates/SQLServer2016-KB5040946-x64.exe"
           );
         }
      )
   }
);

$SqlServerDownloadLinks_Via_Manager = @(
  @{ 
    Version="2016";
    BaseDev    ="https://download.microsoft.com/download/c/5/0/c50d5f5e-1adf-43eb-bf16-205f7eab1944/SQLServer2016-SSEI-Dev.exe"; #SP3, 3.0 Gb
    BaseExpress="https://download.microsoft.com/download/f/a/8/fa83d147-63d1-449c-b22d-5fef9bd5bb46/SQLServer2016-SSEI-Expr.exe" #SP3,
    CU=@(
      @{ Id="1.KB5040946"; Url="https://download.microsoft.com/download/d/a/1/da18aac1-2cd0-4c52-b30d-39c3172cd156/SQLServer2016-KB5040946-x64.exe"; }
    )
  };
  @{
    Version="2017";
    BaseDev    ="https://download.microsoft.com/download/5/A/7/5A7065A2-C81C-4A31-9972-8A31AC9388C1/SQLServer2017-SSEI-Dev.exe";
    BaseExpress="https://download.microsoft.com/download/5/E/9/5E9B18CC-8FD5-467E-B5BF-BADE39C51F73/SQLServer2017-SSEI-Expr.exe"
    CU=@(
      @{ Id="CU31"; Url="https://download.microsoft.com/download/C/4/F/C4F908C9-98ED-4E5F-88D5-7D6A5004AEBD/SQLServer2017-KB5016884-x64.exe"; }
    )
  };
  @{ 
    Version="2019";
    BaseDev    ="https://download.microsoft.com/download/d/a/2/da259851-b941-459d-989c-54a18a5d44dd/SQL2019-SSEI-Dev.exe";
    BaseExpress="https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe"
    CU=@(
      @{ Id="CU28"; Url="https://download.microsoft.com/download/6/e/7/6e72dddf-dfa4-4889-bc3d-e5d3a0fd11ce/SQLServer2019-KB5039747-x64.exe"; }
    )
  };
  @{ 
    Version="2022";
    BaseDev    ="https://download.microsoft.com/download/c/c/9/cc9c6797-383c-4b24-8920-dc057c1de9d3/SQL2022-SSEI-Dev.exe";
    BaseExpress="https://download.microsoft.com/download/5/1/4/5145fe04-4d30-4b85-b0d1-39533663a2f1/SQL2022-SSEI-Expr.exe"
    CU=@(
      @{ Id="CU14"; Url="https://download.microsoft.com/download/9/6/8/96819b0c-c8fb-4b44-91b5-c97015bbda9f/SQLServer2022-KB5038325-x64.exe"; }
    )
  };
)

# $SqlServerDownloadLinks | ConvertTo-Json -Depth 32

# Include File: [\Includes.SqlServer\Create-LocalDB-Instance.ps1]
# Using latest SQLLocalDB.exe
function Create-LocalDB-Instance([string] $instanceName, [string] $optionalVersion) {
  $pars = @("create", "`"$instanceName`"");
  if ($optionalVersion) { $pars += $optionalVersion }
  $title = "Create LocalDB instance `"$instanceName`""
  if ($optionalVersion) { $title += " version $optionalVersion" }
  return Invoke-LocalDB-Executable -Title $title -Version "Latest" -Parameters @($pars)
}


# Include File: [\Includes.SqlServer\Download-2010-SQLServer-and-Extract.ps1]
function Download-2010-SQLServer-and-Extract {
  Param(
    [string] $version,  # (2014|2012|2008R2|2008)-(x86|x64)
    [string] $mediaType # Core|Advanced|LocalDB (localdb is missing for 2008R2)
  )

  $rootMedia=Combine-Path "$(Get-SqlServer-Media-Folder)" "SQL-$version-$mediaType-Compressed"
  $rootSetup=Combine-Path "$(Get-SqlServer-Setup-Folder)" "SQL-$version-$mediaType"
  $mediaPath = $rootMedia
  $ext = IIf ($mediaType -eq "LocalDB") ".msi" ".exe"
  $exeName = "SQL-$mediaType-$Version-ENU$ext"
  $exeArchive = Combine-Path $mediaPath $exeName
  $setupPath="$rootSetup"
  if ($mediaType -eq "LocalDB") { 
    $exeArchive = Combine-Path $setupPath $exeName
    $mediaPath = $null;
  }

  Write-Host "Download Media for '$version $mediaType'"
  $url="";
  $isDeveloper = [bool]($mediaType -eq "Developer");
  foreach($meta in $SqlServer2010DownloadLinks) {
    if ($meta.Version -eq $version) {
      $url = $meta[$mediaType]
      $urlFormat=$meta["$($mediaType)Format"]
      if ($urlFormat) {
        $ext = ".$urlFormat".ToLower();
        $ext = Try-Get-FileExtension-by-Uri $url;
        $exeName = "SQL-$mediaType-$Version-ENU$ext"
        $exeArchive = Combine-Path $mediaPath $exeName
      }
    }
  }
  if (-not $url) {
    Write-Host "Unknown SQL Server version $version $mediaType" -ForegroundColor DarkRed;
    return @{};
  }

  Write-Host "Downloading media for version $version $mediaType. URL(s) is '$url'. Setup file is '$exeArchive'";
  $isDownloadOk = Download-File-FailFree-and-Cached $exeArchive @($url)
  if (-not $isDownloadOk) {
    Write-Host "Download media for version $version $mediaType failed. URL(s) is '$url'" -ForegroundColor DarkRed;
    return @{};
  }

  $ret = @{ Version=$version; MediaType=$mediaType; };
  if ($mediaType -eq "LocalDB") {
    $ret["Launcher"] = $exeArchive;
    $ret["Setup"] = $setupPath;
  }
  else 
  {
    if ($null -eq $urlFormat)
    {
      $isExtractOk = ExtractSqlServerSetup "SQL Server $version $mediaType" $exeArchive $setupPath "/Q"
      if ($isExtractOk) {
        $ret["Launcher"] = Combine-Path $setupPath "Setup.exe";
        $ret["Setup"] = $setupPath;
        $ret["Media"] = $mediaPath;
      } else {
        return @{};
      }
    } elseif ($urlFormat -eq "ISO-In-Archive") {
      $isoFolder = Combine-Path $mediaPath "iso"
      Write-Host "Extracting '$exeArchive' to '$isoFolder'"
      $isExtract1Ok = Extract-Archive-by-Default-Full-7z "$exeArchive" "$isoFolder"
      $isoFile = Get-ChildItem -Path "$isoFolder" -Filter "*.iso" | Select -First 1
      Write-Host "ISO found: '$($isoFile.FullName)' ($($isoFile.Length.ToString("n0")) bytes). Extracting it to '$setupPath'";
      $isExtract2Ok = Extract-Archive-by-Default-Full-7z "$($isoFile.FullName)" "$setupPath"
      $ret["Launcher"] = Combine-Path $setupPath "Setup.exe";
      $ret["Setup"] = $setupPath;
      $ret["Media"] = $mediaPath;
    } elseif ($urlFormat -eq "Archive") {
      Write-Host "Extracting '$exeArchive' to '$setupPath'"
      $isExtract1Ok = Extract-Archive-by-Default-Full-7z "$exeArchive" "$setupPath"
      $ret["Launcher"] = Combine-Path $setupPath "Setup.exe";
      $ret["Setup"] = $setupPath;
      $ret["Media"] = $mediaPath;
    } else {
      Write-Host "Warning! Unknown format '$urlFormat' for '$url')" -ForegroundColor DarkRed
    }
  }

  ApplyCommonSqlServerState $ret;
  return $ret;
}

# Include File: [\Includes.SqlServer\Download-Fresh-SQLServer-and-Extract.ps1]
function Download-Fresh-SQLServer-and-Extract {
  Param(
    [string] $version,  # 2016|2017|2019|2022
    [string] $mediaType # LocalDB|Core|Advanced|Developer
  )

  $rootMedia=Combine-Path "$(Get-SqlServer-Media-Folder)" "SQL-$version-$mediaType-Compressed"
  $rootSetup=Combine-Path "$(Get-SqlServer-Setup-Folder)" "SQL-$version-$mediaType"
  if ($mediaType -eq "LocalDB") {
     $mediaPath = $rootSetup
  } else {
     $mediaPath = $rootMedia
  }
  $setupPath="$rootSetup"

  Write-Host "Download and Extract SQL Server $version media '$mediaType' to `"$setupPath`""
  $url="";
  foreach($meta in $SqlServerDownloadLinks) {
    if ($meta.Version -eq $version) {
      $url = $meta["$mediaType"]
    }
  }
  if (-not $url) {
    Write-Host "Unknown SQL Server version $version" -ForegroundColor DarkRed;
    return @{};
  }

  Troubleshoot-Info "URL(s) IS " -Highlight "$url"
  $urlList = IIF ($url -is [array]) $url @($url);
  foreach($nextUrl in $urlList) {
    $fileName=[System.IO.Path]::GetFileName($nextUrl); # TODO: trim /download at the end
    $fileFull = Combine-Path $mediaPath $fileName
    $isDownloadOk = Download-File-FailFree-and-Cached $fileFull @($nextUrl)
    if (-not $isDownloadOk) {
      Write-Host "Download bootstrapper for version $version failed. Unable to download URL '$nextUrl' as '$fileFull'" -ForegroundColor DarkRed;
      return @{};
    }
  }
  
  $ext = IIF ($mediaType -eq "LocalDB") "msi" "exe"
  $exeArchive = Get-ChildItem -Path "$mediaPath" -Filter "*.$ext" | Select -First 1
  Write-Host "(Exe|Msi) Archive `"$exeArchive`""
  $ret = @{ Version=$version; MediaType=$mediaType; };
  if ($mediaType -eq "LocalDB") {
    $ret["Launcher"] = Combine-Path $setupPath $exeArchive;
    $ret["Setup"] = $setupPath;
  }
  else
  {
    $quietArg = IIF (Is-BuildServer) "/Q" "/QS"
    $isExtractOk = ExtractSqlServerSetup "SQL Server $version $mediaType" $exeArchive.FullName $setupPath "$quietArg"
    if ($isExtractOk) {
      $ret["Launcher"] = Combine-Path $setupPath "Setup.exe";
      $ret["Setup"] = $setupPath;
      $ret["Media"] = $mediaPath;
    } else {
      return @{};
    }
  }

  ApplyCommonSqlServerState $ret;
  return $ret;
}



























function Download-Fresh-SQLServer-and-Extract-Via-Manager {
  Param(
    [string] $version,  # 2016|2017|2019|2022
    [string] $mediaType # LocalDB|Core|Advanced|Developer
  )

  $key="SQL-$version-$mediaType"
  $root=Combine-Path "$(Get-SqlServer-Downloads-Folder)" $key
  if ($mediaType -eq "LocalDB") {
     $mediaPath = $root
  } else {
     $mediaPath = Combine-Path "$(Get-SqlServer-Downloads-Folder)" "SQL-Setup-Compressed" "SQL-$Version-$mediaType" 
  }
  $setupPath="$root"

  Write-Host "Download Bootstrapper for '$key'"
  $url="";
  $isDeveloper = [bool]($mediaType -eq "Developer");
  foreach($meta in $SqlServerDownloadLinks) {
    if ($meta.Version -eq $version) {
      $url = IIf $isDeveloper $meta.BaseDev $meta.BaseExpress
    }
  }
  if (-not $url) {
    Write-Host "Unknown SQL Server version $version" -ForegroundColor DarkRed;
    return @{};
  }

  $exeBootstrap = Combine-Path "$(Get-SqlServer-Downloads-Folder)" "SQL-Setup-Bootstrapper" "SQL-$Version-$(IIf $isDeveloper "Developer" "Express")-Bootstapper.exe"

  $isRawOk = Download-File-FailFree-and-Cached $exeBootstrap @($url)
  if (-not $isRawOk) {
    Write-Host "Download bootstrapper for version $version failed. URL is '$url'" -ForegroundColor DarkRed;
    return @{};
  }
  
  Write-Host "Download SQL Server $version media '$mediaType'"
  $mt = IIf $isDeveloper "CAB" $mediaType;
  # echo Y | "%outfile%" /ENU /Q /Action=Download /MEDIATYPE=%MT% /MEDIAPATH="%Work%\SETUPFILES"
  $startAt = [System.Diagnostics.Stopwatch]::StartNew()
  $hideProgressBar=IIF (Is-BuildServer) " /HIDEPROGRESSBAR" "";
  & cmd.exe @("/c", "echo Y | `"$exeBootstrap`" /ENU /Q$hideProgressBar /Action=Download /MEDIATYPE=$mt /MEDIAPATH=`"$mediaPath`"")
  # & "$exeBootstrap" @("/ENU", "/Q", "/Action=Download", "/MEDIATYPE=$mt", "/MEDIAPATH=`"$mediaPath`"");
  if (-not $?) {
    Write-Host "Media download for version $version $mediaType. failed" -ForegroundColor DarkRed;
    return @{};
  }
  
  try { $length = Get-Folder-Size $mediaPath; } catch {}; $milliSeconds = $startAt.ElapsedMilliseconds;
  $size=""; if ($length -gt 0) { $size=" ($($length.ToString("n0")) bytes)"; }
  $speed=""; if ($length -gt 0 -and $milliSeconds -gt 0) { $speed=" Speed is $(($length*1000/1024/$milliSeconds).ToString("n0")) Kb/s."; }
  $duration=""; if ($milliSeconds -gt 0) {$duration=" It took $(($milliSeconds/1000.).ToString("n1")) seconds."; }
  Write-Host "Media '$mediaPath'$($size) download complete.$($duration)$($speed)"

  $ret = @{ Version=$version; MediaType=$mediaType; };
  if ($mediaType -eq "LocalDB") {
    $ret["Launcher"] = Combine-Path $mediaPath "en-US" "SqlLocalDB.msi";
    $ret["Setup"] = $setupPath;
  }
  else
  {
    $exeArchive = Get-ChildItem -Path "$mediaPath" -Filter "*.exe" | Select -First 1
    $quietArg = IIF (Is-BuildServer) "/Q" "/QS"
    $isExtractOk = ExtractSqlServerSetup "SQL Server $version $mediaType" $exeArchive.FullName $setupPath "$quietArg"
    if ($isExtractOk) {
      $ret["Launcher"] = Combine-Path $setupPath "Setup.exe";
      $ret["Setup"] = $setupPath;
      $ret["Media"] = $mediaPath;
    } else {
      return @{};
    }
  }

  ApplyCommonSqlServerState $ret;
  return $ret;
}

# Include File: [\Includes.SqlServer\Download-SQLServer-and-Extract.ps1]
function Download-SQLServer-and-Extract {
  Param(
    [string] $version,  # 2016|2017|2019|2022
    [string] $mediaType # LocalDB|Core|Advanced|Developer
  )
  $major = ($version.Substring(0,4)) -as [int];
  $is2020 = $major -ge 2016;
  if ($is2020) { 
    Download-Fresh-SQLServer-and-Extract $version $mediaType;
  } else {
    Download-2010-SQLServer-and-Extract $version $mediaType;
  }
}

function ApplyCommonSqlServerState([Hashtable] $ret) {
  if ($ret.Launcher) {
    try { $size = (New-Object System.IO.FileInfo($ret.Launcher)).Length; } catch {Write-Host "Warning $($_)"; }
    $ret["LauncherSize"] = $size;
  }
  if ($ret.Setup) {
    $ret["SetupSize"] = Get-Folder-Size $ret.Setup;
    if (Is-SqlServer-Setup-Cache-Enabled) { 
      $ret["SetupHash"] = Get-Smarty-FolderHash $ret.Setup "SHA512"
    }
  }
  if ($ret.Media) {
    $ret["MediaSize"] = Get-Folder-Size $ret.Media;
    # $ret["MediaHash"] = Get-Smarty-FolderHash $ret.Media "SHA512"
  }
}

function ExtractSqlServerSetup([string] $title, [string] $exeArchive, [string] $setupPath, [string] $quietArg <# /QS (Fresh) | /Q (prev) #>) {
  Write-Host "Extracting $title using [$($exeArchive)] to [$($setupPath)]"
  $extractStatus = Execute-Process-Smarty "$title Unpacker" "$($exeArchive)" @("$quietArg", "/X:`"$setupPath`"") -WaitTimeout 1800
  # $startAt = [System.Diagnostics.Stopwatch]::StartNew()
  # $extractApp = Start-Process "$($exeArchive)" -ArgumentList @("$quietArg", "/x:`"$setupPath`"") -PassThru
  # if ($extractApp -and $extractApp.Id) {
  # Wait-Process -Id $extractApp.Id
  # if ($extractApp.ExitCode -ne 0) {
  # Write-Host "Extracting $title failed. Exit code $($extractApp.ExitCode)." -ForegroundColor DarkRed;
  # return $false;
  # }
  # } else {
  # Write-Host "Extracting $title. failed." -ForegroundColor DarkRed;
  # return $false;
  # }
  # Write-Host "Extraction of $title took $($startAt.ElapsedMilliseconds.ToString("n0")) ms"
  return ($extractStatus.ExitCode -eq 0);
}

# Include File: [\Includes.SqlServer\Download-SqlServer-Update.ps1]
function Download-SqlServer-Update {
  Param(
    [string] $version,  # 2005...2022
    [string] $mediaType, # LocalDB|Core|Advanced|Developer
    [object] $update # {Id=;Url=;}
  )
  $ret = $update.Clone();
  $key="SQL-$version-Update-$($update.Id)"
  $archivePath = Combine-Path "$(Get-SqlServer-Media-Folder)" $key
  $archiveName = [System.IO.Path]::GetFileName($update.Url); # TODO: trim /download at the end
  $archiveFullName = Combine-Path $archivePath $archiveName;
  Write-Host "Downloading SQL Server update '$($update.Id)' for version $version $mediaType. URL(s) is '$($update.Url)'"
  # $isDownloadOk = Download-File-FailFree-and-Cached $archiveFullName @("$($update.Url)")
  $isDownloadOk = Download-File-FailFree-and-Cached $archiveFullName @($update.Url)
  if (-not $isDownloadOk) {
    Write-Host "Download SQL Server update '$($update.Id)' for version $version $mediaType failed. URL is '$($update.Url)'" -ForegroundColor DarkRed;
    return $ret;
  }

  $ret["UpdateId"] = $update.Id;
  $ret["UpdateFolder"] = $archivePath;
  $ret["UpdateLauncher"] = $archiveFullName;
  $ret["UpdateSize"] = (new-object System.IO.FileInfo($archiveFullName)).Length;
  return $ret;
}

<#
Name Value
---- -----
Url https://download.microsoft.com/download/C/4/F/C4F908C9-98ED-4E5F-88D5-7D6A5004AEBD/SQLServer2017-KB5016884-x64.exe
Id CU31
UpdateId CU31
UpdateSize 561206208
UpdateFolder C:\SQL-Downloads\SQL-2017-CU31
UpdateLauncher C:\SQL-Downloads\SQL-2017-CU31\SQLServer2017-KB5016884-x64.exe
#>


# Include File: [\Includes.SqlServer\Enumerate-SQLServer-Downloads.ps1]
function Enumerate-SQLServer-Downloads() {
  $versions = "2005-x86", "2008-x86", "2008-x64", "2008R2-x86", "2008R2-x64", "2012-x86", "2012-x64", "2014-x86", "2014-x64", "2016", "2017", "2019", "2022";
  [array]::Reverse($versions);
  $mediaTypes = "LocalDB", "Core", "Advanced", "Developer";
  [array]::Reverse($mediaTypes);
  foreach($version in $versions) {
  foreach($mediaType in $mediaTypes) {
    $meta = Find-SQLServer-Meta $version $mediaType;
    if ($meta) {
      $meta;
    }
  }}
}

function Enumerate-Plain-SQLServer-Downloads() {
  foreach($meta in Enumerate-SQLServer-Downloads) {
    $baselineKeywords="$($meta.Version) $($meta.MediaType)"
    if ("$($meta.CU)") {
      foreach($update in $meta.CU) {
        $counter++;
        @{ Version=$meta.Version; MediaType=$meta.MediaType; UpdateId=$update.Id; Update=$update; Keywords="$baselineKeywords $($update.Id)"; NormalizedKeywords="$baselineKeywords Update"; };
      }
    }
    @{ Version=$meta.Version; MediaType=$meta.MediaType; Keywords="$baselineKeywords"; NormalizedKeywords="$baselineKeywords"};
  }
}

# Include File: [\Includes.SqlServer\Find-LocalDb-SqlServer.ps1]
# Super Fast
function Find-LocalDb-SqlServer() {
  $parentKey = Get-Item -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server Local DB\Installed Versions" -EA SilentlyContinue
  $candidates = @();
  if ($parentKey) {
    foreach($subName in $parentKey.GetSubKeyNames()) { $candidates += $subName; }
  }
  [Array]::Sort($candidates);
  $last = $candidates | Select -Last 1;
  if (-not $last) { return; }
  if ($last -like "11.*") { $instance="(LocalDB)\v11.0" } else { $instance="(LocalDB)\MSSqlLocalDB" };
  @{ InstallerVersion = $last; Instance = $instance; }
}

function Start-LocalDb-SqlServer([int] $timeoutSec = 60) {
  $localDb = Find-LocalDb-SqlServer;
  $startAt = [System.Diagnostics.Stopwatch]::StartNew();
  if ($localDb) { 
    $conStr = "Data Source=$($localDb.Instance);Integrated Security=True;Pooling=false;Timeout=2";
    do {
        try { 
        $con = New-Object System.Data.SqlClient.SqlConnection($conStr);
        $con.Open();
        return $true;
        } catch {
        }
    } while($startAt.ElapsedMilliseconds -le ($timeoutSec * 1000));
    Write-Host "Warning! can't start SQL Server Local DB v$($localDb.Version) '$($localDb.Instance)' during $($startAt.ElapsedMilliseconds / 1000) seconds" -ForegroundColor DarkRed
  }
}

# Find-LocalDb-SqlServers
# Start-LocalDb-SqlServer -timeout 1


# Include File: [\Includes.SqlServer\Find-LocalDb-SqlServers.ps1]
function Find-LocalDb-SqlServers() {
  $parentKey = Get-Item -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server Local DB\Installed Versions" -EA SilentlyContinue
  $candidates = @();
  if ($parentKey) {
    # Write-Host $parentKey.PSPath
    foreach($subName in $parentKey.GetSubKeyNames()) { 
      $parentInstance = Get-ItemProperty -Path "$($parentKey.PSPath)\$subName" -Name ParentInstance -EA SilentlyContinue | % {$_.ParentInstance}
      if ($parentInstance) {
        # Version value
        $versionKey1 = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\$parentInstance\Setup"
        # CurrentVersion value
        $versionKey2 = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\$parentInstance\MSSQLServer\CurrentVersion"
        $version1 = Get-ItemProperty -Path "$versionKey1" -Name Version -EA SilentlyContinue | % {$_.Version}
        $version2 = Get-ItemProperty -Path "$versionKey2" -Name CurrentVersion -EA SilentlyContinue | % {$_.CurrentVersion}
        $version = $null;
        if ($version1) { $version = $version1 } 
        elseif ($version2) { $version = $version2 } 
      }
      $verInternal = $subName.Replace(".", "") # 16.0 --> 160
      $pathToolsKey = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\$verInternal\Tools\ClientSetup"
      $pathTools = Get-ItemProperty -Path "$pathToolsKey" -Name Path -EA SilentlyContinue | % {$_.Path}
      if ($pathTools) { 
        $exe = "$pathTools\SQLLocalDB.exe".Replace("\\", "\")
        $localDbExe = $null;
        try { if (([System.IO.File]::Exists($exe))) { $localDbExe = $exe } } catch { }
      }

      $newItem = [pscustomobject] @{ 
        ShortVersion = $subName; 
        Version = $version; 
        ParentInstance = $parentInstance; 
        Exe = $localDbExe;
      };
      $newItem | Add-Member -MemberType ScriptMethod -Name "GetInstances" -Value { ParseNonEmptyTrimmedLines((& "$($this.Exe)" @("i") | Out-String)) }
      $candidates += $newItem
    }
  }
  return @($candidates | ? { "$($_.ShortVersion)" -and "$($_.Version)" -and "$($_.ParentInstance)" -and "$($_.Exe)" } | Sort-Object -Property ShortVersion -Descending);
}


function Test-Show-LocalDb-SqlServers-with-Instances() {
  Find-LocalDb-SqlServers | ft -Property ShortVersion, Version, Exe -AutoSize | Out-Host
  $withInstances = Find-LocalDb-SqlServers | % { $_ | Add-Member -MemberType NoteProperty -Name "Instances" -Value $_.GetInstances(); $_ }
  $withInstances | ft -Property ShortVersion, Version, Exe, Instances -AutoSize | Out-Host

  $instances = @($withInstances | Select -First 1 | % { $_.Instances })
  Write-Host "Total $($instances.Count) instance(s): $instances"
  foreach($instance in $instances) {
    $ver = Query-SqlServer-Version -Title "LocalDB `"$instance`"" -Instance "(localdb)\$instance" -Timeout 60
    Write-Line -TextYellow $instance -TextGreen " $ver"
  }
}


# Include File: [\Includes.SqlServer\Find-Local-SqlServers.ps1]
# Super Fast
function Find-Local-SqlServers() {
  $candidates = @(@{RegPath="Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSSQLServer";Service="MSSQLSERVER";Instance="(local)";});
  $regNamebase = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server"
  $namedKey = Get-Item -Path $regNamebase -EA SilentlyContinue
  if ($namedKey) {
    foreach($subName in $namedKey.GetSubKeyNames()) { $candidates += @{RegPath="$($regNamebase)\$subName";Service="MSSQL`$$($subName)"; Instance="(local)\$subName";}}
  }
  # $candidates | % { [pscustomObject] $_ } | ft -AutoSize | out-Host
  foreach($candidate in $candidates) { 
    $currentVersion = Get-ItemProperty -Path "$($candidate.RegPath)\MSSQLServer\CurrentVersion" -Name CurrentVersion -EA SilentlyContinue | % {$_.CurrentVersion}
    if ($currentVersion) { $candidate["InstallerVersion"] = $currentVersion; }

    $regService = Get-Item -Path "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\$($candidate.Service)" -EA SilentlyContinue
    if ($regService) { $candidate["ServiceExists"] = $true; }
  }
  # Write-Host "FINAL" -ForegroundColor DarkGreen
  # $candidates | % { [pscustomObject] $_ } | ft -AutoSize | out-Host
  $candidates | ? { $_.ServiceExists } | % { $_.Remove("ServiceExists"); $_.Remove("RegPath"); $_; }
}

# Find-Local-SqlServers | % { [pscustomObject] $_ } | ft -AutoSize | Out-String -Width 1234 | Out-Host
# Get-Service -Name (Find-Local-SqlServers | % {$_.Service}) | ft -AutoSize

# Write-Host "STOP SERVICES" -ForegroundColor DarkGreen
# Get-Service -Name (Find-Local-SqlServers | % {$_.Service}) | % { if ($_.Status -ne "Stopped") { Write-Host "Stopping $($_.Name)"; Stop-Service "$($_.Name)" -Force }}

# Write-Host "START SERVICES" -ForegroundColor DarkGreen
# Get-Service -Name (Find-Local-SqlServers | % {$_.Service}) | % { if ($_.Status -ne "Running") { Write-Host "Starting $($_.Name)"; Start-Service "$($_.Name)" }}

# Include File: [\Includes.SqlServer\Find-SQLServer-Meta.ps1]
function Find-SQLServer-Meta([string] $version, [string] $mediaType) {
  $ret = @{ Version = $version; MediaType = $mediaType; }

  $missingParticular = $null -ne ($SqlServerAlreadyUpdatedList | where { $_.Version -eq $version -and $_.MediaType -eq $mediaType});
  $isMissingUpdates = $missingParticular -or ($mediaType -eq "LocalDB");

  foreach($meta in $SqlServerDownloadLinks) {
    if ($meta.Version -eq $version) {
      # In case of via manager
      # $url = IIf ($mediaType -eq "Developer") $meta.BaseDev $meta.BaseExpress
      $url = $meta["$mediaType"]
      $ret["Url"] = $url;
      if (-not $isMissingUpdates) { $ret["CU"] = $meta.CU; }
      return $ret;
    }
  }

  foreach($meta in $SqlServer2010DownloadLinks) {
    if ($meta.Version -eq $version) {
      $url = $meta[$mediaType];
      if ("$url") { 
        $ret["Url"] = $url;
        if (-not $isMissingUpdates) { $ret["CU"] = $meta.CU; }
        return $ret;
      }
    }
  }
}

# Include File: [\Includes.SqlServer\Find-SqlServer-SetupLogs.ps1]
function Find-SqlServer-SetupLogs() {
  $sysDrive = "$($ENV:SystemDrive)"
  $folders = @("$sysDrive\Program Files", "$sysDrive\Program Files (x86)", "C:\Program Files", "C:\Program Files (x86)", "$($Env:ProgramFiles)", "${Env:ProgramFiles(x86)}")
  $folders = ($folders | sort | Get-Unique)
  $ret=@()
  foreach($folder in $folders) {
    $sqlFolder=[System.IO.Path]::Combine($folder, "Microsoft SQL Server");
    if ([System.IO.Directory]::Exists($sqlFolder)) {
        $subDirs = Get-ChildItem -Path $sqlFolder -Directory -Force -ErrorAction SilentlyContinue
        foreach($subDir in $subDirs) {
          $ver=$subDir.Name
          $verSubFolder=$subDir.FullName
          $logFolder=[System.IO.Path]::Combine($verSubFolder, "Setup Bootstrap\LOG");
          if ([System.IO.Directory]::Exists($logFolder)) {
            $ret += [System.IO.Path]::GetFullPath($logFolder)
          }
        }
    }
  }
  $ret = $ret | sort | Get-Unique
  # "C:\Program Files (x86)\Microsoft SQL Server\90\Setup Bootstrap\LOG"
  # "C:\Program Files\Microsoft SQL Server\150\Setup Bootstrap\Log"
  $ret
}

# (Find-SqlServer-SetupLogs).Length; Find-SqlServer-SetupLogs


# Include File: [\Includes.SqlServer\Get-Builtin-Windows-Group-Name.ps1]
function Get-Builtin-Windows-Group-Name([string] $groupKind) {
   # Windows Only
   $sid="";
   #users: S-1-5-32-545, administrators: S-1-5-32-544, power users: S-1-5-32-547
   if     ($groupKind -eq "Users")          { $sid="S-1-5-32-545"; }
   elseif ($groupKind -eq "Administrators") { $sid="S-1-5-32-544"; }
   elseif ($groupKind -eq "PowerUsers")     { $sid="S-1-5-32-547"; }
   $ret=""
   if ($sid) {
     if (Has-Cmd "Get-CIMInstance")     { $group=Get-CIMInstance Win32_Group; } 
     elseif (Has-Cmd "Get-WmiObject")   { $group=Get-WmiObject   Win32_Group; } 
     $ret = ($group | where { $_.SID -eq "$sid" } | Select -First 1).Name;
   }
   return $ret;
}
# @("Administrators", "PowerUsers", "Users") | % { "$($_): '$(Get-Builtin-Windows-Group-Name $_)'" }
# Get-WmiObject Win32_Group | ft *
# Include File: [\Includes.SqlServer\Get-SqlServer-Downloads-Folder.ps1]
function Get-SqlServer-Setup-Folder() {
  return (GetPersistentTempFolder "SQLSERVERS_SETUP_FOLDER" "SQL-Setup");
}

function Get-SqlServer-Media-Folder() {
  return (GetPersistentTempFolder "SQLSERVERS_MEDIA_FOLDER" "SQL Media");
}

# Include File: [\Includes.SqlServer\Install-SQLServer.ps1]
<#
 
META
----
LauncherSize 133032
SetupHash 2D975BB24E872383C63D676105DFAF548972FA82970A8DB0AAC4E161555C8411466850F1442DDD988542FDB1B2205C08C1A8A97EEB9F1DD3AF7B49BFB80D7A21
SetupSize 1349103806
Version 2022
MediaType Developer
Setup C:\Users\VSSADM~1\AppData\Local\Temp\PS1 Repo Downloads\SQL-2022-Developer
Launcher C:\Users\VSSADM~1\AppData\Local\Temp\PS1 Repo Downloads\SQL-2022-Developer\Setup.exe
MediaSize 1233861846
MediaHash 70DF481879338B608231D7FD0F518642CFAF481708E2EA6343DC217C56D5AF7182008EDB8CDF4D96652D44BD7A4B7FA6F3CD1CBAF7A8E3771760B169390F46F4
Media C:\Users\VSSADM~1\AppData\Local\Temp\PS1 Repo Downloads\SQL-Setup-Compressed\SQL-2022-Developer
 
UPDATE
------
UpdateFolder C:\Users\VSSADM~1\AppData\Local\Temp\PS1 Repo Downloads\SQL-2022-CU14
Url https://download.microsoft.com/download/9/6/8/96819b0c-c8fb-4b44-91b5-c97015bbda9f/SQLServer2022-KB5038325-x64.exe
UpdateSize 463846640
UpdateId CU14
UpdateLauncher C:\Users\VSSADM~1\AppData\Local\Temp\PS1 Repo Downloads\SQL-2022-CU14\SQLServer2022-KB5038325-x64.exe
 
#>


function Install-SQLServer {
  Param(
    [object] $meta,
    [object] $update, # optional nullable,
    [string] $instanceName,
    [string[]] $optionsOverride = @()
  )

  if (-not $meta.LauncherSize) {
    $err="[Install-SQLServer] Invalid `$meta argument. Probably download failed";
    Write-Host $err;
    $meta | Format-Table-Smarty
    return @{ Error=$err; }
  }

  if ($meta.MediaType -eq "LocalDB") {
     Write-Host "Installing LocalDB MSI `"$($meta.Launcher)`" Version $($meta.Version)"
     $logDir = if ("$($ENV:SYSTEM_ARTIFACTSDIRECTORY)") { "$($ENV:SYSTEM_ARTIFACTSDIRECTORY)" } else { "$($ENV:TEMP)" }
     $setupCommandLine = @("/c", "msiexec.exe", "/i", "`"$($meta.Launcher)`"", "IACCEPTSQLLOCALDBLICENSETERMS=YES", "/qn", "/L*v", "SqlLocalDB-Setup-$($meta.Version).log");
     $setupStatus = Execute-Process-Smarty "SQL LocalDB $($meta.Version) Setup" "cmd.exe" $setupCommandLine -WaitTimeout 1800
     $setupStatus | Format-Table-Smarty | Out-Host
     return $setupStatus;
  }

  if ($update) {
    $isUpdateValid = ("$($update.UpdateSize)" -and "$($update.UpdateFolder)" -and "$($update.UpdateLauncher)")
    if (-not $isUpdateValid) {
      $err="[Install-SQLServer] Invalid `$update argument. Probably download failed";
      Write-Host $err;
      $update | Format-Table-Smarty | Out-Host
      return @{ Error = $err };
    }
  }

  $sqlAdministratorsGroup = Get-Builtin-Windows-Group-Name "Administrators";
  if (-not $sqlAdministratorsGroup) { $sqlAdministratorsGroup = "Administrators"; }
  $sqlAdministratorsGroup = "BUILTIN\$($sqlAdministratorsGroup)";

  $defaultOptions = @{
    InstallTo = Combine-Path "$(Get-System-Drive)" "SQL";
    Password = "Meaga`$trong";
    Tcp = 1;
    NamedPipe = 1;
    SysAdmins = "$sqlAdministratorsGroup";
    Features = "SQLENGINE,FullText";
    Collation = ""; # Depends on System Language, todo: SQL_Latin1_General_CP1_CI_AS or SQL_Latin1_General_CP1_CI_AS
    DbDir = "";         #
    DbLogDir = "";      #
    TempDbDir = "";     # Using /Slahs optional overrides
    TempDbLogDir = ""   #
    BackupDir = "";     #
    Startup = "Automatic"; # Manual | Disabled
  }
  $options = $defaultOptions.Clone();
  # Apply $args
  $extraArguments=@();
  foreach($a in $optionsOverride) {
    try { $p="$a".IndexOf("="); $k="$a".SubString(0,$p); if (($p+1) -eq "$a".Length) { $v=""; } else { $v="$a".SubString($p+1); }} catch { $k=""; $v=""; }
    $v = "$v" -replace "{InstanceName}", $instanceName;
    if ("$k" -ne "" -and (-not "$k".StartsWith("/")) ) { 
      $options[$k] = $v; 
      Write-Host "Overridden option '$k' = `"$v`""; 
    } elseif ("$k" -ne "" -and "$k".StartsWith("/") ) { 
      $extraArguments += "$($k)=`"$v`""
      Write-Host "Overridden extra argument '$k' = `"$v`""; 
    }

  }

  $major = ($meta.Version.Substring(0,4)) -as [int];
  $is2020 = $major -ge 2016;
  $setupArg=@();
  $title = "SQL Server $($meta.Version) $($meta.MediaType) Setup"
  if ($major -eq 2005) {
    # SQL_Engine,SQL_Data_Files,SQL_Replication,SQL_FullText,SQL_SharedTools
    $argFeatures = IIf ($meta.MediaType -eq "Core") "SQL_Engine" "SQL_Engine,SQL_FullText";
    $argSQLAUTOSTART = IIf ($options.Startup -eq "Automatic") "1" "0"
    # /qb for unattended with basic UI
    
    $setupArg = "/qn", "ADDLOCAL=$argFeatures", "INSTANCENAME=`"$instanceName`"", 
         "DISABLENETWORKPROTOCOLS=0", # 0: All, 1: None, 2: TCP only
         "SECURITYMODE=SQL", "SAPWD=`"$($options.Password)`"", 
         "INSTALLSQLDIR=`"$($options.InstallTo)`"",
         "SQLAUTOSTART=$argSQLAUTOSTART";

    $setupStatus = Execute-Process-Smarty "SQL Server $($meta.Version) $($meta.MediaType) Setup" $meta.Launcher $setupArg -WaitTimeout 3600
    $setupStatus | Format-Table-Smarty | Out-Host
    $err=$setupStatus.Error;

    # Exec Patch
    if ($update) {
      $title = "SQL Server $($meta.Version) Upgrade to $($update.UpdateId)"
      $updateCommandLine = @("/QUIET", "/Action=Patch", "/InstanceName=$instanceName");
      $upgradeResult = Execute-Process-Smarty "$title" $update.UpdateLauncher $updateCommandLine
      $upgradeResult | Format-Table -AutoSize | Out-String -Width 256 | Out-Host
      if ($upgradeResult.Error) { $err += " " + $upgradeResult.Error; }
    }

    if ($err) { return @{ Error = $err }; }

    # Write-Host "Workaround for 2005 logs"; sleep 1; & taskkill.exe @("/t", "/f", "/im", "setup.exe");
  } else {
<# 2008
"%AppData%\Temp\%KEY%\Setup\Setup.exe" /Q /INDICATEPROGRESS /Action=Install ^
  /ADDCURRENTUSERASSQLADMIN ^
  /FEATURES=SQL ^
  /INSTANCENAME=%NEW_SQL_INSTANCE_NAME% ^
  /SECURITYMODE=SQL /SAPWD=`1qazxsw2 ^
  /SQLSVCACCOUNT="NT AUTHORITY\SYSTEM" ^
  /INSTANCEDIR="%SystemDrive%\SQL" ^
  /INSTALLSHAREDDIR="%SystemDrive%\SQL\x64b" ^
  /INSTALLSHAREDWOWDIR="%SystemDrive%\SQL\x86b" ^
  /SQLSYSADMINACCOUNTS="BUILTIN\ADMINISTRATORS" ^
  /TCPENABLED=1 /NPENABLED=1
  
#>

    $argFeatures = IIf ($meta.MediaType -eq "Advanced") "SQL_Engine,SQL_FullText" "SQL_Engine";
    $argQuiet = IIf ((Is-BuildServer) -or $meta.Version -like "2005*" -or $meta.Version -like "2008-*") "/Q" "/QUIETSIMPLE";
    $argProgress = "/INDICATEPROGRESS";
    $argProgress = "";
    $argADDCURRENTUSERASSQLADMIN = IIf ($meta.MediaType -eq "Developer") "" "/ADDCURRENTUSERASSQLADMIN";
    # 2008 and R2: The setting 'IACCEPTROPENLICENSETERMS' specified is not recognized.
    $argIACCEPTROPENLICENSETERMS = IIF ($major -le 2014) "" "/IACCEPTROPENLICENSETERMS";
    
    # 2008 & 2008 R2 Dev 10.50.6000.34: /AGTSVCACCOUNT="NT AUTHORITY\SYSTEM" required on Windows 2016+
    $argAGTSVCACCOUNT = IIF (($meta.Version -like "2008*") -and $meta.MediaType -eq "Developer") "/AGTSVCACCOUNT=`"NT AUTHORITY\SYSTEM`"" "";
    
    # 2008-xXX features
    $argFeatures = "$($options.Features)"
    if ($meta.Version -match "2008-") { 
      # For Developer use command line "Features=..."
      If ($meta.MediaType -eq "Core") { $argFeatures = "SQLENGINE" } 
      If ($meta.MediaType -eq "Advanced") { $argFeatures = "SQLENGINE,FULLTEXT" } 
    }

    $argENU = IIf ($meta.Version -match "2008-") "" "/ENU"
    $argIACCEPTSQLSERVERLICENSETERMS = IIf ($meta.Version -match "2008-") "" "/IAcceptSQLServerLicenseTerms"

    $hasUpdateSourceArgument = ($major -ge 2012);
    $argUpdateEnabled = IIF ([bool]"$update" -and $hasUpdateSourceArgument) "/UpdateEnabled=True" ""
    $argUpdateSource = If ("$update" -and $hasUpdateSourceArgument) { "/UpdateSource=`"$($update.UpdateFolder)`"" } else { "" };

    $argSQLCOLLATION = IIF ([bool]$options.Collation) "/SQLCOLLATION=`"$($options.Collation)`"" ""
    $argSQLSVCSTARTUPTYPE = IIF ([bool]$options.Startup) "/SQLSVCSTARTUPTYPE=`"$($options.Startup)`"" ""

    $argX86 = IIf ($meta.Version -match "2008-x86" -and $meta.MediaType -eq "Developer" -and (Get-CPU-Architecture-Suffix-for-Windows) -eq "x64") "/X86" "";

    # AddCurrentUserAsSQLAdmin can be used only by Express SKU or set using ROLE.
    $setupArg = "$argQuiet", "$argENU", "$argProgress", "/ACTION=Install",
    "/INSTANCENAME=`"$instanceName`"", 
    "$argX86",
    "/FEATURES=`"$argFeatures`"", 
    "$argIACCEPTSQLSERVERLICENSETERMS", "$argIACCEPTROPENLICENSETERMS", 
    "$argUpdateEnabled", "$argUpdateSource",
    "$argSQLCOLLATION",
    "/INSTANCEDIR=`"$($options.InstallTo)`"", 
    "/SECURITYMODE=`"SQL`"", 
    "/SAPWD=`"$($options.Password)`"", 
    "/SQLSVCACCOUNT=`"NT AUTHORITY\SYSTEM`"", "$argAGTSVCACCOUNT",
    "$argSQLSVCSTARTUPTYPE", 
    "/BROWSERSVCSTARTUPTYPE=AUTOMATIC",
    "$argADDCURRENTUSERASSQLADMIN", 
    "/SQLSYSADMINACCOUNTS=`"$($sqlAdministratorsGroup)`"",
    "/TCPENABLED=$($options.Tcp)", "/NPENABLED=$($options.NamedPipe)";

    # TODO: Remove Existing
    $setupArg += @($extraArguments)

    # Perform Setup, plus upgrade if 2012+
    $setupStatus = Execute-Process-Smarty "$title" $meta.Launcher $setupArg -WaitTimeout 3600
    $setupStatus | Format-Table-Smarty | Out-Host
    
    $err = $setupStatus.Error;

    if ("$update" -and (-not $hasUpdateSourceArgument)) {
      $title = "SQL Server $($meta.Version) Upgrade to $($update.UpdateId)"
      if ($meta.Version -like "2008R2*") { 
        Say "Starting $title"
        $updateCommandLine = @("/QUIET", "/IAcceptSQLServerLicenseTerms", "/Action=Patch", "/InstanceName=$instanceName"); # SP3 ok
        $upgradeResult = Execute-Process-Smarty "$title" $update.UpdateLauncher $updateCommandLine
        $upgradeResult | Format-Table -AutoSize | Out-String -Width 256 | Out-Host
      }
      else {
        # OK on "2008"
        Say "Starting $title"
        $updateCommandLine = @("/QUIET", "/Action=Patch", "/InstanceName=$instanceName");
        $upgradeResult = Execute-Process-Smarty "$title" $update.UpdateLauncher $updateCommandLine
        $upgradeResult | Format-Table -AutoSize | Out-String -Width 256 | Out-Host
      }

      if ($upgradeResult -and $upgradeResult.Error) { $err += " " + $upgradeResult.Error; }
      if ($err) { return @{ Error = $err }; }
    }
  }
}

# Include File: [\Includes.SqlServer\Invoke-LocalDB-Executable.ps1]
# Version: Latest | 16 | 15 | 14 | 13 | 12 | 11 (16.0, 15.0, ... also supported)
function Invoke-LocalDB-Executable([string] $title, [string] $version, [string[]] $parameters) {
  $localDbList = Find-LocalDb-SqlServers
  $exe = $localDbList | Select -First 1 | % { $_.Exe }
  if ($version) { 
    $exe2 = $localDbList | ? { $_.ShortVersion -like "$version*" } | Select -First 1 | % { $_.Exe }
    if ($exe2) { $exe = $exe2 }
  }
  if ($exe) {
    $pars = @($parameters);
    $pars += @($args);
    Troubleshoot-Info "[$title] `"$exe`" $($pars -join " ")"
    & "$exe" @($pars) | Out-Host
    return $?
  } else {
    Write-Line -TextDarkRed "SQLLocalDB.Exe Not Found for version `"$version`""
  }
  return $false
}


# Include File: [\Includes.SqlServer\Invoke-SqlServer-Command.ps1]
function Invoke-SqlServer-Command([string] $title, [string] $connectionString, <# or #>[string] $instance, [string] $sqlCommand, [int] $timeoutSec = 30) {
  if (-not $connectionString) { $connectionString = "Server=$($instance);Integrated Security=SSPI;Connection Timeout=10;Pooling=False" }
  $startAt = [System.Diagnostics.Stopwatch]::StartNew();
  $exception = $null;

  try { 
    $con = New-Object System.Data.SqlClient.SqlConnection($connectionString);
    $con.Open();
    
    $cmd = new-object System.Data.SqlClient.SqlCommand($sqlCommand, $con)
    $cmd.CommandTimeout = $timeoutSec * 1000;
    $__ = $cmd.ExecuteNonQuery();
    $cmd.Dispose();

    $con.Close()
    return;
  } catch { $exception = $_.Exception; <# Write-Host $_.Exception -ForegroundColor DarkGray #> }

  Write-Host "Warning! Can't invoke SQL Command on SQL Server '$($title)' during $($startAt.ElapsedMilliseconds / 1000) seconds$([Environment]::NewLine)$($exception)" -ForegroundColor DarkRed
}

# Invoke-SqlServer-Command -Title "MSSQLSERVER" -Instance "(local)" -Options @{ xp_cmdshell = $true; "clr enabled" = $false; "server trigger recursion" = $true; "min server memory (MB)" = 160; "max server memory (MB)" = 4096 }

# Include File: [\Includes.SqlServer\Is-SqlServer-Setup-Cache-Enabled.ps1]
function Is-SqlServer-Setup-Cache-Enabled() { $false; }
# Include File: [\Includes.SqlServer\ParseNonEmptyTrimmedLines.ps1]
# return array of strings
function ParseNonEmptyTrimmedLines([string] $raw) {
  foreach($line in "$raw".Split([char] 13, [char] 10)) {
    $l = "$line".Trim()
    if ($l.Length -gt 0) { $l }
  }
}
# ParseNonEmptyTrimmedLines "`r`n1`r2`n3`r`n`r`n"

# Include File: [\Includes.SqlServer\Parse-SqlServers-Input.ps1]
function Parse-SqlServers-Input { param( [string] $list)
    # Say "Installing SQL Server(s) by tags: $list"
    $rawServerList = "$list".Split(@([char]44, [char]59));
    foreach($sqlDef in $rawServerList) {
        $arr = $sqlDef.Split(@([char]58));
        $sqlKey = "$($arr[0])".Trim();
        if ($arr.Length -gt 1) { $instanceName=$arr[1].Trim(); } else {  $instanceName=$null; }
        $tags=@("$sqlKey".Split([char]32) | % {$_.Trim()} | where { $_.Length -gt 0 } )
        if ($tags.Count -gt 0) {
            $version = "$($tags[0])";
            try { $major = $version.Substring(0,4) -as [int] } catch {}
            $versionHasBits = ($version -like "*-x86" -or $version -like "*-x64")
            if ((-not $versionHasBits) -and ($major -le 2014)) { 
              $version += IIF ($major -eq 2005) "-x86" "-$(Get-CPU-Architecture-Suffix-for-Windows)"; 
            }
            $missingDeveloper = ($version -like "2005*" -or $version -like "2014-x86" -or $version -like "2008-*");
            $mediaType = IIF ($tags.Count -ge 2) $tags[1] (IIF $missingDeveloper "Advanced" "Developer")
            $rawUpdate = IIF ($tags.Count -ge 3) $tags[2] ""

            if ($instanceName -eq $null -and $mediaType -ne "LocalDB") {
              $instanceName = $mediaType.Substring(0,3).ToUpper() + "_" + $version.Replace("-", "_");
            }
            if ($instanceName -ne $null) {
              $instanceName = "$($instanceName.ToUpper())"
            }

            $normalizedMeta = Find-SQLServer-Meta $version $mediaType
            if (-not $normalizedMeta) {
              Write-Warning "Unknwon SQL Server version $version mediatype $mediaType" 
            }
            else {
              # Version and MediaType are clarified.
              # First (latest) update if not found
              if ($rawUpdate) {
                $update = $normalizedMeta.CU | ? { $_.Id -eq $rawUpdate } | Select -First 1;
                if (-not $update) { $update = $normalizedMeta.CU | Select -First 1; }
                $updateId = IIF ([bool]"$update") $update.Id $null;
              } else {
                $updateId = $null;
                $update = $null;
              }

              @{Version = $version; MediaType = $mediaType; UpdateId = "$updateId"; Update = $update; InstanceName = $instanceName; Definition=$sqlKey }
            }
        }
    }
}
<#
  2014 --> 2014-x64 Developer on x64,
#>

# Include File: [\Includes.SqlServer\Publish-SQLServer-SetupLogs.ps1]
function Publish-SQLServer-SetupLogs([string] $toFolder, $compression=9) {
  New-Item -ItemType Directory -Path "$toFolder" -EA SilentlyContinue | out-null
  $folders = Find-SqlServer-SetupLogs
  $sevenZip = Get-Full7z-Exe-FullPath-for-Windows -Version "1900"
  foreach($logsFolder in $folders) {
    $archiveName=$logsFolder.Substring([System.IO.Path]::GetPathRoot($logsFolder).Length).Replace("\", ([char]8594).ToString())
    Say "Pack '$logsFolder' as `"$toFolder\$archiveName.7z`""
    & "$sevenZip" @("a", "-y", "-mx=$compression", "-ms=on", "-mqs=on", "$toFolder\$archiveName.7z", "$logsFolder\*") | out-null
    if (-not $?) {
      Write-Host "Failed publishing '$archiveName' to folder '$toFolder'" -ForegroundColor DarkRed
      # return $false;
    }
    # return $true;
  }
}

# Include File: [\Includes.SqlServer\Query-SqlServer-Version.ps1]
function Query-SqlServer-Version([string] $title, [string] $connectionString, <# or #>[string] $instance, [int] $timeoutSec = 30) {
  if (-not $connectionString) { $connectionString = "Server=$($instance);Integrated Security=SSPI;Connection Timeout=2;Pooling=False" }
  $startAt = [System.Diagnostics.Stopwatch]::StartNew();
  $exception = $null;
  do {
    try { 
      $sql = @"
SELECT
Cast(ISNULL(ServerProperty('ProductVersion'), '') as nvarchar) + ' ' +
(Case ServerProperty('IsLocalDB') When 1 Then 'LocalDB' Else '' End) + ' ' +
Cast(ISNULL(ServerProperty('Edition'), '') as nvarchar) + ' ' +
Cast(ISNULL(ServerProperty('ProductLevel'), '') as nvarchar) + ' ' +
Cast(ISNULL(ServerProperty('ProductUpdateLevel'), '') as nvarchar) +
(Case ServerProperty('IsFullTextInstalled') When 1 Then ' + Full-text' Else '' End);
"@
;
      $con = New-Object System.Data.SqlClient.SqlConnection($connectionString);
      $con.Open();
      $cmd = new-object System.Data.SqlClient.SqlCommand($sql, $con)
      $cmd.CommandTimeout = 3;
      $rdr = $cmd.ExecuteReader()
      $__ = $rdr.Read()
      $ret = "$($rdr.GetString(0))"
      $ret = $ret.Trim().Replace(" ", " ").Replace(" ", " ").Replace(" ", " ")
      $con.Close()
      return $ret;
    } catch { $exception = $_.Exception; <# Write-Host $_.Exception -ForegroundColor DarkGray #> }
  } while($startAt.ElapsedMilliseconds -le ($timeoutSec * 1000));
  Write-Host "Warning! Can't query version of SQL Server '$($title)' during $($startAt.ElapsedMilliseconds / 1000) seconds$([Environment]::NewLine)$($exception)" -ForegroundColor DarkRed

}

# Query-SqlServer-Version -Title "FAKE" -Instance "(local)\22" -Timeout 2
# Query-SqlServer-Version -Title "SQL 2005" -Instance "(local)\SQL_2005_SP4_X86" -Timeout 2

# Include File: [\Includes.SqlServer\Set-SqlServer-Database-Files-Size.ps1]
# TODO:
function Set-SqlServer-Database-Files-Size([string] $title, [string] $connectionString, <# or #>[string] $instance, [string] $dbName, [string] $dataSize, [string] $dataGrow, [string] $logSize, [string] $logGrow, $timeout = 30)
{
  $sql = @"
Declare @type tinyint; Declare @name sysname; Declare @sql nvarchar(4000);
Declare @newSize varchar(1000); Declare @newGrow varchar(1000);
DECLARE FilesCursor Cursor Static FOR SELECT type, name FROM [$dbName].sys.database_files Where type in (0,1);
Open FilesCursor
While 1=1
Begin
  Fetch Next From FilesCursor Into @type, @name;
  If @@FETCH_STATUS <> 0 Break;
  If @type = 0 Set @newSize = '$dataSize' Else Set @newGrow = '$dataGrow';
  If @type = 1 Set @newSize = '$logSize' Else Set @newGrow = '$logGrow';
  Set @sql = 'ALTER DATABASE [$dbName] MODIFY FILE ( NAME = N''' + @name + ''', SIZE = ' + @newSize + ', FILEGROWTH = ' + @newGrow + ' );';
  -- Print @sql;
  exec (@sql);
End
Close FilesCursor;
Deallocate FilesCursor;
"@
;

}

<#
USE [master]
GO
ALTER DATABASE [tempdb] MODIFY FILE ( NAME = N'tempdev', SIZE = 23552KB , FILEGROWTH = 10%)
GO
ALTER DATABASE [tempdb] MODIFY FILE ( NAME = N'templog', SIZE = 9216KB , FILEGROWTH = 131072KB )
GO
 
 
USE [master]
GO
ALTER DATABASE [tempdb] MODIFY FILE ( NAME = N'temp3', FILEGROWTH = 11%)
GO
ALTER DATABASE [tempdb] MODIFY FILE ( NAME = N'temp6', FILEGROWTH = 12%)
GO
ALTER DATABASE [tempdb] MODIFY FILE ( NAME = N'temp8', FILEGROWTH = 131072KB )
GO
ALTER DATABASE [tempdb] MODIFY FILE ( NAME = N'tempdev', SIZE = 716800KB )
GO
#>


# Include File: [\Includes.SqlServer\Set-SQLServer-Options.ps1]
function Set-SQLServer-Options([string] $title, [string] $connectionString, <# or #>[string] $instance, [hashtable] $options, [int] $timeoutSec = 30) {
  if (-not $connectionString) { $connectionString = "Server=$($instance);Integrated Security=SSPI;Connection Timeout=10;Pooling=False" }
  $startAt = [System.Diagnostics.Stopwatch]::StartNew();
  $exception = $null;
  $keys = @($options | % { $_.Keys })
  $sqlCommands = @("exec sp_configure 'show advanced option', 1", "reconfigure with override;");
  foreach($key in $keys) { 
    $val = $options[$key];
    if ($val -is [bool]) { if ($val) { $val = 1 } else { $val = 0 } }
    $sqlCommands += "exec sp_configure '$key', $val"
  }
  $sqlCommands += "reconfigure with override;";

  do {
    try { 
      $con = New-Object System.Data.SqlClient.SqlConnection($connectionString);
      $con.Open();
      
      Write-Host "Connection to SQL Server $title is Ready for Configuration"
      foreach($sqlCommand in $sqlCommands) {
        $cmd = new-object System.Data.SqlClient.SqlCommand($sqlCommand, $con)
        $cmd.CommandTimeout = 30;
        try { 
          $__ = $cmd.ExecuteNonQuery();
          Write-Host " ok: `"$sqlCommand`""
        } catch {
          Write-Host " fail: `"$sqlCommand`". $($_.Exception.Message)"
        }
        $cmd.Dispose();
      }
      $con.Close()
      return;
    } catch { $exception = $_.Exception; <# Write-Host $_.Exception -ForegroundColor DarkGray #> }
  } while($startAt.ElapsedMilliseconds -le ($timeoutSec * 1000));
  Write-Host "Warning! Can't apply configuration for SQL Server '$($title)' during $($startAt.ElapsedMilliseconds / 1000) seconds$([Environment]::NewLine)$($exception)" -ForegroundColor DarkRed
}

# Set-SQLServer-Options -Title "MSSQLSERVER" -Instance "(local)" -Options @{ xp_cmdshell = $true; "clr enabled" = $false; "server trigger recursion" = $true; "min server memory (MB)" = 160; "max server memory (MB)" = 4096 }

# Include File: [\Includes.SqlServer\Setup-SqlServers.ps1]
function Setup-SqlServers() {
Param(
  [string] $sqlServers,
  [string[]] $optionsOverride = @()
)
$optionsOverride += @($args)

<#
TODO:
  MaxRam,
  MinRam
#>


   Say "Setting up SQL Server(s) `"$sqlServers`". Cpu is '$(Get-Cpu-Name)'. $((Get-Memory-Info).Description)"
   $errors = @();

   $servers = Parse-SqlServers-Input $sqlServers
   $servers | Format-Table -AutoSize | Out-String -Width 256 | Out-Host
   $jsonReport = @();
   foreach($server in $servers) {
     $startAt = [System.Diagnostics.Stopwatch]::StartNew()
     Say "Downloading SQL Server '$($server.Version) $($server.MediaType)'"
     $setupMeta = Download-SQLServer-and-Extract $server.Version $server.MediaType;
     $setupMeta | Format-Table -AutoSize | Out-String -Width 256 | Out-Host
     $resultGetUpdate = $null;
     if ($server.UpdateId) {
       Say "Downloading SQL Server Update $($server.UpdateId) for version '$($server.Version) $($server.MediaType)'"
       $resultGetUpdate = Download-SqlServer-Update $server.Version $server.MediaType $server.Update;
       $resultGetUpdate | Format-Table -AutoSize | Out-String -Width 256 | Out-Host
     }
     $secondsDownload = $startAt.ElapsedMilliseconds / 1000.0;
     $startAt = [System.Diagnostics.Stopwatch]::StartNew()
     Say "Installing $($server.Version) $($server.MediaType)"
     $installStatus = Install-SQLServer $setupMeta $resultGetUpdate $server.InstanceName @($optionsOverride);
     Say "SQL Server '$($server.Definition)' Setup Finished. $((Get-Memory-Info).Description)"
     $secondsInstall = $startAt.ElapsedMilliseconds / 1000.0;
     $jsonReport += @{ Definition=$server.Definition; Version=$server.Version; MediaType=$server.MediaType; SecondsDownload = $secondsDownload; SecondsInstall = $secondsInstall; Cpu = $cpuName; }
     if ($installStatus -and $installStatus.Error) { $errors += "SQL Server '$($server.Definition)' Setup failed. $($installStatus.Error)"; }
   }

   if ("$($ENV:DEBUG_LOG_FOLDER)")
   {
      $reportJsonFullName = "$($ENV:DEBUG_LOG_FOLDER)\SQL Setup Benchmark.json"
      Create-Directory-for-File $reportJsonFullName
      $jsonReport | ConvertTo-Json | Out-File $reportJsonFullName -Force
   }

   return $errors;
}

# Include File: [\Includes.SqlServer\Try-Get-FileName-by-Uri.ps1]
function Try-Get-FileName-by-Uri ([string] $url) {
  try { $uri = New-Object System.Uri $url; } catch { return $null; }
  $path = $uri.AbsolutePath;
  $parts = $path.Split("/");
  $ret = $parts | Select -Last 1;
  if ("$ret" -eq "download") { 
    $ret = $parts | Select -Last 2 | Select -First 1;
  }
  return "$ret";
}

function Try-Get-FileExtension-by-Uri ([string] $url) {
  $name = Try-Get-FileName-by-Uri $url;
  if ($name -eq $null) { return $null; }
  return ([System.IO.Path]::GetExtension($name));
}

# Include File: [\Includes.SqlServer\Uninstall-LocalDB-List.ps1]
function Uninstall-LocalDB-List([string[]] $patterns) {
  # $patterns
  # "*"
  # "2016", "2017"
  $localDbList = @(Get-Speedy-Software-Product-List | ? { $_.Name -match "LocalDB" -and $_.Vendor -match "Microsoft" })
  $names = @($localDbList | % { "$($_.Name)".Trim() })
  Write-Host "Uninstalling Microsoft LocalDB by patterns '$patterns'"
  Write-Host "Total LocalDB Installed: $($localDbList.Count), $names"

  $total = 0;
  foreach($localDb in $localDbList) {
    $isMatch = "$patterns" | ? { if ($_ -eq "*") { return $true }; $localDB.Name -match $_ };
    if ($isMatch) {
      Say "UNINSTALL '$($localDB.Name)'"
      $result = Execute-Process-Smarty "LocalDB Remover for '$($localDB.Name)'" "msiexec.exe" @("/qn", "/x", "$($localDB.IdentifyingNumber)", "/norestart");
      $result | Format-Table-Smarty | Out-Host
      $total++;
    }
  }
  Say "DONE. Total LocalDB Uninstalled: $total"
}



Export-ModuleMember -Function Parse-SqlServers-Input
Export-ModuleMember -Function Setup-SqlServers
Export-ModuleMember -Function Query-SqlServer-Version
Export-ModuleMember -Function Uninstall-LocalDB-List
Export-ModuleMember -Function Set-SQLServer-Options
Export-ModuleMember -Function Bootstrap-Aria2-If-Required
Export-ModuleMember -Function Enumerate-Plain-SQLServer-Downloads
Export-ModuleMember -Function Enumerate-SQLServer-Downloads
Export-ModuleMember -Function Invoke-SqlServer-Command
Export-ModuleMember -Function Find-Local-SqlServers
Export-ModuleMember -Function Say
Export-ModuleMember -Function Troubleshoot-Info
Export-ModuleMember -Function Write-Line
Export-ModuleMember -Function Set-SqlServer-Database-Files-Size

# This works with all the versions and all instances
Export-ModuleMember -Function Find-LocalDb-SqlServers
Export-ModuleMember -Function Create-LocalDB-Instance
Export-ModuleMember -Function Invoke-LocalDB-Executable

# This works with SQLLocalDB|v11.0 instance of the latest version only
Export-ModuleMember -Function Find-LocalDb-SqlServer
Export-ModuleMember -Function Start-LocalDb-SqlServer


Export-ModuleMember -Function Out-String-And-TrimEnd
Export-ModuleMember -Function Append-All-Text
Export-ModuleMember -Function Combine-Path
Export-ModuleMember -Function Create-Directory
Export-ModuleMember -Function Create-Directory-for-File
Export-ModuleMember -Function Download-2010-SQLServer-and-Extract
Export-ModuleMember -Function Download-And-Install-Specific-VC-Runtime
Export-ModuleMember -Function Download-File-FailFree
Export-ModuleMember -Function Download-File-FailFree-and-Cached
Export-ModuleMember -Function Download-File-Managed
Export-ModuleMember -Function Download-Fresh-SQLServer-and-Extract
Export-ModuleMember -Function Download-SQLServer-and-Extract
Export-ModuleMember -Function Download-Specific-VC-Runtime
Export-ModuleMember -Function Download-SqlServer-Update
Export-ModuleMember -Function Execute-Process-Smarty
Export-ModuleMember -Function Extract-Archive-by-Default-Full-7z
Export-ModuleMember -Function ExtractArchiveBy7zMini
Export-ModuleMember -Function ExtractArchiveByDefault7zFull
Export-ModuleMember -Function ExtractSqlServerSetup
Export-ModuleMember -Function Find-SQLServer-Meta
Export-ModuleMember -Function Find-SqlServer-SetupLogs
Export-ModuleMember -Function Format-Table-Smarty
Export-ModuleMember -Function Get-Aria2c-Exe-FullPath-for-Windows
Export-ModuleMember -Function Get-Builtin-Windows-Group-Name
Export-ModuleMember -Function Get-CPU-Architecture-Suffix-for-Windows
Export-ModuleMember -Function Get-Cpu-Name
Export-ModuleMember -Function Get-Folder-Size
Export-ModuleMember -Function Get-Full7z-Exe-FullPath-for-Windows
Export-ModuleMember -Function Get-Github-Latest-Release
Export-ModuleMember -Function Get-Github-Releases
Export-ModuleMember -Function Get-Installed-VC-Runtimes
Export-ModuleMember -Function Get-Memory-Info
Export-ModuleMember -Function Get-Mini7z-Exe-FullPath-for-Windows
Export-ModuleMember -Function Get-Nix-Uname-Value
Export-ModuleMember -Function Get-Os-Platform
Export-ModuleMember -Function Get-PS1-Repo-Downloads-Folder
Export-ModuleMember -Function Get-Random-Free-Port
Export-ModuleMember -Function Get-Smarty-FileHash
Export-ModuleMember -Function Get-Smarty-FolderHash
Export-ModuleMember -Function Get-Speedy-Software-Product-List
Export-ModuleMember -Function Get-SqlServer-Media-Folder
Export-ModuleMember -Function Get-SqlServer-Setup-Folder
Export-ModuleMember -Function Get-System-Drive
Export-ModuleMember -Function Get-Windows-Release-Id
Export-ModuleMember -Function GetPersistentTempFolder
Export-ModuleMember -Function Has-Cmd
Export-ModuleMember -Function IIf
Export-ModuleMember -Function Install-SQLServer
Export-ModuleMember -Function Is-Ansi-Supported
Export-ModuleMember -Function Is-BuildServer
Export-ModuleMember -Function Is-File-Not-Empty
Export-ModuleMember -Function Is-Intel-Emulation-Available
Export-ModuleMember -Function Is-SqlServer-Setup-Cache-Enabled
Export-ModuleMember -Function Is-Vc-Runtime-Installed
Export-ModuleMember -Function Measure-Action
Export-ModuleMember -Function ParseNonEmptyTrimmedLines
Export-ModuleMember -Function Publish-SQLServer-SetupLogs
Export-ModuleMember -Function Reverse-Pipe
Export-ModuleMember -Function Select-WMI-Objects
Export-ModuleMember -Function Remove-Windows-Service-If-Exists
Export-ModuleMember -Function Start-LocalDb-SqlServer
Export-ModuleMember -Function Start-Stopwatch
Export-ModuleMember -Function To-Boolean
Export-ModuleMember -Function To-Sortable-Version-String
Export-ModuleMember -Function Try-BuildServerType
Export-ModuleMember -Function Try-Get-FileExtension-by-Uri
Export-ModuleMember -Function Try-Get-FileName-by-Uri
Export-ModuleMember -Function Write-All-Text
Export-ModuleMember -Function Try-And-Retry

Export-ModuleMember -Function Get-Windows-Power-Plans
Export-ModuleMember -Function Get-Windows-Active-Power-Plan
Export-ModuleMember -Function Get-Windows-Active-Power-Plan-Name
Export-ModuleMember -Function Set-Windows-Power-Plan-by-Name


# Variables are exported for tests only. Should be used directly
Export-ModuleMember -Variable "SqlServerDownloadLinks"
Export-ModuleMember -Variable "SqlServer2010DownloadLinks"
Export-ModuleMember -Variable "SqlServerAlreadyUpdatedList"

Export-ModuleMember -Variable "Full7zLinksMetadata_onWindows"
Export-ModuleMember -Variable "VcRuntimeLinksMetadata"