param() $ErrorActionPreference = "Stop" $mainUrl = "https://cloud.qortal.org/s/devnet_download/download/qortal-DevNet-MAIN.7z" $dbUrl = "https://cloud.qortal.org/s/QortalDevNetDatabaseLatest/download/db-DevNet-LATEST.7z" $ipScriptUrl = "https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/add-public-ip-to-settings-windows.ps1" $defaultSettingsUrl = "https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/default-settings.json" $startValidationUrl = "https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/start-windows.ps1" $qortScriptUrl = "https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/qort" $dependenciesUrl = "https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/setup-dependencies-windows.ps1" $stopScriptUrl = "https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/stop-windows.ps1" $publishShareToken = "PublishDevNetIPsHere" $publishWebDavUrl = "https://cloud.qortal.org/public.php/webdav" $homeDir = $env:USERPROFILE $baseDir = $homeDir $devnetDir = "" $stateFile = "" $downloadDir = "" $mainArchive = "" $dbArchive = "" $defaultSettingsLocal = "" $ipScriptLocal = "" $startValidationLocal = "" $qortScriptLocal = "" $dependenciesLocal = "" $stopScriptLocal = "" $portStartDefault = 23391 $portStart = $null $listenPort = $null function Write-Log { param([string]$Message) $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$ts] $Message" } function Read-HostTimeout { param( [string]$Prompt, [int]$Seconds, [string]$DefaultValue = "" ) Write-Host -NoNewline $Prompt if ($Host.Name -eq "ConsoleHost" -and $Host.UI.RawUI) { $raw = $Host.UI.RawUI $buffer = "" $deadline = (Get-Date).AddSeconds($Seconds) while ((Get-Date) -lt $deadline) { if ($raw.KeyAvailable) { $key = $raw.ReadKey("NoEcho,IncludeKeyDown") if ($key.VirtualKeyCode -eq 13) { Write-Host "" return $buffer } if ($key.VirtualKeyCode -eq 8) { if ($buffer.Length -gt 0) { $buffer = $buffer.Substring(0, $buffer.Length - 1) Write-Host -NoNewline "`b `b" } continue } $buffer += $key.Character Write-Host -NoNewline $key.Character } else { Start-Sleep -Milliseconds 50 } } Write-Host "" return $DefaultValue } if ([Console]::IsInputRedirected) { Write-Host "" return $DefaultValue } $task = [System.Threading.Tasks.Task[string]]::Run([Func[string]]{ [Console]::ReadLine() }) try { if ($task.Wait($Seconds * 1000)) { return $task.Result } } catch { Write-Host "" return $DefaultValue } Write-Host "" return $DefaultValue } function Add-PathIfMissing { param([string]$Dir) if ([string]::IsNullOrWhiteSpace($Dir)) { return } if (-not (Test-Path $Dir)) { return } $pathParts = $env:Path -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } if ($pathParts -notcontains $Dir) { $env:Path = ($pathParts + $Dir) -join ';' } } function Find-7Zip { $candidates = @() $programFiles = $env:ProgramFiles if (-not $programFiles) { $programFiles = [Environment]::GetFolderPath("ProgramFiles") } $programFilesX86 = ${env:ProgramFiles(x86)} if (-not $programFilesX86) { $programFilesX86 = [Environment]::GetFolderPath("ProgramFilesX86") } foreach ($base in @($programFiles, $programFilesX86)) { if ($base) { $candidates += (Join-Path -Path $base -ChildPath "7-Zip\7z.exe") } } foreach ($candidate in $candidates) { if ($candidate -and (Test-Path $candidate)) { return $candidate } } return $null } function Find-JavaBin { $roots = @() $programFiles = $env:ProgramFiles if (-not $programFiles) { $programFiles = [Environment]::GetFolderPath("ProgramFiles") } $programFilesX86 = ${env:ProgramFiles(x86)} if (-not $programFilesX86) { $programFilesX86 = [Environment]::GetFolderPath("ProgramFilesX86") } foreach ($base in @($programFiles, $programFilesX86)) { if ($base) { $roots += (Join-Path -Path $base -ChildPath "Eclipse Adoptium") $roots += (Join-Path -Path $base -ChildPath "Java") } } $roots = $roots | Where-Object { $_ -and (Test-Path $_) } foreach ($root in $roots) { $found = Get-ChildItem -Path $root -Recurse -Filter "java.exe" -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match '\\bin\\java\.exe$' } | Select-Object -ExpandProperty FullName -First 1 if ($found) { return $found } } return $null } function Ensure-Dependencies { $missing = @() if (-not (Get-Command 7z -ErrorAction SilentlyContinue)) { $missing += "7z" } if (-not (Get-Command java -ErrorAction SilentlyContinue)) { $missing += "java" } if ($missing.Count -eq 0) { return } Write-Log "Missing dependencies detected: $($missing -join ', ')" New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null $localFallback = Join-Path $PSScriptRoot "setup-dependencies-windows.ps1" if (Test-Path $localFallback) { $depScript = $localFallback } else { Invoke-WebRequest -Uri $dependenciesUrl -OutFile $dependenciesLocal $depScript = $dependenciesLocal } & powershell -ExecutionPolicy Bypass -File $depScript $sevenZipExe = Find-7Zip if ($sevenZipExe) { Add-PathIfMissing -Dir (Split-Path -Parent $sevenZipExe) } $javaExe = Find-JavaBin if ($javaExe) { Add-PathIfMissing -Dir (Split-Path -Parent $javaExe) } if (-not (Get-Command 7z -ErrorAction SilentlyContinue)) { throw "Dependencies are still missing after setup. Please install manually." } if (-not (Get-Command java -ErrorAction SilentlyContinue)) { throw "Java is still missing after setup. Please install Java 11+ manually." } } $useDefaultBase = Read-HostTimeout "Use default base path $homeDir? [Y/n] (auto-continue in 20s): " 20 "" switch -Regex ($useDefaultBase) { '^(n|no)$' { $baseDirInput = Read-HostTimeout "Enter the base path to install into (auto-continue in 20s): " 20 "" if ([string]::IsNullOrWhiteSpace($baseDirInput)) { throw "Base path cannot be empty." } $baseDir = $baseDirInput } default { $baseDir = $homeDir } } $devnetDir = Join-Path $baseDir "qortal-DevNet" $stateFile = Join-Path $baseDir "qortal-DevNet.setup.state" $downloadDir = Join-Path $baseDir "qortal-DevNet-downloads" $mainArchive = Join-Path $downloadDir "qortal-DevNet-MAIN.7z" $dbArchive = Join-Path $downloadDir "db-DevNet-LATEST.7z" $defaultSettingsLocal = Join-Path $downloadDir "default-settings.json" $ipScriptLocal = Join-Path $downloadDir "add-public-ip-to-settings-windows.ps1" $startValidationLocal = Join-Path $downloadDir "start-windows.ps1" $qortScriptLocal = Join-Path $downloadDir "qort" $dependenciesLocal = Join-Path $downloadDir "setup-dependencies-windows.ps1" $stopScriptLocal = Join-Path $downloadDir "stop-windows.ps1" Ensure-Dependencies $useDefaultPorts = Read-HostTimeout "Use default port range $portStartDefault-$($portStartDefault + 3)? [Y/n] (auto-continue in 20s): " 20 "" switch -Regex ($useDefaultPorts) { '^(n|no)$' { $portStartInput = Read-HostTimeout "Enter the starting port for the range (auto-continue in 20s): " 20 "" if ([string]::IsNullOrWhiteSpace($portStartInput)) { throw "Port start cannot be empty." } if (-not ($portStartInput -match '^\d+$')) { throw "Port start must be a number." } $portStart = [int]$portStartInput } default { $portStart = $portStartDefault } } if ($portStart -lt 1024 -or $portStart -gt 65532) { throw "Port start must be between 1024 and 65532." } $listenPort = $portStart + 1 function Step-Done { param([string]$Name) if (-not (Test-Path $stateFile)) { return $false } return (Select-String -Path $stateFile -SimpleMatch $Name -Quiet) } function Mark-Done { param([string]$Name) Add-Content -Path $stateFile -Value $Name } function Ensure-File { param([string]$Url, [string]$Dest) if (Test-Path $Dest) { $item = Get-Item $Dest if ($item.Length -gt 0) { Write-Log "Using existing $([System.IO.Path]::GetFileName($Dest))" return } } Invoke-WebRequest -Uri $Url -OutFile $Dest } function Unblock-IfPresent { param([string]$Path) if (Test-Path $Path) { Unblock-File -Path $Path -ErrorAction SilentlyContinue } } function Port-In-Use { param([int]$Port) if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) { return (Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue) -ne $null } $netstat = netstat -ano 2>$null if ($LASTEXITCODE -ne 0 -or -not $netstat) { return $null } return $netstat -match "[:.]$Port\s+LISTENING" } $portRangeInUse = $false $portCheckUnknown = $false foreach ($p in @($portStart, $portStart + 1, $portStart + 2, $portStart + 3)) { $inUse = Port-In-Use -Port $p if ($inUse -eq $true) { $portRangeInUse = $true break } if ($inUse -eq $null) { $portCheckUnknown = $true } } if ($portRangeInUse) { Write-Log "Port range $portStart-$($portStart + 3) appears in use; shifting by 10000" $portStart = $portStart + 10000 if ($portStart -gt 65532) { throw "Port range exceeds 65535 after shifting." } $listenPort = $portStart + 1 } elseif ($portCheckUnknown) { Write-Log "Warning: unable to detect if ports are in use; continuing with selected range." } function Get-PublicIp { $urls = @( "https://api.ipify.org", "https://ipv4.icanhazip.com", "https://checkip.amazonaws.com", "https://ifconfig.me", "https://canhazip.com" ) foreach ($url in $urls) { try { $ip = (Invoke-WebRequest -Uri $url -TimeoutSec 8 -Headers @{ "User-Agent" = "Mozilla/5.0" }).Content.Trim() if ($ip -match '^\d{1,3}(\.\d{1,3}){3}$') { return $ip } } catch { continue } } return $null } function Publish-PublicIp { param([string]$Ip, [int]$Port, [string]$HostName, [string]$TempFile) $content = "$Ip`:$Port" Set-Content -Path $TempFile -Value $content -Encoding ASCII $remoteName = "$HostName.txt" $url = "$publishWebDavUrl/$remoteName" $sec = New-Object System.Security.SecureString $cred = New-Object System.Management.Automation.PSCredential($publishShareToken, $sec) try { Invoke-WebRequest -Uri $url -Method Put -InFile $TempFile -Credential $cred | Out-Null return $true } catch { $altName = "$HostName-$((Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss')).txt" $altUrl = "$publishWebDavUrl/$altName" try { Invoke-WebRequest -Uri $altUrl -Method Put -InFile $TempFile -Credential $cred | Out-Null return $true } catch { return $false } } } if (-not (Get-Command 7z -ErrorAction SilentlyContinue)) { throw "7z not found. Please install 7-Zip and ensure it is in PATH." } Write-Log "Step 1/7: Download files..." New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null Ensure-File -Url $mainUrl -Dest $mainArchive Ensure-File -Url $dbUrl -Dest $dbArchive Ensure-File -Url $defaultSettingsUrl -Dest $defaultSettingsLocal Ensure-File -Url $ipScriptUrl -Dest $ipScriptLocal Ensure-File -Url $startValidationUrl -Dest $startValidationLocal Ensure-File -Url $qortScriptUrl -Dest $qortScriptLocal Ensure-File -Url $stopScriptUrl -Dest $stopScriptLocal Unblock-IfPresent -Path $ipScriptLocal Unblock-IfPresent -Path $startValidationLocal Unblock-IfPresent -Path $stopScriptLocal if (-not (Step-Done "01-download-files")) { Mark-Done "01-download-files" } if (-not (Step-Done "02-prepare-devnet-dir")) { Write-Log "Step 2/7: Prepare devnet directory..." if (Test-Path $devnetDir) { $backupDir = Join-Path $homeDir "backup-qortal-DevNet" if (Test-Path $backupDir) { $backupDir = "$backupDir-$(Get-Date -Format yyyyMMdd_HHmmss)" } Write-Log "Existing $devnetDir found; moving to $backupDir" Move-Item -Path $devnetDir -Destination $backupDir } Write-Log "Creating $devnetDir" New-Item -ItemType Directory -Path $devnetDir -Force | Out-Null Mark-Done "02-prepare-devnet-dir" } if (-not (Step-Done "03-extract-main")) { Write-Log "Step 3/7: Extract devnet main archive..." & 7z x $mainArchive "-o$devnetDir" | Out-Null $tempDir = Join-Path $devnetDir "temp" if (Test-Path $tempDir) { Write-Log "Normalizing devnet main files from temp/ into $devnetDir" Copy-Item -Path (Join-Path $tempDir "*") -Destination $devnetDir -Recurse -Force Remove-Item -Path $tempDir -Recurse -Force } Mark-Done "03-extract-main" } if (-not (Step-Done "04-extract-db")) { Write-Log "Step 4/7: Extract devnet DB archive..." $dbPath = Join-Path $devnetDir "db" if (Test-Path $dbPath) { $dbBackup = "$dbPath.bak-$(Get-Date -Format yyyyMMdd_HHmmss)" Write-Log "Existing db folder found; moving to $dbBackup" Move-Item -Path $dbPath -Destination $dbBackup } & 7z x $dbArchive "-o$devnetDir" | Out-Null if (-not (Test-Path $dbPath)) { throw "Expected db/ folder in devnet DB archive, but it was not found." } Mark-Done "04-extract-db" } if (-not (Step-Done "05-configure-settings")) { Write-Log "Step 5/7: Configure settings..." $settingsPath = Join-Path $devnetDir "settings.json" if (Test-Path $settingsPath) { $settingsBackup = Join-Path $devnetDir "backup-settings.json" if (Test-Path $settingsBackup) { $settingsBackup = "$settingsBackup-$(Get-Date -Format yyyyMMdd_HHmmss)" } Write-Log "Existing settings.json found; moving to $settingsBackup" Move-Item -Path $settingsPath -Destination $settingsBackup } Copy-Item -Path $defaultSettingsLocal -Destination $settingsPath -Force $ipScriptPath = Join-Path $devnetDir "add-public-ip-to-settings.ps1" Copy-Item -Path $ipScriptLocal -Destination $ipScriptPath -Force Unblock-IfPresent -Path $ipScriptPath $env:QORTAL_PORT_START = $portStart.ToString() Write-Log "Running public IP script..." & powershell -ExecutionPolicy Bypass -File $ipScriptPath Mark-Done "05-configure-settings" } if (-not (Step-Done "06-publish-public-ip")) { Write-Log "Step 6/7: Publish public IP..." $hostName = $env:COMPUTERNAME $publishTmp = Join-Path $downloadDir "devnet-public-ip.txt" $publicIp = Get-PublicIp if ($publicIp) { if (Publish-PublicIp -Ip $publicIp -Port $listenPort -HostName $hostName -TempFile $publishTmp) { Write-Log "Published $publicIp`:$listenPort for $hostName" } else { Write-Log "Warning: failed to publish public IP to Nextcloud share." } } else { Write-Log "Warning: could not determine public IP to publish." } Mark-Done "06-publish-public-ip" } if (-not (Step-Done "07-install-start-script")) { Write-Log "Step 7/7: Install start/stop scripts..." $startValidationPath = Join-Path $devnetDir "start-windows.ps1" Copy-Item -Path $startValidationLocal -Destination $startValidationPath -Force $startPath = Join-Path $devnetDir "start.ps1" if (Test-Path $startPath) { $startBackup = "$startPath.bak-$(Get-Date -Format yyyyMMdd_HHmmss)" Move-Item -Path $startPath -Destination $startBackup Write-Log "Backed up existing start.ps1 to $startBackup" } Copy-Item -Path $startValidationPath -Destination $startPath -Force Unblock-IfPresent -Path $startPath Unblock-IfPresent -Path $startValidationPath $stopPath = Join-Path $devnetDir "stop.ps1" if (Test-Path $stopPath) { $stopBackup = "$stopPath.bak-$(Get-Date -Format yyyyMMdd_HHmmss)" Move-Item -Path $stopPath -Destination $stopBackup Write-Log "Backed up existing stop.ps1 to $stopBackup" } Copy-Item -Path $stopScriptLocal -Destination $stopPath -Force $stopScriptPath = Join-Path $devnetDir "stop-windows.ps1" Copy-Item -Path $stopScriptLocal -Destination $stopScriptPath -Force Unblock-IfPresent -Path $stopPath Unblock-IfPresent -Path $stopScriptPath $qortPath = Join-Path $devnetDir "qort" Copy-Item -Path $qortScriptLocal -Destination $qortPath -Force Mark-Done "07-install-start-script" } Write-Log "Done."