Skip to content

Commit 3f7ccae

Browse files
authored
Record payload size in uninstall registry (#122)
1 parent 0c863e9 commit 3f7ccae

File tree

8 files changed

+67
-21
lines changed

8 files changed

+67
-21
lines changed

src/DotnetPackaging.Exe.Installer/Core/DefaultInstallerPayload.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public sealed class DefaultInstallerPayload : IInstallerPayload
1111
public Task<Result<InstallerMetadata>> GetMetadata(CancellationToken ct = default)
1212
=> EnsureLoaded(ct).Map(p => p.Metadata);
1313

14+
public Task<Result<long>> GetContentSize(CancellationToken ct = default)
15+
=> EnsureLoaded(ct).Map(p => p.ContentSizeBytes);
16+
1417
public Task<Result> CopyContents(string targetDirectory, IObserver<Progress>? progressObserver = null, CancellationToken ct = default)
1518
=> EnsureLoaded(ct).Bind(p => Task.Run(() =>
1619
PayloadExtractor.CopyContentTo(p, targetDirectory, progressObserver), ct));

src/DotnetPackaging.Exe.Installer/Core/IInstallerPayload.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public interface IInstallerPayload
77
{
88
Task<Result<InstallerMetadata>> GetMetadata(CancellationToken ct = default);
99

10+
Task<Result<long>> GetContentSize(CancellationToken ct = default);
11+
1012
Task<Result> CopyContents(
1113
string targetDirectory,
1214
IObserver<Progress>? progressObserver = null,

src/DotnetPackaging.Exe.Installer/Core/Installer.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ namespace DotnetPackaging.Exe.Installer.Core;
66

77
internal static class Installer
88
{
9-
public static Result<string> Install(string targetDir, InstallerMetadata meta)
9+
public static Result<string> Install(string targetDir, InstallerMetadata meta, long payloadSizeBytes)
1010
{
1111
return ResolveMainExe(targetDir, meta)
1212
.Tap(exePath => ShortcutService.TryCreateStartMenuShortcut(meta.ApplicationName, exePath))
13-
.Tap(exePath => RegisterUninstaller(targetDir, meta, exePath));
13+
.Tap(exePath => RegisterUninstaller(targetDir, meta, exePath, payloadSizeBytes));
1414
}
1515

16-
private static void RegisterUninstaller(string targetDir, InstallerMetadata meta, string mainExePath)
16+
private static void RegisterUninstaller(string targetDir, InstallerMetadata meta, string mainExePath, long payloadSizeBytes)
1717
{
1818
if (Environment.ProcessPath is null)
1919
{
@@ -44,11 +44,12 @@ private static void RegisterUninstaller(string targetDir, InstallerMetadata meta
4444
WindowsRegistryService.Register(
4545
meta.AppId,
4646
meta.ApplicationName,
47-
meta.Version,
48-
meta.Vendor,
49-
targetDir,
50-
$"\"{uninstallerPath}\" --uninstall",
51-
mainExePath);
47+
meta.Version,
48+
meta.Vendor,
49+
targetDir,
50+
$"\"{uninstallerPath}\" --uninstall",
51+
mainExePath,
52+
payloadSizeBytes);
5253
}
5354
catch (Exception ex)
5455
{

src/DotnetPackaging.Exe.Installer/Core/MetadataFilePayload.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public Task<Result<InstallerMetadata>> GetMetadata(CancellationToken ct = defaul
4949
}, ex => $"Failed to read metadata: {ex.Message}"));
5050
}
5151

52+
public Task<Result<long>> GetContentSize(CancellationToken ct = default)
53+
{
54+
return Task.FromResult(Result.Failure<long>("Disk-only payload does not provide installation content."));
55+
}
56+
5257
public Task<Result> CopyContents(string targetDirectory, IObserver<Progress>? progressObserver = null, CancellationToken ct = default)
5358
{
5459
return Task.FromResult(Result.Failure("Disk-only payload does not provide installation content."));

src/DotnetPackaging.Exe.Installer/Core/PayloadExtractor.cs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,12 @@ public static Result CopyContentTo(InstallerPayload payload, string targetDirect
152152
}
153153
catch { }
154154

155-
var fileEntries = contentEntries.Where(entry => !IsDirectoryEntry(entry));
156-
long totalBytes = fileEntries.Sum(entry => entry.Length);
155+
var fileEntries = contentEntries.Where(entry => !IsDirectoryEntry(entry)).ToList();
156+
long totalBytes = payload.ContentSizeBytes;
157+
if (totalBytes <= 0)
158+
{
159+
totalBytes = fileEntries.Sum(entry => entry.Length);
160+
}
157161
long safeTotal = totalBytes == 0 ? 1 : totalBytes;
158162
long copiedBytes = 0;
159163
var targetRoot = System.IO.Path.GetFullPath(targetDirectory);
@@ -212,10 +216,11 @@ private static Result<InstallerPayload> CreatePayload(PayloadLocation location)
212216

213217
var result = bytesResult
214218
.Bind(bytes => ReadMetadata(bytes)
215-
.Map(metadata => new InstallerPayload(
216-
metadata,
219+
.Map(payloadMetadata => new InstallerPayload(
220+
payloadMetadata.Metadata,
217221
ByteSource.FromBytes(bytes),
218-
location.TempDir)));
222+
location.TempDir,
223+
payloadMetadata.ContentSizeBytes)));
219224

220225
TryDeleteFile(location.ZipPath);
221226

@@ -227,7 +232,7 @@ private static Result<InstallerPayload> CreatePayload(PayloadLocation location)
227232
return result;
228233
}
229234

230-
private static Result<InstallerMetadata> ReadMetadata(byte[] zipBytes)
235+
private static Result<PayloadMetadata> ReadMetadata(byte[] zipBytes)
231236
{
232237
return Result.Try(() =>
233238
{
@@ -236,10 +241,18 @@ private static Result<InstallerMetadata> ReadMetadata(byte[] zipBytes)
236241
var metaEntry = archive.GetEntry("metadata.json") ?? throw new InvalidOperationException("metadata.json missing");
237242
using var entryStream = metaEntry.Open();
238243
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
239-
return JsonSerializer.Deserialize<InstallerMetadata>(entryStream, opts)!;
244+
var metadata = JsonSerializer.Deserialize<InstallerMetadata>(entryStream, opts)!;
245+
var contentSizeBytes = archive.Entries
246+
.Where(IsContentEntry)
247+
.Where(entry => !IsDirectoryEntry(entry))
248+
.Sum(entry => entry.Length);
249+
250+
return new PayloadMetadata(metadata, contentSizeBytes);
240251
}, ex => $"Error reading payload metadata: {ex.Message}");
241252
}
242253

254+
private sealed record PayloadMetadata(InstallerMetadata Metadata, long ContentSizeBytes);
255+
243256
private static Maybe<PayloadLocation> TryExtractFromManagedResource()
244257
{
245258
try
@@ -591,10 +604,19 @@ private static Result<InstallerPayload> CreateDebugPayload()
591604
}
592605

593606
var tempDir = Directory.CreateTempSubdirectory("dp-inst-debug-").FullName;
594-
return new InstallerPayload(metadata, ByteSource.FromBytes(ms.ToArray()), tempDir);
607+
var payloadBytes = ms.ToArray();
608+
var payloadMetadataResult = ReadMetadata(payloadBytes);
609+
if (payloadMetadataResult.IsFailure)
610+
{
611+
throw new InvalidOperationException($"Failed to prepare debug payload: {payloadMetadataResult.Error}");
612+
}
613+
614+
var payloadMetadata = payloadMetadataResult.Value;
615+
616+
return new InstallerPayload(metadata, ByteSource.FromBytes(payloadBytes), tempDir, payloadMetadata.ContentSizeBytes);
595617
}, ex => $"Failed to create debug payload: {ex.Message}");
596618
}
597619
#endif
598620
}
599621

600-
public sealed record InstallerPayload(InstallerMetadata Metadata, IByteSource Content, string WorkingDirectory);
622+
public sealed record InstallerPayload(InstallerMetadata Metadata, IByteSource Content, string WorkingDirectory, long ContentSizeBytes);

src/DotnetPackaging.Exe.Installer/Core/WindowsRegistryService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class WindowsRegistryService
1010
{
1111
private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall";
1212

13-
public static Result Register(string appId, string applicationName, string version, string publisher, string installLocation, string uninstallString, string displayIcon)
13+
public static Result Register(string appId, string applicationName, string version, string publisher, string installLocation, string uninstallString, string displayIcon, long estimatedSizeBytes)
1414
{
1515
return Result.Try(() =>
1616
{
@@ -23,6 +23,12 @@ public static Result Register(string appId, string applicationName, string versi
2323
key.SetValue("DisplayIcon", displayIcon);
2424
key.SetValue("NoModify", 1);
2525
key.SetValue("NoRepair", 1);
26+
27+
if (estimatedSizeBytes > 0)
28+
{
29+
var estimatedSizeKb = (int)Math.Clamp((estimatedSizeBytes + 1023) / 1024, 1, int.MaxValue);
30+
key.SetValue("EstimatedSize", estimatedSizeKb, RegistryValueKind.DWord);
31+
}
2632
}, ex => $"Failed to create registry keys: {ex.Message}");
2733
}
2834

src/DotnetPackaging.Exe.Installer/Installation/Wizard/Install/InstallViewModel.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ private Task<Result<InstallationResult>> DoInstall(InstallerMetadata installerMe
2828
return Task.Run(async () =>
2929
{
3030
Log.Information("Starting installation to {InstallDirectory}", installDirectory);
31-
31+
32+
var payloadSizeResult = await payload.GetContentSize().ConfigureAwait(false);
33+
if (payloadSizeResult.IsFailure)
34+
{
35+
Log.Warning("Failed to determine payload size: {Error}", payloadSizeResult.Error);
36+
}
37+
var payloadSize = payloadSizeResult.IsSuccess ? payloadSizeResult.Value : 0;
38+
3239
var copyRes = await payload.CopyContents(installDirectory, progress).ConfigureAwait(false);
3340
if (copyRes.IsFailure)
3441
{
@@ -38,7 +45,7 @@ private Task<Result<InstallationResult>> DoInstall(InstallerMetadata installerMe
3845

3946
Log.Information("Contents copied successfully, registering installation");
4047

41-
var installResult = Core.Installer.Install(installDirectory, installerMetadata)
48+
var installResult = Core.Installer.Install(installDirectory, installerMetadata, payloadSize)
4249
.Map(exePath => new InstallationResult(installerMetadata, installDirectory, exePath))
4350
.Bind(result => InstallationRegistry.Register(result).Map(() => result));
4451

src/DotnetPackaging.Exe.Installer/Installation/Wizard/InstallWizard.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private static Task<Result<InstallationResult>> InstallApplicationAsync(Installe
5353
{
5454
return Task.Run(() =>
5555
PayloadExtractor.CopyContentTo(payload, installDir, progressObserver)
56-
.Bind(() => Core.Installer.Install(installDir, payload.Metadata))
56+
.Bind(() => Core.Installer.Install(installDir, payload.Metadata, payload.ContentSizeBytes))
5757
.Map(exePath => new InstallationResult(payload.Metadata, installDir, exePath)));
5858
}
5959

0 commit comments

Comments
 (0)