Skip to content

Commit 8abb521

Browse files
committed
Installer/finalizer in c#
1 parent 8e29df2 commit 8abb521

File tree

15 files changed

+273
-780
lines changed

15 files changed

+273
-780
lines changed

Directory.Build.props

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@
99
<Architecture Condition="'$(Architecture)' == '' AND '$(BuildArchitecture)' == 'ppc64le'">$(BuildArchitecture)</Architecture>
1010
<Architecture Condition="'$(Architecture)' == '' AND '$(BuildArchitecture)' == 'loongarch64'">$(BuildArchitecture)</Architecture>
1111
<Architecture Condition="'$(Architecture)' == ''">x64</Architecture>
12-
13-
<!--
14-
The finalizer.nativeproject needs to have Platform set to build for the correct architecture, but this value being set below isn't used by the project because of the build hierarchy when using the CMake SDK.
15-
Instead, the Platform being set here is prior to calling the Arcade SDK, which sets PlatformName in RepoDefaults.props and modifies the OutputPath in ProjectLayout.props to include a PlatformName folder in the path.
16-
Note: The redist-installer project does use Architecture. The Arcade SDK does not use either BuildArchitecture or Architecture.
17-
-->
18-
<Platform Condition="'$(SetPlatformFromArchitecture)' == 'true' And ('$(Platform)' == '' Or '$(Platform)' == 'AnyCPU')">$(Architecture)</Platform>
1912
</PropertyGroup>
2013

2114
<Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />

NuGet.config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
<add key="vs-impl" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" />
2929
<!-- Used for Rich Navigation indexing task -->
3030
<add key="richnav" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-buildservices/nuget/v3/index.json" />
31+
32+
<!-- remove once https://www.nuget.org/packages/WixToolset.Dtf.WindowsInstaller is uploaded to dotnet feed -->
33+
<add key="local" value="c:\temp\packages" />
3134
</packageSources>
3235
<disabledPackageSources>
3336
<clear />

eng/dependabot/Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<!-- Packages in this file have versions updated periodically by Dependabot. Versions managed by Darc/Maestro should be in ..\Packages.props. -->
44

55
<ItemGroup>
6+
<PackageVersion Include="WixToolset.Dtf.WindowsInstaller" Version="4.0.6" />
7+
68
<!--Test dependencies-->
79
<PackageVersion Include="Verify.Xunit" Version="25.0.2" />
810
<PackageVersion Include="Verify.DiffPlex" Version="3.0.0" />

global.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,9 @@
1313
"version": "16.8"
1414
}
1515
},
16-
"native-tools": {
17-
"cmake": "latest"
18-
},
1916
"msbuild-sdks": {
2017
"Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.24551.1",
2118
"Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.24551.1",
22-
"Microsoft.Build.NoTargets": "3.7.0",
23-
"Microsoft.DotNet.CMake.Sdk": "9.0.0-beta.24217.1"
19+
"Microsoft.Build.NoTargets": "3.7.0"
2420
}
2521
}

sdk.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Installer", "Installer", "{
495495
EndProject
496496
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "core-sdk-tasks", "src\Installer\core-sdk-tasks\core-sdk-tasks.csproj", "{5593C59B-442B-4783-8527-74F6E41668D9}"
497497
EndProject
498-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "finalizer-build", "src\Installer\finalizer\finalizer-build.csproj", "{32DA04FF-A951-43EA-B2FA-86A825009A97}"
498+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "finalizer", "src\Installer\finalizer\finalizer.csproj", "{32DA04FF-A951-43EA-B2FA-86A825009A97}"
499499
EndProject
500500
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redist-installer", "src\Installer\redist-installer\redist-installer.csproj", "{FAADC193-BA41-449D-97CE-0EF82836046A}"
501501
EndProject

src/Installer/finalizer/CMakeLists.txt

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
<Project>
22

3-
<PropertyGroup>
4-
<SetPlatformFromArchitecture>true</SetPlatformFromArchitecture>
5-
</PropertyGroup>
6-
73
<Import Project="..\..\..\Directory.Build.props" />
84

9-
</Project>
5+
</Project>

src/Installer/finalizer/Program.cs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using Microsoft.Win32;
7+
using WixToolset.Dtf.WindowsInstaller;
8+
9+
if (args.Length < 3)
10+
{
11+
Console.WriteLine("Invalid arguments. Usage: finalizer <logPath> <sdkVersion> <platform>");
12+
return;
13+
}
14+
15+
string logPath = args[0];
16+
string sdkVersion = args[1];
17+
string platform = args[2];
18+
19+
Console.WriteLine($"{nameof(logPath)}: {logPath}");
20+
Console.WriteLine($"{nameof(sdkVersion)}: {sdkVersion}");
21+
Console.WriteLine($"{nameof(platform)}: {platform}");
22+
23+
try
24+
{
25+
// Step 1: Parse and format SDK feature band version
26+
string featureBandVersion = ParseSdkVersion(sdkVersion);
27+
28+
// Step 2: Check if SDK feature band is installed
29+
bool isInstalled = DetectSdk(featureBandVersion, platform);
30+
if (isInstalled)
31+
{
32+
Console.WriteLine($"SDK with feature band {featureBandVersion} is already installed.");
33+
return;
34+
}
35+
36+
// Step 3: Remove dependent components if necessary
37+
bool restartRequired = RemoveDependent(featureBandVersion);
38+
if (restartRequired)
39+
{
40+
Console.WriteLine("A restart may be required after removing the dependent component.");
41+
}
42+
43+
// Step 4: Delete workload records
44+
DeleteWorkloadRecords(featureBandVersion, platform);
45+
46+
// Step 5: Clean up install state file
47+
RemoveInstallStateFile(featureBandVersion, platform);
48+
49+
// Final reboot check
50+
if (restartRequired || IsRebootPending())
51+
{
52+
Console.WriteLine("A system restart is recommended to complete the operation.");
53+
}
54+
else
55+
{
56+
Console.WriteLine("Operation completed successfully. No restart is required.");
57+
}
58+
}
59+
catch (Exception ex)
60+
{
61+
Console.WriteLine($"Error: {ex}");
62+
}
63+
64+
static string ParseSdkVersion(string sdkVersion)
65+
{
66+
var parts = sdkVersion.Split('.');
67+
if (parts.Length < 3)
68+
throw new ArgumentException("Invalid SDK version format.");
69+
70+
if (!int.TryParse(parts[2], out int patch) || patch < 100)
71+
throw new ArgumentException("Invalid patch level in SDK version.");
72+
73+
int featureBand = patch - (patch % 100);
74+
return $"{parts[0]}.{parts[1]}.{featureBand}";
75+
}
76+
77+
static bool DetectSdk(string featureBandVersion, string platform)
78+
{
79+
string registryPath = $@"SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\{platform}\sdk";
80+
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath))
81+
{
82+
if (key == null)
83+
{
84+
Console.WriteLine("SDK registry path not found.");
85+
return false;
86+
}
87+
88+
foreach (var valueName in key.GetValueNames())
89+
{
90+
if (valueName.Contains(featureBandVersion))
91+
{
92+
Console.WriteLine($"SDK version detected: {valueName}");
93+
return true;
94+
}
95+
}
96+
}
97+
return false;
98+
}
99+
100+
static bool RemoveDependent(string dependent)
101+
{
102+
Installer.SetInternalUI(InstallUIOptions.Silent);
103+
104+
// Open the installer dependencies registry key
105+
// This has to be an exhaustive search as we're not looking for a specific provider key, but for a specific dependent
106+
// that could be registered against any provider key.
107+
using var hkInstallerDependenciesKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Classes\Installer\Dependencies", writable: true);
108+
if (hkInstallerDependenciesKey == null)
109+
{
110+
Console.WriteLine("Installer dependencies key does not exist.");
111+
return false; // No dependencies to remove
112+
}
113+
114+
// Iterate over each provider key in the dependencies
115+
foreach (string providerKeyName in hkInstallerDependenciesKey.GetSubKeyNames())
116+
{
117+
Console.WriteLine($"Processing provider key: {providerKeyName}");
118+
119+
using var hkProviderKey = hkInstallerDependenciesKey.OpenSubKey(providerKeyName, writable: true);
120+
if (hkProviderKey == null) continue;
121+
122+
// Open the Dependents subkey
123+
using var hkDependentsKey = hkProviderKey.OpenSubKey("Dependents", writable: true);
124+
if (hkDependentsKey == null) continue;
125+
126+
// Check if the dependent exists and continue if it does not
127+
string[] dependentsKeys = hkDependentsKey.GetSubKeyNames();
128+
bool dependentExists = false;
129+
130+
foreach (string dependentsKeyName in dependentsKeys)
131+
{
132+
if (string.Equals(dependentsKeyName, dependent, StringComparison.OrdinalIgnoreCase))
133+
{
134+
dependentExists = true;
135+
break;
136+
}
137+
}
138+
139+
if (!dependentExists)
140+
{
141+
continue; // Skip to the next provider key if the dependent does not exist
142+
}
143+
144+
Console.WriteLine($"Dependent match found: {dependent}");
145+
146+
// Attempt to remove the dependent key
147+
try
148+
{
149+
hkDependentsKey.DeleteSubKey(dependent);
150+
Console.WriteLine("Dependent deleted");
151+
}
152+
catch (Exception ex)
153+
{
154+
Console.WriteLine($"Exception while removing dependent key: {ex.Message}");
155+
return false;
156+
}
157+
158+
// Check if any dependents are left
159+
if (hkDependentsKey.SubKeyCount == 0)
160+
{
161+
// No remaining dependents, handle product uninstallation
162+
try
163+
{
164+
string productCode = hkProviderKey.GetValue("ProductId").ToString();
165+
166+
// Configure the product to be absent
167+
Installer.ConfigureProduct(productCode, 0, InstallState.Absent, "");
168+
Console.WriteLine("Product configured to absent successfully.");
169+
}
170+
catch (Exception ex)
171+
{
172+
Console.WriteLine($"Error handling product configuration: {ex.Message}");
173+
return false;
174+
}
175+
}
176+
return true;
177+
}
178+
179+
return false;
180+
}
181+
182+
static void DeleteWorkloadRecords(string featureBandVersion, string platform)
183+
{
184+
string workloadKey = $@"SOFTWARE\Microsoft\dotnet\InstalledWorkloads\Standalone\{platform}";
185+
186+
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(workloadKey, writable: true))
187+
{
188+
if (key != null)
189+
{
190+
key.DeleteSubKeyTree(featureBandVersion, throwOnMissingSubKey: false);
191+
Console.WriteLine($"Deleted workload records for '{featureBandVersion}'.");
192+
}
193+
else
194+
{
195+
Console.WriteLine("No workload records found to delete.");
196+
}
197+
}
198+
}
199+
200+
static void RemoveInstallStateFile(string featureBandVersion, string platform)
201+
{
202+
string programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
203+
string installStatePath = Path.Combine(programDataPath, "dotnet", "workloads", platform, featureBandVersion, "installstate", "default.json");
204+
205+
if (File.Exists(installStatePath))
206+
{
207+
File.Delete(installStatePath);
208+
Console.WriteLine($"Deleted install state file: {installStatePath}");
209+
210+
var dir = new DirectoryInfo(installStatePath).Parent;
211+
while (dir != null && dir.Exists && dir.GetFiles().Length == 0 && dir.GetDirectories().Length == 0)
212+
{
213+
dir.Delete();
214+
dir = dir.Parent;
215+
}
216+
}
217+
else
218+
{
219+
Console.WriteLine("Install state file does not exist.");
220+
}
221+
}
222+
223+
static bool IsRebootPending()
224+
{
225+
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending"))
226+
{
227+
if (key != null)
228+
{
229+
Console.WriteLine("Reboot is pending due to component-based servicing.");
230+
return true;
231+
}
232+
}
233+
234+
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Session Manager"))
235+
{
236+
var value = key?.GetValue("PendingFileRenameOperations");
237+
if (value != null)
238+
{
239+
Console.WriteLine("Pending file rename operations indicate a reboot is pending.");
240+
return true;
241+
}
242+
}
243+
244+
Console.WriteLine("No reboot pending.");
245+
return false;
246+
}

0 commit comments

Comments
 (0)