diff --git a/CHANGELOG/7.4.md b/CHANGELOG/7.4.md index 89a3f64e37c..f999c20315f 100644 --- a/CHANGELOG/7.4.md +++ b/CHANGELOG/7.4.md @@ -1,5 +1,82 @@ # 7.4 Changelog +## [7.4.14] + +### General Cmdlet Updates and Fixes + +- Fix `PSMethodInvocationConstraints.GetHashCode` method (#26959) + +### Tools + +- Add merge conflict marker detection to `linux-ci` workflow and refactor existing actions to use reusable `get-changed-files` action (#26362) +- Add reusable `get-changed-files` action and refactor existing actions (#26361) +- Refactor analyze job to reusable workflow and enable on Windows CI (#26342) + +### Tests + +- Skip the flaky `Update-Help` test for the `PackageManagement` module (#26871) +- Fix `$PSDefaultParameterValues` leak causing tests to skip unexpectedly (#26869) +- Add GitHub Actions annotations for Pester test failures (#26800) +- Mark flaky `Update-Help` web tests as pending to unblock CI (#26805) +- Update the `Update-Help` tests to use `-Force` to remove read-only files (#26786) +- Fix merge conflict checker for empty file lists and filter `*.cs` files (#26387) +- Add markdown link verification for PRs (#26340) + +### Build and Packaging Improvements + + + + + +Update .NET SDK to 8.0.419 + + + + +Update MaxVisitCount and MaxHashtableKeyCount if visitor safe value context indicates SkipLimitCheck is true (Internal 38882) +Hardcode Official templates (#26962) +Split TPN manifest and Component Governance manifest (#26961) +Correct the package name for .deb and .rpm packages (#26960) +Bring over all changes for MSIX packaging template (#26933) +.NET Resolution and Store Publishing Updates (#26930) +Update Application Insights package version to 2.23.0 (#26883) +Update metadata.json to update the Latest attribute with a better name (#26872) +Update Get-ChangeLog to handle backport PRs correctly (#26870) +Remove unused runCodesignValidationInjection variable from pipeline templates (#26868) +Refactor: Centralize xUnit tests into reusable workflow and remove legacy verification (#26864) +Fix buildinfo.json uploading for preview, LTS, and stable releases (#26863) +Fix macOS preview package identifier detection to use version string (#26774) +Update the macOS package name for preview releases to match the previous pattern (#26435) +Fix condition syntax for StoreBroker package tasks in MSIX pipeline (#26434) +Fix template path for rebuild branch check in package.yml (#26433) +Add rebuild branch support with conditional MSIX signing (#26418) +Move package validation to package pipeline (#26417) +Backport Store publishing improvements (#26401) +Fix path to metadata.json in channel selection script (#26399) +Optimize/split Windows package signing (#26413) +Improve ADO package build and validation across platforms (#26405) +Separate Store Automation Service Endpoints, Resolve AppID (#26396) +Fix the task name to not use the pre-release task (#26395) +Remove usage of fpm for DEB package generation (#26382) +Replace fpm with native macOS packaging tools (pkgbuild/productbuild) (#26344) +Replace fpm with native rpmbuild for RPM package generation (#26337) +Add log grouping to build.psm1 for collapsible GitHub Actions logs (#26363) +Convert Azure DevOps Linux Packaging pipeline to GitHub Actions workflow (#26336) +Integrate Windows packaging into windows-ci workflow using reusable workflow (#26335) +Add network isolation policy parameter to vPack pipeline (#26339) +GitHub Workflow cleanup (#26334) +Add build to vPack Pipeline (#25980) +Update vPack name (#26222) + + + + +### Documentation and Help Content + +- Update Third Party Notices (#26892) + +[7.4.14]: https://github.com/PowerShell/PowerShell/compare/v7.4.13...v7.4.14 + ## [7.4.13] ### Build and Packaging Improvements diff --git a/src/Modules/PSGalleryModules.csproj b/src/Modules/PSGalleryModules.csproj index 9136df5c7b3..3903b164b09 100644 --- a/src/Modules/PSGalleryModules.csproj +++ b/src/Modules/PSGalleryModules.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/System.Management.Automation/engine/lang/interface/PSToken.cs b/src/System.Management.Automation/engine/lang/interface/PSToken.cs index 306a64a0b61..46a9b644167 100644 --- a/src/System.Management.Automation/engine/lang/interface/PSToken.cs +++ b/src/System.Management.Automation/engine/lang/interface/PSToken.cs @@ -157,6 +157,7 @@ public static PSTokenType GetPSTokenType(Token token) /* AndAnd */ PSTokenType.Operator, /* OrOr */ PSTokenType.Operator, /* Ampersand */ PSTokenType.Operator, + /* AmpersandExclaim */ PSTokenType.Operator, /* Pipe */ PSTokenType.Operator, /* Comma */ PSTokenType.Operator, /* MinusMinus */ PSTokenType.Operator, @@ -303,6 +304,7 @@ public static PSTokenType GetPSTokenType(Token token) /* Hidden */ PSTokenType.Keyword, /* Base */ PSTokenType.Keyword, /* Default */ PSTokenType.Keyword, + /* Clean */ PSTokenType.Keyword, #endregion Flags for keywords diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 1592d2e7e7d..fbc16b9ee76 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -5346,6 +5346,7 @@ private StatementAst FunctionDeclarationRule(Token functionToken) case TokenKind.AndAnd: case TokenKind.OrOr: case TokenKind.Ampersand: + case TokenKind.AmpersandExclaim: case TokenKind.Variable: case TokenKind.SplattedVariable: case TokenKind.HereStringExpandable: @@ -5849,6 +5850,7 @@ private PipelineBaseAst PipelineChainRule() Token currentChainOperatorToken = null; Token nextToken = null; bool background = false; + bool backgroundThreadJob = false; while (true) { // Look for the next pipeline in the chain, @@ -5938,6 +5940,24 @@ private PipelineBaseAst PipelineChainRule() background = true; goto default; + // ThreadJob background operator + case TokenKind.AmpersandExclaim: + SkipToken(); + nextToken = PeekToken(); + + switch (nextToken.Kind) + { + case TokenKind.AndAnd: + case TokenKind.OrOr: + SkipToken(); + ReportError(nextToken.Extent, nameof(ParserStrings.BackgroundOperatorInPipelineChain), ParserStrings.BackgroundOperatorInPipelineChain); + return new ErrorStatementAst(ExtentOf(currentPipelineChain ?? nextPipeline, nextToken.Extent)); + } + + background = true; + backgroundThreadJob = true; + goto default; + // No more chain operators -- return default: // If we haven't seen a chain yet, pass through the pipeline @@ -5951,15 +5971,18 @@ private PipelineBaseAst PipelineChainRule() // Set background on the pipeline AST nextPipeline.Background = true; + nextPipeline.BackgroundThreadJob = backgroundThreadJob; return nextPipeline; } - return new PipelineChainAst( + var chainAst = new PipelineChainAst( ExtentOf(currentPipelineChain.Extent, nextPipeline.Extent), currentPipelineChain, nextPipeline, currentChainOperatorToken.Kind, background); + chainAst.BackgroundThreadJob = backgroundThreadJob; + return chainAst; } // Assemble the new chain statement AST @@ -6008,6 +6031,7 @@ private PipelineBaseAst PipelineRule( Token nextToken = null; bool scanning = true; bool background = false; + bool backgroundThreadJob = false; ExpressionAst expr = startExpression; while (scanning) { @@ -6125,6 +6149,20 @@ private PipelineBaseAst PipelineRule( background = true; break; + case TokenKind.AmpersandExclaim: + if (!allowBackground) + { + // Handled by invoking rule + scanning = false; + continue; + } + + SkipToken(); + scanning = false; + background = true; + backgroundThreadJob = true; + break; + case TokenKind.Pipe: SkipToken(); SkipNewlines(); @@ -6156,7 +6194,12 @@ private PipelineBaseAst PipelineRule( return null; } - return new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements, background); + var pipeline = new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements, background); + if (backgroundThreadJob) + { + pipeline.BackgroundThreadJob = true; + } + return pipeline; } private RedirectionAst RedirectionRule(RedirectionToken redirectionToken, RedirectionAst[] redirections, ref IScriptExtent extent) @@ -6316,6 +6359,7 @@ private ExpressionAst GetCommandArgument(CommandArgumentContext context, Token t case TokenKind.AndAnd: case TokenKind.OrOr: case TokenKind.Ampersand: + case TokenKind.AmpersandExclaim: case TokenKind.MinusMinus: case TokenKind.Comma: UngetToken(token); @@ -6520,6 +6564,7 @@ internal Ast CommandRule(bool forDynamicKeyword) case TokenKind.AndAnd: case TokenKind.OrOr: case TokenKind.Ampersand: + case TokenKind.AmpersandExclaim: UngetToken(token); scanning = false; continue; diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 0325cf94aeb..d37dd394326 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -5610,7 +5610,9 @@ public PipelineChainAst( /// public override Ast Copy() { - return new PipelineChainAst(Extent, CopyElement(LhsPipelineChain), CopyElement(RhsPipeline), Operator, Background); + var copy = new PipelineChainAst(Extent, CopyElement(LhsPipelineChain), CopyElement(RhsPipeline), Operator, Background); + copy.BackgroundThreadJob = this.BackgroundThreadJob; + return copy; } internal override object Accept(ICustomAstVisitor visitor) @@ -5675,6 +5677,11 @@ public virtual ExpressionAst GetPureExpression() { return null; } + + /// + /// Indicates that this pipeline should be run in the background as a ThreadJob. + /// + public bool BackgroundThreadJob { get; internal set; } } /// @@ -5793,7 +5800,9 @@ public override ExpressionAst GetPureExpression() public override Ast Copy() { var newPipelineElements = CopyElements(this.PipelineElements); - return new PipelineAst(this.Extent, newPipelineElements, this.Background); + var copy = new PipelineAst(this.Extent, newPipelineElements, this.Background); + copy.BackgroundThreadJob = this.BackgroundThreadJob; + return copy; } #region Visitors diff --git a/src/System.Management.Automation/engine/parser/token.cs b/src/System.Management.Automation/engine/parser/token.cs index 3cee7580ff9..8a5bc98437f 100644 --- a/src/System.Management.Automation/engine/parser/token.cs +++ b/src/System.Management.Automation/engine/parser/token.cs @@ -428,6 +428,9 @@ public enum TokenKind /// The null conditional index access operator '?[]'. QuestionLBracket = 104, + /// The ThreadJob background operator '&!'. + AmpersandExclaim = 106, + #endregion Operators #region Keywords @@ -881,7 +884,7 @@ public static class TokenTraits /* QuestionQuestion */ TokenFlags.BinaryOperator | TokenFlags.BinaryPrecedenceCoalesce, /* QuestionDot */ TokenFlags.SpecialOperator | TokenFlags.DisallowedInRestrictedMode, /* QuestionLBracket */ TokenFlags.None, - /* Reserved slot 7 */ TokenFlags.None, + /* AmpersandExclaim */ TokenFlags.SpecialOperator | TokenFlags.ParseModeInvariant, /* Reserved slot 8 */ TokenFlags.None, /* Reserved slot 9 */ TokenFlags.None, /* Reserved slot 10 */ TokenFlags.None, @@ -1081,7 +1084,7 @@ public static class TokenTraits /* QuestionQuestion */ "??", /* QuestionDot */ "?.", /* QuestionLBracket */ "?[", - /* Reserved slot 7 */ string.Empty, + /* AmpersandExclaim */ "&!", /* Reserved slot 8 */ string.Empty, /* Reserved slot 9 */ string.Empty, /* Reserved slot 10 */ string.Empty, @@ -1160,10 +1163,10 @@ public static class TokenTraits static TokenTraits() { Diagnostics.Assert( - s_staticTokenFlags.Length == ((int)TokenKind.Clean + 1), + s_staticTokenFlags.Length == ((int)TokenKind.AmpersandExclaim + 1), "Table size out of sync with enum - _staticTokenFlags"); Diagnostics.Assert( - s_tokenText.Length == ((int)TokenKind.Clean + 1), + s_tokenText.Length == ((int)TokenKind.AmpersandExclaim + 1), "Table size out of sync with enum - _tokenText"); // Some random assertions to make sure the enum and the traits are in sync Diagnostics.Assert(GetTraits(TokenKind.Begin) == (TokenFlags.Keyword | TokenFlags.ScriptBlockBlockName), diff --git a/src/System.Management.Automation/engine/parser/tokenizer.cs b/src/System.Management.Automation/engine/parser/tokenizer.cs index e2aed94cc98..4804d574fd0 100644 --- a/src/System.Management.Automation/engine/parser/tokenizer.cs +++ b/src/System.Management.Automation/engine/parser/tokenizer.cs @@ -4975,12 +4975,19 @@ internal Token NextToken() return ScanNumber(c); case '&': - if (PeekChar() == '&') + c1 = PeekChar(); + if (c1 == '&') { SkipChar(); return NewToken(TokenKind.AndAnd); } + if (c1 == '!') + { + SkipChar(); + return NewToken(TokenKind.AmpersandExclaim); + } + return NewToken(TokenKind.Ampersand); case '|': diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index d584666ab62..75e855439b1 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -28,6 +28,13 @@ namespace System.Management.Automation { internal static class PipelineOps { + // PowerShell magic/automatic variable names that should not be auto-prefixed with $using: + // when constructing a background job script block for the &! operator. + private static readonly HashSet s_backgroundJobMagicVariables = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "PID", "PSVersionTable", "PSEdition", "PSHOME", "HOST", "TRUE", "FALSE", "NULL" + }; + private static CommandProcessorBase AddCommand(PipelineProcessor pipe, CommandParameterInternal[] commandElements, CommandBaseAst commandBaseAst, @@ -550,51 +557,110 @@ internal static void InvokePipelineInBackground( CommandProcessorBase commandProcessor = null; // For background jobs rewrite the pipeline as a Start-Job command - var scriptblockBodyString = pipelineAst.Extent.Text; - var pipelineOffset = pipelineAst.Extent.StartOffset; - var variables = pipelineAst.FindAll(static x => x is VariableExpressionAst, true); - - // Minimize allocations by initializing the stringbuilder to the size of the source string + space for ${using:} * 2 - System.Text.StringBuilder updatedScriptblock = new System.Text.StringBuilder(scriptblockBodyString.Length + 18); - int position = 0; - - // Prefix variables in the scriptblock with $using: - foreach (var v in variables) + ScriptBlock sb; + + // Check if the pipeline is already a script block expression (e.g., {1+1} &!) + // In this case, we should use the script block directly instead of wrapping it + // Note: PipelineElements is only available on PipelineAst, not PipelineBaseAst + var scriptBlockExpr = pipelineAst is PipelineAst pipeline && + pipeline.PipelineElements.Count == 1 && + pipeline.PipelineElements[0] is CommandExpressionAst cmdExpr && + cmdExpr.Expression is ScriptBlockExpressionAst sbExpr + ? sbExpr + : null; + + if (scriptBlockExpr != null) + { + // The pipeline is already a script block - use the ScriptBlock from the AST directly. + // Using ScriptBlock.Create("{ content }") would create a script that returns a ScriptBlock + // object rather than executing the body. Getting the ScriptBlock from the ScriptBlockAst + // avoids all text manipulation and correctly executes the body. + // Users are expected to use explicit $using: scoping in their scriptblock to capture + // outer variables (e.g., { $using:testVar } &!). + sb = scriptBlockExpr.ScriptBlock.GetScriptBlock(); + } + else { - var variableName = ((VariableExpressionAst)v).VariablePath.UserPath; + // The pipeline is a regular command - wrap it in a script block. + // Auto-inject $using: for variables that exist in the current scope. + var scriptblockBodyString = pipelineAst.Extent.Text; + var pipelineOffset = pipelineAst.Extent.StartOffset; + var variables = pipelineAst.FindAll(static x => x is VariableExpressionAst, true); - // Skip variables that don't exist - if (funcContext._executionContext.EngineSessionState.GetVariable(variableName) == null) + // Minimize allocations by initializing the stringbuilder to the size of the source string + space for ${using:} * 2 + System.Text.StringBuilder updatedScriptblock = new System.Text.StringBuilder(scriptblockBodyString.Length + 18); + int position = 0; + + // Prefix variables in the scriptblock with $using: + foreach (var v in variables) { - continue; + var variableName = ((VariableExpressionAst)v).VariablePath.UserPath; + + // Skip variables that don't exist + if (funcContext._executionContext.EngineSessionState.GetVariable(variableName) == null) + { + continue; + } + + // Strip global: prefix if present, then check against magic variable names + var cleanVariableName = variableName.StartsWith("global:", StringComparison.OrdinalIgnoreCase) + ? variableName.Substring(7) + : variableName; + + if (!s_backgroundJobMagicVariables.Contains(cleanVariableName)) + { + updatedScriptblock.Append(scriptblockBodyString.AsSpan(position, v.Extent.StartOffset - pipelineOffset - position)); + updatedScriptblock.Append("${using:"); + updatedScriptblock.Append(CodeGeneration.EscapeVariableName(variableName)); + updatedScriptblock.Append('}'); + position = v.Extent.EndOffset - pipelineOffset; + } } - // Skip PowerShell magic variables - if (!Regex.Match( - variableName, - "^(global:){0,1}(PID|PSVersionTable|PSEdition|PSHOME|HOST|TRUE|FALSE|NULL)$", - RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Success) + updatedScriptblock.Append(scriptblockBodyString.AsSpan(position)); + sb = ScriptBlock.Create(updatedScriptblock.ToString()); + } + + // Use Start-ThreadJob if BackgroundThreadJob is set, otherwise use Start-Job + CmdletInfo commandInfo; + bool usingThreadJob = false; + if (pipelineAst.BackgroundThreadJob) + { + // Use CommandTypes.Cmdlet only to avoid resolving a user-defined function that + // shadows the real Start-ThreadJob cmdlet, which would cause &! to silently fall + // back to Start-Job even when the ThreadJob module is installed. + var threadJobCmdlet = context.SessionState.InvokeCommand.GetCommand("Start-ThreadJob", CommandTypes.Cmdlet) as CmdletInfo; + if (threadJobCmdlet != null) + { + commandInfo = threadJobCmdlet; + usingThreadJob = true; + } + else { - updatedScriptblock.Append(scriptblockBodyString.AsSpan(position, v.Extent.StartOffset - pipelineOffset - position)); - updatedScriptblock.Append("${using:"); - updatedScriptblock.Append(CodeGeneration.EscapeVariableName(variableName)); - updatedScriptblock.Append('}'); - position = v.Extent.EndOffset - pipelineOffset; + // Fall back to Start-Job if Start-ThreadJob cmdlet is not available + commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand)); } } - - updatedScriptblock.Append(scriptblockBodyString.AsSpan(position)); - var sb = ScriptBlock.Create(updatedScriptblock.ToString()); - var commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand)); + else + { + commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand)); + } + commandProcessor = context.CommandDiscovery.LookupCommandProcessor(commandInfo, CommandOrigin.Internal, false, context.EngineSessionState); - var workingDirectoryParameter = CommandParameterInternal.CreateParameterWithArgument( - parameterAst: pipelineAst, - parameterName: "WorkingDirectory", - parameterText: null, - argumentAst: pipelineAst, - value: context.SessionState.Path.CurrentLocation.Path, - spaceAfterParameter: false); + // Only add WorkingDirectory parameter for Start-Job, not for Start-ThreadJob + // Start-ThreadJob doesn't support the WorkingDirectory parameter + if (!usingThreadJob) + { + var workingDirectoryParameter = CommandParameterInternal.CreateParameterWithArgument( + parameterAst: pipelineAst, + parameterName: "WorkingDirectory", + parameterText: null, + argumentAst: pipelineAst, + value: context.SessionState.Path.CurrentLocation.Path, + spaceAfterParameter: false); + commandProcessor.AddParameter(workingDirectoryParameter); + } var scriptBlockParameter = CommandParameterInternal.CreateParameterWithArgument( parameterAst: pipelineAst, @@ -604,7 +670,6 @@ internal static void InvokePipelineInBackground( value: sb, spaceAfterParameter: false); - commandProcessor.AddParameter(workingDirectoryParameter); commandProcessor.AddParameter(scriptBlockParameter); pipelineProcessor.Add(commandProcessor); pipelineProcessor.LinkPipelineSuccessOutput(outputPipe ?? new Pipe(new List())); diff --git a/test/powershell/Language/Operators/ThreadJobBackgroundOperator.Tests.ps1 b/test/powershell/Language/Operators/ThreadJobBackgroundOperator.Tests.ps1 new file mode 100644 index 00000000000..c8a93b77007 --- /dev/null +++ b/test/powershell/Language/Operators/ThreadJobBackgroundOperator.Tests.ps1 @@ -0,0 +1,324 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "ThreadJob Background Operator &! Tests" -Tag CI { + BeforeAll { + # Ensure ThreadJob module is available + $threadJobAvailable = $null -ne (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue) + + if (-not $threadJobAvailable) { + Write-Warning "Start-ThreadJob command not available. Tests may fall back to regular jobs." + } + } + + Context "Runtime ThreadJob Tests" { + It "Creates a background job with &! operator" { + $job = Write-Output "Hello from ThreadJob" &! + try { + $job | Should -Not -BeNullOrEmpty + $job | Should -BeOfType [System.Management.Automation.Job] + + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Receives output from ThreadJob background job" { + $job = Write-Output "Test Output" &! + try { + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Be "Test Output" + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Runs simple expression as ThreadJob" { + $job = 1 + 1 &! + try { + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Be 2 + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Validates ThreadJob is created when Start-ThreadJob is available" -Skip:(-not $threadJobAvailable) { + $job = Write-Output "ThreadJob Test" &! + try { + $job | Should -Not -BeNullOrEmpty + $job.PSJobTypeName | Should -Be 'ThreadJob' + + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Falls back to regular job when Start-ThreadJob is unavailable" -Skip:$threadJobAvailable { + # This test runs only when ThreadJob is not available + $job = Write-Output "Fallback Test" &! + try { + $job | Should -Not -BeNullOrEmpty + $job | Should -BeOfType [System.Management.Automation.Job] + # Should not be a ThreadJob + $job.PSJobTypeName | Should -Not -Be 'ThreadJob' + + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It 'Captures variables automatically without explicit $using:' { + $testVar = "CapturedValue" + $job = Write-Output $testVar &! + try { + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Be "CapturedValue" + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It 'Captures variables with explicit $using: in scriptblock' { + $testVar = "CapturedValueWithUsing" + $job = { $using:testVar } &! + try { + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Be "CapturedValueWithUsing" + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Runs pipeline as ThreadJob" { + $job = 1,2,3 | ForEach-Object { $_ * 2 } &! + try { + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Be @(2, 4, 6) + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Works with variable assignment" { + $job = 1 + 2 &! + try { + $job | Should -Not -BeNullOrEmpty + + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Be 3 + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Can be combined with && operator" { + $job = testexe -returncode 0 && Write-Output "success" &! + try { + $job | Should -Not -BeNullOrEmpty + $job | Should -BeOfType [System.Management.Automation.Job] + + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Contain "0" + $result | Should -Contain "success" + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Works with command execution" { + $job = Get-Process -Id $PID | Select-Object -ExpandProperty Name &! + try { + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $result = $completedJob | Receive-Job + $result | Should -Not -BeNullOrEmpty + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Handles errors in ThreadJob" { + $job = { throw "Test Error" } &! + try { + $completedJob = $job | Wait-Job -Timeout 30 + if (-not $completedJob) { + throw "Job did not complete within the allotted timeout (30 seconds)." + } + + $completedJob.State | Should -Be 'Failed' + } + finally { + if ($null -ne $job) { + $job | Remove-Job -Force -ErrorAction Ignore + } + } + } + + It "Creates multiple ThreadJobs" { + $job1 = Write-Output "Job1" &! + $job2 = Write-Output "Job2" &! + $job3 = Write-Output "Job3" &! + try { + $completedJobs = $job1, $job2, $job3 | Wait-Job -Timeout 30 + if ($completedJobs.Count -ne 3) { + throw "Not all jobs completed within the allotted timeout (30 seconds)." + } + + $results = $completedJobs | Receive-Job + $results | Should -Contain "Job1" + $results | Should -Contain "Job2" + $results | Should -Contain "Job3" + } + finally { + $job1, $job2, $job3 | Where-Object { $null -ne $_ } | Remove-Job -Force -ErrorAction Ignore + } + } + } + + Context "Syntax Validation Tests" { + It "Rejects &! with && in invalid syntax" { + $tokens = $errors = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput('testexe -returncode 0 &! && testexe -returncode 1', [ref]$tokens, [ref]$errors) + + $errors.Count | Should -BeGreaterThan 0 + $errors[0].ErrorId | Should -Be 'BackgroundOperatorInPipelineChain' + } + + It "Rejects &! with || in invalid syntax" { + $tokens = $errors = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput('testexe -returncode 0 &! || testexe -returncode 1', [ref]$tokens, [ref]$errors) + + $errors.Count | Should -BeGreaterThan 0 + $errors[0].ErrorId | Should -Be 'BackgroundOperatorInPipelineChain' + } + } + + Context "Parser AST Tests" { + It "Parses &! operator correctly" { + $ast = [System.Management.Automation.Language.Parser]::ParseInput('Write-Output "test" &!', [ref]$null, [ref]$null) + $pipelineAst = $ast.EndBlock.Statements[0] + $pipelineAst | Should -BeOfType [System.Management.Automation.Language.PipelineAst] + $pipelineAst.Background | Should -Be $true + $pipelineAst.BackgroundThreadJob | Should -Be $true + } + + It "Distinguishes between & and &! operators" { + $ast1 = [System.Management.Automation.Language.Parser]::ParseInput('Write-Output "test" &', [ref]$null, [ref]$null) + $pipelineAst1 = $ast1.EndBlock.Statements[0] + $pipelineAst1.Background | Should -Be $true + $pipelineAst1.BackgroundThreadJob | Should -Be $false + + $ast2 = [System.Management.Automation.Language.Parser]::ParseInput('Write-Output "test" &!', [ref]$null, [ref]$null) + $pipelineAst2 = $ast2.EndBlock.Statements[0] + $pipelineAst2.Background | Should -Be $true + $pipelineAst2.BackgroundThreadJob | Should -Be $true + } + } + + Context "Tokenizer Tests" { + It "Tokenizes &! as AmpersandExclaim" { + $tokens = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput('Write-Output "test" &!', [ref]$tokens, [ref]$null) + + $ampersandExclaimToken = $tokens | Where-Object { $_.Kind -eq [System.Management.Automation.Language.TokenKind]::AmpersandExclaim } + $ampersandExclaimToken | Should -Not -BeNullOrEmpty + $ampersandExclaimToken.Text | Should -Be '&!' + } + + It "Distinguishes & and &! tokens" { + $tokens1 = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput('test &', [ref]$tokens1, [ref]$null) + $ampToken = $tokens1 | Where-Object { $_.Kind -eq [System.Management.Automation.Language.TokenKind]::Ampersand } + $ampToken | Should -Not -BeNullOrEmpty + + $tokens2 = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput('test &!', [ref]$tokens2, [ref]$null) + $ampExclaimToken = $tokens2 | Where-Object { $_.Kind -eq [System.Management.Automation.Language.TokenKind]::AmpersandExclaim } + $ampExclaimToken | Should -Not -BeNullOrEmpty + } + } +} diff --git a/tools/packaging/boms/windows.json b/tools/packaging/boms/windows.json index 8248b3d03aa..8d900902bc2 100644 --- a/tools/packaging/boms/windows.json +++ b/tools/packaging/boms/windows.json @@ -1216,6 +1216,11 @@ "FileType": "NonProduct", "Architecture": null }, + { + "Pattern": "Modules\\Microsoft.PowerShell.PSResourceGet\\.signature.p7s", + "FileType": "NonProduct", + "Architecture": null + }, { "Pattern": "Modules\\Microsoft.PowerShell.PSResourceGet\\Microsoft.PowerShell.PSResourceGet.pdb", "FileType": "NonProduct",
Update .NET SDK to 8.0.419
Get-ChangeLog