$env:DS_GRIDDESIGN_CHANNEL='stable' # Public bootstrap. The installed manager owns its private directory and UI. $ErrorActionPreference='Stop' $ProgressPreference='SilentlyContinue' Set-StrictMode -Version 2 function ManagerFail([string]$Code){throw $Code} function ManagerFields($Object,[string[]]$Expected){ $actual=@($Object.PSObject.Properties.Name | Sort-Object) $wanted=@($Expected | Sort-Object) if(($actual -join "`n") -cne ($wanted -join "`n")){ManagerFail 'manager_metadata_invalid'} } function ManagerDownload([string]$Url,[string]$Path,[long]$Limit){ $response=$null;$input=$null;$output=$null try{ $response=$script:ManagerHttp.GetAsync($Url,[Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() if(!$response.IsSuccessStatusCode -or [long]$response.Content.Headers.ContentLength -gt $Limit){ManagerFail 'manager_download_failed'} $input=$response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() $output=[IO.File]::Open($Path,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None) $buffer=New-Object byte[] 16384;$total=0L while(($count=$input.Read($buffer,0,$buffer.Length)) -gt 0){$total+=$count;if($total -gt $Limit){ManagerFail 'manager_download_too_large'};$output.Write($buffer,0,$count)} if(!$total){ManagerFail 'manager_download_empty'} }finally{if($output){$output.Dispose()};if($input){$input.Dispose()};if($response){$response.Dispose()}} } function ManagerReadJson([string]$Path,[long]$Limit=65536){ $item=Get-Item -LiteralPath $Path -Force if($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $item.Length -gt $Limit){ManagerFail 'manager_metadata_invalid'} [IO.File]::ReadAllText($Path) | ConvertFrom-Json } function ManagerPublication($Manifest,[string]$Channel){ ManagerFields $Manifest @('schema','channel','product_id','published_at','platforms') $product='rw.datasolutions.installation-manager';if($Channel -eq 'canary'){$product+='.canary'} if($Manifest.schema -cne 'ds.installation-manager-publication/v1' -or $Manifest.channel -cne $Channel -or $Manifest.product_id -cne $product){ManagerFail 'manager_publication_invalid'} if($Manifest.published_at -isnot [string] -or $Manifest.published_at -cnotmatch 'Z$'){ManagerFail 'manager_publication_invalid'} $null=[DateTimeOffset]::Parse($Manifest.published_at,[Globalization.CultureInfo]::InvariantCulture) ManagerFields $Manifest.platforms @('windows','linux') $unit=$Manifest.platforms.windows ManagerFields $unit @('arch','package_receipt_sha256','source_revisions','assets','bootstrap') if($unit.arch -cne 'x86_64' -or $unit.package_receipt_sha256 -cnotmatch '^[a-f0-9]{64}$'){ManagerFail 'manager_publication_invalid'} ManagerFields $unit.source_revisions @('ds-cli','ds-web') foreach($revision in @($unit.source_revisions.'ds-cli',$unit.source_revisions.'ds-web')){if($revision -cnotmatch '^[a-f0-9]{40}$'){ManagerFail 'manager_source_invalid'}} $names=@('package.receipt.json','ds-install-manager.ps1','install-ds-core-windows.ps1');$seen=@{} if(@($unit.assets).Count -ne $names.Count){ManagerFail 'manager_publication_invalid'} foreach($asset in $unit.assets){ ManagerFields $asset @('filename','size_bytes','sha256','path') if($asset.filename -cnotin $names -or $seen.ContainsKey($asset.filename) -or $asset.sha256 -cnotmatch '^[a-f0-9]{64}$' -or $asset.size_bytes -is [bool] -or $asset.size_bytes -isnot [ValueType] -or [long]$asset.size_bytes -ne $asset.size_bytes -or $asset.size_bytes -le 0 -or $asset.size_bytes -gt 131072){ManagerFail 'manager_asset_invalid'} $seen[$asset.filename]=$true if($asset.path -cne "/installation-manager/$($unit.package_receipt_sha256)/$($asset.filename)"){ManagerFail 'manager_asset_invalid'} if($asset.filename -ceq 'package.receipt.json' -and ($asset.sha256 -cne $unit.package_receipt_sha256 -or $asset.size_bytes -gt 65536)){ManagerFail 'manager_receipt_invalid'} } $bootstrap=$unit.bootstrap;ManagerFields $bootstrap @('filename','path','sha256','size_bytes') if($bootstrap.filename -cne 'install-manager.ps1' -or $bootstrap.sha256 -cnotmatch '^[a-f0-9]{64}$' -or $bootstrap.size_bytes -le 0 -or $bootstrap.size_bytes -gt 131072 -or $bootstrap.path -cne "/installation-manager/$($bootstrap.sha256)/install-manager.ps1"){ManagerFail 'manager_bootstrap_invalid'} return $unit } function ManagerVerifyPackage($Unit,[string]$Directory){ foreach($asset in $Unit.assets){ $path=Join-Path $Directory $asset.filename;$item=Get-Item -LiteralPath $path -Force if($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $item.Length -ne $asset.size_bytes -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -cne $asset.sha256){ManagerFail 'manager_package_invalid'} } $receipt=ManagerReadJson (Join-Path $Directory 'package.receipt.json') ManagerFields $receipt @('schema','platform','arch','source_sha','web_source_sha','files') if($receipt.schema -cne 'ds.installation-manager-package/v1' -or $receipt.platform -cne 'windows' -or $receipt.arch -cne 'x86_64' -or $receipt.source_sha -cne $Unit.source_revisions.'ds-cli' -or $receipt.web_source_sha -cne $Unit.source_revisions.'ds-web'){ManagerFail 'manager_package_source_invalid'} $members=@($Unit.assets | Where-Object {$_.filename -cne 'package.receipt.json'});$seen=@{} if(@($receipt.files).Count -ne $members.Count){ManagerFail 'manager_package_invalid'} foreach($member in $receipt.files){ ManagerFields $member @('filename','size_bytes','sha256') $match=@($members | Where-Object {$_.filename -ceq $member.filename}) if($match.Count -ne 1 -or $seen.ContainsKey($member.filename) -or $match[0].size_bytes -ne $member.size_bytes -or $match[0].sha256 -cne $member.sha256){ManagerFail 'manager_package_invalid'} $seen[$member.filename]=$true } } if($env:OS -ne 'Windows_NT' -or ![Environment]::Is64BitProcess){ManagerFail 'manager_platform_unavailable'} if(-not ('DsManagerBootstrapPlatform' -as [type])){ Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public static class DsManagerBootstrapPlatform { [StructLayout(LayoutKind.Sequential)] private struct Info { public ushort arch, reserved; public uint page; public IntPtr minimum, maximum; public UIntPtr mask; public uint processors, type, granularity; public ushort level, revision; } [DllImport("kernel32.dll")] private static extern void GetNativeSystemInfo(out Info info); public static bool IsX64() { Info info; GetNativeSystemInfo(out info); return info.arch == 9; } } '@ } if(![DsManagerBootstrapPlatform]::IsX64()){ManagerFail 'manager_platform_unavailable'} $channel=if($env:DS_GRIDDESIGN_CHANNEL){$env:DS_GRIDDESIGN_CHANNEL}else{'stable'} if($channel -cnotin @('stable','canary')){ManagerFail 'manager_lane_invalid'} $component=if($env:DS_INSTALLATION_MANAGER_COMPONENT){$env:DS_INSTALLATION_MANAGER_COMPONENT}else{'desktop'} if($component -cnotin @('desktop','core')){ManagerFail 'manager_component_invalid'} $origin=if($channel -eq 'canary'){'https://electrification-rural.web.app'}else{'https://datasolutions.rw'} Add-Type -AssemblyName System.Net.Http $handler=New-Object Net.Http.HttpClientHandler;$handler.AllowAutoRedirect=$false $script:ManagerHttp=New-Object Net.Http.HttpClient($handler) $script:ManagerHttp.Timeout=[TimeSpan]::FromSeconds(60) $scratch=Join-Path ([IO.Path]::GetTempPath()) ('ds-manager-bootstrap-'+[Guid]::NewGuid().ToString('N')) [void][IO.Directory]::CreateDirectory($scratch) try{ $publication=Join-Path $scratch 'publication.json' ManagerDownload "$origin/installation-manager/$channel.json" $publication 65536 $unit=ManagerPublication (ManagerReadJson $publication) $channel foreach($asset in $unit.assets){ManagerDownload ($origin+$asset.path) (Join-Path $scratch $asset.filename) $asset.size_bytes} ManagerVerifyPackage $unit $scratch & (Join-Path $scratch 'ds-install-manager.ps1') -Action Setup -Channel $channel -Yes if(!$?){ManagerFail 'manager_setup_failed'} $installed=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) "Programs\DS Installation Manager\$channel\versions\$($unit.package_receipt_sha256)\ds-install-manager.ps1" if($component -eq 'core'){& $installed -Action Install -Component Core -Channel $channel -Yes} else{& $installed -UI -Channel $channel} if(!$?){ManagerFail 'manager_execution_failed'} }finally{ $script:ManagerHttp.Dispose();$handler.Dispose() # Only this newly created, resolved scratch directory is disposable. $resolved=[IO.Path]::GetFullPath($scratch);$parent=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()) if($resolved.StartsWith($parent,[StringComparison]::OrdinalIgnoreCase) -and [IO.Path]::GetFileName($resolved) -like 'ds-manager-bootstrap-*'){ Remove-Item -LiteralPath $resolved -Recurse -Force } }