<# .SYNOPSIS Fully automated elevated installer and configurator for Reticulum Network Stack (rnsd) on Windows 10/11. .DESCRIPTION - Requires elevation (Run as Administrator). - Filters out conflicting WindowsApps execution aliases and prioritizes Machine PATH. - Comprehensively checks for existing real Python installations via where.exe / Get-Command. - Installs Python 3.12 (machine scope) via WinGet (prompts if newer version exists, auto-installs alongside if older). - Installs NSSM via WinGet and copies the architecture-matched binary to C:\ProgramData\Reticulum\bin. - Ensures python, pip, nssm, and RNS tools are on the system PATH with highest priority. - Installs/upgrades the rns and nomadnet packages via 'python -m pip install'. - Installs UbuntuMono Nerd Font (system-wide via WinGet or direct release zip fallback). - Creates C:\ProgramData\Reticulum (shared config location usable by LocalSystem service and CLI tools). - Writes the embedded sane default configuration in BOM-less UTF-8 (backing up any existing config). - Grants Modify permissions using universal Well-Known SID (*S-1-5-32-545). - Idempotently configures Windows Firewall rules across all profiles for Reticulum ports and active Python executable. - Registers rnsd as an auto-starting Windows service via NSSM with clean parameter separation, log rotation, and restart policies. - Creates dynamic stty and fixed grep shims, plus rns-status / rns-path wrappers. - Starts the service and performs basic validation. - Sets PYTHONIOENCODING=utf-8 and PYTHONUTF8=1 globally, registers TrueTypeFont on Win10 <= 1809, and converts // comments in Windows Terminal settings.json prior to parsing. - Creates desktop shortcuts that launch NomadNet with --rnsconfig and dynamic %USERPROFILE% working directories. .NOTES After successful installation, the service runs as LocalSystem and uses: Config directory : C:\ProgramData\Reticulum Config file : C:\ProgramData\Reticulum\config Binaries/Shims : C:\ProgramData\Reticulum\bin Logs : C:\ProgramData\Reticulum\logs\ CLI tools should be invoked with: rnstatus --config C:\ProgramData\Reticulum (or 'rns-status') rnpath --config C:\ProgramData\Reticulum (or 'rns-path') #> #Requires -RunAsAdministrator #Requires -Version 5.1 $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $RNS_CONFIG_DIR = "C:\ProgramData\Reticulum" $RNS_BIN_DIR = Join-Path $RNS_CONFIG_DIR "bin" $RNS_LOG_DIR = Join-Path $RNS_CONFIG_DIR "logs" function Write-Status { param([string]$Message, [string]$Color = 'Cyan') Write-Host "[*] $Message" -ForegroundColor $Color } function Write-Success { param([string]$Message) Write-Host "[+] $Message" -ForegroundColor Green } function Write-Warn { param([string]$Message) Write-Host "[!] $Message" -ForegroundColor Yellow } # --------------------------------------------------------------------------- # Phase 0: Elevation & environment sanity # --------------------------------------------------------------------------- Write-Status "Starting Reticulum (rnsd) elevated installation..." New-Item -Path $RNS_CONFIG_DIR, $RNS_BIN_DIR, $RNS_LOG_DIR -ItemType Directory -Force | Out-Null # --------------------------------------------------------------------------- # Phase 1: Filter out WindowsApps execution aliases and ensure Machine PATH priority # --------------------------------------------------------------------------- Write-Status "Inspecting PATH for conflicting WindowsApps Python execution aliases..." try { $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' $wherePython = & where.exe python 2>$null $ErrorActionPreference = $oldEAP $realPythonPaths = @() if ($wherePython) { $realPythonPaths = @($wherePython | Where-Object { $_ -notlike "*\WindowsApps\*" -and (Test-Path $_) }) } # Rebuild current process PATH with Machine PATH taking strict precedence over User PATH (and stripping WindowsApps from current session) $machinePath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::Machine) $userPath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User) $cleanUserPath = ($userPath -split ";" | Where-Object { $_ -notlike "*\WindowsApps*" -and $_.Trim() -ne "" }) -join ";" $env:Path = "$machinePath;$cleanUserPath" Write-Success "Machine PATH prioritized over User PATH (WindowsApps stubs bypassed)." } catch { Write-Warn "Could not fully filter WindowsApps aliases (non-fatal): $_" } # --------------------------------------------------------------------------- # Phase 2: Check Existing Python and Install Dependencies via WinGet # --------------------------------------------------------------------------- Write-Status "Checking existing Python installations..." $pythonCandidate = $null if ($realPythonPaths.Count -gt 0) { $pythonCandidate = $realPythonPaths[0] } else { $cmd = Get-Command python.exe -All -ErrorAction SilentlyContinue | Where-Object { $_.Source -notlike "*\WindowsApps\*" -and (Test-Path $_.Source) } | Select-Object -First 1 if ($cmd) { $pythonCandidate = $cmd.Source } } $installPython312 = $true $useExistingPython = $false $existingPyRoot = $null $existingPyScripts = $null if ($pythonCandidate) { $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' $pyVersionStr = & $pythonCandidate -V 2>&1 | Out-String $ErrorActionPreference = $oldEAP if ($pyVersionStr -match "Python (\d+)\.(\d+)") { $pyMajor = [int]$matches[1] $pyMinor = [int]$matches[2] $existingPyRoot = Split-Path -Path $pythonCandidate -Parent $existingPyScripts = Join-Path $existingPyRoot "Scripts" if ($pyMajor -eq 3 -and $pyMinor -gt 12) { Write-Warn "Found an existing Python installation newer than 3.12 (Version $pyMajor.$pyMinor at $pythonCandidate)." $title = "Newer Python Version Detected" $message = "You have Python $pyMajor.$pyMinor installed, which is newer than the recommended version (3.12). Do you want to proceed using your existing Python version, or install Python 3.12 alongside it?" try { $choice1 = New-Object System.Management.Automation.Host.ChoiceDescription "&Use Existing Python $pyMajor.$pyMinor", "Proceed with the existing version" $choice2 = New-Object System.Management.Automation.Host.ChoiceDescription "&Install Python 3.12", "Install the recommended Python 3.12 alongside it" $options = [System.Management.Automation.Host.ChoiceDescription[]]($choice1, $choice2) $result = $host.ui.PromptForChoice($title, $message, $options, 0) } catch { Write-Warn "Could not display interactive prompt. Defaulting to use existing Python $pyMajor.$pyMinor." $result = 0 } if ($result -eq 0) { Write-Success "Proceeding with existing Python $pyMajor.$pyMinor." $installPython312 = $false $useExistingPython = $true } else { Write-Status "Proceeding with Python 3.12 installation." } } elseif ($pyMajor -eq 3 -and $pyMinor -eq 12) { Write-Success "Python 3.12 is already installed and active at $pythonCandidate." $installPython312 = $false $useExistingPython = $true } else { Write-Status "Found older Python version ($pyMajor.$pyMinor). Will automatically install Python 3.12 alongside it." } } } else { Write-Status "No existing non-stub Python installation found. Will install Python 3.12." } $wingetArgs = @( '--accept-source-agreements', '--accept-package-agreements', '--disable-interactivity' ) $wingetAlreadyInstalledCodes = @( 0, # success -1978335189, # 0x8A150015 - no available upgrade / already installed 2316632087, # already installed (older winget) -1978335212 # 0x8A15000C - no applicable update found ) function Invoke-WingetInstall { param( [string]$Id, [string[]]$ExtraParams = @() ) Write-Status "winget install $Id ..." $callArgs = @('install', '--id', $Id, '-e') + $ExtraParams + $wingetArgs $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' & winget @callArgs | Out-Null $code = $LASTEXITCODE $ErrorActionPreference = $oldEAP if ($code -in $wingetAlreadyInstalledCodes) { Write-Success "$Id is installed (or already present). Exit code: $code" return $true } else { Write-Warn "winget install of $Id returned exit code $code" return $false } } if ($installPython312) { Write-Status "Installing Python 3.12 (machine scope)..." $pythonOk = Invoke-WingetInstall -Id "Python.Python.3.12" -ExtraParams @('--scope', 'machine') if (-not $pythonOk) { Write-Warn "Python.Python.3.12 install reported a non-success code. Continuing if Python is already usable..." } } Write-Status "Installing NSSM via WinGet..." $nssmOk = Invoke-WingetInstall -Id "NSSM.NSSM" if (-not $nssmOk) { Write-Warn "NSSM.NSSM install reported a non-success code. Continuing if nssm.exe is already resolvable..." } Write-Success "Python and NSSM installation phase completed." # --------------------------------------------------------------------------- # Phase 3: Copy nssm.exe to C:\ProgramData\Reticulum\bin and persist PATH entries # --------------------------------------------------------------------------- Write-Status "Resolving installation paths, staging NSSM in $RNS_BIN_DIR, and updating system PATH..." $Py312Root = $null $Py312Scripts = $null if ($installPython312) { $regPaths = @( "HKLM:\SOFTWARE\Python\PythonCore\3.12\InstallPath", "HKLM:\SOFTWARE\WOW6432Node\Python\PythonCore\3.12\InstallPath" ) foreach ($rp in $regPaths) { $PythonReg = Get-ItemProperty -Path $rp -ErrorAction SilentlyContinue if ($PythonReg -and $PythonReg.'(default)') { $Py312Root = $PythonReg.'(default)'.TrimEnd('\') break } } if (-not $Py312Root) { $Py312Root = if (Test-Path "C:\Program Files\Python312\python.exe") { "C:\Program Files\Python312" } elseif (Test-Path "C:\Program Files (x86)\Python312-32\python.exe") { "C:\Program Files (x86)\Python312-32" } else { "C:\Program Files\Python312" } } $Py312Scripts = Join-Path $Py312Root "Scripts" } # Locate nssm.exe and copy to C:\ProgramData\Reticulum\bin\nssm.exe $TargetNssmExe = Join-Path $RNS_BIN_DIR "nssm.exe" $SourceNssmExe = $null $archFolder = if ([Environment]::Is64BitOperatingSystem) { "win64" } else { "win32" } $wingetNssm = Get-ChildItem -Path "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Filter "nssm.exe" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match $archFolder } | Select-Object -First 1 if (-not $wingetNssm) { $wingetNssm = Get-ChildItem -Path "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Filter "nssm.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 } if ($wingetNssm) { $SourceNssmExe = $wingetNssm.FullName } else { $NssmCmd = Get-Command nssm.exe -ErrorAction SilentlyContinue if ($NssmCmd) { $SourceNssmExe = $NssmCmd.Source } } if ($SourceNssmExe -and (Test-Path $SourceNssmExe)) { if ($SourceNssmExe -ne $TargetNssmExe) { Copy-Item -Path $SourceNssmExe -Destination $TargetNssmExe -Force } Write-Success "Staged nssm.exe at $TargetNssmExe" } elseif (Test-Path $TargetNssmExe) { Write-Success "Using existing nssm.exe at $TargetNssmExe" } else { Write-Warn "Could not locate nssm.exe. Service registration may fail later if NSSM is missing." } # Update Machine PATH with highest priority for Python and C:\ProgramData\Reticulum\bin $CurrentSysPath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::Machine) $CurrentPathArray = $CurrentSysPath -split ";" | Where-Object { $_.Trim() -ne "" } $NewPathArray = @() # 1. Highest Priority: Python 3.12 (if installing/enforcing it) or Existing Python (if selected) if ($installPython312) { if ($Py312Root -and (Test-Path $Py312Root)) { $NewPathArray += $Py312Root } if ($Py312Scripts -and (Test-Path $Py312Scripts)) { $NewPathArray += $Py312Scripts } } elseif ($useExistingPython) { if ($existingPyRoot -and (Test-Path $existingPyRoot)) { $NewPathArray += $existingPyRoot } if ($existingPyScripts -and (Test-Path $existingPyScripts)) { $NewPathArray += $existingPyScripts } } # 2. High Priority: Reticulum shared binary & shim directory (C:\ProgramData\Reticulum\bin) if (Test-Path $RNS_BIN_DIR) { $NewPathArray += $RNS_BIN_DIR } # 3. Existing System PATH $NewPathArray += $CurrentPathArray # Deduplicate case-insensitively while keeping highest priority (first occurrence wins) $UniquePathArray = @() foreach ($p in $NewPathArray) { $norm = $p.TrimEnd('\') if (-not ($UniquePathArray | Where-Object { $_.TrimEnd('\') -ieq $norm })) { $UniquePathArray += $p } } $Updated = $UniquePathArray -join ";" if ($Updated -ne $CurrentSysPath) { [Environment]::SetEnvironmentVariable("Path", $Updated, [EnvironmentVariableTarget]::Machine) Write-Success "Updated Machine PATH to ensure correct toolchain priority." } else { Write-Success "Machine PATH already properly configured." } # Refresh current process PATH with Machine PATH first $machinePath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::Machine) $userPath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User) $cleanUserPath = ($userPath -split ";" | Where-Object { $_ -notlike "*\WindowsApps*" -and $_.Trim() -ne "" }) -join ";" $env:Path = "$machinePath;$cleanUserPath" # --------------------------------------------------------------------------- # Phase 4: Validate toolchain and install Reticulum via '-m pip install' # --------------------------------------------------------------------------- Write-Status "Validating toolchain and installing Reticulum Network Stack..." $ActivePythonExe = if ($installPython312 -and $Py312Root -and (Test-Path (Join-Path $Py312Root "python.exe"))) { Join-Path $Py312Root "python.exe" } elseif ($useExistingPython -and $existingPyRoot -and (Test-Path (Join-Path $existingPyRoot "python.exe"))) { Join-Path $existingPyRoot "python.exe" } else { (Get-Command python.exe -ErrorAction SilentlyContinue | Where-Object { $_.Source -notlike "*\WindowsApps\*" } | Select-Object -First 1).Source } if (-not $ActivePythonExe -or -not (Test-Path $ActivePythonExe)) { throw "python.exe not resolvable after PATH update." } $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' & $ActivePythonExe --version if ($LASTEXITCODE -ne 0) { $ErrorActionPreference = $oldEAP; throw "python --version failed." } & $ActivePythonExe -m pip install --upgrade pip setuptools wheel --no-warn-script-location if ($LASTEXITCODE -ne 0) { $ErrorActionPreference = $oldEAP; throw "Failed to upgrade pip/setuptools/wheel." } Write-Status "Installing Reticulum (rns) via python -m pip..." & $ActivePythonExe -m pip install --upgrade rns --no-warn-script-location if ($LASTEXITCODE -ne 0) { $ErrorActionPreference = $oldEAP; throw "Failed to install/upgrade rns package." } Write-Status "Installing Nomad Network (nomadnet) via python -m pip..." & $ActivePythonExe -m pip install --upgrade nomadnet --no-warn-script-location if ($LASTEXITCODE -ne 0) { $ErrorActionPreference = $oldEAP; throw "Failed to install/upgrade nomadnet package." } $ErrorActionPreference = $oldEAP # Confirm key tools $tools = @('rnsd', 'rnstatus', 'rnpath', 'nomadnet') foreach ($t in $tools) { $cmd = Get-Command $t -ErrorAction SilentlyContinue if (-not $cmd) { throw "Tool '$t' not found on PATH after install." } } Write-Success "Reticulum tools and NomadNet installed and resolvable." # --------------------------------------------------------------------------- # Phase 4b: Install UbuntuMono Nerd Font (system-wide) # --------------------------------------------------------------------------- Write-Status "Installing UbuntuMono Nerd Font..." $fontInstalled = $false try { $fontOk = Invoke-WingetInstall -Id "DEVCOM.UbuntuMonoNerdFont" if ($fontOk) { $fontInstalled = $true Write-Success "UbuntuMono Nerd Font installed via WinGet." } } catch { Write-Warn "WinGet font installation encountered an error: $_" } if (-not $fontInstalled) { Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue $installedFonts = [System.Drawing.Text.InstalledFontCollection]::new().Families.Name 2>$null if ($installedFonts -and ($installedFonts -contains "UbuntuMono Nerd Font Mono" -or $installedFonts -contains "UbuntuMono Nerd Font Propo")) { $fontInstalled = $true Write-Success "UbuntuMono Nerd Font is already present on system." } else { Write-Status "Downloading UbuntuMono Nerd Font from GitHub..." try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $zipUrl = "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/UbuntuMono.zip" $tempZip = Join-Path $env:TEMP "UbuntuMono.zip" $tempDir = Join-Path $env:TEMP "UbuntuMono_Extract" Invoke-WebRequest -Uri $zipUrl -OutFile $tempZip -UseBasicParsing if (Test-Path $tempDir) { Remove-Item -Path $tempDir -Recurse -Force } Expand-Archive -Path $tempZip -DestinationPath $tempDir -Force $fontFiles = Get-ChildItem -Path $tempDir -Filter "*.ttf" -Recurse $fontRegKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" $systemFontsDir = "$env:SystemRoot\Fonts" foreach ($f in $fontFiles) { $targetFile = Join-Path $systemFontsDir $f.Name if (-not (Test-Path $targetFile)) { Copy-Item -Path $f.FullName -Destination $targetFile -Force $fontName = [System.IO.Path]::GetFileNameWithoutExtension($f.Name) + " (TrueType)" Set-ItemProperty -Path $fontRegKey -Name $fontName -Value $f.Name -Force | Out-Null } } Remove-Item -Path $tempZip -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $fontInstalled = $true Write-Success "UbuntuMono Nerd Font installed system-wide via direct download." } catch { Write-Warn "Direct font installation fallback failed (non-fatal): $_" } } } # --------------------------------------------------------------------------- # Phase 5: Write BOM-less UTF-8 configuration and apply SID-based ACLs # --------------------------------------------------------------------------- Write-Status "Writing default configuration (UTF-8 without BOM)..." $configContent = @' # This is your Reticulum config file. # It contains a sane default configuration to get you # started. Do not edit unless you're familiar with # RNSd configuration. # To see a more verbose, and much longer, # configuration example, you can run the command: # rnsd --exampleconfig [reticulum] enable_transport = False share_instance = Yes instance_name = default panic_on_interface_error = No discover_interfaces = True shared_instance_port = 37428 instance_control_port = 37429 shared_instance_type = tcp [logging] # Valid log levels are 0 through 8: # 0: Log only critical information # 1: Log errors and lower log levels # 2: Log warnings and lower log levels # 3: Log notices and lower log levels # 4: Log info and lower (this is the default) # 5: Verbose logging # 6: Debug logging # 7: Path logging # 8: Extreme logging loglevel = 3 # The interfaces section defines the physical and virtual # interfaces Reticulum will use to communicate on. This # section will contain examples for a variety of interface # types. You can modify these or use them as a basis for # your own config, or simply remove the unused ones. [interfaces] # This interface enables communication with other # link-local Reticulum nodes over UDP. It does not # need any functional IP infrastructure like routers # or DHCP servers, but will require that at least link- # local IPv6 is enabled in your operating system, which # should be enabled by default in almost any OS. See # the Reticulum Manual for more configuration options. [[Default Interface]] type = AutoInterface enabled = true name = Default Interface mode = full [[MIA Thunderhost]] type = TCPClientInterface enabled = yes mode = full target_host = mia.us.thunderhost.net target_port = 4242 transport_identity = fbbb8b55dd3d7f4dbe15ebebf280c299 [[LGA Thunderhost]] type = TCPClientInterface enabled = yes mode = full target_host = lga.us.thunderhost.net target_port = 4242 transport_identity = 04b9ff836e921ff4b85fe766f7ce929e [[LAS Thunderhost]] type = TCPClientInterface enabled = yes mode = full target_host = las.us.thunderhost.net target_port = 4242 transport_identity = 3fff205b64faa64fd6fcc6cd01063cc5 '@ $configPath = Join-Path $RNS_CONFIG_DIR "config" if (Test-Path $configPath) { $backupPath = "$configPath.bak" Copy-Item -Path $configPath -Destination $backupPath -Force Write-Status "Backed up existing configuration to $backupPath" } $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($configPath, $configContent, $utf8NoBom) Write-Success "Configuration written (UTF-8 no BOM) to $configPath" # Grant Modify permissions using universal Well-Known SID *S-1-5-32-545 (BUILTIN\Users) Write-Status "Granting Modify permissions on $RNS_CONFIG_DIR to BUILTIN\Users (*S-1-5-32-545)..." try { $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' & icacls $RNS_CONFIG_DIR /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q | Out-Null $icaclsCode = $LASTEXITCODE $ErrorActionPreference = $oldEAP if ($icaclsCode -eq 0) { Write-Success "Modify rights granted to BUILTIN\Users (*S-1-5-32-545) across all locales." } else { Write-Warn "icacls exited with code $icaclsCode" } } catch { Write-Warn "ACL adjustment encountered an issue (non-fatal): $_" } # --------------------------------------------------------------------------- # Phase 6: Configure Windows Firewall (Idempotent, All Profiles) # --------------------------------------------------------------------------- Write-Status "Configuring Windows Firewall rules for Reticulum..." try { $ruleNames = @( "Reticulum AutoInterface (UDP-In)", "Reticulum AutoInterface (UDP-Out)", "Reticulum Shared Instance (TCP-In)", "Reticulum Daemon (rnsd)", "Nomad Network" ) foreach ($rn in $ruleNames) { Remove-NetFirewallRule -DisplayName $rn -ErrorAction SilentlyContinue | Out-Null } # Open Default AutoInterface and Shared Instance Ports across Any profile (Domain, Private, Public) New-NetFirewallRule -DisplayName "Reticulum AutoInterface (UDP-In)" -Direction Inbound -Profile Any -Protocol UDP -LocalPort 29716, 42671 -Action Allow -ErrorAction SilentlyContinue | Out-Null New-NetFirewallRule -DisplayName "Reticulum AutoInterface (UDP-Out)" -Direction Outbound -Profile Any -Protocol UDP -LocalPort 29716, 42671 -Action Allow -ErrorAction SilentlyContinue | Out-Null New-NetFirewallRule -DisplayName "Reticulum Shared Instance (TCP-In)" -Direction Inbound -Profile Any -Protocol TCP -LocalPort 37428, 37429 -InterfaceAlias "Loopback Pseudo-Interface 1" -Action Allow -ErrorAction SilentlyContinue | Out-Null # Whitelist the active Python executable (which owns all sockets for both rnsd and nomadnet) if ($ActivePythonExe -and (Test-Path $ActivePythonExe)) { New-NetFirewallRule -DisplayName "Reticulum Daemon (rnsd)" -Direction Inbound -Profile Any -Program $ActivePythonExe -Action Allow -ErrorAction SilentlyContinue | Out-Null New-NetFirewallRule -DisplayName "Reticulum Daemon (rnsd)" -Direction Outbound -Profile Any -Program $ActivePythonExe -Action Allow -ErrorAction SilentlyContinue | Out-Null } Write-Success "Windows Firewall rules idempotently configured across all profiles." } catch { Write-Warn "Could not automatically configure Windows Firewall (non-fatal): $_" } # --------------------------------------------------------------------------- # Phase 7: Register and configure NSSM service # --------------------------------------------------------------------------- Write-Status "Registering ReticulumDaemon service via NSSM..." $ServiceName = "ReticulumDaemon" $NssmBin = if (Test-Path $TargetNssmExe) { $TargetNssmExe } else { (Get-Command nssm.exe -ErrorAction SilentlyContinue).Source } if (-not $NssmBin) { throw "Could not resolve nssm.exe binary." } $RnsdPath = (Get-Command rnsd.exe -ErrorAction SilentlyContinue).Source if (-not $RnsdPath) { $RnsdPath = (Get-Command rnsd -ErrorAction SilentlyContinue).Source } if (-not $RnsdPath) { throw "Could not resolve rnsd executable." } $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' # Check if service already exists; only stop and remove if present $existingSvc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($existingSvc) { Write-Status "Existing service found. Stopping and removing previous registration..." & $NssmBin stop $ServiceName 2>&1 | Out-Null & $NssmBin remove $ServiceName confirm 2>&1 | Out-Null Start-Sleep -Seconds 1 } # Install with binary path only, then set AppParameters explicitly to avoid quote splitting Write-Status "Installing service via NSSM..." & $NssmBin install $ServiceName "$RnsdPath" 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Warn "nssm install returned exit code $LASTEXITCODE. Checking service..." $svcCheck = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if (-not $svcCheck) { $ErrorActionPreference = $oldEAP throw "nssm install failed and service was not created." } } # Operational settings Write-Status "Configuring service settings..." & $NssmBin set $ServiceName AppParameters "--config `"$RNS_CONFIG_DIR`"" 2>&1 | Out-Null & $NssmBin set $ServiceName AppDirectory "$RNS_CONFIG_DIR" 2>&1 | Out-Null & $NssmBin set $ServiceName Description "Reticulum Network Stack background routing daemon" 2>&1 | Out-Null & $NssmBin set $ServiceName Start SERVICE_AUTO_START 2>&1 | Out-Null & $NssmBin set $ServiceName AppEnvironmentExtra "PYTHONIOENCODING=utf-8" "PYTHONUTF8=1" 2>&1 | Out-Null # Logging + rotation & $NssmBin set $ServiceName AppStdout "$RNS_LOG_DIR\rnsd.stdout.log" 2>&1 | Out-Null & $NssmBin set $ServiceName AppStderr "$RNS_LOG_DIR\rnsd.stderr.log" 2>&1 | Out-Null & $NssmBin set $ServiceName AppStdoutCreationDisposition 4 2>&1 | Out-Null & $NssmBin set $ServiceName AppStderrCreationDisposition 4 2>&1 | Out-Null & $NssmBin set $ServiceName AppRotateFiles 1 2>&1 | Out-Null & $NssmBin set $ServiceName AppRotateOnline 1 2>&1 | Out-Null & $NssmBin set $ServiceName AppRotateSeconds 86400 2>&1 | Out-Null & $NssmBin set $ServiceName AppRotateBytes 5242880 2>&1 | Out-Null # Restart behavior & $NssmBin set $ServiceName AppRestartDelay 5000 2>&1 | Out-Null & $NssmBin set $ServiceName AppThrottle 1500 2>&1 | Out-Null $ErrorActionPreference = $oldEAP Write-Success "Service registered: $ServiceName" # --------------------------------------------------------------------------- # Phase 8a: Create dynamic stty and fixed grep shims + rns wrappers # --------------------------------------------------------------------------- Write-Status "Creating dynamic stty and grep shims in $RNS_BIN_DIR..." $shimDir = $RNS_BIN_DIR try { # Check for existing native stty (ignoring our own .cmd shim if re-running) $existingStty = Get-Command stty -ErrorAction SilentlyContinue | Where-Object { $_.Extension -match '\.exe$' -or $_.Source -notmatch 'stty\.cmd$' } | Select-Object -First 1 if (-not $existingStty) { $sttyShim = @' @echo off REM Dynamic stty shim for NomadNet / Python TUIs on Windows if "%1"=="size" ( for /f "tokens=*" %%a in ('powershell -NoProfile -Command "$s=$Host.UI.RawUI.WindowSize;\"$($s.Height) $($s.Width)\"" 2^>nul') do ( echo %%a exit /b 0 ) echo 24 80 exit /b 0 ) if "%1"=="-a" ( for /f "tokens=1,2" %%a in ('powershell -NoProfile -Command "$s=$Host.UI.RawUI.WindowSize;\"$($s.Height) $($s.Width)\"" 2^>nul') do ( echo speed 38400 baud; rows %%a; columns %%b; exit /b 0 ) echo speed 38400 baud; rows 24; columns 80; exit /b 0 ) exit /b 0 '@ $sttyPath = Join-Path $shimDir "stty.cmd" Set-Content -Path $sttyPath -Value $sttyShim -Encoding ASCII -Force Write-Success "Created dynamic stty shim at $sttyPath" } else { Write-Success "Native 'stty' found at $($existingStty.Source), skipping shim creation." } # Check for existing native grep (ignoring our own .cmd shim if re-running) $existingGrep = Get-Command grep -ErrorAction SilentlyContinue | Where-Object { $_.Extension -match '\.exe$' -or $_.Source -notmatch 'grep\.cmd$' } | Select-Object -First 1 if (-not $existingGrep) { $grepShim = @' @echo off REM Minimal grep shim for NomadNet on Windows setlocal enabledelayedexpansion if "%~1"=="" exit /b 1 set "pattern=%~1" shift if "%~1"=="" ( findstr /i /c:"%pattern%" exit /b %ERRORLEVEL% ) :loop if "%~1"=="" goto :eof findstr /i /c:"%pattern%" "%~1" 2>nul shift goto :loop '@ $grepPath = Join-Path $shimDir "grep.cmd" Set-Content -Path $grepPath -Value $grepShim -Encoding ASCII -Force Write-Success "Created grep shim at $grepPath" } else { Write-Success "Native 'grep' found at $($existingGrep.Source), skipping shim creation." } } catch { Write-Warn "Could not create stty/grep shims: $_" } Write-Status "Creating convenience wrappers for rnstatus / rnpath..." try { $rnstatusWrapper = @" @echo off rnstatus --config "C:\ProgramData\Reticulum" %* "@ $rnpathWrapper = @" @echo off rnpath --config "C:\ProgramData\Reticulum" %* "@ Set-Content -Path (Join-Path $shimDir "rns-status.cmd") -Value $rnstatusWrapper -Encoding ASCII -Force Set-Content -Path (Join-Path $shimDir "rns-path.cmd") -Value $rnpathWrapper -Encoding ASCII -Force Write-Success "Created wrappers: rns-status.cmd and rns-path.cmd in $shimDir" } catch { Write-Warn "Could not create convenience wrappers: $_" } # --------------------------------------------------------------------------- # Phase 8b: Start service and validate # --------------------------------------------------------------------------- Write-Status "Starting ReticulumDaemon service..." try { Start-Service -Name $ServiceName -ErrorAction Stop Start-Sleep -Seconds 4 } catch { Write-Warn "Start-Service failed: $_. Trying nssm start..." try { $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' & $NssmBin start $ServiceName 2>&1 | Out-Null $ErrorActionPreference = $oldEAP Start-Sleep -Seconds 4 } catch { Write-Warn "nssm start also failed: $_" } } $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($svc) { Write-Host "Service status: $($svc.Status) (StartType: $($svc.StartType))" -ForegroundColor $(if ($svc.Status -eq 'Running') { 'Green' } else { 'Yellow' }) } else { Write-Warn "Service $ServiceName not found after registration." } Write-Status "Recent daemon log output:" $stdout = Join-Path $RNS_LOG_DIR "rnsd.stdout.log" $stderr = Join-Path $RNS_LOG_DIR "rnsd.stderr.log" if (Test-Path $stdout) { Get-Content $stdout -Tail 15 -ErrorAction SilentlyContinue } if (Test-Path $stderr) { $errTail = Get-Content $stderr -Tail 10 -ErrorAction SilentlyContinue if ($errTail) { Write-Warn "stderr tail:" $errTail } } $oldEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' Write-Status "Querying stack health (rnstatus)..." & rnstatus --config $RNS_CONFIG_DIR 2>&1 Write-Status "Querying path table (rnpath)..." & rnpath --config $RNS_CONFIG_DIR 2>&1 $ErrorActionPreference = $oldEAP Write-Host "" Write-Success "Installation and service configuration complete." # --------------------------------------------------------------------------- # Phase 9: Configure UTF-8 Env, System-Wide Fonts, and Desktop Shortcuts # --------------------------------------------------------------------------- Write-Status "Configuring UTF-8 environment variables, system-wide console fonts, and shortcuts..." try { $fontFace = "UbuntuMono Nerd Font Mono" $fontFamily = 0x00000036 $fontSize = 0x00100000 # 16px raster height $codePage = 65001 # UTF-8 # Phase 9.1: Pass PYTHONIOENCODING=utf-8 and PYTHONUTF8=1 globally and in current session [Environment]::SetEnvironmentVariable("PYTHONIOENCODING", "utf-8", [EnvironmentVariableTarget]::Machine) [Environment]::SetEnvironmentVariable("PYTHONUTF8", "1", [EnvironmentVariableTarget]::Machine) $env:PYTHONIOENCODING = "utf-8" $env:PYTHONUTF8 = "1" Write-Success "Set PYTHONIOENCODING=utf-8 and PYTHONUTF8=1 at Machine scope." # Phase 9.2: Only register Console\TrueTypeFont 000... entry on Windows 10 build 1809 (17763) and older $osBuild = [int][Environment]::OSVersion.Version.Build if ($osBuild -le 17763) { $consoleFontKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont" if (Test-Path $consoleFontKey) { $existing = Get-ItemProperty -Path $consoleFontKey $alreadyRegistered = $false foreach ($prop in $existing.PSObject.Properties) { if ($prop.Value -eq $fontFace) { $alreadyRegistered = $true; break } } if (-not $alreadyRegistered) { $zeros = ($existing.PSObject.Properties.Name | Where-Object { $_ -match '^0+$' }).Count $slot = "0" * ($zeros + 1) New-ItemProperty -Path $consoleFontKey -Name $slot -Value $fontFace -PropertyType String -Force | Out-Null } Write-Success "Registered '$fontFace' in Console\TrueTypeFont (Windows build $osBuild <= 17763)." } } else { Write-Status "Skipping legacy Console\TrueTypeFont registration on Windows build $osBuild (> 1809)." } # 1. Apply font globally to all loaded Classic Console registries (CMD/PowerShell) $hives = @("Registry::HKEY_USERS\.DEFAULT") $hives += (Get-ChildItem "Registry::HKEY_USERS" -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match "^S-1-5-21-[\d\-]+$" }).PSPath foreach ($hive in $hives) { $consoleKey = Join-Path $hive "Console" if (-not (Test-Path $consoleKey)) { New-Item -Path $consoleKey -Force | Out-Null } New-ItemProperty -Path $consoleKey -Name "FaceName" -Value $fontFace -PropertyType String -Force | Out-Null New-ItemProperty -Path $consoleKey -Name "FontFamily" -Value $fontFamily -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $consoleKey -Name "FontSize" -Value $fontSize -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $consoleKey -Name "CodePage" -Value $codePage -PropertyType DWord -Force | Out-Null } Write-Success "Configured Classic Console system-wide default font to $fontFace." # Phase 9.3: Parse and convert // C-Style comments in Windows Terminal settings.json into valid JSON properties before ConvertFrom-Json $userPaths = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin @('Public','Default','Default User') } foreach ($user in $userPaths) { $wtSettingsPaths = @( "$($user.FullName)\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json", "$($user.FullName)\AppData\Local\Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json", "$($user.FullName)\AppData\Local\Microsoft\Windows Terminal\settings.json" ) foreach ($wtPath in $wtSettingsPaths) { if (Test-Path $wtPath) { try { $rawLines = Get-Content -Path $wtPath -Encoding UTF8 $convertedLines = New-Object System.Collections.Generic.List[string] foreach ($line in $rawLines) { if ($line -match '^\s*//(.*)$') { continue } # Strip trailing unquoted // comments at the end of a property line (ignoring :// in URLs/paths) $cleanLine = [regex]::Replace($line, '(?