diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8bc8b..850f222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- **Orphaned `scrcpy` dependency dropped from the Android Devices module.** `AndroidDevicesModule` declared `scrcpy` (GitHub-sourced, `Genymobile/scrcpy`) as a `ModuleDependency`, but nothing in the app has invoked `scrcpy.exe` since the ws-scrcpy-web sidecar replaced the native-scrcpy mirror in Phase 8a (2026-04-11). Impact: (1) drops a per-dependency-sync GitHub releases API call; (2) removing `scripts/dependencies/fetch-scrcpy.ps1` (auto-discovered by `stage-seed.ps1`) stops bundling the ~40 MB `scrcpy-win64` payload — `scrcpy.exe` + SDL/ffmpeg DLLs + `scrcpy-server` + a duplicate `adb.exe` — into every MSI; (3) `SeedHydrator` now prunes a pre-existing `\dependencies\scrcpy\` on launch (best-effort, logged) so existing installs reclaim the space on upgrade. Synthetic test fixtures that used `"scrcpy"` as a dependency name were renamed to `"fake-tool"`. README dependency tables updated to match. + ### Fixed - **`release.yml` publish-job download-artifact path** — added explicit `name: windows-final` + `path: artifacts/windows-final/` to the `actions/download-artifact@v8.0.1` step. **Symptom:** v1.1.0's release.yml run completed with all 3 jobs green, but the published v1.1.0 GitHub Release contained only `SHA256SUMS` (195 bytes); the MSI + nupkg + releases.stable.json were missing despite being present in the workflow artifact. **Root cause:** `actions/download-artifact@v8` with no `name:` filter and `path: artifacts/` flattens a single artifact's files directly into `artifacts/` (no auto-subdirectory), but the `softprops/action-gh-release@v3.0.0` globs below targeted `artifacts/windows-final/*.msi` / `*.nupkg` / `releases..json` — zero matches, silent skip per softprops's "Pattern '...' does not match any files" advisory. Drift introduced when Dependabot bumped download-artifact (PR #4, v4 → v8) on 2026-05-18 — v4 may have behaved differently for single-artifact downloads; either way, the explicit `name:` + nested `path:` is the deterministic fix that also future-proofs against any version-specific behavior. **Recovery:** v1.1.0 release patched 2026-05-19 via manual `gh run download` + `gh release upload` of the original artifact bytes (SHAs verified against existing SHA256SUMS); no rebuild required, original Sigstore attestation unchanged + still verifies against the patched MSI. diff --git a/README.md b/README.md index d2751e8..766068e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Control Menu replaces a collection of PowerShell scripts with a cross-platform w - **Cameras** — View LTS/Hikvision CCTV cameras via [go2rtc](https://github.com/AlexxIT/go2rtc) RTSP-to-browser streaming. Configurable camera count with encrypted credential storage. go2rtc is auto-installed and updated via the dependency manager. - **Jellyfin Media Server** — Database date updates, cast & crew image refresh (background worker with resume support), Docker container management, automated backups with configurable retention - **Utilities** — Image-to-ICO icon conversion (PNG, JPG, BMP, GIF, WEBP, TIFF via SkiaSharp) with native file picker, Windows Zone.Identifier file unblocker -- **Dependency Management** — Auto-installs and updates ADB, scrcpy, sqlite3, and go2rtc to a self-contained `dependencies/` folder. Configurable install paths per tool. Version checks via GitHub API and direct URL scraping. Services are automatically stopped before binary updates and restarted after. Docker and ws-scrcpy-web are externally managed (configured in Settings → General). +- **Dependency Management** — Auto-installs and updates ADB, sqlite3, and go2rtc to a self-contained `dependencies/` folder. Configurable install paths per tool. Version checks via GitHub API and direct URL scraping. Services are automatically stopped before binary updates and restarted after. Docker and ws-scrcpy-web are externally managed (configured in Settings → General). ## Features @@ -95,7 +95,7 @@ tests/ControlMenu.Tests/ | SkiaSharp for images | Cross-platform replacement for System.Drawing.Common | | ws-scrcpy-web via iframe | Screen mirroring without native scrcpy binary dependency | | File System Access API | Native OS file dialogs for icon converter (Chrome/Edge) | -| Self-contained dependencies | 5 auto-managed tools in `dependencies/`; 2 external (Docker, ws-scrcpy-web) | +| Self-contained dependencies | 3 auto-managed tools in `dependencies/`; 2 external (Docker, ws-scrcpy-web) | ## Dependencies @@ -105,7 +105,6 @@ Control Menu manages two types of dependencies: | Tool | Source | Purpose | |------|--------|---------| | ADB | Google (DirectUrl) | Android device management | -| scrcpy | GitHub (Genymobile/scrcpy) | Screen mirroring server binary | | sqlite3 | sqlite.org (DirectUrl) | Jellyfin database operations | | go2rtc | GitHub (AlexxIT/go2rtc) | RTSP-to-browser camera streaming | diff --git a/scripts/dependencies/fetch-scrcpy.ps1 b/scripts/dependencies/fetch-scrcpy.ps1 deleted file mode 100644 index 1714a48..0000000 --- a/scripts/dependencies/fetch-scrcpy.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# Fetches scrcpy (Windows x64) from the Genymobile GitHub release, verifies -# SHA-256, stages into publish/seed/dependencies/scrcpy/. - -. "$PSScriptRoot\_Fetcher.ps1" - -# ---- Pinned constants (bump together) -------------------------------------- -$Version = '3.1' -$Url = "https://github.com/Genymobile/scrcpy/releases/download/v$Version/scrcpy-win64-v$Version.zip" -$Sha256 = '0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca' -# ---------------------------------------------------------------------------- - -Write-Host "[fetch-scrcpy] scrcpy v$Version" -$cache = Get-CmCacheDir -Name 'scrcpy' -Version $Version -$zip = Join-Path $cache "scrcpy.zip" -$extract = Join-Path $cache 'extract' - -Invoke-CmDownload -Url $Url -DestFile $zip -ExpectedSha256 $Sha256 -if (-not (Test-Path (Join-Path $extract "scrcpy-win64-v$Version\scrcpy.exe"))) { - Expand-CmZip -Archive $zip -DestDir $extract -} - -# scrcpy zip extracts to a versioned top-level dir; flatten. -Copy-CmStage -From (Join-Path $extract "scrcpy-win64-v$Version") -LeafName 'scrcpy' diff --git a/scripts/stage-seed.ps1 b/scripts/stage-seed.ps1 index 5578826..2e56ad6 100644 --- a/scripts/stage-seed.ps1 +++ b/scripts/stage-seed.ps1 @@ -5,7 +5,6 @@ # Inputs: scripts/dependencies/fetch-*.ps1 (each downloads + verifies + stages) # Output: publish/seed/dependencies/ # platform-tools/ adb.exe + DLLs -# scrcpy/ scrcpy.exe + libs # sqlite3/ sqlite3.exe # go2rtc/ go2rtc.exe # diff --git a/src/ControlMenu.Common/Seeding/SeedHydrator.cs b/src/ControlMenu.Common/Seeding/SeedHydrator.cs index 76bc438..ea27915 100644 --- a/src/ControlMenu.Common/Seeding/SeedHydrator.cs +++ b/src/ControlMenu.Common/Seeding/SeedHydrator.cs @@ -14,13 +14,27 @@ namespace ControlMenu.Common.Seeding; /// public static class SeedHydrator { - public record Result(int Copied, int Skipped); + public record Result(int Copied, int Skipped, int Pruned); + + /// + /// Dependencies that were previously seeded/installed but have since been + /// removed from the app. The dependency manager never cleans a removed dep's + /// directory, so prunes them from + /// <dataRoot>\dependencies\ on launch to reclaim disk space. + /// Best-effort — a delete failure (locked file, permissions) is swallowed so + /// it can never block launch; the next launch retries. + /// + private static readonly string[] RetiredLeaves = ["scrcpy"]; public static Result Hydrate(string currentDir, string targetDependenciesDir) { + // Prune retired deps first so existing installs reclaim the space even in + // dev mode (no seed/), where the hydration loop below early-returns. + var pruned = PruneRetiredLeaves(targetDependenciesDir); + var seedRoot = Path.Combine(currentDir, "seed", "dependencies"); if (!Directory.Exists(seedRoot)) - return new Result(0, 0); + return new Result(0, 0, pruned); Directory.CreateDirectory(targetDependenciesDir); @@ -42,7 +56,32 @@ public static Result Hydrate(string currentDir, string targetDependenciesDir) copied++; } - return new Result(copied, skipped); + return new Result(copied, skipped, pruned); + } + + private static int PruneRetiredLeaves(string targetDependenciesDir) + { + if (!Directory.Exists(targetDependenciesDir)) + return 0; + + var pruned = 0; + foreach (var leaf in RetiredLeaves) + { + var dir = Path.Combine(targetDependenciesDir, leaf); + if (!Directory.Exists(dir)) + continue; + try + { + Directory.Delete(dir, recursive: true); + pruned++; + } + catch + { + // Best-effort: a locked file or permission issue must never block + // launch. The next launch retries. + } + } + return pruned; } private static void CopyTree(string source, string dest) diff --git a/src/ControlMenu/Modules/AndroidDevices/AndroidDevicesModule.cs b/src/ControlMenu/Modules/AndroidDevices/AndroidDevicesModule.cs index 82fbf38..7ce8531 100644 --- a/src/ControlMenu/Modules/AndroidDevices/AndroidDevicesModule.cs +++ b/src/ControlMenu/Modules/AndroidDevices/AndroidDevicesModule.cs @@ -32,18 +32,6 @@ public class AndroidDevicesModule : IToolModule : "https://dl.google.com/android/repository/platform-tools_r{version}-linux.zip", InstallPath = Path.Combine(DepsRoot, "platform-tools") }, - new ModuleDependency - { - Name = "scrcpy", - ExecutableName = "scrcpy", - VersionCommand = "scrcpy --version", - VersionPattern = @"scrcpy ([\d.]+)", - SourceType = UpdateSourceType.GitHub, - GitHubRepo = "Genymobile/scrcpy", - ProjectHomeUrl = "https://github.com/Genymobile/scrcpy", - AssetPattern = @"scrcpy-win64-v[\d.]+\.zip", - InstallPath = Path.Combine(DepsRoot, "scrcpy") - }, // node was a CM dependency when Managed-mode spawned the ws-scrcpy-web // node process. External-mode-only refactor removed all process-spawn // code; node is no longer invoked anywhere in CM. Declaration removed diff --git a/src/ControlMenuLauncher/Program.cs b/src/ControlMenuLauncher/Program.cs index ba6d5ec..aae2d56 100644 --- a/src/ControlMenuLauncher/Program.cs +++ b/src/ControlMenuLauncher/Program.cs @@ -87,9 +87,9 @@ private static int Main(string[] args) try { var hydrate = SeedHydrator.Hydrate(paths.GetCurrentDir(), paths.GetDependenciesDir()); - if (hydrate.Copied > 0 || hydrate.Skipped > 0) + if (hydrate.Copied > 0 || hydrate.Skipped > 0 || hydrate.Pruned > 0) { - LauncherLogger.Info($"seed hydrate: copied={hydrate.Copied} skipped={hydrate.Skipped}"); + LauncherLogger.Info($"seed hydrate: copied={hydrate.Copied} skipped={hydrate.Skipped} pruned={hydrate.Pruned}"); } } catch (Exception ex) diff --git a/tests/ControlMenu.Common.Tests/Seeding/SeedHydratorTests.cs b/tests/ControlMenu.Common.Tests/Seeding/SeedHydratorTests.cs index 5dc9364..f922cff 100644 --- a/tests/ControlMenu.Common.Tests/Seeding/SeedHydratorTests.cs +++ b/tests/ControlMenu.Common.Tests/Seeding/SeedHydratorTests.cs @@ -99,12 +99,38 @@ public void Hydrate_PartialOverlap_OnlyMissingLeavesCopied() [Fact] public void Hydrate_NestedDirectoriesInSeed_AreCopiedRecursively() { - Seed("scrcpy", "scrcpy.exe", "EXE"); - Seed("scrcpy", "lib/sub/data.bin", "NESTED"); + Seed("fake-tool", "fake-tool.exe", "EXE"); + Seed("fake-tool", "lib/sub/data.bin", "NESTED"); SeedHydrator.Hydrate(_currentDir, _depsDir); - Assert.True(File.Exists(Path.Combine(_depsDir, "scrcpy", "lib", "sub", "data.bin"))); - Assert.Equal("NESTED", File.ReadAllText(Path.Combine(_depsDir, "scrcpy", "lib", "sub", "data.bin"))); + Assert.True(File.Exists(Path.Combine(_depsDir, "fake-tool", "lib", "sub", "data.bin"))); + Assert.Equal("NESTED", File.ReadAllText(Path.Combine(_depsDir, "fake-tool", "lib", "sub", "data.bin"))); + } + + [Fact] + public void Hydrate_PrunesRetiredScrcpyLeaf_FromDataRoot() + { + // An existing install hydrated scrcpy/ before it was retired. Hydrate must + // delete it on launch so the ~40MB is reclaimed. + var scrcpyDir = Path.Combine(_depsDir, "scrcpy"); + Directory.CreateDirectory(Path.Combine(scrcpyDir, "lib")); + File.WriteAllText(Path.Combine(scrcpyDir, "scrcpy.exe"), "OLD"); + File.WriteAllText(Path.Combine(scrcpyDir, "lib", "SDL2.dll"), "OLD"); + + var result = SeedHydrator.Hydrate(_currentDir, _depsDir); + + Assert.Equal(1, result.Pruned); + Assert.False(Directory.Exists(scrcpyDir)); + } + + [Fact] + public void Hydrate_NoRetiredLeaf_PrunesNothing() + { + Seed("platform-tools", "adb.exe", "ADB"); + + var result = SeedHydrator.Hydrate(_currentDir, _depsDir); + + Assert.Equal(0, result.Pruned); } } diff --git a/tests/ControlMenu.Tests/Modules/AndroidDevices/AndroidDevicesModuleTests.cs b/tests/ControlMenu.Tests/Modules/AndroidDevices/AndroidDevicesModuleTests.cs index dcde968..490c223 100644 --- a/tests/ControlMenu.Tests/Modules/AndroidDevices/AndroidDevicesModuleTests.cs +++ b/tests/ControlMenu.Tests/Modules/AndroidDevices/AndroidDevicesModuleTests.cs @@ -22,11 +22,13 @@ public class AndroidDevicesModuleTests public void Icon_IsPhoneIcon() => Assert.Equal("bi-phone", _module.Icon); [Fact] - public void Dependencies_IncludesAdbAndScrcpy() + public void Dependencies_IncludesAdb_AndNotScrcpy() { var deps = _module.Dependencies.ToList(); Assert.Contains(deps, d => d.Name == "adb"); - Assert.Contains(deps, d => d.Name == "scrcpy"); + // scrcpy was removed — the native client has been orphaned since the + // ws-scrcpy-web sidecar replaced it (Phase 8a). Guard re-introduction. + Assert.DoesNotContain(deps, d => d.Name == "scrcpy"); } [Fact] @@ -99,14 +101,6 @@ public void AdbDependency_HasCorrectVersionCommand() Assert.Equal("adb", adb.ExecutableName); } - [Fact] - public void ScrcpyDependency_HasGitHubSource() - { - var scrcpy = _module.Dependencies.First(d => d.Name == "scrcpy"); - Assert.Equal(UpdateSourceType.GitHub, scrcpy.SourceType); - Assert.Equal("Genymobile/scrcpy", scrcpy.GitHubRepo); - } - private static IServiceProvider BuildServiceProviderWithTypes(params DeviceType[] typesPresent) { var services = new ServiceCollection(); diff --git a/tests/ControlMenu.Tests/Services/DependencyManagerServiceTests.cs b/tests/ControlMenu.Tests/Services/DependencyManagerServiceTests.cs index fc935fe..69c3a91 100644 --- a/tests/ControlMenu.Tests/Services/DependencyManagerServiceTests.cs +++ b/tests/ControlMenu.Tests/Services/DependencyManagerServiceTests.cs @@ -225,23 +225,23 @@ public async Task GetUpdateAvailableCountAsync_ReturnsCorrectCount() [Fact] public async Task CheckDependencyAsync_GitHub_DetectsUpdateAvailable() { - var (installDir, localExe) = MakeLocalInstall("scrcpy-install", "scrcpy"); + var (installDir, localExe) = MakeLocalInstall("fake-tool-install", "fake-tool"); var module = new FakeModule("android-module", "Android", [ new ModuleDependency { - Name = "scrcpy", - ExecutableName = "scrcpy", - VersionCommand = "scrcpy --version", - VersionPattern = @"scrcpy ([\d.]+)", + Name = "fake-tool", + ExecutableName = "fake-tool", + VersionCommand = "fake-tool --version", + VersionPattern = @"fake-tool ([\d.]+)", SourceType = UpdateSourceType.GitHub, - GitHubRepo = "Genymobile/scrcpy", + GitHubRepo = "example/fake-tool", InstallPath = installDir } ]); _mockExecutor.Setup(e => e.ExecuteAsync(localExe, "--version", null, default)) - .ReturnsAsync(new CommandResult(0, "scrcpy 3.3.2", "", false)); + .ReturnsAsync(new CommandResult(0, "fake-tool 3.3.2", "", false)); var depId = Guid.NewGuid(); using var setupDb = _dbFactory.CreateDbContext(); @@ -249,7 +249,7 @@ public async Task CheckDependencyAsync_GitHub_DetectsUpdateAvailable() { Id = depId, ModuleId = "android-module", - Name = "scrcpy", + Name = "fake-tool", SourceType = UpdateSourceType.GitHub, Status = DependencyStatus.UpToDate, InstalledVersion = "3.3.2" diff --git a/tests/ControlMenu.Tests/Services/DependencyScanTests.cs b/tests/ControlMenu.Tests/Services/DependencyScanTests.cs index 4a5dbf7..baf1ef9 100644 --- a/tests/ControlMenu.Tests/Services/DependencyScanTests.cs +++ b/tests/ControlMenu.Tests/Services/DependencyScanTests.cs @@ -88,12 +88,12 @@ public async Task ScanForDependenciesAsync_ReportsNotFoundWhenMissing() [ new ModuleDependency { - Name = "scrcpy", - ExecutableName = "scrcpy", - VersionCommand = "scrcpy --version", - VersionPattern = @"scrcpy ([\d.]+)", + Name = "fake-tool", + ExecutableName = "fake-tool", + VersionCommand = "fake-tool --version", + VersionPattern = @"fake-tool ([\d.]+)", SourceType = UpdateSourceType.GitHub, - ProjectHomeUrl = "https://github.com/Genymobile/scrcpy" + ProjectHomeUrl = "https://github.com/example/fake-tool" } ]); @@ -106,7 +106,7 @@ public async Task ScanForDependenciesAsync_ReportsNotFoundWhenMissing() Assert.Single(results); var result = results[0]; Assert.False(result.Found); - Assert.Equal("scrcpy", result.Name); + Assert.Equal("fake-tool", result.Name); Assert.Equal("Not found", result.Source); Assert.Null(result.Version); Assert.Null(result.Path);