diff --git a/install-AdHoc-Backup.ps1 b/install-AdHoc-Backup.ps1 index f5c670d..56d79bc 100644 --- a/install-AdHoc-Backup.ps1 +++ b/install-AdHoc-Backup.ps1 @@ -1,61 +1,458 @@ -# install-AdHoc-Backup.ps1 -# Uso tipico: -# iwr -useb https:///install-AdHoc-Backup.ps1 | iex -# Opzioni: -# ... } -ForceConfig # sovrascrive backup.conf se già esiste -# ... } -NoRun # installa senza eseguire il backup -# ... } -AdHocArgs '-WhatIf' # argomenti aggiuntivi da passare allo script di backup +<# ===================================================================== + install-AdHoc-Backup.ps1 + - Menù interattivo: + 1) Installa lo script e l'ambiente + 2) Crea pianificazione (Scheduled Task) + 3) Modifica configurazione (backup.conf) con guida in italiano + Requisiti: + - Eseguire come Amministratore per creare la pianificazione con SYSTEM. + - Tenere nella stessa cartella: + - AdHoc-Backup.ps1 + - backup.conf + + Note: + - Rispettate le chiavi del tuo backup.conf e il suo "manuale" (liste con |, + commenti con #, ecc.). Alcune spiegazioni sono riportate nei prompt. + - Rclone: viene posizionato in $BackupRoot\RClone\rclone.exe. Se non presente, + lo script prova a copiarlo da installazioni esistenti o può scaricarlo. +===================================================================== #> + +[CmdletBinding()] param( - [string]$InstallDir = 'C:\polo\scripts', - [string]$ScriptUrl = 'https://gitea.poloinformatico.it/Mattia/Backup-AdHoc/raw/branch/main/AdHoc_Backup.ps1', - [string]$ConfigUrl = 'https://gitea.poloinformatico.it/Mattia/Backup-AdHoc/raw/branch/main/backup.conf', - [switch]$ForceConfig, - [switch]$NoRun, - [string[]]$AdHocArgs + [string]$ConfigPath, # Facoltativo: percorso a backup.conf (default: .\backup.conf) + [string]$BackupRootOverride # Facoltativo: forza BackupRoot (altrimenti legge da backup.conf) ) $ErrorActionPreference = 'Stop' -try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 } catch {} +Set-StrictMode -Version Latest -# 1) Crea cartella di destinazione -if (-not (Test-Path -LiteralPath $InstallDir)) { - New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null +function Write-Title($text) { + Write-Host "" + Write-Host "=== $text ===" -ForegroundColor Cyan } -# 2) Scarica i due file -$ScriptPath = Join-Path $InstallDir 'AdHoc-Backup.ps1' # <- nome finale con trattino -$ConfigPath = Join-Path $InstallDir 'backup.conf' - -Write-Host "Scarico script: $ScriptUrl -> $ScriptPath" -Invoke-WebRequest -UseBasicParsing -Uri $ScriptUrl -OutFile $ScriptPath - -if ((-not (Test-Path -LiteralPath $ConfigPath)) -or $ForceConfig.IsPresent) { - Write-Host "Scarico config: $ConfigUrl -> $ConfigPath" - Invoke-WebRequest -UseBasicParsing -Uri $ConfigUrl -OutFile $ConfigPath -} else { - Write-Host "Config già presente, non sovrascrivo (usa -ForceConfig per forzare)." +function Pause-Enter($msg="Premi INVIO per continuare...") { + Write-Host "" + Read-Host $msg | Out-Null } -# 3) Sblocca eventuali zone mark -foreach ($p in @($ScriptPath, $ConfigPath)) { - if (Test-Path -LiteralPath $p) { - Unblock-File -LiteralPath $p -ErrorAction SilentlyContinue - } +function Read-Default($prompt, $default) { + if ([string]::IsNullOrEmpty($default)) { + return Read-Host "$prompt" + } else { + $ans = Read-Host "$prompt [$default]" + if ([string]::IsNullOrEmpty($ans)) { return $default } + return $ans + } } -# 4) Esegui lo script (se non esplicitamente evitato) -if (-not $NoRun.IsPresent) { - Write-Host "Avvio: $ScriptPath" - # Se il tuo AdHoc-Backup richiede la config, la passiamo in modo esplicito: - $argsToPass = @() - if (Test-Path -LiteralPath $ConfigPath) { - $argsToPass += @('-Config', $ConfigPath) - } - if ($AdHocArgs) { - $argsToPass += $AdHocArgs - } - - # Esegui in un processo figlio con ExecutionPolicy limitata al processo - & powershell -NoProfile -ExecutionPolicy Bypass -File $ScriptPath @argsToPass +function Read-YesNo($prompt, [bool]$default=$true) { + $defTxt = if ($default) { "S/n" } else { "s/N" } + while ($true) { + $ans = Read-Host "$prompt ($defTxt)" + if ([string]::IsNullOrWhiteSpace($ans)) { return $default } + switch ($ans.ToLower()) { + 's' { return $true } + 'si' { return $true } + 'y' { return $true } + 'n' { return $false } + 'no' { return $false } + default { Write-Host "Inserisci 's' o 'n'." -ForegroundColor Yellow } + } + } } + +function Test-Admin { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object Security.Principal.WindowsPrincipal($id) + return $p.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +} + +# ---------- Config I/O (preserva commenti e ordine) ---------- +class ConfigDoc { + [System.Collections.Generic.List[string]]$Lines + [hashtable]$Map + [hashtable]$Index + ConfigDoc() { + $this.Lines = [System.Collections.Generic.List[string]]::new() + $this.Map = @{} + $this.Index = @{} + } +} + +function Load-Config([string]$path) { + if (-not (Test-Path -LiteralPath $path)) { + throw "File di configurazione non trovato: $path" + } + $doc = [ConfigDoc]::new() + $raw = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction Stop + foreach ($ln in ($raw -split "`r?`n", [System.StringSplitOptions]::None)) { + $doc.Lines.Add($ln) + } + for ($i=0; $i -lt $doc.Lines.Count; $i++) { + $line = $doc.Lines[$i] + if ($line -match '^\s*#') { continue } + if ($line -match '^\s*([A-Za-z0-9_]+)\s*=(.*)$') { + $k = $matches[1] + $v = ($matches[2]).Trim() + $doc.Map[$k] = $v + $doc.Index[$k] = $i + } + } + return $doc +} + +function Save-Config([ConfigDoc]$doc, [string]$path) { + $backup = "$path.bak_$(Get-Date -Format 'yyyyMMdd_HHmmss')" + Copy-Item -LiteralPath $path -Destination $backup -Force -ErrorAction Stop + + # Riscrivo solo le linee delle chiavi (preservo commenti/righe vuote) + foreach ($k in $doc.Map.Keys) { + $v = [string]$doc.Map[$k] + if ($doc.Index.ContainsKey($k)) { + $idx = [int]$doc.Index[$k] + $doc.Lines[$idx] = "$k=$v" + } else { + # Aggiungo nuove chiavi alla fine + if (-not $doc.Lines[$doc.Lines.Count-1].StartsWith('# AGGIUNTE AUTO')) { + $doc.Lines.Add("") + $doc.Lines.Add("# AGGIUNTE AUTO (create da install-AdHoc-Backup.ps1)") + } + $doc.Lines.Add("$k=$v") + } + } + [IO.File]::WriteAllLines($path, $doc.Lines, [Text.UTF8Encoding]::new($false)) + return $backup +} + +function Set-ConfigValue([ConfigDoc]$doc, [string]$key, [string]$value) { + $doc.Map[$key] = $value +} + +# ---------- Validatori ---------- +function Validate-IntNonNeg($valText, [int]$min=0, [int]$max=[int]::MaxValue) { + if ([string]::IsNullOrWhiteSpace($valText)) { return $null } # mantieni + [int]$n = 0 + if (-not [int]::TryParse($valText, [ref]$n)) { throw "Numero non valido." } + if ($n -lt $min -or $n -gt $max) { throw "Valore fuori range ($min..$max)." } + return $n +} +function Validate-Port($valText) { return (Validate-IntNonNeg $valText 1 65535) } + +function Normalize-BoolText($text, [bool]$current) { + if ([string]::IsNullOrWhiteSpace($text)) { return $current } + switch ($text.Trim().ToLower()) { + 'true' { return $true } + 'false' { return $false } + 's' { return $true } + 'si' { return $true } + 'y' { return $true } + 'n' { return $false } + 'no' { return $false } + default { throw "Valore non valido. Usa: true/false oppure s/n" } + } +} + +function Read-Secret($prompt, $currentPlain) { + Write-Host "$prompt (lascia vuoto per mantenere l'attuale)" -ForegroundColor Yellow + $sec = Read-Host -AsSecureString + if ($sec.Length -eq 0) { return $currentPlain } + return [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)) +} + +# ---------- Editor guidato backup.conf ---------- +function Edit-BackupConfig { + param([string]$cfgPath) + + Write-Title "Modifica configurazione (backup.conf)" + $doc = Load-Config $cfgPath + + # Mappa variabili -> descrizione & validazione + $vars = @( + @{Key='BackupRoot'; Desc='Cartella radice dei backup (archivi, log, tool).'; Example='C:\Backups_AdHoc' }, + @{Key='LocalRetentionDaysFiles'; Desc='Giorni di retention dei file in locale (\Files). 0=disabilita.'; Validate='int' }, + @{Key='LocalRetentionDaysDb'; Desc='Giorni di retention dei database in locale (\Databases). 0=disabilita.'; Validate='int' }, + @{Key='RemoteRetentionDays'; Desc='Giorni di retention su remoto (rclone).'; Validate='int' }, + @{Key='KeepLocalArchives'; Desc='true=mantiene copie locali, false=le rimuove dopo upload.'; Validate='bool' }, + @{Key='EnableFileBackup'; Desc='true=abilita backup di cartelle/sorgenti in .7z.'; Validate='bool' }, + @{Key='EnableRcloneUpload'; Desc='true=abilita upload dei backup con rclone.'; Validate='bool' }, + @{Key='ArchiveSources'; Desc='Sorgenti da archiviare, separa con | (es.: C:\Dati|D:\Export|\\nas\share).'; Example='C:\Zucchetti\ahr90|C:\Zucchetti\NetSetup' }, + + @{Key='EnableSqlBackup'; Desc='true=abilita backup dei DB SQL Server.'; Validate='bool' }, + @{Key='SqlInstance'; Desc='Istanza SQL (es.: localhost, .\SQLEXPRESS, 192.168.1.10,1433, nome\istanza).'}, + @{Key='SqlUseWindowsAuth'; Desc='true=Windows Authentication; false=SQL Auth (usa SqlUser/SqlPassword).'; Validate='bool' }, + @{Key='SqlUser'; Desc='Utente SQL (usato solo se SqlUseWindowsAuth=false).'}, + @{Key='SqlPassword'; Desc='Password SQL (usata solo se SqlUseWindowsAuth=false).'; Secret=$true }, + @{Key='DbInclude'; Desc='Elenco DB da includere (|). Vuoto = auto-detect dei DB utente online.'; Example='DBProduzione|DBCRM|DBContabilita' }, + @{Key='DbExclude'; Desc='DB da escludere se DbInclude è vuoto (default: master|model|msdb|tempdb).', Example='master|model|msdb|tempdb' }, + @{Key='SqlCompressStage'; Desc='true= comprime la cartella _sql_stage in .7z dopo il backup.'; Validate='bool' }, + @{Key='SqlDropBakAfterZip'; Desc='true= elimina i .bak dopo la compressione.'; Validate='bool' }, + + @{Key='SevenZipCompressionLevel'; Desc='Livello compressione 7-Zip (0..9). 1–3 spesso è il miglior compromesso.'; Validate='int' }, + + @{Key='RcloneRemoteDest'; Desc='Destinazione rclone in formato REMOTE:percorso (usa %COMPUTERNAME% se utile).'; Example='dropbox:/Backups_AdHoc/%COMPUTERNAME%' }, + @{Key='RcloneBwl'; Desc='Limitazione di banda (es: 10M). Vuoto = nessun limite.'}, + @{Key='RcloneExtraArgs'; Desc='Argomenti extra rclone separati da | (es: --fast-list|--s3-chunk-size=64M).'}, + + @{Key='MailEnabled'; Desc='true= invia una mail di report a fine job.'; Validate='bool' }, + @{Key='MailSmtpHost'; Desc='Host SMTP (relay).'}, + @{Key='MailSmtpPort'; Desc='Porta SMTP (tipico 587).'; Validate='port' }, + @{Key='MailUseAuth'; Desc='true= il relay richiede autenticazione (compila utente/password).'; Validate='bool' }, + @{Key='MailUser'; Desc='Utente SMTP (se richiesto).'}, + @{Key='MailPassword'; Desc='Password SMTP (se richiesta).'; Secret=$true }, + @{Key='MailFrom'; Desc='Indirizzo mittente.'}, + @{Key='MailTo'; Desc='Destinatari separati da |.'; Example='it@azienda.it|sysadmin@azienda.it' }, + @{Key='MailSubjectPref'; Desc='Prefisso oggetto (puoi lasciare spazio finale).' } + ) + + $changes = @{} + + foreach ($v in $vars) { + $key = $v.Key + $cur = $doc.Map[$key] + + Write-Host "" + Write-Host ">> $key" -ForegroundColor Green + Write-Host " $($v.Desc)" + if ($v.ContainsKey('Example') -and $v.Example) { Write-Host " Esempio: $($v.Example)" -ForegroundColor DarkGray } + + if ($v.ContainsKey('Secret') -and $v.Secret) { + $newVal = Read-Secret "Nuovo valore per $key" $cur + } else { + $newValRaw = Read-Default "Inserisci un nuovo valore per $key (INVIO = mantieni)" $cur + # Validazione + if ($v.ContainsKey('Validate')) { + switch ($v.Validate) { + 'int' { $n = Validate-IntNonNeg $newValRaw | Out-Null; if ($null -ne $n) { $newValRaw = [string]$n } } + 'bool' { $b = Normalize-BoolText $newValRaw ($cur -eq 'true'); $newValRaw = if ($b) { 'true' } else { 'false' } } + 'port' { $p = Validate-Port $newValRaw | Out-Null; if ($null -ne $p) { $newValRaw = [string]$p } } + } + } + $newVal = $newValRaw + } + + if ($newVal -ne $cur) { + $changes[$key] = @{Old=$cur; New=$newVal} + Set-ConfigValue -doc $doc -key $key -value $newVal + } + } + + if ($changes.Count -gt 0) { + Write-Host "" + Write-Title "Riepilogo modifiche" + $changes.GetEnumerator() | ForEach-Object { + "{0} : '{1}' -> '{2}'" -f $_.Key, $_.Value.Old, $_.Value.New + } | Write-Host + + if (Read-YesNo "Confermi il salvataggio delle modifiche?") { + $bk = Save-Config -doc $doc -path $cfgPath + Write-Host "Configurazione salvata: $cfgPath" -ForegroundColor Cyan + Write-Host "Backup creato: $bk" -ForegroundColor DarkCyan + } else { + Write-Host "Annullato. Nessuna modifica scritta." -ForegroundColor Yellow + } + } else { + Write-Host "Nessuna modifica apportata." -ForegroundColor Yellow + } + + Pause-Enter +} + +# ---------- Installazione ambiente ---------- +function Ensure-Folder($path) { + if (-not (Test-Path -LiteralPath $path)) { + New-Item -ItemType Directory -Path $path -Force | Out-Null + } +} + +function Install-Environment { + param([string]$cfgPath) + + Write-Title "Installazione ambiente" + $doc = Load-Config $cfgPath + + # Determina BackupRoot + $backupRoot = if ($BackupRootOverride) { $BackupRootOverride } elseif ($doc.Map['BackupRoot']) { $doc.Map['BackupRoot'] } else { 'C:\Backups_AdHoc' } + $backupRoot = Read-Default "BackupRoot di destinazione" $backupRoot + Set-ConfigValue -doc $doc -key 'BackupRoot' -value $backupRoot + $null = Save-Config -doc $doc -path $cfgPath + + # Struttura cartelle + $folders = @( + $backupRoot, + Join-Path $backupRoot 'Logs', + Join-Path $backupRoot 'Out', + Join-Path $backupRoot 'Files', + Join-Path $backupRoot 'Databases', + Join-Path $backupRoot '_sql_stage', + Join-Path $backupRoot 'Bin', + Join-Path $backupRoot 'RClone' + ) + foreach ($f in $folders) { Ensure-Folder $f } + + # Copia script principali + $srcScript = Join-Path $PSScriptRoot 'AdHoc-Backup.ps1' + if (-not (Test-Path -LiteralPath $srcScript)) { + throw "AdHoc-Backup.ps1 non trovato in $PSScriptRoot" + } + Copy-Item -LiteralPath $srcScript -Destination (Join-Path $backupRoot 'AdHoc-Backup.ps1') -Force + + # Copia backup.conf + Copy-Item -LiteralPath $cfgPath -Destination (Join-Path $backupRoot 'backup.conf') -Force + + # Rclone: binario + config locale nello stesso folder ($BackupRoot\RClone) + Ensure-Rclone -TargetRoot $backupRoot + + Write-Host "Installazione completata in: $backupRoot" -ForegroundColor Cyan + Pause-Enter +} + +function Ensure-Rclone { + param([string]$TargetRoot) + + $rcloneDir = Join-Path $TargetRoot 'RClone' + $rcloneExe = Join-Path $rcloneDir 'rclone.exe' + $rcloneConf = Join-Path $rcloneDir 'rclone.conf' + + Ensure-Folder $rcloneDir + + if (-not (Test-Path -LiteralPath $rcloneExe)) { + Write-Host "rclone.exe non trovato in $rcloneDir, provo a reperirlo..." -ForegroundColor Yellow + $candidates = @( + "$env:ProgramFiles\rclone\rclone.exe", + "$env:ProgramFiles\Rclone\rclone.exe", + "$env:ProgramFiles(x86)\rclone\rclone.exe", + "$env:ProgramFiles(x86)\Rclone\rclone.exe", + "$env:SystemRoot\System32\rclone.exe" + ) | Where-Object { Test-Path -LiteralPath $_ } + + if ($candidates.Count -gt 0) { + Copy-Item -LiteralPath $candidates[0] -Destination $rcloneExe -Force + Write-Host "Copiato rclone da: $($candidates[0])" -ForegroundColor Green + } + else { + # Tentativo di download (puoi saltarlo se non vuoi traffico Internet) + try { + $zipUrl = "https://downloads.rclone.org/rclone-current-windows-amd64.zip" + $tmpZip = Join-Path $env:TEMP "rclone-current.zip" + Write-Host "Scarico rclone da $zipUrl ..." -ForegroundColor Yellow + Invoke-WebRequest -Uri $zipUrl -OutFile $tmpZip -UseBasicParsing + $tmpDir = Join-Path $env:TEMP "rclone_unzip_$([guid]::NewGuid().ToString('N'))" + Expand-Archive -LiteralPath $tmpZip -DestinationPath $tmpDir -Force + $found = Get-ChildItem -Path $tmpDir -Filter rclone.exe -Recurse | Select-Object -First 1 + if ($null -ne $found) { + Copy-Item -LiteralPath $found.FullName -Destination $rcloneExe -Force + Write-Host "rclone scaricato e installato." -ForegroundColor Green + } else { + Write-Host "Impossibile trovare rclone.exe nello ZIP. Mettilo manualmente in $rcloneDir" -ForegroundColor Red + } + } catch { + Write-Host "Download rclone fallito: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Puoi copiare manualmente rclone.exe in $rcloneDir" -ForegroundColor Yellow + } + } + } + + # Config rclone: importa quella utente se esiste, altrimenti crea skeleton + if (-not (Test-Path -LiteralPath $rcloneConf)) { + $userConf = Join-Path $env:APPDATA 'rclone\rclone.conf' + if (Test-Path -LiteralPath $userConf) { + Copy-Item -LiteralPath $userConf -Destination $rcloneConf -Force + Write-Host "Importata configurazione rclone da $userConf" -ForegroundColor Green + } else { + @" +# rclone.conf (skeleton) — definisci qui il REMOTE usato in backup.conf (RcloneRemoteDest) +# Esempio dropbox: +# [dropbox] +# type = dropbox +"@ | Set-Content -LiteralPath $rcloneConf -Encoding UTF8 -NoNewline + Write-Host "Creato skeleton rclone.conf in $rcloneConf" -ForegroundColor Yellow + } + } + + # NB: il nostro backup script userà --config "$BackupRoot\RClone\rclone.conf" come da linea guida. +} + +# ---------- Pianificazione ---------- +function Create-Schedule { + param([string]$cfgPath) + + if (-not (Test-Admin)) { + throw "Per creare la pianificazione occorrono privilegi di Amministratore." + } + + $doc = Load-Config $cfgPath + $backupRoot = if ($BackupRootOverride) { $BackupRootOverride } elseif ($doc.Map['BackupRoot']) { $doc.Map['BackupRoot'] } else { 'C:\Backups_AdHoc' } + + $scriptPath = Join-Path $backupRoot 'AdHoc-Backup.ps1' + $confPath = Join-Path $backupRoot 'backup.conf' + if (-not (Test-Path -LiteralPath $scriptPath)) { throw "Script backup non trovato in $scriptPath (esegui prima l'installazione)." } + if (-not (Test-Path -LiteralPath $confPath)) { throw "Configurazione non trovata in $confPath (esegui prima l'installazione)." } + + Write-Title "Crea pianificazione (Scheduled Task)" + $taskName = Read-Default "Nome attività" "AdHoc Backup Giornaliero" + + # Ora (HH:mm) + while ($true) { + $timeTxt = Read-Default "Orario giornaliero (HH:mm)" "22:30" + if ([TimeSpan]::TryParse($timeTxt, [ref]([TimeSpan]$null))) { break } + try { [DateTime]::ParseExact($timeTxt, 'HH:mm', $null) | Out-Null; break } catch { Write-Host "Formato non valido. Usa HH:mm (es. 22:30)" -ForegroundColor Yellow } + } + + $atTime = [DateTime]::ParseExact($timeTxt, 'HH:mm', $null).TimeOfDay + $trigger = New-ScheduledTaskTrigger -Daily -At $atTime + + $psArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Conf `"$confPath`"" + $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $psArgs + + $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable ` + -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) ` + -MultipleInstances IgnoreNew + + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null + + Write-Host "Attività pianificata creata: $taskName, orario $timeTxt (account SYSTEM)." -ForegroundColor Cyan + Pause-Enter +} + +# ---------- Menù ---------- +function Show-Menu { + Clear-Host + Write-Host "" + Write-Host "==========================" -ForegroundColor DarkCyan + Write-Host " INSTALL AZIENDALE BACKUP" -ForegroundColor DarkCyan + Write-Host "==========================" -ForegroundColor DarkCyan + Write-Host "1) Installa lo script e l'ambiente" + Write-Host "2) Crea pianificazione (Scheduled Task)" + Write-Host "3) Modifica configurazione (backup.conf)" + Write-Host "4) Esci" + Write-Host "" +} + +# Percorso di default per backup.conf +if (-not $ConfigPath) { $ConfigPath = Join-Path $PSScriptRoot 'backup.conf' } + +while ($true) { + Show-Menu + $choice = Read-Default "Seleziona un'opzione" "1" + switch ($choice) { + '1' { + Install-Environment -cfgPath $ConfigPath + } + '2' { + Create-Schedule -cfgPath $ConfigPath + } + '3' { + Edit-BackupConfig -cfgPath $ConfigPath + } + '4' { break } + default { + Write-Host "Opzione non valida." -ForegroundColor Yellow + Pause-Enter + } + } +} + +Write-Host "Uscita." -ForegroundColor DarkGray