Skip to content

Commit 920c411

Browse files
authored
Test host for in-proc COM module validation (microsoft#5910)
Fixes microsoft#5869 and microsoft#5870 in the current code and will be backported to 1.12 ## Change Adds a test host that leverages the in-proc COM module (as built locally) to validate various lifetime management scenarios. It has numerous options for controlling the scenarios, as well as an extensible model for adding different tests in the future. It was able to hit the faults described in the linked issues, and the fixes are included: - Move the `ServerShutdownSynchronization` to integrate with the signal monitoring directly, rather than registering as an object. This also means that there is no need to unregister, preventing the construction of a `TerminationSignalHandler`. - Remove the call that terminates COM objects from the `DLL_PROCESS_DETACH` handler. If the caller is using the module properly via COM, the `DllCanUnloadNow` calls should take care of things. If the process is exiting without that call, leaking objects will be fine (the caller didn't uninitialize COM, so it is doubtful they care about a clean exit). Additionally, these changes were made to support additional scenarios: - Add the WinRT type names to the manifest file so that WinRT activation can work. - Fix the activation factories to be counted objects for the module. - Add a setting that allows the caller to prevent `DllCanUnloadNow` from checking the object state, always returning that it cannot unload now. This can be used to prevent the module from being unloaded (and any active static objects alive) even if no external objects are active.
1 parent 69adc94 commit 920c411

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1894
-136
lines changed

.github/actions/spelling/expect.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ BFirst
5757
bigcatalog
5858
BITMAPINFOHEADER
5959
bitmask
60+
BKMG
6061
bkup
6162
blargle
6263
blockedbypolicy
@@ -337,6 +338,7 @@ MINORVERSION
337338
missingdependency
338339
mkgmtime
339340
MMmmbbbb
341+
MODULEENTRY
340342
mof
341343
monicka
342344
MPNS
@@ -349,6 +351,7 @@ MSIXSTRM
349351
msstore
350352
MSZIP
351353
mszyml
354+
mta
352355
Mugiwara
353356
Multideclaration
354357
mysource
@@ -429,6 +432,7 @@ positionals
429432
posix
430433
postuninstall
431434
powershellgallery
435+
PPROCESS
432436
pri
433437
PRIMARYKEY
434438
processthreads
@@ -437,6 +441,7 @@ PRODUCTICON
437441
propkey
438442
PROPVARIANT
439443
proxystub
444+
psapi
440445
pscustomobject
441446
pseudocode
442447
PSHOST
@@ -476,6 +481,7 @@ rgp
476481
rgpsz
477482
rhs
478483
riid
484+
roapi
479485
Roblox
480486
ronomon
481487
rowid
@@ -518,11 +524,14 @@ sid
518524
Sideload
519525
SIGNATUREHASH
520526
silentpreferred
527+
SINGLETHREADED
521528
Skipx
522529
sku
523530
SLAPI
524531
SMTO
525532
SNAME
533+
SNAPMODULE
534+
SNAPTHREAD
526535
sortof
527536
sourceforge
528537
SOURCESDIRECTORY
@@ -555,11 +564,14 @@ tellp
555564
temppath
556565
testexampleinstaller
557566
thiscouldbeapc
567+
THREADENTRY
558568
threehundred
559569
timespan
560570
Tlg
571+
tlhelp
561572
TLSCAs
562573
tombstoned
574+
Toolhelp
563575
transitioning
564576
trimstart
565577
ttl

azure-pipelines.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ jobs:
213213
Contents: |
214214
AppInstallerCLIE2ETests\**
215215
AppInstallerCLITests\**
216+
ComInprocTestbed\**
216217
Microsoft.Management.Configuration\**
217218
Microsoft.Management.Configuration.UnitTests\**
218219
Microsoft.Management.Configuration.OutOfProc\**

src/AppInstallerCLI.sln

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F49C
226226
PowerShell\scripts\Initialize-LocalWinGetModules.ps1 = PowerShell\scripts\Initialize-LocalWinGetModules.ps1
227227
EndProjectSection
228228
EndProject
229+
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ComInprocTestbed", "ComInprocTestbed\ComInprocTestbed.vcxproj", "{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}"
230+
EndProject
229231
Global
230232
GlobalSection(SolutionConfigurationPlatforms) = preSolution
231233
Debug|ARM64 = Debug|ARM64
@@ -1022,6 +1024,30 @@ Global
10221024
{33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x64.Build.0 = Release|x64
10231025
{33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x86.ActiveCfg = Release|x86
10241026
{33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x86.Build.0 = Release|x86
1027+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|ARM64.ActiveCfg = Debug|ARM64
1028+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|ARM64.Build.0 = Debug|ARM64
1029+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x64.ActiveCfg = Debug|x64
1030+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x64.Build.0 = Debug|x64
1031+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x86.ActiveCfg = Debug|Win32
1032+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x86.Build.0 = Debug|Win32
1033+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|ARM64.ActiveCfg = Release|ARM64
1034+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|ARM64.Build.0 = Release|ARM64
1035+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x64.ActiveCfg = Release|x64
1036+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x64.Build.0 = Release|x64
1037+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x86.ActiveCfg = Release|Win32
1038+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x86.Build.0 = Release|Win32
1039+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|ARM64.ActiveCfg = Release|ARM64
1040+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|ARM64.Build.0 = Release|ARM64
1041+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x64.ActiveCfg = Release|x64
1042+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x64.Build.0 = Release|x64
1043+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x86.ActiveCfg = Release|Win32
1044+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x86.Build.0 = Release|Win32
1045+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|ARM64.ActiveCfg = Release|ARM64
1046+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|ARM64.Build.0 = Release|ARM64
1047+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x64.ActiveCfg = Release|x64
1048+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x64.Build.0 = Release|x64
1049+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x86.ActiveCfg = Release|Win32
1050+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x86.Build.0 = Release|Win32
10251051
EndGlobalSection
10261052
GlobalSection(SolutionProperties) = preSolution
10271053
HideSolutionNode = FALSE
@@ -1058,6 +1084,7 @@ Global
10581084
{A33223D2-550B-4D99-A53D-488B1F68683E} = {60618CAC-2995-4DF9-9914-45C6FC02C995}
10591085
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
10601086
{F49C4C89-447E-4D15-B38B-5A8DCFB134AF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9}
1087+
{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
10611088
EndGlobalSection
10621089
GlobalSection(ExtensibilityGlobals) = postSolution
10631090
SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857}

src/AppInstallerCLICore/Public/ShutdownMonitoring.h

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ namespace AppInstaller::ShutdownMonitoring
6666
};
6767

6868
// Coordinates shutdown across server components
69-
struct ServerShutdownSynchronization : public ICancellable
69+
struct ServerShutdownSynchronization
7070
{
7171
using ShutdownCompleteCallback = void (*)();
7272

7373
// Initializes the monitoring system and sets up a callback to be invoked when shutdown is completed.
74-
static void Initialize(ShutdownCompleteCallback callback);
74+
static void Initialize(ShutdownCompleteCallback callback, bool createTerminationSignalHandler = true);
7575

7676
// "Interface" for a single component to synchronize with.
7777
struct ComponentSystem
@@ -93,18 +93,20 @@ namespace AppInstaller::ShutdownMonitoring
9393
// Waits for the shutdown to complete.
9494
static void WaitForShutdown();
9595

96-
// Listens for a termination signal.
97-
void Cancel(CancelReason reason, bool force) override;
98-
9996
private:
100-
ServerShutdownSynchronization();
97+
ServerShutdownSynchronization() = default;
10198
~ServerShutdownSynchronization();
10299

100+
friend TerminationSignalHandler;
101+
103102
static ServerShutdownSynchronization& Instance();
104103

105104
// Runs the actual shutdown process and invokes the callback.
106105
void SynchronizeShutdown(CancelReason reason);
107106

107+
// Listens for a termination signal.
108+
void Signal(CancelReason reason);
109+
108110
ShutdownCompleteCallback m_callback = nullptr;
109111
std::mutex m_componentsLock;
110112
std::vector<ComponentSystem> m_components;

src/AppInstallerCLICore/ShutdownMonitoring.cpp

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,22 @@ namespace AppInstaller::ShutdownMonitoring
183183
// Returns FALSE if no contexts attached; TRUE otherwise.
184184
BOOL TerminationSignalHandler::InformListeners(CancelReason reason, bool force)
185185
{
186-
std::lock_guard<std::mutex> lock{ m_listenersLock };
186+
BOOL result = FALSE;
187187

188-
if (m_listeners.empty())
189188
{
190-
return FALSE;
191-
}
189+
std::lock_guard<std::mutex> lock{ m_listenersLock };
190+
result = m_listeners.empty() ? FALSE : TRUE;
192191

193-
for (auto& listener : m_listeners)
194-
{
195-
listener->Cancel(reason, force);
192+
for (auto& listener : m_listeners)
193+
{
194+
listener->Cancel(reason, force);
195+
}
196196
}
197197

198-
return TRUE;
198+
// Notify shutdown synchronization as well
199+
ServerShutdownSynchronization::Instance().Signal(reason);
200+
201+
return result;
199202
}
200203

201204
void TerminationSignalHandler::CreateWindowAndStartMessageLoop()
@@ -283,9 +286,16 @@ namespace AppInstaller::ShutdownMonitoring
283286
}
284287
}
285288

286-
void ServerShutdownSynchronization::Initialize(ShutdownCompleteCallback callback)
289+
void ServerShutdownSynchronization::Initialize(ShutdownCompleteCallback callback, bool createTerminationSignalHandler)
287290
{
288291
Instance().m_callback = callback;
292+
293+
// Force the creation of the TerminationSignalHandler singleton so that the process can listen for termination signals even if
294+
// it never attempts to run anything that explicitly registers for cancellation callbacks.
295+
if (createTerminationSignalHandler)
296+
{
297+
TerminationSignalHandler::Instance();
298+
}
289299
}
290300

291301
void ServerShutdownSynchronization::AddComponent(const ComponentSystem& component)
@@ -322,8 +332,17 @@ namespace AppInstaller::ShutdownMonitoring
322332
instance.m_shutdownComplete.wait();
323333
}
324334

325-
void ServerShutdownSynchronization::Cancel(CancelReason reason, bool)
335+
void ServerShutdownSynchronization::Signal(CancelReason reason)
326336
{
337+
{
338+
// Check for registered components before creating a thread to do nothing
339+
std::lock_guard<std::mutex> lock{ m_componentsLock };
340+
if (m_components.empty())
341+
{
342+
return;
343+
}
344+
}
345+
327346
std::lock_guard<std::mutex> lock{ m_threadLock };
328347

329348
if (!m_shutdownThread.joinable())
@@ -332,14 +351,8 @@ namespace AppInstaller::ShutdownMonitoring
332351
}
333352
}
334353

335-
ServerShutdownSynchronization::ServerShutdownSynchronization()
336-
{
337-
TerminationSignalHandler::Instance()->AddListener(this);
338-
}
339-
340354
ServerShutdownSynchronization::~ServerShutdownSynchronization()
341355
{
342-
TerminationSignalHandler::Instance()->RemoveListener(this);
343356
if (m_shutdownThread.joinable())
344357
{
345358
m_shutdownThread.detach();

src/AppInstallerCLIE2ETests/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class Constants
3232
public const string PowerShellModulePathParameter = "PowerShellModulePath";
3333
public const string SkipTestSourceParameter = "SkipTestSource";
3434
public const string ForcedExperimentalFeaturesParameter = "ForcedExperimentalFeatures";
35+
public const string InprocTestbedPathParameter = "InprocTestbedPath";
36+
public const string InprocTestbedUseTestPackageParameter = "InprocTestbedUseTestPackage";
3537

3638
// Test Sources
3739
public const string DefaultWingetSourceName = @"winget";

src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,6 @@ public static RunCommandResult RunAICLICommand(string command, string parameters
126126
}
127127
}
128128

129-
string inputMsg =
130-
"AICLI path: " + TestSetup.Parameters.AICLIPath +
131-
" Command: " + command +
132-
" Parameters: " + parameters + correlationParameter +
133-
(string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) +
134-
" Timeout: " + timeOut;
135-
136-
TestContext.Out.WriteLine($"Starting command run. {inputMsg}");
137-
138129
return RunAICLICommandViaDirectProcess(command, parameters + correlationParameter, stdIn, timeOut, throwOnTimeout);
139130
}
140131

@@ -1162,17 +1153,27 @@ public static string CopyInstallerFileToARPInstallSourceDirectory(string install
11621153
/// <summary>
11631154
/// Run winget command via direct process.
11641155
/// </summary>
1156+
/// <param name="executablePath">The executable to run.</param>
11651157
/// <param name="command">Command to run.</param>
11661158
/// <param name="parameters">Parameters.</param>
11671159
/// <param name="stdIn">Optional std in.</param>
11681160
/// <param name="timeOut">Optional timeout.</param>
11691161
/// <param name="throwOnTimeout">Throw on timeout.</param>
11701162
/// <returns>The result of the command.</returns>
1171-
private static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout)
1163+
public static RunCommandResult RunProcess(string executablePath, string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout)
11721164
{
1165+
string inputMsg =
1166+
"Exe path: " + executablePath +
1167+
" Command: " + command +
1168+
" Parameters: " + parameters +
1169+
(string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) +
1170+
" Timeout: " + timeOut;
1171+
1172+
TestContext.Out.WriteLine($"Starting command run. {inputMsg}");
1173+
11731174
RunCommandResult result = new ();
11741175
Process p = new Process();
1175-
p.StartInfo = new ProcessStartInfo(TestSetup.Parameters.AICLIPath, command + ' ' + parameters);
1176+
p.StartInfo = new ProcessStartInfo(executablePath, command + ' ' + parameters);
11761177
p.StartInfo.UseShellExecute = false;
11771178

11781179
p.StartInfo.StandardOutputEncoding = Encoding.UTF8;
@@ -1236,12 +1237,26 @@ private static RunCommandResult RunAICLICommandViaDirectProcess(string command,
12361237
}
12371238
else if (throwOnTimeout)
12381239
{
1239-
throw new TimeoutException($"Direct winget command run timed out: {command} {parameters}");
1240+
throw new TimeoutException($"Direct command run timed out: {command} {parameters}");
12401241
}
12411242

12421243
return result;
12431244
}
12441245

1246+
/// <summary>
1247+
/// Run winget command via direct process.
1248+
/// </summary>
1249+
/// <param name="command">Command to run.</param>
1250+
/// <param name="parameters">Parameters.</param>
1251+
/// <param name="stdIn">Optional std in.</param>
1252+
/// <param name="timeOut">Optional timeout.</param>
1253+
/// <param name="throwOnTimeout">Throw on timeout.</param>
1254+
/// <returns>The result of the command.</returns>
1255+
private static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout)
1256+
{
1257+
return RunProcess(TestSetup.Parameters.AICLIPath, command, parameters, stdIn, timeOut, throwOnTimeout);
1258+
}
1259+
12451260
/// <summary>
12461261
/// Run command result.
12471262
/// </summary>

src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ private TestSetup()
3131
this.VerboseLogging = this.InitializeBoolParam(Constants.VerboseLoggingParameter, true);
3232
this.LooseFileRegistration = this.InitializeBoolParam(Constants.LooseFileRegistrationParameter);
3333
this.SkipTestSource = this.InitializeBoolParam(Constants.SkipTestSourceParameter, this.IsDefault);
34+
this.InprocTestbedUseTestPackage = this.InitializeBoolParam(Constants.InprocTestbedUseTestPackageParameter);
3435

3536
// For packaged context, default to AppExecutionAlias
3637
this.AICLIPath = this.InitializeStringParam(Constants.AICLIPathParameter, this.PackagedContext ? "WinGetDev.exe" : TestCommon.GetTestFile("winget.exe"));
@@ -45,6 +46,7 @@ private TestSetup()
4546
this.MsixInstallerPath = this.InitializeFileParam(Constants.MsixInstallerPathParameter);
4647
this.MsiInstallerV2Path = this.InitializeFileParam(Constants.MsiInstallerV2PathParameter);
4748
this.FontPath = this.InitializeFileParam(Constants.FontPathParameter);
49+
this.InprocTestbedPath = this.InitializeFileParam(Constants.InprocTestbedPathParameter);
4850

4951
this.ForcedExperimentalFeatures = this.InitializeStringArrayParam(Constants.ForcedExperimentalFeaturesParameter);
5052
}
@@ -130,6 +132,16 @@ public static TestSetup Parameters
130132
/// </summary>
131133
public string PackageCertificatePath { get; }
132134

135+
/// <summary>
136+
/// Gets the inproc testbed executable path.
137+
/// </summary>
138+
public string InprocTestbedPath { get; }
139+
140+
/// <summary>
141+
/// Gets a value indicating whether to use the test package or not.
142+
/// </summary>
143+
public bool InprocTestbedUseTestPackage { get; }
144+
133145
/// <summary>
134146
/// Gets a value indicating whether to skip creating test source.
135147
/// </summary>

0 commit comments

Comments
 (0)