[CmdletBinding()] param() $ErrorActionPreference = 'Stop' $projectPath = Split-Path -Parent $MyInvocation.MyCommand.Path $runtimePath = Join-Path $projectPath 'storage\framework\demo-runtime' $statePath = Join-Path $runtimePath 'state.json' $phpLog = Join-Path $runtimePath 'laravel.log' $phpErrorLog = Join-Path $runtimePath 'laravel-error.log' $tunnelLog = Join-Path $runtimePath 'cloudflared.log' $tunnelErrorLog = Join-Path $runtimePath 'cloudflared-error.log' $phpExecutable = 'C:\xampp\php\php.exe' $mysqlAdmin = 'C:\xampp\mysql\bin\mysqladmin.exe' $mysqlStart = 'C:\xampp\mysql_start.bat' $cloudflared = 'C:\Program Files (x86)\cloudflared\cloudflared.exe' $localUrl = 'http://127.0.0.1:8080' # Algumas instalações do Windows expõem Path e PATH simultaneamente. # Start-Process trata-as sem distinguir maiúsculas e falha com chave duplicada. $windowsPath = $env:Path Remove-Item Env:PATH -ErrorAction SilentlyContinue $env:Path = $windowsPath New-Item -ItemType Directory -Path $runtimePath -Force | Out-Null foreach ($requiredFile in @($phpExecutable, $mysqlAdmin, $mysqlStart, $cloudflared)) { if (-not (Test-Path -LiteralPath $requiredFile)) { throw "Ficheiro necessário não encontrado: $requiredFile" } } function Test-LocalUrl { param([string]$Url) try { $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 3 return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500 } catch { return $false } } function Test-MySql { & $mysqlAdmin ping --host=127.0.0.1 --silent 2>$null return $LASTEXITCODE -eq 0 } function Set-EnvValue { param( [string[]]$Lines, [string]$Key, [string]$Value ) $pattern = '^\s*' + [regex]::Escape($Key) + '=' $matches = @($Lines | Where-Object { $_ -match $pattern }) if ($matches.Count -gt 0) { $updated = $false return ,@($Lines | ForEach-Object { if (-not $updated -and $_ -match $pattern) { $updated = $true "$Key=$Value" } else { $_ } }) } return ,@($Lines + "$Key=$Value") } $state = [ordered]@{ startedAt = (Get-Date).ToString('o') mysqlStartedByDemo = $false laravelPid = $null cloudflaredPid = $null publicUrl = $null } Write-Host 'A preparar a demonstração MAKOMBE...' -ForegroundColor Cyan if (-not (Test-MySql)) { Write-Host 'A iniciar o MySQL do XAMPP...' Start-Process -FilePath $mysqlStart -WindowStyle Hidden | Out-Null $state.mysqlStartedByDemo = $true $mysqlReady = $false for ($attempt = 0; $attempt -lt 30; $attempt++) { Start-Sleep -Seconds 1 if (Test-MySql) { $mysqlReady = $true break } } if (-not $mysqlReady) { throw 'O MySQL não ficou disponível dentro do tempo esperado.' } } else { Write-Host 'MySQL já se encontra ativo.' -ForegroundColor DarkGray } Write-Host 'A guardar uma cópia de segurança da base de dados...' & $phpExecutable (Join-Path $projectPath 'artisan') database:backup if ($LASTEXITCODE -ne 0) { throw 'A cópia de segurança falhou. A demonstração não será iniciada por este script.' } if (-not (Test-LocalUrl "$localUrl/up")) { Write-Host 'A iniciar o servidor Laravel...' $laravel = Start-Process -FilePath $phpExecutable ` -ArgumentList @('artisan', 'serve', '--host=127.0.0.1', '--port=8080') ` -WorkingDirectory $projectPath ` -WindowStyle Hidden ` -RedirectStandardOutput $phpLog ` -RedirectStandardError $phpErrorLog ` -PassThru $state.laravelPid = $laravel.Id $laravelReady = $false for ($attempt = 0; $attempt -lt 30; $attempt++) { Start-Sleep -Seconds 1 if (Test-LocalUrl "$localUrl/up") { $laravelReady = $true break } } if (-not $laravelReady) { throw "O Laravel não ficou disponível. Consulte $phpErrorLog" } } else { Write-Host 'Servidor Laravel já se encontra ativo.' -ForegroundColor DarkGray } $existingTunnel = Get-Process -Name cloudflared -ErrorAction SilentlyContinue | Select-Object -First 1 if ($existingTunnel) { Write-Host 'A substituir o túnel anterior para obter um endereço confirmado...' Stop-Process -Id $existingTunnel.Id -Force Start-Sleep -Seconds 1 } Remove-Item -LiteralPath $tunnelLog, $tunnelErrorLog -Force -ErrorAction SilentlyContinue Write-Host 'A criar o endereço público seguro...' $tunnel = Start-Process -FilePath $cloudflared ` -ArgumentList @('tunnel', '--url', $localUrl, '--no-autoupdate') ` -WindowStyle Hidden ` -RedirectStandardOutput $tunnelLog ` -RedirectStandardError $tunnelErrorLog ` -PassThru $state.cloudflaredPid = $tunnel.Id $publicUrl = $null for ($attempt = 0; $attempt -lt 45; $attempt++) { Start-Sleep -Seconds 1 if (Test-Path -LiteralPath $tunnelErrorLog) { $logText = Get-Content -LiteralPath $tunnelErrorLog -Raw -ErrorAction SilentlyContinue $match = [regex]::Match($logText, 'https://[a-z0-9-]+\.trycloudflare\.com') if ($match.Success) { $publicUrl = $match.Value break } } } if (-not $publicUrl) { throw "Não foi possível obter o endereço público. Consulte $tunnelErrorLog" } # O túnel rápido recebe um subdomínio diferente em cada arranque. Mantém a # aplicação e os callbacks OAuth alinhados com o endereço que acabou de abrir. $envPath = Join-Path $projectPath '.env' if (-not (Test-Path -LiteralPath $envPath)) { throw 'O ficheiro .env não foi encontrado; não é possível alinhar os callbacks OAuth.' } $publicBaseUrl = $publicUrl.TrimEnd('/') $envLines = @(Get-Content -LiteralPath $envPath) foreach ($setting in @( @{ Key = 'APP_URL'; Value = $publicBaseUrl }, @{ Key = 'GOOGLE_REDIRECT_URI'; Value = "$publicBaseUrl/auth/google/callback" }, @{ Key = 'GOOGLE_REGISTER_REDIRECT_URI'; Value = "$publicBaseUrl/auth/google/register/callback" }, @{ Key = 'GOOGLE_CALENDAR_REDIRECT_URI'; Value = "$publicBaseUrl/settings/integrations/google-calendar/callback" } )) { $envLines = Set-EnvValue -Lines $envLines -Key $setting.Key -Value $setting.Value } Set-Content -LiteralPath $envPath -Value $envLines -Encoding UTF8 & $phpExecutable (Join-Path $projectPath 'artisan') config:clear if ($LASTEXITCODE -ne 0) { throw 'Não foi possível limpar a cache de configuração do Laravel.' } $state.publicUrl = $publicUrl $state | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8 Set-Clipboard -Value $publicUrl Write-Host '' Write-Host 'Demonstração disponível:' -ForegroundColor Green Write-Host $publicUrl -ForegroundColor Yellow Write-Host 'Callbacks Google OAuth atualizados para este endereço:' -ForegroundColor DarkGray Write-Host "$publicBaseUrl/auth/google/callback" -ForegroundColor DarkGray Write-Host "$publicBaseUrl/auth/google/register/callback" -ForegroundColor DarkGray Write-Host '' Write-Host 'O endereço foi copiado para a área de transferência.' -ForegroundColor Green Write-Host 'Mantenha este computador ligado e conectado à Internet.' -ForegroundColor DarkGray Write-Host 'Para encerrar, execute parar-demonstracao.ps1.' -ForegroundColor DarkGray