From 74493daedb129bcf0e1bbb39ea1a2b793aa12f5f Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:09:04 -0300 Subject: [PATCH 1/6] feat(app): local startup and crash log Writes startup steps and unhandled exceptions to %LOCALAPPDATA%\ControlFS\logs so startup failures can be diagnosed (no passwords or file contents). Co-Authored-By: Claude Opus 5.5 --- build/Test-Startup.ps1 | 3 ++ src/ControlFS.App/App.cs | 24 ++++++++++++-- src/ControlFS.App/AppLog.cs | 47 +++++++++++++++++++++++++++ src/ControlFS.App/Program.cs | 26 +++++++++++---- src/ControlFS.App/Views/MainWindow.cs | 3 ++ 5 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 src/ControlFS.App/AppLog.cs diff --git a/build/Test-Startup.ps1 b/build/Test-Startup.ps1 index 0d6a5f26..95c8ba75 100644 --- a/build/Test-Startup.ps1 +++ b/build/Test-Startup.ps1 @@ -45,6 +45,9 @@ if (Test-Path $appData) { } if ($alive) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } +Get-ChildItem (Split-Path -Parent $Exe) -File | Where-Object { $_.Extension -in '.pri', '.json' -or $_.Name -like 'Microsoft.UI.Xaml*' -or $_.Name -like 'Microsoft.WindowsAppRuntime*' } | + Select-Object Name, Length | Out-String -Width 200 | Set-Content (Join-Path $OutDir "package-files.txt") +Write-Host "---- arquivos relevantes do pacote ----"; Get-Content (Join-Path $OutDir "package-files.txt") $report | ConvertTo-Json | Set-Content (Join-Path $OutDir "report.json") Write-Host ($report | ConvertTo-Json) Write-Host "---- eventos ----" diff --git a/src/ControlFS.App/App.cs b/src/ControlFS.App/App.cs index a8df7b47..7f512806 100644 --- a/src/ControlFS.App/App.cs +++ b/src/ControlFS.App/App.cs @@ -15,11 +15,29 @@ public sealed class App : Microsoft.UI.Xaml.Application, IXamlMetadataProvider private readonly XamlControlsXamlMetaDataProvider _provider = new(); private MainWindow? _window; + public App() + { + UnhandledException += (_, e) => AppLog.Crash(e.Exception, "Application.UnhandledException: " + e.Message); + AppLog.Info("App criado"); + } + protected override void OnLaunched(LaunchActivatedEventArgs args) { - Resources.MergedDictionaries.Add(new XamlControlsResources()); - _window = new MainWindow(); - _window.Activate(); + try + { + AppLog.Info("OnLaunched: carregando XamlControlsResources"); + Resources.MergedDictionaries.Add(new XamlControlsResources()); + AppLog.Info("OnLaunched: criando janela"); + _window = new MainWindow(); + AppLog.Info("OnLaunched: ativando janela"); + _window.Activate(); + AppLog.Info("Janela ativada"); + } + catch (Exception ex) + { + AppLog.Crash(ex, "App.OnLaunched"); + throw; + } } public IXamlType GetXamlType(Type type) => _provider.GetXamlType(type); diff --git a/src/ControlFS.App/AppLog.cs b/src/ControlFS.App/AppLog.cs new file mode 100644 index 00000000..490a8356 --- /dev/null +++ b/src/ControlFS.App/AppLog.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Reflection; + +namespace ControlFS.App; + +/// +/// Log local mínimo de inicialização e falhas, em %LOCALAPPDATA%\ControlFS\logs (rotação simples por tamanho). +/// Nunca contém senhas nem conteúdo de arquivos; fica só no computador do usuário. +/// +internal static class AppLog +{ + private const long MaxBytes = 512 * 1024; + private static readonly object Gate = new(); + + public static string Directory { get; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ControlFS", "logs"); + + public static void Info(string message) => Write("startup.log", "INFO", message); + + public static void Crash(Exception exception, string context) + { + var detail = $"{context}\n{exception.GetType().FullName} (HRESULT 0x{exception.HResult:X8}): {exception.Message}\n{exception}"; + Write("crash.log", "FATAL", detail); + Write("startup.log", "FATAL", $"{context}: {exception.GetType().Name}: {exception.Message}"); + } + + public static void Session() + { + var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "?"; + Info($"ControlFS {version} · {Environment.OSVersion.VersionString} · {(Environment.Is64BitProcess ? "x64" : "x86")} · .NET {Environment.Version}"); + } + + private static void Write(string file, string level, string message) + { + try + { + lock (Gate) + { + System.IO.Directory.CreateDirectory(Directory); + var path = Path.Join(Directory, file); + if (File.Exists(path) && new FileInfo(path).Length > MaxBytes) File.Move(path, path + ".old", overwrite: true); + File.AppendAllText(path, $"{DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture)} [{level}] {message}{Environment.NewLine}"); + } + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + } +} diff --git a/src/ControlFS.App/Program.cs b/src/ControlFS.App/Program.cs index 633b450f..7a1bc206 100644 --- a/src/ControlFS.App/Program.cs +++ b/src/ControlFS.App/Program.cs @@ -8,12 +8,26 @@ public static class Program [STAThread] public static void Main() { - WinRT.ComWrappersSupport.InitializeComWrappers(); - Microsoft.UI.Xaml.Application.Start(_callbackParams => + AppLog.Session(); + AppDomain.CurrentDomain.UnhandledException += (_, e) => { - // Continuações assíncronas e o AppController rodam na thread de UI. - SynchronizationContext.SetSynchronizationContext(new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread())); - _ = new App(); - }); + if (e.ExceptionObject is Exception ex) AppLog.Crash(ex, "AppDomain.UnhandledException"); + }; + try + { + WinRT.ComWrappersSupport.InitializeComWrappers(); + Microsoft.UI.Xaml.Application.Start(_callbackParams => + { + // Continuações assíncronas e o AppController rodam na thread de UI. + SynchronizationContext.SetSynchronizationContext(new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread())); + _ = new App(); + }); + AppLog.Info("Encerrado normalmente"); + } + catch (Exception ex) + { + AppLog.Crash(ex, "Program.Main"); + throw; + } } } diff --git a/src/ControlFS.App/Views/MainWindow.cs b/src/ControlFS.App/Views/MainWindow.cs index 8121774e..08b3436d 100644 --- a/src/ControlFS.App/Views/MainWindow.cs +++ b/src/ControlFS.App/Views/MainWindow.cs @@ -48,8 +48,10 @@ public MainWindow() _app = new AppController(new LocalFileSystemProvider(), new ArchiveService(), settingsStore, _updates); _input = new InputHost(_app, DispatcherQueue); + AppLog.Info("MainWindow: serviços criados; montando layout"); Content = _root; _root.Content = BuildLayout(); + AppLog.Info("MainWindow: layout montado"); _root.PreviewKeyDown += (_, e) => { if (e.Key == Windows.System.VirtualKey.F11) { ToggleFullScreen(); e.Handled = true; return; } @@ -75,6 +77,7 @@ public MainWindow() _app.ExitRequested += Close; _input.StatusChanged += Render; _app.Start(); + AppLog.Info($"MainWindow: controlador iniciado; entrada: {(_input.BackendReady ? _input.BackendDescription : "SDL indisponível: " + _input.BackendError)}"); } private Grid BuildLayout() From de74129836d22c452e8ada3cd7d49e965dc9b52c Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:13:10 -0300 Subject: [PATCH 2/6] fix(app): generate the app PRI so WinUI finds its theme resources; single-file portable exe The app crashed on startup (0xC000027B, "Cannot locate resource from ms-appx:///Microsoft.UI.Xaml/Themes/themeresources.xaml") because an unpackaged app needs EnableMsixTooling to produce ControlFS.pri. The portable build is now a single ControlFS-Portable-x64.exe that keeps its data in ControlFS_Data next to it. Release and smoke workflows now launch the portable exe and the installed app before anything is published. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 30 +++++----- .github/workflows/smoke.yml | 30 +++++++--- .gitignore | 3 + build/New-ReleaseManifest.ps1 | 2 +- build/Publish-ControlFS.ps1 | 49 ++++++++++------- build/Test-Installed.ps1 | 24 ++++++++ build/Test-Startup.ps1 | 9 +-- src/ControlFS.App/AppLog.cs | 6 +- src/ControlFS.App/AppPaths.cs | 55 +++++++++++++++++++ src/ControlFS.App/ControlFS.App.csproj | 12 ++++ src/ControlFS.App/Views/MainWindow.cs | 5 +- src/ControlFS.Application/AppController.cs | 3 + .../GitHubReleaseUpdateService.cs | 8 +-- 13 files changed, 175 insertions(+), 61 deletions(-) create mode 100644 build/Test-Installed.ps1 create mode 100644 src/ControlFS.App/AppPaths.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e097c5b..b32b8f21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: "### Downloads" "" '- **Installer (recommended):** `ControlFS-Setup-x64.exe` — per-user install, no admin; updates itself automatically.' - '- **Portable:** `ControlFS-Portable-x64.zip` — unzip anywhere and run `ControlFS.exe`; updates are manual.' + '- **Portable:** `ControlFS-Portable-x64.exe` — a single executable; keeps its data in the `ControlFS_Data` folder next to it; updates are manual.' '- **Integrity:** `SHA256SUMS.txt`; `release-manifest.json` + `.sig` are the signed update manifest the app verifies.' '- Windows 11 x64. Not code-signed yet: Windows SmartScreen may warn (see the code signing policy).' "" @@ -94,22 +94,18 @@ jobs: shell: pwsh run: .\build\Publish-ControlFS.ps1 -Runtime x64 -Target All -Version "${{ steps.version.outputs.version }}" - # Instala e desinstala em silêncio numa pasta temporária do runner: prova que o instalador funciona - # sem administrador, grava o marcador de atualização e remove os arquivos ao desinstalar. - - name: Smoke-test installer (silent install and uninstall) + # Abre o app de verdade antes de publicar: o .exe portátil e a versão instalada (instalar, abrir, desinstalar). + - name: Smoke-test portable exe (launch) shell: pwsh run: | - $dir = Join-Path $env:RUNNER_TEMP "controlfs-install" - $p = Start-Process -Wait -PassThru -FilePath dist\ControlFS-Setup-x64.exe -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/SP-',"/DIR=$dir" - if ($p.ExitCode -ne 0) { throw "Instalação falhou (código $($p.ExitCode))" } - foreach ($f in "ControlFS.exe", "ControlFS.installed", "SDL3.dll", "SharpCompress.dll", "unins000.exe") { - if (-not (Test-Path -LiteralPath (Join-Path $dir $f))) { throw "Instalação incompleta: falta $f" } - } - $u = Start-Process -Wait -PassThru -FilePath (Join-Path $dir "unins000.exe") -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' - if ($u.ExitCode -ne 0) { throw "Desinstalação falhou (código $($u.ExitCode))" } - Start-Sleep -Seconds 3 - if (Test-Path -LiteralPath (Join-Path $dir "ControlFS.exe")) { throw "Desinstalação deixou o executável" } - Write-Host "Instalação e desinstalação silenciosas OK" + $dir = Join-Path $env:RUNNER_TEMP "portable" + New-Item -ItemType Directory -Force -Path $dir | Out-Null + Copy-Item dist\ControlFS-Portable-x64.exe (Join-Path $dir "ControlFS.exe") + .\build\Test-Startup.ps1 -Exe (Join-Path $dir "ControlFS.exe") -OutDir smoke-portable -Seconds 40 + + - name: Smoke-test installer (install, launch, uninstall) + shell: pwsh + run: .\build\Test-Installed.ps1 -Setup dist\ControlFS-Setup-x64.exe -OutDir smoke-installed # A chave privada existe só no secret UPDATE_SIGNING_KEY; o script confere a assinatura contra a chave # pública embutida no app e falha se não bater. Sem manifesto assinado, o app não aceita a atualização. @@ -125,7 +121,7 @@ jobs: name: ControlFS-${{ steps.version.outputs.version }} path: | dist/ControlFS-Setup-x64.exe - dist/ControlFS-Portable-x64.zip + dist/ControlFS-Portable-x64.exe dist/SHA256SUMS.txt dist/release-manifest.json dist/release-manifest.json.sig @@ -143,7 +139,7 @@ jobs: body_path: release-notes.md files: | dist/ControlFS-Setup-x64.exe - dist/ControlFS-Portable-x64.zip + dist/ControlFS-Portable-x64.exe dist/SHA256SUMS.txt dist/release-manifest.json dist/release-manifest.json.sig diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index cb5b5523..59bde666 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -41,9 +41,9 @@ jobs: path: smoke-released source: - name: Portable from source + name: Launch portable exe and installed app runs-on: windows-latest - timeout-minutes: 30 + timeout-minutes: 40 env: DOTNET_CLI_TELEMETRY_OPTOUT: "1" DOTNET_NOLOGO: "1" @@ -52,16 +52,28 @@ jobs: - uses: actions/setup-dotnet@v4 with: global-json-file: global.json - - name: Build portable + - name: Install Inno Setup (if missing) shell: pwsh - run: .\build\Publish-ControlFS.ps1 -Runtime x64 -Target Portable -Version 0.0.0-smoke - - name: Launch and collect evidence + run: | + if (-not (Test-Path "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe")) { choco install innosetup -y --no-progress } + - name: Build portable exe and installer + shell: pwsh + run: .\build\Publish-ControlFS.ps1 -Runtime x64 -Target All -Version 0.0.0-smoke + - name: Launch portable exe shell: pwsh run: | - Expand-Archive dist\ControlFS-Portable-x64.zip -DestinationPath (Join-Path $env:RUNNER_TEMP portable) - .\build\Test-Startup.ps1 -Exe (Join-Path $env:RUNNER_TEMP "portable\ControlFS.exe") -OutDir smoke-source + $dir = Join-Path $env:RUNNER_TEMP "portable" + New-Item -ItemType Directory -Force -Path $dir | Out-Null + Copy-Item dist\ControlFS-Portable-x64.exe (Join-Path $dir "ControlFS.exe") + .\build\Test-Startup.ps1 -Exe (Join-Path $dir "ControlFS.exe") -OutDir smoke-portable -Seconds 40 + - name: Install, launch and uninstall + if: always() + shell: pwsh + run: .\build\Test-Installed.ps1 -Setup dist\ControlFS-Setup-x64.exe -OutDir smoke-installed - uses: actions/upload-artifact@v4 if: always() with: - name: smoke-source - path: smoke-source + name: smoke-evidence + path: | + smoke-portable + smoke-installed diff --git a/.gitignore b/.gitignore index 67e0a30d..c2a4b6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ release-notes.md Thumbs.db Desktop.ini *.suo + +# Dados da versão portátil (preferências e logs ao lado do .exe) +ControlFS_Data/ diff --git a/build/New-ReleaseManifest.ps1 b/build/New-ReleaseManifest.ps1 index 05b91f3b..1803ba05 100644 --- a/build/New-ReleaseManifest.ps1 +++ b/build/New-ReleaseManifest.ps1 @@ -27,7 +27,7 @@ $manifest = [ordered]@{ version = $Version releasedAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") installer = Get-Asset "ControlFS-Setup-x64.exe" - portable = Get-Asset "ControlFS-Portable-x64.zip" + portable = Get-Asset "ControlFS-Portable-x64.exe" } $json = $manifest | ConvertTo-Json -Compress -Depth 4 $bytes = [Text.UTF8Encoding]::new($false).GetBytes($json) diff --git a/build/Publish-ControlFS.ps1 b/build/Publish-ControlFS.ps1 index fe6eeb95..939945e5 100644 --- a/build/Publish-ControlFS.ps1 +++ b/build/Publish-ControlFS.ps1 @@ -1,10 +1,11 @@ <# .SYNOPSIS - Publica o ControlFS (win-x64, self-contained) e gera em dist/: - ControlFS-Setup-x64.exe (instalador por usuário), ControlFS-Portable-x64.zip e SHA256SUMS.txt. + Publica o ControlFS (WinUI 3, sem pacote MSIX, self-contained) e gera em dist/: + ControlFS-Portable-x64.exe um único .exe portátil (dados em ControlFS_Data ao lado dele) + ControlFS-Setup-x64.exe instalador por usuário (Inno Setup; dados em %LOCALAPPDATA%\ControlFS) + SHA256SUMS.txt .EXAMPLE - .\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.2 - .\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.2 -Target Portable + .\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.3 #> param( [ValidateSet("x64")] [string] $Runtime = "x64", @@ -15,35 +16,43 @@ $ErrorActionPreference = "Stop" if ($Version -notmatch '^\d+\.\d+\.\d+(-[0-9A-Za-z.\-]+)?$') { throw "Versão inválida: $Version" } $root = Split-Path -Parent $PSScriptRoot -$out = Join-Path $root "artifacts/publish-$Runtime" +$project = Join-Path $root "src/ControlFS.App/ControlFS.App.csproj" +$singleDir = Join-Path $root "artifacts/publish-$Runtime-single" +$appDir = Join-Path $root "artifacts/publish-$Runtime-app" $dist = Join-Path $root "dist" -Remove-Item -LiteralPath $out, $dist -Recurse -Force -ErrorAction SilentlyContinue +Remove-Item -LiteralPath $singleDir, $appDir, $dist -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $dist | Out-Null - -# O RID vem do próprio ControlFS.App.csproj: passar -r aqui propagaria o RID às bibliotecas e quebraria o restore travado. -# Versão numérica para o assembly (sem sufixo de pré-release). $numeric = ($Version -split '-')[0] -dotnet publish (Join-Path $root "src/ControlFS.App/ControlFS.App.csproj") -c Release --self-contained true ` - -p:Platform=$Runtime -p:Version=$Version -p:AssemblyVersion="$numeric.0" -p:FileVersion="$numeric.0" ` - -p:DebugType=none -p:DebugSymbols=false -o $out --nologo -if ($LASTEXITCODE -ne 0) { throw "dotnet publish falhou" } -foreach ($required in "ControlFS.exe", "SDL3.dll", "SharpCompress.dll") { - if (-not (Test-Path -LiteralPath (Join-Path $out $required))) { throw "Pacote incompleto: falta $required" } +function Invoke-Publish([string]$OutDir, [bool]$SingleFile) { + # O RID vem do próprio ControlFS.App.csproj: passar -r aqui propagaria o RID às bibliotecas e quebraria o restore travado. + dotnet publish $project -c Release --self-contained true ` + -p:Platform=$Runtime "-p:PublishSingleFile=$($SingleFile.ToString().ToLowerInvariant())" ` + -p:Version=$Version -p:AssemblyVersion="$numeric.0" -p:FileVersion="$numeric.0" ` + -o $OutDir --nologo + if ($LASTEXITCODE -ne 0) { throw "dotnet publish falhou ($OutDir)" } + if (-not (Test-Path -LiteralPath (Join-Path $OutDir "ControlFS.exe"))) { throw "Publish não gerou ControlFS.exe em $OutDir" } } -Copy-Item -LiteralPath (Join-Path $root "LICENSE"), (Join-Path $root "THIRD_PARTY_NOTICES.md") -Destination $out $files = @() if ($Target -in "All", "Portable") { - $zip = Join-Path $dist "ControlFS-Portable-$Runtime.zip" - Compress-Archive -Path (Join-Path $out "*") -DestinationPath $zip - $files += $zip + Write-Host "Publicando versão portátil (um único .exe)..." + Invoke-Publish $singleDir $true + $portable = Join-Path $dist "ControlFS-Portable-$Runtime.exe" + Copy-Item -LiteralPath (Join-Path $singleDir "ControlFS.exe") -Destination $portable + $files += $portable } if ($Target -in "All", "Installer") { + Write-Host "Publicando versão para o instalador (pasta)..." + Invoke-Publish $appDir $false + foreach ($required in "ControlFS.exe", "ControlFS.pri", "SDL3.dll", "SharpCompress.dll") { + if (-not (Test-Path -LiteralPath (Join-Path $appDir $required))) { throw "Pacote incompleto: falta $required" } + } + Copy-Item -LiteralPath (Join-Path $root "LICENSE"), (Join-Path $root "THIRD_PARTY_NOTICES.md") -Destination $appDir $iscc = @("${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", "$env:ProgramFiles\Inno Setup 6\ISCC.exe", "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe") | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $iscc) { throw "Inno Setup 6 (ISCC.exe) não encontrado." } - & $iscc /Q "/DAppVersion=$Version" "/DNumericVersion=$numeric.0" "/DSourceDir=$out" "/DOutputDir=$dist" (Join-Path $PSScriptRoot "ControlFS.iss") + & $iscc /Q "/DAppVersion=$Version" "/DNumericVersion=$numeric.0" "/DSourceDir=$appDir" "/DOutputDir=$dist" (Join-Path $PSScriptRoot "ControlFS.iss") if ($LASTEXITCODE -ne 0) { throw "ISCC falhou" } $files += Join-Path $dist "ControlFS-Setup-$Runtime.exe" } diff --git a/build/Test-Installed.ps1 b/build/Test-Installed.ps1 new file mode 100644 index 00000000..a3080dc6 --- /dev/null +++ b/build/Test-Installed.ps1 @@ -0,0 +1,24 @@ +<# +.SYNOPSIS + Instala o ControlFS em silêncio numa pasta temporária, confere os arquivos, abre o app instalado + (Test-Startup.ps1) e desinstala. Falha se qualquer etapa falhar. Usado pela CI. +#> +param( + [Parameter(Mandatory = $true)] [string] $Setup, + [Parameter(Mandatory = $true)] [string] $OutDir +) +$ErrorActionPreference = "Stop" +$dir = Join-Path $env:RUNNER_TEMP "controlfs-install" +$p = Start-Process -Wait -PassThru -FilePath $Setup -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-', "/DIR=$dir" +if ($p.ExitCode -ne 0) { throw "Instalação falhou (código $($p.ExitCode))" } +foreach ($f in "ControlFS.exe", "ControlFS.pri", "ControlFS.installed", "SDL3.dll", "SharpCompress.dll", "unins000.exe") { + if (-not (Test-Path -LiteralPath (Join-Path $dir $f))) { throw "Instalação incompleta: falta $f" } +} +& (Join-Path $PSScriptRoot "Test-Startup.ps1") -Exe (Join-Path $dir "ControlFS.exe") -OutDir $OutDir +$startupOk = $LASTEXITCODE -eq 0 +$u = Start-Process -Wait -PassThru -FilePath (Join-Path $dir "unins000.exe") -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART' +if ($u.ExitCode -ne 0) { throw "Desinstalação falhou (código $($u.ExitCode))" } +Start-Sleep -Seconds 3 +if (Test-Path -LiteralPath (Join-Path $dir "ControlFS.exe")) { throw "Desinstalação deixou o executável" } +if (-not $startupOk) { throw "O app instalado não abriu" } +Write-Host "Instalar, abrir e desinstalar: OK" diff --git a/build/Test-Startup.ps1 b/build/Test-Startup.ps1 index 95c8ba75..a36519f1 100644 --- a/build/Test-Startup.ps1 +++ b/build/Test-Startup.ps1 @@ -11,6 +11,7 @@ param( $ErrorActionPreference = "Continue" New-Item -ItemType Directory -Force -Path $OutDir | Out-Null $started = Get-Date +Remove-Item (Join-Path $env:LOCALAPPDATA "ControlFS\logs"), (Join-Path (Split-Path -Parent $Exe) "ControlFS_Data\logs") -Recurse -Force -ErrorAction SilentlyContinue $p = Start-Process -FilePath $Exe -WorkingDirectory (Split-Path -Parent $Exe) -PassThru Start-Sleep -Seconds $Seconds $p.Refresh() @@ -38,10 +39,10 @@ $events = Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = Where-Object { $_.ProviderName -in '.NET Runtime', 'Application Error', 'Windows Error Reporting', 'SideBySide', 'Application Hang' } $events | Format-List TimeCreated, ProviderName, Id, Message | Out-String -Width 400 | Set-Content (Join-Path $OutDir "events.txt") -$appData = Join-Path $env:LOCALAPPDATA "ControlFS" -if (Test-Path $appData) { - Get-ChildItem $appData -Recurse | Select-Object FullName, Length | Out-String -Width 400 | Set-Content (Join-Path $OutDir "appdata.txt") - Get-ChildItem (Join-Path $appData "logs") -Filter *.log -ErrorAction SilentlyContinue | ForEach-Object { Copy-Item $_.FullName $OutDir } +foreach ($data in (Join-Path $env:LOCALAPPDATA "ControlFS"), (Join-Path (Split-Path -Parent $Exe) "ControlFS_Data")) { + if (-not (Test-Path $data)) { continue } + Get-ChildItem $data -Recurse | Select-Object FullName, Length | Out-String -Width 400 | Add-Content (Join-Path $OutDir "appdata.txt") + Get-ChildItem (Join-Path $data "logs") -Filter *.log -ErrorAction SilentlyContinue | ForEach-Object { Copy-Item $_.FullName $OutDir -Force } } if ($alive) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } diff --git a/src/ControlFS.App/AppLog.cs b/src/ControlFS.App/AppLog.cs index 490a8356..8a2a3aed 100644 --- a/src/ControlFS.App/AppLog.cs +++ b/src/ControlFS.App/AppLog.cs @@ -4,7 +4,7 @@ namespace ControlFS.App; /// -/// Log local mínimo de inicialização e falhas, em %LOCALAPPDATA%\ControlFS\logs (rotação simples por tamanho). +/// Log local mínimo de inicialização e falhas, em <pasta de dados>\logs (rotação simples por tamanho). /// Nunca contém senhas nem conteúdo de arquivos; fica só no computador do usuário. /// internal static class AppLog @@ -12,7 +12,7 @@ internal static class AppLog private const long MaxBytes = 512 * 1024; private static readonly object Gate = new(); - public static string Directory { get; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ControlFS", "logs"); + public static string Directory => Path.Join(AppPaths.DataDirectory, "logs"); public static void Info(string message) => Write("startup.log", "INFO", message); @@ -26,7 +26,7 @@ public static void Crash(Exception exception, string context) public static void Session() { var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "?"; - Info($"ControlFS {version} · {Environment.OSVersion.VersionString} · {(Environment.Is64BitProcess ? "x64" : "x86")} · .NET {Environment.Version}"); + Info($"ControlFS {version} · {(AppPaths.IsInstalled ? "instalado" : "portátil")} · {Environment.OSVersion.VersionString} · {(Environment.Is64BitProcess ? "x64" : "x86")} · .NET {Environment.Version}"); } private static void Write(string file, string level, string message) diff --git a/src/ControlFS.App/AppPaths.cs b/src/ControlFS.App/AppPaths.cs new file mode 100644 index 00000000..717d0bf3 --- /dev/null +++ b/src/ControlFS.App/AppPaths.cs @@ -0,0 +1,55 @@ +namespace ControlFS.App; + +/// +/// Onde o app guarda dados. Instalado (marcador ControlFS.installed ao lado do .exe): %LOCALAPPDATA%\ControlFS. +/// Portátil: pasta ControlFS_Data ao lado do .exe; se ela não aceitar escrita, usa %LOCALAPPDATA%\ControlFS e avisa. +/// Usa o caminho do processo (não AppContext.BaseDirectory), que no .exe único aponta para a pasta de extração. +/// +internal static class AppPaths +{ + public const string InstalledMarker = "ControlFS.installed"; + public const string PortableDataFolder = "ControlFS_Data"; + + static AppPaths() + { + ExeDirectory = Path.GetDirectoryName(Environment.ProcessPath) ?? AppContext.BaseDirectory; + IsInstalled = File.Exists(Path.Join(ExeDirectory, InstalledMarker)); + var local = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ControlFS"); + if (IsInstalled) + { + DataDirectory = local; + return; + } + var portable = Path.Join(ExeDirectory, PortableDataFolder); + if (IsWritable(portable)) + { + DataDirectory = portable; + } + else + { + DataDirectory = local; + Notice = $"A pasta do ControlFS não permite gravação; preferências e logs ficam em {local}."; + } + } + + public static string ExeDirectory { get; } + public static bool IsInstalled { get; } + public static string DataDirectory { get; } + public static string? Notice { get; } + + private static bool IsWritable(string directory) + { + try + { + Directory.CreateDirectory(directory); + var probe = Path.Join(directory, ".write-test-" + Guid.NewGuid().ToString("N")); + File.WriteAllText(probe, string.Empty); + File.Delete(probe); + return true; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return false; + } + } +} diff --git a/src/ControlFS.App/ControlFS.App.csproj b/src/ControlFS.App/ControlFS.App.csproj index 314d5e7e..a70c6512 100644 --- a/src/ControlFS.App/ControlFS.App.csproj +++ b/src/ControlFS.App/ControlFS.App.csproj @@ -14,9 +14,21 @@ x64 win-x64 None + + true $(DefineConstants);DISABLE_XAML_GENERATED_MAIN app.manifest + + + true + true + true + embedded + false + false + true diff --git a/src/ControlFS.App/Views/MainWindow.cs b/src/ControlFS.App/Views/MainWindow.cs index 08b3436d..7433b05a 100644 --- a/src/ControlFS.App/Views/MainWindow.cs +++ b/src/ControlFS.App/Views/MainWindow.cs @@ -43,8 +43,8 @@ public sealed class MainWindow : Window public MainWindow() { Title = "ControlFS"; - var settingsStore = new JsonSettingsStore(JsonSettingsStore.DefaultDirectory()); - _updates = GitHubReleaseUpdateService.CreateDefault(); + var settingsStore = new JsonSettingsStore(AppPaths.DataDirectory); + _updates = GitHubReleaseUpdateService.CreateDefault(AppPaths.IsInstalled, Path.Join(AppPaths.DataDirectory, "updates")); _app = new AppController(new LocalFileSystemProvider(), new ArchiveService(), settingsStore, _updates); _input = new InputHost(_app, DispatcherQueue); @@ -77,6 +77,7 @@ public MainWindow() _app.ExitRequested += Close; _input.StatusChanged += Render; _app.Start(); + if (AppPaths.Notice is { } notice) _app.ShowNotice(notice); AppLog.Info($"MainWindow: controlador iniciado; entrada: {(_input.BackendReady ? _input.BackendDescription : "SDL indisponível: " + _input.BackendError)}"); } diff --git a/src/ControlFS.Application/AppController.cs b/src/ControlFS.Application/AppController.cs index f1002faa..bef1dad7 100644 --- a/src/ControlFS.Application/AppController.cs +++ b/src/ControlFS.Application/AppController.cs @@ -365,6 +365,9 @@ internal void Track(Task task) internal void RaiseChanged() => Changed?.Invoke(); + /// Aviso não modal no rodapé (ex.: pasta de dados alternativa no modo portátil). + public void ShowNotice(string message) => SetStatus(message); + internal void SetStatus(string message) { StatusMessage = message; diff --git a/src/ControlFS.Infrastructure.Updates/GitHubReleaseUpdateService.cs b/src/ControlFS.Infrastructure.Updates/GitHubReleaseUpdateService.cs index a348ee96..580020eb 100644 --- a/src/ControlFS.Infrastructure.Updates/GitHubReleaseUpdateService.cs +++ b/src/ControlFS.Infrastructure.Updates/GitHubReleaseUpdateService.cs @@ -33,14 +33,12 @@ public GitHubReleaseUpdateService(ReleaseVersion currentVersion, bool isInstalle public ReleaseVersion CurrentVersion { get; } public bool IsInstalled { get; } - /// Configuração padrão do app: versão do assembly, marcador do instalador e %LOCALAPPDATA%\ControlFS\updates. - public static GitHubReleaseUpdateService CreateDefault() + /// Configuração do app: versão do assembly; instalado/portátil e pasta de downloads decididos pelo app. + public static GitHubReleaseUpdateService CreateDefault(bool isInstalled, string downloadDirectory) { var informational = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion; var version = ReleaseVersion.TryParse(informational, out var v) ? v : ReleaseVersion.Parse("0.0.0"); - var installed = File.Exists(Path.Join(AppContext.BaseDirectory, InstalledMarkerName)); - var dir = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ControlFS", "updates"); - return new GitHubReleaseUpdateService(version, installed, dir); + return new GitHubReleaseUpdateService(version, isInstalled, downloadDirectory); } public async Task CheckAsync(bool includePrereleases, CancellationToken cancellationToken) From 5f3d7bccb1a6f780abd7d8488a8c3a56c1f5da1c Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:14:15 -0300 Subject: [PATCH 3/6] fix(build): publish the single-file portable without the locked restore PublishSingleFile adds an implicit Microsoft.NET.ILLink.Tasks reference from the SDK that the lock files don't list. The installer build (published first) and CI keep the locked restore. Co-Authored-By: Claude Opus 5.5 --- build/Publish-ControlFS.ps1 | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/build/Publish-ControlFS.ps1 b/build/Publish-ControlFS.ps1 index 939945e5..ae7ddbf5 100644 --- a/build/Publish-ControlFS.ps1 +++ b/build/Publish-ControlFS.ps1 @@ -26,22 +26,19 @@ $numeric = ($Version -split '-')[0] function Invoke-Publish([string]$OutDir, [bool]$SingleFile) { # O RID vem do próprio ControlFS.App.csproj: passar -r aqui propagaria o RID às bibliotecas e quebraria o restore travado. + # PublishSingleFile acrescenta implicitamente Microsoft.NET.ILLink.Tasks (do próprio SDK), que os lock files não + # listam. Só nesta publicação o restore não é travado nem reescreve os lock files; as versões continuam fixadas + # em Directory.Packages.props e o build da CI segue em modo travado. + $lockArgs = if ($SingleFile) { @("-p:RestoreLockedMode=false", "-p:RestorePackagesWithLockFile=false") } else { @() } dotnet publish $project -c Release --self-contained true ` -p:Platform=$Runtime "-p:PublishSingleFile=$($SingleFile.ToString().ToLowerInvariant())" ` -p:Version=$Version -p:AssemblyVersion="$numeric.0" -p:FileVersion="$numeric.0" ` - -o $OutDir --nologo + @lockArgs -o $OutDir --nologo if ($LASTEXITCODE -ne 0) { throw "dotnet publish falhou ($OutDir)" } if (-not (Test-Path -LiteralPath (Join-Path $OutDir "ControlFS.exe"))) { throw "Publish não gerou ControlFS.exe em $OutDir" } } $files = @() -if ($Target -in "All", "Portable") { - Write-Host "Publicando versão portátil (um único .exe)..." - Invoke-Publish $singleDir $true - $portable = Join-Path $dist "ControlFS-Portable-$Runtime.exe" - Copy-Item -LiteralPath (Join-Path $singleDir "ControlFS.exe") -Destination $portable - $files += $portable -} if ($Target -in "All", "Installer") { Write-Host "Publicando versão para o instalador (pasta)..." Invoke-Publish $appDir $false @@ -57,6 +54,13 @@ if ($Target -in "All", "Installer") { $files += Join-Path $dist "ControlFS-Setup-$Runtime.exe" } +if ($Target -in "All", "Portable") { + Write-Host "Publicando versão portátil (um único .exe)..." + Invoke-Publish $singleDir $true + $portable = Join-Path $dist "ControlFS-Portable-$Runtime.exe" + Copy-Item -LiteralPath (Join-Path $singleDir "ControlFS.exe") -Destination $portable + $files += $portable +} # LF explícito: "sha256sum -c" / "shasum -c" falham com CRLF no nome do arquivo. $lines = foreach ($f in $files) { "$((Get-FileHash -LiteralPath $f -Algorithm SHA256).Hash.ToLowerInvariant()) $(Split-Path -Leaf $f)" } [IO.File]::WriteAllText((Join-Path $dist "SHA256SUMS.txt"), (($lines -join "`n") + "`n"), [Text.Encoding]::ASCII) From 8059a26d35e1fc0d024c6c647c32642a8e3f5b3f Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:17:54 -0300 Subject: [PATCH 4/6] fix(build): keep lock files for the single-file restore; explicit success exit in startup test Co-Authored-By: Claude Opus 5.5 --- build/Publish-ControlFS.ps1 | 6 +++--- build/Test-Startup.ps1 | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build/Publish-ControlFS.ps1 b/build/Publish-ControlFS.ps1 index ae7ddbf5..3d67b5c1 100644 --- a/build/Publish-ControlFS.ps1 +++ b/build/Publish-ControlFS.ps1 @@ -27,9 +27,9 @@ $numeric = ($Version -split '-')[0] function Invoke-Publish([string]$OutDir, [bool]$SingleFile) { # O RID vem do próprio ControlFS.App.csproj: passar -r aqui propagaria o RID às bibliotecas e quebraria o restore travado. # PublishSingleFile acrescenta implicitamente Microsoft.NET.ILLink.Tasks (do próprio SDK), que os lock files não - # listam. Só nesta publicação o restore não é travado nem reescreve os lock files; as versões continuam fixadas - # em Directory.Packages.props e o build da CI segue em modo travado. - $lockArgs = if ($SingleFile) { @("-p:RestoreLockedMode=false", "-p:RestorePackagesWithLockFile=false") } else { @() } + # listam. Só nesta publicação (feita por último) o restore não é travado; as versões continuam fixadas em + # Directory.Packages.props, o instalador é publicado antes em modo travado e o build da CI segue travado. + $lockArgs = if ($SingleFile) { @("-p:RestoreLockedMode=false") } else { @() } dotnet publish $project -c Release --self-contained true ` -p:Platform=$Runtime "-p:PublishSingleFile=$($SingleFile.ToString().ToLowerInvariant())" ` -p:Version=$Version -p:AssemblyVersion="$numeric.0" -p:FileVersion="$numeric.0" ` diff --git a/build/Test-Startup.ps1 b/build/Test-Startup.ps1 index a36519f1..011cf7e2 100644 --- a/build/Test-Startup.ps1 +++ b/build/Test-Startup.ps1 @@ -56,3 +56,4 @@ Get-Content (Join-Path $OutDir "events.txt") | Select-Object -First 80 Get-ChildItem $OutDir -Filter *.log | ForEach-Object { Write-Host "---- $($_.Name) ----"; Get-Content $_.FullName | Select-Object -First 120 } if (-not $alive -or $report.mainWindowHandle -eq 0) { Write-Host "::error::ControlFS não ficou aberto com janela"; exit 1 } Write-Host "ControlFS abriu e manteve a janela por $Seconds s" +exit 0 From cc1bb6f1ddf42840cb9ad715977ede57c670e62d Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:22:03 -0300 Subject: [PATCH 5/6] fix(build): pass publish arguments as an explicit list Co-Authored-By: Claude Opus 5.5 --- build/Publish-ControlFS.ps1 | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/build/Publish-ControlFS.ps1 b/build/Publish-ControlFS.ps1 index 3d67b5c1..d412a959 100644 --- a/build/Publish-ControlFS.ps1 +++ b/build/Publish-ControlFS.ps1 @@ -29,11 +29,13 @@ function Invoke-Publish([string]$OutDir, [bool]$SingleFile) { # PublishSingleFile acrescenta implicitamente Microsoft.NET.ILLink.Tasks (do próprio SDK), que os lock files não # listam. Só nesta publicação (feita por último) o restore não é travado; as versões continuam fixadas em # Directory.Packages.props, o instalador é publicado antes em modo travado e o build da CI segue travado. - $lockArgs = if ($SingleFile) { @("-p:RestoreLockedMode=false") } else { @() } - dotnet publish $project -c Release --self-contained true ` - -p:Platform=$Runtime "-p:PublishSingleFile=$($SingleFile.ToString().ToLowerInvariant())" ` - -p:Version=$Version -p:AssemblyVersion="$numeric.0" -p:FileVersion="$numeric.0" ` - @lockArgs -o $OutDir --nologo + $pubArgs = [System.Collections.Generic.List[string]]@( + "publish", $project, "-c", "Release", "--self-contained", "true", + "-p:Platform=$Runtime", "-p:PublishSingleFile=$($SingleFile.ToString().ToLowerInvariant())", + "-p:Version=$Version", "-p:AssemblyVersion=$numeric.0", "-p:FileVersion=$numeric.0", + "-o", $OutDir, "--nologo") + if ($SingleFile) { $pubArgs.Add("-p:RestoreLockedMode=false") } + & dotnet @pubArgs if ($LASTEXITCODE -ne 0) { throw "dotnet publish falhou ($OutDir)" } if (-not (Test-Path -LiteralPath (Join-Path $OutDir "ControlFS.exe"))) { throw "Publish não gerou ControlFS.exe em $OutDir" } } From 9fc167e4f943c81626f99e3963f8b962d6d68772 Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:27:24 -0300 Subject: [PATCH 6/6] docs: 0.1.0-alpha.3 notes (startup fix, single-file portable) Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.en-US.md | 13 +++++++++++++ CHANGELOG.md | 13 +++++++++++++ PROGRESS.md | 12 ++++++++++++ README.md | 6 +++--- README.pt-BR.md | 6 +++--- docs/GUIDE.md | 5 +++-- docs/GUIDE.pt-BR.md | 5 +++-- docs/TESTING.md | 2 +- docs/build-and-release.md | 6 ++++-- 9 files changed, 55 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index e15eb197..0e83039e 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -4,6 +4,19 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor ## [Unreleased] +## [0.1.0-alpha.3] +### Fixes +- The app closed right after opening (installed and portable) in 0.1.0-alpha.1 and 0.1.0-alpha.2: the app resource file (`ControlFS.pri`) was missing and WinUI couldn't find its styles. Fixed. + +### What's new +- Single-file portable version, `ControlFS-Portable-x64.exe`: keeps settings and logs in the `ControlFS_Data` folder next to it (if that folder isn't writable, it uses `%LOCALAPPDATA%\ControlFS` and tells you). +- Local startup and crash log in `logs` inside the data folder, to diagnose problems (no passwords or file contents). +- Every release is now published only after CI actually opens the portable and the installed app on Windows and confirms the window shows up. + +### Updating +- From 0.1.0-alpha.1 or 0.1.0-alpha.2: those versions didn't open, so they can't update themselves. Download and run `ControlFS-Setup-x64.exe` once. + + ## [0.1.0-alpha.2] ### What's new - Windows installer (`ControlFS-Setup-x64.exe`): per-user install, no admin, Start menu shortcut and an optional desktop shortcut. diff --git a/CHANGELOG.md b/CHANGELOG.md index b1eeebaf..9ad58a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH ## [Unreleased] +## [0.1.0-alpha.3] +### Correções +- O app fechava logo ao abrir (instalado e portátil) nas versões 0.1.0-alpha.1 e 0.1.0-alpha.2: faltava o arquivo de recursos do app (`ControlFS.pri`) e o WinUI não encontrava seus estilos. Corrigido. + +### Novidades +- Versão portátil em um único arquivo, `ControlFS-Portable-x64.exe`: guarda preferências e logs na pasta `ControlFS_Data` ao lado dele (se a pasta não aceitar gravação, usa `%LOCALAPPDATA%\ControlFS` e avisa). +- Log local de inicialização e falhas em `logs` dentro da pasta de dados, para diagnosticar problemas (sem senhas nem conteúdo de arquivos). +- Cada release agora só é publicada depois que a CI abre de verdade o portátil e a versão instalada num Windows e confirma que a janela aparece. + +### Atualizando +- Da 0.1.0-alpha.1 ou 0.1.0-alpha.2: essas versões não abriam, então não conseguem se atualizar sozinhas. Baixe e rode o `ControlFS-Setup-x64.exe` uma vez. + + ## [0.1.0-alpha.2] ### Novidades - Instalador para Windows (`ControlFS-Setup-x64.exe`): instala por usuário, sem administrador, com atalho no menu Iniciar e opção de atalho na Área de Trabalho. diff --git a/PROGRESS.md b/PROGRESS.md index 3a74d19b..e54fc62f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -30,6 +30,18 @@ não foi aberto numa sessão Windows interativa**. Nenhum controle físico foi t - **Configuração:** JSON versionado com gravação atômica e recuperação de arquivo corrompido. - **Documentação e CI:** todos os documentos exigidos; ADRs 0001–0004; workflow de CI (não executado). +## Correção de inicialização (0.1.0-alpha.3) + +- **As releases 0.1.0-alpha.1 e 0.1.0-alpha.2 não abriam** (instalado e portátil): o processo terminava em ~1 s com + `0xC000027B` em `Microsoft.UI.Xaml.dll`. Reproduzido pelo novo workflow `smoke.yml` num Windows do GitHub; o log + novo mostrou `Cannot locate resource from 'ms-appx:///Microsoft.UI.Xaml/Themes/themeresources.xaml'`. Causa: app sem + pacote MSIX precisa de `EnableMsixTooling=true` para gerar `ControlFS.pri`. Os testes anteriores cobriam a lógica, + mas nunca abriam o app — lacuna fechada: CI e release agora abrem o portátil e o instalado antes de publicar. +- Evidência pós-correção (runner `windows-latest`, Windows Server 2025 26100): portátil `.exe` único e app instalado + abrem, mantêm a janela "ControlFS" (40 s e 25 s), log até "Janela ativada", SDL 3.5.0 carregado; print mostra a + interface desenhada e o aviso de nova versão vindo do GitHub real. Instalar → abrir → desinstalar: OK. +- Portátil agora é `ControlFS-Portable-x64.exe` (arquivo único) com dados em `ControlFS_Data` ao lado. + ## Instalador e atualizações (0.1.0-alpha.2) - Instalador Inno Setup por usuário, sem administrador (`build/ControlFS.iss`), com marcador que habilita atualização. diff --git a/README.md b/README.md index cd2dd61a..04825950 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ A **file manager for Windows made for the controller**, with a **built-in extrac ## Download -Get **[0.1.0-alpha.2](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.2)** (pre-release): +Get **[0.1.0-alpha.3](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.3)** (pre-release): - **`ControlFS-Setup-x64.exe`** (recommended): per-user install, no admin, **updates itself automatically** (verified, signed updates). -- **`ControlFS-Portable-x64.zip`**: unzip anywhere and run `ControlFS.exe`; it tells you about new versions, replacing it is manual. +- **`ControlFS-Portable-x64.exe`**: a single executable that keeps its data in the `ControlFS_Data` folder next to it; it tells you about new versions, replacing it is manual. -Windows 11 x64. Not code-signed yet, so SmartScreen may warn ([policy](docs/CODE_SIGNING.md)). On 0.1.0-alpha.1? Run the installer once; updates are automatic from then on. +Windows 11 x64. Not code-signed yet, so SmartScreen may warn ([policy](docs/CODE_SIGNING.md)). On 0.1.0-alpha.1 or alpha.2? Those didn't open: run the installer once; updates are automatic from then on. ## How it works diff --git a/README.pt-BR.md b/README.pt-BR.md index 0e9669b8..9bc8090d 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -13,12 +13,12 @@ Um **gerenciador de arquivos para Windows feito para o controle**, com **extrato ## Download -Baixe a **[0.1.0-alpha.2](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.2)** (pré-lançamento): +Baixe a **[0.1.0-alpha.3](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.3)** (pré-lançamento): - **`ControlFS-Setup-x64.exe`** (recomendado): instala por usuário, sem admin, e **se atualiza sozinho** (atualizações assinadas e verificadas). -- **`ControlFS-Portable-x64.zip`**: descompacte em qualquer pasta e rode o `ControlFS.exe`; ele avisa de novas versões, a troca é manual. +- **`ControlFS-Portable-x64.exe`**: um único executável que guarda os dados na pasta `ControlFS_Data` ao lado dele; avisa de novas versões, a troca é manual. -Windows 11 x64. Ainda sem assinatura de código, então o SmartScreen pode avisar ([política](docs/CODE_SIGNING.md)). Está na 0.1.0-alpha.1? Rode o instalador uma vez; daí em diante as atualizações são automáticas. +Windows 11 x64. Ainda sem assinatura de código, então o SmartScreen pode avisar ([política](docs/CODE_SIGNING.md)). Está na 0.1.0-alpha.1 ou alpha.2? Elas não abriam: rode o instalador uma vez; daí em diante as atualizações são automáticas. ## Como funciona diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 0c37a7a0..6566e047 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -48,10 +48,11 @@ The **portable** version only tells you a new version exists; download it from t ## Privacy -Everything stays on your PC. Settings live in `%LOCALAPPDATA%\ControlFS\settings.json`. Passwords are never saved or logged. Core features never use the network. The only network access is the update check, which sends nothing but a `ControlFS/` User-Agent to GitHub and can be turned off. +Everything stays on your PC. Settings and logs live in `%LOCALAPPDATA%\ControlFS` (installed) or in `ControlFS_Data` next to `ControlFS-Portable-x64.exe` (portable). Passwords are never saved or logged. Core features never use the network. The only network access is the update check, which sends nothing but a `ControlFS/` User-Agent to GitHub and can be turned off. ## Troubleshooting +- **The app doesn't open:** send us `logs\startup.log` and `logs\crash.log` from the data folder above (they contain no passwords or file contents). - **No controller detected:** plug it in and press a button; the header shows the active pad. Keyboard and mouse always work. - **Windows SmartScreen warning:** the build isn't code-signed yet ([policy](CODE_SIGNING.md)). - **"Format recognized but not supported":** only ZIP is supported for now. @@ -66,5 +67,5 @@ dotnet build ControlFS.slnx dotnet test ControlFS.slnx ``` -The app runs only on Windows 11 x64. Installer + portable (needs Inno Setup 6): `.\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.2`. +The app runs only on Windows 11 x64. Installer + portable (needs Inno Setup 6): `.\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.3`. More in [build-and-release.md](build-and-release.md) (Portuguese). diff --git a/docs/GUIDE.pt-BR.md b/docs/GUIDE.pt-BR.md index 6ab900d7..77b9a563 100644 --- a/docs/GUIDE.pt-BR.md +++ b/docs/GUIDE.pt-BR.md @@ -48,10 +48,11 @@ A versão **portátil** só avisa que existe versão nova; baixe-a na página da ## Privacidade -Tudo fica no seu PC. As preferências ficam em `%LOCALAPPDATA%\ControlFS\settings.json`. Senhas nunca são gravadas nem registradas. As funções centrais nunca usam a rede. O único acesso à rede é a verificação de atualizações, que envia ao GitHub apenas o User-Agent `ControlFS/` e pode ser desligada. +Tudo fica no seu PC. Preferências e logs ficam em `%LOCALAPPDATA%\ControlFS` (instalado) ou em `ControlFS_Data` ao lado do `ControlFS-Portable-x64.exe` (portátil). Senhas nunca são gravadas nem registradas. As funções centrais nunca usam a rede. O único acesso à rede é a verificação de atualizações, que envia ao GitHub apenas o User-Agent `ControlFS/` e pode ser desligada. ## Solução de problemas +- **O app não abre:** envie `logs\startup.log` e `logs\crash.log` da pasta de dados acima (não contêm senhas nem conteúdo de arquivos). - **Controle não detectado:** conecte e aperte um botão; o cabeçalho mostra o controle ativo. Teclado e mouse sempre funcionam. - **Aviso do SmartScreen:** o build ainda não tem assinatura de código ([política](CODE_SIGNING.md)). - **"Formato reconhecido, mas não suportado":** por enquanto só ZIP. @@ -66,5 +67,5 @@ dotnet build ControlFS.slnx dotnet test ControlFS.slnx ``` -O app roda só no Windows 11 x64. Instalador + portátil (requer Inno Setup 6): `.\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.2`. +O app roda só no Windows 11 x64. Instalador + portátil (requer Inno Setup 6): `.\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.3`. Mais em [build-and-release.md](build-and-release.md). diff --git a/docs/TESTING.md b/docs/TESTING.md index b5c13bc8..8fd7ee5a 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -5,7 +5,7 @@ versão, Windows, controle e conexão. Resultados de controles vão para `contro ## Antes de cada release -- [ ] Baixar `ControlFS-Portable-x64.zip` da release, conferir o SHA-256 com `SHA256SUMS.txt`, descompactar e abrir `ControlFS.exe` numa conta padrão. +- [ ] Baixar `ControlFS-Portable-x64.exe` da release, conferir o SHA-256 com `SHA256SUMS.txt` e abrir numa conta padrão; a pasta `ControlFS_Data` aparece ao lado. - [ ] Tela inicial mostra Downloads/Documentos reais e as unidades. - [ ] Teclado físico: setas, Enter, Esc, Espaço, F2, F10, F11 (tela cheia). - [ ] Conectar um controle **depois** de abrir o app: o cabeçalho mostra o controle ao apertar um botão. diff --git a/docs/build-and-release.md b/docs/build-and-release.md index 7320df02..d8209f27 100644 --- a/docs/build-and-release.md +++ b/docs/build-and-release.md @@ -40,8 +40,10 @@ artifacts\ControlFS-win-x64\ControlFS.exe - `release.yml`: push de tag `vX.Y.Z` ou `vX.Y.Z-pre.N` roda os testes, gera `ControlFS-Portable-x64.zip` + `SHA256SUMS.txt` com `build/Publish-ControlFS.ps1` e publica a release (pré-lançamento quando há sufixo). As notas vêm de `CHANGELOG.en-US.md`, com link para `CHANGELOG.md`; o workflow falha se faltar a seção em algum dos dois. -- Release inclui também `ControlFS-Setup-x64.exe` (Inno Setup, por usuário), testado por instalação e desinstalação - silenciosas no runner, e `release-manifest.json` + `.sig` assinados com o secret `UPDATE_SIGNING_KEY` +- Release inclui `ControlFS-Setup-x64.exe` (Inno Setup, por usuário) e `ControlFS-Portable-x64.exe` (arquivo único). + Antes de publicar, a CI **abre de verdade** o portátil e o app instalado (`build/Test-Startup.ps1`, + `build/Test-Installed.ps1`: processo vivo, janela, eventos do Windows, logs e print). O workflow `smoke.yml` faz o + mesmo em PRs, na `main` e sob demanda para uma release já publicada. Inclui também e `release-manifest.json` + `.sig` assinados com o secret `UPDATE_SIGNING_KEY` (`build/New-ReleaseManifest.ps1` confere a assinatura com a chave pública do app antes de publicar). - `codeql.yml` (C# e workflows) e `dependabot.yml` (NuGet e Actions, mensal, agrupado).