Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ jobs:
matrix:
os: [ windows-latest, macos-latest, ubuntu-latest ]
runs-on: ${{ matrix.os }}
# A normal run finishes in well under 30 minutes (Windows, the slowest, is
# ~12-14 minutes); this caps a hung test instead of letting it ride
# GitHub's 6-hour default.
timeout-minutes: 30
env:
DOTNET_NOLOGO: true
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
Expand Down
68 changes: 3 additions & 65 deletions src/PowerShellEditorServices.Hosting/Internal/PsesLoadContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,72 +76,10 @@ private static bool IsSatisfyingAssembly(AssemblyName requiredAssemblyName, stri
return false;
}

return IsSatisfyingAssembly(requiredAssemblyName, AssemblyName.GetAssemblyName(assemblyPath));
}

// Internal (rather than private) purely so it can be unit tested with constructed
// AssemblyName instances; it has no file-system dependency of its own.
internal static bool IsSatisfyingAssembly(AssemblyName requiredAssemblyName, AssemblyName asmToLoadName)
{
// The simple name must match (case-insensitively, as assembly names are).
if (!string.Equals(asmToLoadName.Name, requiredAssemblyName.Name, StringComparison.OrdinalIgnoreCase))
{
return false;
}

// The candidate must be at least the requested version. We still accept newer
// versions, since shared framework and $PSHOME assemblies are generally
// forward-compatible via the runtime's binding.
if (asmToLoadName.Version < requiredAssemblyName.Version)
{
return false;
}

// The strong-name identity must match. Previously only the simple name and version
// were compared, so a same-named assembly with a *different* public key token (i.e.
// a genuinely different assembly) was treated as a drop-in replacement and would then
// fail at runtime with a FileLoadException/TypeLoadException. Requiring the public key
// token to match means we only short-circuit to a $PSHOME/Common assembly that can
// actually satisfy the reference; otherwise we fall through and let the default load
// context resolve it with its own (laxer) rules.
if (!PublicKeyTokensMatch(requiredAssemblyName, asmToLoadName))
{
return false;
}

// The culture must match so we never substitute a satellite resource assembly for the
// neutral one (or vice versa).
return string.Equals(
asmToLoadName.CultureName ?? string.Empty,
requiredAssemblyName.CultureName ?? string.Empty,
StringComparison.OrdinalIgnoreCase);
}

private static bool PublicKeyTokensMatch(AssemblyName requiredAssemblyName, AssemblyName candidateAssemblyName)
{
byte[] requiredToken = requiredAssemblyName.GetPublicKeyToken();

// A reference to a non-strong-named assembly imposes no public key token requirement.
if (requiredToken is null || requiredToken.Length == 0)
{
return true;
}

byte[] candidateToken = candidateAssemblyName.GetPublicKeyToken();
if (candidateToken is null || candidateToken.Length != requiredToken.Length)
{
return false;
}

for (int i = 0; i < requiredToken.Length; i++)
{
if (requiredToken[i] != candidateToken[i])
{
return false;
}
}
AssemblyName asmToLoadName = AssemblyName.GetAssemblyName(assemblyPath);

return true;
return string.Equals(asmToLoadName.Name, requiredAssemblyName.Name, StringComparison.OrdinalIgnoreCase)
&& asmToLoadName.Version >= requiredAssemblyName.Version;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Microsoft.PowerShell.EditorServices.Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" PrivateAssets="all" />
<PackageReference Include="System.IO.Pipes.AccessControl" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,34 +217,61 @@ private string GenerateLoggingScript(params string[] logStatements)
/// <summary>
/// Reads the next output line from the test script log file. Useful in assertions to verify script progress against breakpointing.
/// </summary>
private async Task<string> ReadScriptLogLineAsync()
private async Task<string> ReadScriptLogLineAsync(CancellationToken cancellationToken = default)
{
while (scriptLogReader is null)
// Reading a log line should be near-instant, but the script we read
// from is driven by the debugger and can fail to produce output (for
// example if an attach or breakpoint never lands). Cap the wait so
// the test fails fast with a clear message instead of spinning
// forever -- a busy-spin here previously pegged the CPU and starved
// xUnit's cooperative test timeout, hanging CI for the full six hours.
// Keep this cap meaningfully below the tightest per-test xUnit
// `Timeout` (15s on `CanAttachScriptWithPathMappings`) so this
// descriptive message wins instead of xUnit's generic timeout.
using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(10));
CancellationToken token = timeoutCts.Token;

try
{
try
while (scriptLogReader is null)
{
scriptLogReader = new StreamReader(
new FileStream(
testScriptLogPath,
FileMode.OpenOrCreate,
FileAccess.Read, // Because we use append, its OK to create the file ahead of the script
FileShare.ReadWrite
)
);
try
{
scriptLogReader = new StreamReader(
new FileStream(
testScriptLogPath,
FileMode.OpenOrCreate,
FileAccess.Read, // Because we use append, its OK to create the file ahead of the script
FileShare.ReadWrite
)
);
}
catch (IOException) //Sadly there does not appear to be a xplat way to wait for file availability, but luckily this does not appear to fire often.
{
await Task.Delay(500, token);
}
}
catch (IOException) //Sadly there does not appear to be a xplat way to wait for file availability, but luckily this does not appear to fire often.

// return valid lines only
string nextLine = string.Empty;
while (string.IsNullOrEmpty(nextLine))
{
await Task.Delay(500);
nextLine = await scriptLogReader.ReadLineAsync(token); //Might return null if at EOF because we created it above but the script hasn't written to it yet
if (string.IsNullOrEmpty(nextLine))
{
// At EOF waiting for the script to write more: yield and
// back off so we don't busy-spin the CPU while polling.
await Task.Delay(100, token);
}
}
return nextLine;
}

// return valid lines only
string nextLine = string.Empty;
while (nextLine is null || nextLine.Length == 0)
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
{
nextLine = await scriptLogReader.ReadLineAsync(); //Might return null if at EOF because we created it above but the script hasn't written to it yet
throw new TimeoutException(
$"Timed out waiting for the test script to write a log line to '{testScriptLogPath}'.");
}
return nextLine;
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@
<ProjectReference Include="..\PowerShellEditorServices.Test.Shared\PowerShellEditorServices.Test.Shared.csproj" />
</ItemGroup>

<!-- The Hosting assembly (and thus PsesLoadContext) only exists on .NET Core. -->
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<ProjectReference Include="..\..\src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj" />
</ItemGroup>

<!-- PowerShell 7.4.x -->
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.PowerShell.SDK" />
Expand Down
134 changes: 0 additions & 134 deletions test/PowerShellEditorServices.Test/Session/PsesLoadContextTests.cs

This file was deleted.

Loading