Skip to content

Commit 9a92293

Browse files
committed
feat: add zero-config SetSource() API and manifest.json auto-discovery
Add SetSource(url, key, token?, scheme?, reportUrl?) as a minimal bootstrap entry-point. Identity metadata (MainAppName, ClientVersion, UpdateAppName, UpdatePath, …) is auto-discovered from generalupdate.manifest.json generated by GeneralUpdate.Tools, with hard-coded defaults as final fallback. Changes: - ManifestInfo: DTO for generalupdate.manifest.json with camelCase JSON mapping - AppMetadataDiscoverer: fills missing identity fields from manifest or defaults - GeneralUpdateBootstrap.SetSource(): 5-parameter convenience entry-point - SetConfig() now auto-invokes Discover() to fill gaps - InitializeFromEnvironment() stays IPC-only for the Upgrade path - Configinfo.Validate(): InstallPath is no longer required (defaults to BaseDirectory) - HttpParameterJsonContext: registered ManifestInfo for source-gen serialization - ClientTest: converted from SetConfig(Configinfo) to SetSource() + manifest.json - UpgradeTest: clarified IPC-only config flow (no manifest needed)
1 parent db54d4f commit 9a92293

9 files changed

Lines changed: 196 additions & 32 deletions

File tree

src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,18 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
214214
}
215215

216216
/// <summary>
217-
/// Applies the primary configuration object. Validates required fields, maps to
218-
/// the internal <see cref="GlobalConfigInfo"/>, and initialises the blacklist
219-
/// matcher for file exclusion during update operations.
217+
/// Applies the primary configuration object. Missing identity fields are
218+
/// auto-discovered from <c>generalupdate.manifest.json</c> or defaults.
219+
/// Validates required fields, maps to the internal <see cref="GlobalConfigInfo"/>,
220+
/// and initialises the blacklist matcher for file exclusion during update operations.
220221
/// </summary>
221-
/// <param name="configInfo">User-facing configuration. Must have non-null
222-
/// <c>UpdateUrl</c>, <c>AppSecretKey</c>, <c>ClientVersion</c>, <c>InstallPath</c>.</param>
222+
/// <param name="configInfo">User-facing configuration. Secrets (<c>UpdateUrl</c>,
223+
/// <c>AppSecretKey</c>, <c>Token</c>) must be set. Identity fields are auto-filled
224+
/// when omitted.</param>
223225
/// <returns>This bootstrap instance for chaining.</returns>
224226
public GeneralUpdateBootstrap SetConfig(Configinfo configInfo)
225227
{
228+
configInfo = AppMetadataDiscoverer.Discover(configInfo);
226229
configInfo.Validate();
227230
_configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo);
228231

@@ -276,6 +279,28 @@ public GeneralUpdateBootstrap SetConfig(string filePath)
276279
return SetConfig(config);
277280
}
278281

282+
/// <summary>
283+
/// Minimal entry-point: supply only secrets. Identity metadata is auto-discovered
284+
/// from <c>generalupdate.manifest.json</c> and hard-coded defaults.
285+
/// </summary>
286+
/// <returns>This bootstrap instance for chaining.</returns>
287+
public GeneralUpdateBootstrap SetSource(
288+
string appSecretKey,
289+
string updateUrl,
290+
string? reportUrl = null,
291+
string? token = null,
292+
string? scheme = null)
293+
{
294+
return SetConfig(new Configinfo
295+
{
296+
UpdateUrl = updateUrl,
297+
AppSecretKey = appSecretKey,
298+
Token = token,
299+
Scheme = scheme,
300+
ReportUrl = reportUrl
301+
});
302+
}
303+
279304
/// <summary>
280305
/// Configure the <see cref="DiffPipeline"/> via a fluent builder action.
281306
/// If not called, a default pipeline is built with <see cref="BsdiffDiffer"/>,
@@ -318,6 +343,9 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func<UpdateInfoEventArgs
318343
private void InitializeFromEnvironment()
319344
{
320345
// Read ProcessInfo via AES-encrypted file IPC.
346+
// The Upgrade process is only ever launched by the Client — no IPC means
347+
// there is nothing to do. The Client's manifest.json flows through IPC,
348+
// so the Upgrade never needs to load one directly.
321349
var processInfo = new EncryptedFileProcessInfoProvider().Receive();
322350
if (processInfo == null) return;
323351

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
3+
namespace GeneralUpdate.Core.Configuration;
4+
5+
/// <summary>
6+
/// Fills missing identity fields in a <see cref="Configinfo"/> by probing
7+
/// <c>generalupdate.manifest.json</c>, then falling back to hard-coded defaults.
8+
/// </summary>
9+
public static class AppMetadataDiscoverer
10+
{
11+
/// <summary>
12+
/// Discover and fill every null-or-empty identity field in <paramref name="seed"/>.
13+
/// </summary>
14+
public static Configinfo Discover(Configinfo seed)
15+
{
16+
if (seed == null)
17+
throw new System.ArgumentNullException(nameof(seed));
18+
19+
var manifest = ManifestInfo.Load();
20+
21+
// Identity fields — manifest overrides defaults, not just nulls.
22+
// (BaseConfigInfo pre-fills UpdateAppName with "Update.exe", so simple
23+
// null-coalescing would never pick up the manifest value.)
24+
if (manifest != null)
25+
{
26+
if (!string.IsNullOrWhiteSpace(manifest.MainAppName))
27+
seed.MainAppName = manifest.MainAppName;
28+
if (!string.IsNullOrWhiteSpace(manifest.UpdateAppName))
29+
seed.UpdateAppName = manifest.UpdateAppName;
30+
if (!string.IsNullOrWhiteSpace(manifest.ClientVersion))
31+
seed.ClientVersion = manifest.ClientVersion;
32+
if (!string.IsNullOrWhiteSpace(manifest.UpgradeClientVersion))
33+
seed.UpgradeClientVersion = manifest.UpgradeClientVersion;
34+
if (!string.IsNullOrWhiteSpace(manifest.ProductId))
35+
seed.ProductId = manifest.ProductId;
36+
if (!string.IsNullOrWhiteSpace(manifest.UpdatePath))
37+
seed.UpdatePath = manifest.UpdatePath;
38+
if (!string.IsNullOrWhiteSpace(manifest.AppType)
39+
&& Enum.TryParse<AppType>(manifest.AppType, out var at))
40+
seed.AppType = at;
41+
}
42+
43+
// Hard-coded fallbacks only when neither seed nor manifest provided a value.
44+
seed.MainAppName ??= "Client";
45+
seed.UpdateAppName ??= "Update.exe";
46+
seed.InstallPath ??= AppDomain.CurrentDomain.BaseDirectory;
47+
48+
return seed;
49+
}
50+
}

src/c#/GeneralUpdate.Core/Configuration/Configinfo.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ public class Configinfo : BaseConfigInfo
7070
/// <c>AppSecretKey</c>: Must not be empty.</item>
7171
/// <item>
7272
/// <c>ClientVersion</c>: Must not be empty.</item>
73-
/// <item>
74-
/// <c>InstallPath</c>: Must not be empty.</item>
7573
/// </list>
7674
/// <para>
7775
/// This method is typically called at the end of the <see cref="ConfiginfoBuilder.Build" /> method
@@ -101,9 +99,6 @@ public void Validate()
10199

102100
if (string.IsNullOrWhiteSpace(ClientVersion))
103101
throw new ArgumentException("ClientVersion cannot be empty");
104-
105-
if (string.IsNullOrWhiteSpace(InstallPath))
106-
throw new ArgumentException("InstallPath cannot be empty");
107102
}
108103
}
109104
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System;
2+
using System.IO;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using GeneralUpdate.Core.JsonContext;
6+
7+
namespace GeneralUpdate.Core.Configuration;
8+
9+
/// <summary>
10+
/// DTO for <c>generalupdate.manifest.json</c> — the application identity file generated by
11+
/// <c>GeneralUpdate.Tools</c>. Contains only identity and structural metadata; secrets are
12+
/// supplied in code via <see cref="GeneralUpdateBootstrap.SetSource"/>.
13+
/// </summary>
14+
public class ManifestInfo
15+
{
16+
[JsonPropertyName("mainAppName")]
17+
public string MainAppName { get; set; }
18+
19+
[JsonPropertyName("clientVersion")]
20+
public string ClientVersion { get; set; }
21+
22+
[JsonPropertyName("appType")]
23+
public string AppType { get; set; }
24+
25+
[JsonPropertyName("updateAppName")]
26+
public string UpdateAppName { get; set; } = "Update.exe";
27+
28+
[JsonPropertyName("upgradeClientVersion")]
29+
public string UpgradeClientVersion { get; set; }
30+
31+
[JsonPropertyName("productId")]
32+
public string ProductId { get; set; }
33+
34+
[JsonPropertyName("updatePath")]
35+
public string UpdatePath { get; set; }
36+
37+
private const string FileName = "generalupdate.manifest.json";
38+
39+
/// <summary>
40+
/// Load manifest from <see cref="AppDomain.CurrentDomain.BaseDirectory"/>.
41+
/// Returns <c>null</c> when the file does not exist.
42+
/// </summary>
43+
public static ManifestInfo? Load()
44+
{
45+
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, FileName);
46+
return Load(path);
47+
}
48+
49+
/// <summary>
50+
/// Load manifest from a specific path. Returns <c>null</c> when the file does not exist.
51+
/// </summary>
52+
public static ManifestInfo? Load(string path)
53+
{
54+
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
55+
return null;
56+
57+
try
58+
{
59+
var json = File.ReadAllText(path);
60+
return JsonSerializer.Deserialize(json, HttpParameterJsonContext.Default.ManifestInfo);
61+
}
62+
catch
63+
{
64+
// Malformed JSON, permission denied, etc. — treat as missing.
65+
return null;
66+
}
67+
}
68+
69+
/// <summary>
70+
/// Convert this manifest to a minimally populated <see cref="Configinfo"/>.
71+
/// Caller must still supply secrets (<c>UpdateUrl</c>, <c>AppSecretKey</c>, <c>Token</c>).
72+
/// </summary>
73+
public Configinfo ToConfiginfo()
74+
{
75+
return new Configinfo
76+
{
77+
MainAppName = MainAppName,
78+
ClientVersion = ClientVersion,
79+
UpdateAppName = UpdateAppName ?? "Update.exe",
80+
UpgradeClientVersion = UpgradeClientVersion,
81+
ProductId = ProductId,
82+
UpdatePath = UpdatePath,
83+
AppType = Enum.TryParse<AppType>(AppType, out var at) ? at : null
84+
};
85+
}
86+
}

src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ namespace GeneralUpdate.Core.JsonContext;
1111
[JsonSerializable(typeof(string))]
1212
[JsonSerializable(typeof(Dictionary<string, object>))]
1313
[JsonSerializable(typeof(Configinfo))]
14+
[JsonSerializable(typeof(ManifestInfo))]
1415
public partial class HttpParameterJsonContext: JsonSerializerContext;

tests/ClientTest/ClientTest.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@
1111
<ProjectReference Include="..\..\src\c#\GeneralUpdate.Core\GeneralUpdate.Core.csproj" />
1212
</ItemGroup>
1313

14+
<ItemGroup>
15+
<None Update="generalupdate.manifest.json">
16+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17+
</None>
18+
</ItemGroup>
19+
1420
</Project>

tests/ClientTest/Program.cs

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,19 @@
1010
Console.WriteLine($"Started at {DateTime.Now}");
1111
Console.WriteLine($"Running from: {AppDomain.CurrentDomain.BaseDirectory}");
1212

13-
var updateUrl = "http://localhost:5000/Upgrade/Verification";
14-
var reportUrl = "http://localhost:5000/Upgrade/Report";
13+
// Secrets come from code — never from files.
14+
var updateUrl = "http://localhost:5000/Upgrade/Verification";
15+
var reportUrl = "http://localhost:5000/Upgrade/Report";
1516
var appSecretKey = Environment.GetEnvironmentVariable("APP_SECRET_KEY") ?? "dfeb5833-975e-4afb-88f1-6278ee9aeff6";
16-
var productId = Environment.GetEnvironmentVariable("PRODUCT_ID") ?? "2d974e2a-31e6-4887-9bb1-b4689e98c77a";
17-
var clientVersion = Environment.GetEnvironmentVariable("CLIENT_VERSION") ?? "1.0.0.0";
1817

1918
Console.WriteLine($"UpdateUrl: {updateUrl}");
20-
Console.WriteLine($"ReportUrl: {reportUrl}");
21-
Console.WriteLine($"ClientVersion: {clientVersion}");
22-
Console.WriteLine($"ProductId: {productId}");
2319
Console.WriteLine();
2420

25-
var config = new Configinfo
26-
{
27-
UpdateUrl = updateUrl,
28-
ReportUrl = reportUrl,
29-
UpdatePath = "Upgrade",
30-
UpdateAppName = "UpgradeTest.exe",
31-
MainAppName = "ClientTest.exe",
32-
InstallPath = AppDomain.CurrentDomain.BaseDirectory,
33-
ClientVersion = clientVersion,
34-
UpgradeClientVersion = "1.0.0.0",
35-
ProductId = productId,
36-
AppSecretKey = appSecretKey,
37-
};
38-
21+
// Identity metadata (MainAppName, ClientVersion, UpdateAppName, UpdatePath, …)
22+
// is auto-discovered from generalupdate.manifest.json (generated by GeneralUpdate.Tools).
23+
// SetSource params: (appSecretKey, updateUrl, reportUrl?, token?, scheme?)
3924
await new GeneralUpdateBootstrap()
40-
.SetConfig(config)
25+
.SetSource(appSecretKey, updateUrl, reportUrl)
4126
.Option(UpdateOptions.AppType, AppType.Client)
4227
.Hooks<ClientTestHooks>()
4328
.AddListenerMultiDownloadStatistics(OnDownloadStatistics)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"mainAppName": "ClientTest.exe",
3+
"clientVersion": "1.0.0",
4+
"appType": "Client",
5+
"updateAppName": "UpgradeTest.exe",
6+
"upgradeClientVersion": "1.0.0",
7+
"productId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a",
8+
"updatePath": "Upgrade"
9+
}

tests/UpgradeTest/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
Console.WriteLine($"Started at {DateTime.Now}");
1111
Console.WriteLine($"Running from: {AppDomain.CurrentDomain.BaseDirectory}");
1212

13+
// Config comes from the encrypted IPC file written by the Client process.
14+
// The Client's generalupdate.manifest.json + SetSource flows through IPC,
15+
// so the Upgrade never needs to load a manifest directly.
16+
1317
await new GeneralUpdateBootstrap()
1418
.Option(UpdateOptions.AppType, AppType.Upgrade)
1519
.Hooks<UpgradeTestHooks>()

0 commit comments

Comments
 (0)