[CmdletBinding()] param( [string]$Destination = 'C:\NODEDC\dc-amd-connector', [string]$NasAddress = '172.22.0.222', [string]$BindAddress = '172.22.0.183', [int]$Port = 8791, [bool]$EnableAutoStart = $true ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Require-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this installer from an elevated PowerShell window.' } } function Test-PackageChecksums([string]$Root) { $manifest = Join-Path $Root 'SHA256SUMS' if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) { throw "Checksum manifest not found: $manifest" } foreach ($line in Get-Content -LiteralPath $manifest) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $parts = $line -split '\s{2,}', 2 if ($parts.Count -ne 2 -or $parts[0] -notmatch '^[a-f0-9]{64}$') { throw "Invalid checksum line: $line" } $relative = $parts[1] -replace '^\./', '' $file = Join-Path $Root $relative $actual = (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $parts[0]) { throw "Checksum mismatch: $relative" } } } function Get-DockerDesktopStartCommand([string]$DockerCliPath) { & $DockerCliPath desktop start --help 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { return "`"$DockerCliPath`" desktop start --detach" } $dockerRoot = Split-Path (Split-Path (Split-Path $DockerCliPath -Parent) -Parent) -Parent $candidates = @( (Join-Path $dockerRoot 'Docker Desktop.exe'), (Join-Path $env:ProgramFiles 'Docker\Docker\Docker Desktop.exe'), (Join-Path $env:LOCALAPPDATA 'Programs\Docker\Docker\Docker Desktop.exe') ) | Select-Object -Unique $desktopExe = $candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 if (-not $desktopExe) { throw 'Unable to find Docker Desktop start command or Docker Desktop.exe for restart recovery.' } return "start `"`" `"$desktopExe`"" } function New-DockerDesktopAutoStart([string]$StartCommand) { $startup = [Environment]::GetFolderPath([Environment+SpecialFolder]::Startup) if ([string]::IsNullOrWhiteSpace($startup)) { throw 'Unable to resolve the current user Startup folder.' } $launcher = Join-Path $startup 'NODE.DC DC AMD Connector - Docker Desktop.cmd' if (Test-Path -LiteralPath $launcher) { throw "Docker Desktop auto-start launcher already exists: $launcher" } @( '@echo off', 'rem NODE.DC DC AMD Connector - starts Docker Desktop after this user signs in.', $StartCommand ) | Set-Content -LiteralPath $launcher -Encoding ascii return $launcher } Require-Administrator $source = Split-Path -Parent $PSCommandPath if (-not (Test-Path -LiteralPath (Join-Path $source 'VERSION') -PathType Leaf)) { throw 'Run install.ps1 from the DC AMD Connector package folder.' } Test-PackageChecksums $source if (Test-Path -LiteralPath $Destination) { throw "Destination already exists; refusing to overwrite: $Destination" } $null = [Net.IPAddress]::Parse($NasAddress) $null = [Net.IPAddress]::Parse($BindAddress) if ($Port -lt 1 -or $Port -gt 65535) { throw "Port must be between 1 and 65535: $Port" } $dockerCli = (Get-Command docker -CommandType Application -ErrorAction Stop | Select-Object -First 1).Path if ([string]::IsNullOrWhiteSpace($dockerCli) -or -not (Test-Path -LiteralPath $dockerCli -PathType Leaf)) { throw 'Docker CLI executable could not be resolved.' } & $dockerCli version --format '{{.Server.Os}}/{{.Server.Arch}} {{.Server.Version}}' | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Docker Desktop engine is not available. Start it and wait for Engine running.' } $dockerDesktopStartCommand = Get-DockerDesktopStartCommand $dockerCli $destinationCreated = $false $firewallCreated = $false $autoStartLauncher = $null $parent = Split-Path -Parent $Destination try { New-Item -ItemType Directory -Force $parent | Out-Null Copy-Item -LiteralPath $source -Destination $Destination -Recurse $destinationCreated = $true Test-PackageChecksums $Destination Set-Location $Destination New-Item -ItemType Directory -Force runtime | Out-Null $tokenPath = Join-Path $Destination 'runtime\connector-access' if (Test-Path -LiteralPath $tokenPath) { throw "Runtime token already exists: $tokenPath" } $bytes = New-Object byte[] 48 $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($bytes) } finally { $rng.Dispose() } $token = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_') [IO.File]::WriteAllText($tokenPath, $token, [Text.UTF8Encoding]::new($false)) Copy-Item -LiteralPath (Join-Path $Destination '.env.example') -Destination (Join-Path $Destination '.env') @( "AMD_CONNECTOR_BIND_IP=$BindAddress", "AMD_CONNECTOR_PORT=$Port" ) | Set-Content -LiteralPath (Join-Path $Destination '.env') -Encoding ascii $ruleName = 'NODE.DC DC AMD Connector (NAS only)' if (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue) { throw "Firewall rule already exists; refusing to replace: $ruleName" } New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -Profile Any -LocalAddress $BindAddress -LocalPort $Port -RemoteAddress $NasAddress | Out-Null $firewallCreated = $true if ($EnableAutoStart) { $autoStartLauncher = New-DockerDesktopAutoStart $dockerDesktopStartCommand } & $dockerCli compose up -d --build $state = '' foreach ($attempt in 1..30) { $state = & $dockerCli inspect dc-amd-connector --format '{{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}}' if ($LASTEXITCODE -ne 0) { throw 'Container inspection failed after Docker Compose apply.' } if ($state -eq 'running health=healthy') { break } Start-Sleep -Seconds 2 } if ($state -ne 'running health=healthy') { throw "Connector did not become healthy: $state" } Write-Host "DC AMD Connector installed: $state" Write-Host "Bound to $BindAddress`:$Port; inbound firewall scope is NAS $NasAddress." if ($autoStartLauncher) { Write-Host "Docker Desktop will start at the next sign-in through: $autoStartLauncher" } Write-Host 'The connector access token remains only in runtime\connector-access. Do not print or send it in chat.' } catch { if ($autoStartLauncher -and (Test-Path -LiteralPath $autoStartLauncher)) { Remove-Item -LiteralPath $autoStartLauncher -Force -ErrorAction SilentlyContinue } if ($firewallCreated) { Remove-NetFirewallRule -DisplayName 'NODE.DC DC AMD Connector (NAS only)' -ErrorAction SilentlyContinue } if ($destinationCreated -and (Test-Path -LiteralPath $Destination)) { Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue } throw }