diff --git a/assets/files.wxs b/assets/files.wxs
index 14483912ea..1f593873d2 100644
--- a/assets/files.wxs
+++ b/assets/files.wxs
@@ -1825,6 +1825,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -2195,6 +2205,8 @@
+
+
diff --git a/src/Modules/PSGalleryModules.csproj b/src/Modules/PSGalleryModules.csproj
index 7087e08195..3ba7aebf45 100644
--- a/src/Modules/PSGalleryModules.csproj
+++ b/src/Modules/PSGalleryModules.csproj
@@ -7,6 +7,7 @@
+
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1
index 3660606284..25421dc978 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1
@@ -645,6 +645,78 @@ try
}
}
+ Describe "ThreadJob Constrained Language Tests" -Tags 'Feature','RequireAdminOnWindows' {
+
+ BeforeAll {
+
+ $sb = { $ExecutionContext.SessionState.LanguageMode }
+ }
+
+ It "ThreadJob script must run in ConstrainedLanguage mode with system lock down" {
+
+ try
+ {
+ $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
+ Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
+
+ $results = Start-ThreadJob -ScriptBlock { $ExecutionContext.SessionState.LanguageMode } | Wait-Job | Receive-Job
+ $results | Should BeExactly "ConstrainedLanguage"
+ }
+ finally
+ {
+ Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
+ }
+ }
+
+ It "ThreadJob script block using variable must run in ConstrainedLanguage mode with system lock down" {
+
+ try
+ {
+ $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
+ Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
+
+ $results = Start-ThreadJob -ScriptBlock { & $using:sb } | Wait-Job | Receive-Job
+ $results | Should BeExactly "ConstrainedLanguage"
+ }
+ finally
+ {
+ Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
+ }
+ }
+
+ It "ThreadJob script block argument variable must run in ConstrainedLanguage mode with system lock down" {
+
+ try
+ {
+ $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
+ Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
+
+ $results = Start-ThreadJob -ScriptBlock { param ($sb) & $sb } -ArgumentList $sb | Wait-Job | Receive-Job
+ $results | Should BeExactly "ConstrainedLanguage"
+ }
+ finally
+ {
+ Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
+ }
+ }
+
+ It "ThreadJob script block piped variable must run in ConstrainedLanguage mode with system lock down" {
+
+ try
+ {
+ $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
+ Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
+
+ $results = $sb | Start-ThreadJob -ScriptBlock { $input | foreach { & $_ } } | Wait-Job | Receive-Job
+ $results | Should BeExactly "ConstrainedLanguage"
+ }
+ finally
+ {
+ Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
+ }
+ }
+ }
+
# End Describe blocks
}
finally
diff --git a/test/powershell/Modules/ThreadJob/ThreadJob.Tests.ps1 b/test/powershell/Modules/ThreadJob/ThreadJob.Tests.ps1
new file mode 100644
index 0000000000..e25d1f0ff8
--- /dev/null
+++ b/test/powershell/Modules/ThreadJob/ThreadJob.Tests.ps1
@@ -0,0 +1,373 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+Describe 'Basic ThreadJob Tests' -Tags 'CI' {
+
+ BeforeAll {
+
+ $scriptFilePath1 = Join-Path $testdrive "TestThreadJobFile1.ps1"
+ @'
+ for ($i=0; $i -lt 10; $i++)
+ {
+ Write-Output "Hello $i"
+ }
+'@ > $scriptFilePath1
+
+ $scriptFilePath2 = Join-Path $testdrive "TestThreadJobFile2.ps1"
+ @'
+ param ($arg1, $arg2)
+ Write-Output $arg1
+ Write-Output $arg2
+'@ > $scriptFilePath2
+
+ $scriptFilePath3 = Join-Path $testdrive "TestThreadJobFile3.ps1"
+ @'
+ $input | foreach {
+ Write-Output $_
+ }
+'@ > $scriptFilePath3
+
+ $scriptFilePath4 = Join-Path $testdrive "TestThreadJobFile4.ps1"
+ @'
+ Write-Output $using:Var1
+ Write-Output $($using:Array1)[2]
+ Write-Output @(,$using:Array1)
+'@ > $scriptFilePath4
+
+ $scriptFilePath5 = Join-Path $testdrive "TestThreadJobFile5.ps1"
+ @'
+ param ([string]$param1)
+ Write-Output "$param1 $using:Var1 $using:Var2"
+'@ > $scriptFilePath5
+ }
+
+ AfterEach {
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ }
+
+ It 'ThreadJob with ScriptBlock' {
+
+ $job = Start-ThreadJob -ScriptBlock { "Hello" }
+ $results = $job | Receive-Job -Wait
+ $results | Should -Be "Hello"
+ }
+
+ It 'ThreadJob with ScriptBlock and Initialization script' {
+
+ $job = Start-ThreadJob -ScriptBlock { "Goodbye" } -InitializationScript { "Hello" }
+ $results = $job | Receive-Job -Wait
+ $results[0] | Should -Be "Hello"
+ $results[1] | Should -Be "Goodbye"
+ }
+
+ It 'ThreadJob with ScriptBlock and Argument list' {
+
+ $job = Start-ThreadJob -ScriptBlock { param ($arg1, $arg2) $arg1; $arg2 } -ArgumentList @("Hello","Goodbye")
+ $results = $job | Receive-Job -Wait
+ $results[0] | Should -Be "Hello"
+ $results[1] | Should -Be "Goodbye"
+ }
+
+ It 'ThreadJob with ScriptBlock and piped input' {
+
+ $job = "Hello","Goodbye" | Start-ThreadJob -ScriptBlock { $input | foreach { $_ } }
+ $results = $job | Receive-Job -Wait
+ $results[0] | Should -Be "Hello"
+ $results[1] | Should -Be "Goodbye"
+ }
+
+ It 'ThreadJob with ScriptBlock and Using variables' {
+
+ $Var1 = "Hello"
+ $Var2 = "Goodbye"
+ $Var3 = 102
+ $Var4 = 1..5
+ $global:GVar1 = "GlobalVar"
+ $job = Start-ThreadJob -ScriptBlock {
+ Write-Output $using:Var1
+ Write-Output $using:Var2
+ Write-Output $using:Var3
+ Write-Output ($using:Var4)[1]
+ Write-Output @(,$using:Var4)
+ Write-Output $using:GVar1
+ }
+
+ $results = $job | Receive-Job -Wait
+ $results[0] | Should -Be $Var1
+ $results[1] | Should -Be $Var2
+ $results[2] | Should -Be $Var3
+ $results[3] | Should -Be 2
+ $results[4] | Should -Be $Var4
+ $results[5] | Should -Be $global:GVar1
+ }
+
+ It 'ThreadJob with ScriptBlock and Using variables and argument list' {
+
+ $Var1 = "Hello"
+ $Var2 = 52
+ $job = Start-ThreadJob -ScriptBlock {
+ param ([string] $param1)
+
+ "$using:Var1 $param1 $using:Var2"
+ } -ArgumentList "There"
+
+ $results = $job | Receive-Job -Wait
+ $results | Should -Be "Hello There 52"
+ }
+
+ It 'ThreadJob with ScriptFile' {
+
+ $job = Start-ThreadJob -FilePath $scriptFilePath1
+ $results = $job | Receive-Job -Wait
+ $results.Count | Should -Be 10
+ $results[9] | Should -Be "Hello 9"
+ }
+
+ It 'ThreadJob with ScriptFile and Initialization script' {
+
+ $job = Start-ThreadJob -FilePath $scriptFilePath1 -Initialization { "Goodbye" }
+ $results = $job | Receive-Job -Wait
+ $results.Count | Should -Be 11
+ $results[0] | Should -Be "Goodbye"
+ }
+
+ It 'ThreadJob with ScriptFile and Argument list' {
+
+ $job = Start-ThreadJob -FilePath $scriptFilePath2 -ArgumentList @("Hello","Goodbye")
+ $results = $job | Receive-Job -Wait
+ $results[0] | Should -Be "Hello"
+ $results[1] | Should -Be "Goodbye"
+ }
+
+ It 'ThreadJob with ScriptFile and piped input' {
+
+ $job = "Hello","Goodbye" | Start-ThreadJob -FilePath $scriptFilePath3
+ $results = $job | Receive-Job -Wait
+ $results[0] | Should -Be "Hello"
+ $results[1] | Should -Be "Goodbye"
+ }
+
+ It 'ThreadJob with ScriptFile and Using variables' {
+
+ $Var1 = "Hello!"
+ $Array1 = 1..10
+
+ $job = Start-ThreadJob -FilePath $scriptFilePath4
+ $results = $job | Receive-Job -Wait
+ $results[0] | Should -Be $Var1
+ $results[1] | Should -Be 3
+ $results[2] | Should -Be $Array1
+ }
+
+ It 'ThreadJob with ScriptFile and Using variables with argument list' {
+
+ $Var1 = "There"
+ $Var2 = 60
+ $job = Start-ThreadJob -FilePath $scriptFilePath5 -ArgumentList "Hello"
+ $results = $job | Receive-Job -Wait
+ $results | Should -Be "Hello There 60"
+ }
+
+ It 'ThreadJob with terminating error' {
+
+ $job = Start-ThreadJob -ScriptBlock { throw "MyError!" }
+ $job | Wait-Job
+ $job.JobStateInfo.Reason.Message | Should -Be "MyError!"
+ }
+
+ It 'ThreadJob and Error stream output' {
+
+ $job = Start-ThreadJob -ScriptBlock { Write-Error "ErrorOut" } | Wait-Job
+ $job.Error | Should -Be "ErrorOut"
+ }
+
+ It 'ThreadJob and Warning stream output' {
+
+ $job = Start-ThreadJob -ScriptBlock { Write-Warning "WarningOut" } | Wait-Job
+ $job.Warning | Should -Be "WarningOut"
+ }
+
+ It 'ThreadJob and Verbose stream output' {
+
+ $job = Start-ThreadJob -ScriptBlock { $VerbosePreference = 'Continue'; Write-Verbose "VerboseOut" } | Wait-Job
+ $job.Verbose | Should Match "VerboseOut"
+ }
+
+ It 'ThreadJob and Verbose stream output' {
+
+ $job = Start-ThreadJob -ScriptBlock { $DebugPreference = 'Continue'; Write-Debug "DebugOut" } | Wait-Job
+ $job.Debug | Should -Be "DebugOut"
+ }
+
+ It 'ThreadJob ThrottleLimit and Queue' {
+
+ try
+ {
+ # Start four thread jobs with ThrottleLimit set to two
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 2
+ Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 }
+ Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 }
+ Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 }
+
+ # Allow jobs to start
+ Start-Sleep -Seconds 1
+
+ $numRunningThreadJobs = (Get-Job | where { ($_.PSJobTypeName -eq "ThreadJob") -and ($_.State -eq "Running") }).Count
+ $numQueuedThreadJobs = (Get-Job | where { ($_.PSJobTypeName -eq "ThreadJob") -and ($_.State -eq "NotStarted") }).Count
+
+ $numRunningThreadJobs | Should -Be 2
+ $numQueuedThreadJobs | Should -Be 2
+ }
+ finally
+ {
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ }
+
+ $numThreadJobs = (Get-Job | where PSJobTypeName -eq "ThreadJob").Count
+ $numThreadJobs | Should -Be 0
+ }
+
+ It 'ThreadJob Runspaces should be cleaned up at completion' {
+
+ try
+ {
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ $rsStartCount = (Get-Runspace).Count
+
+ # Start four thread jobs with ThrottleLimit set to two
+ $Job1 = Start-ThreadJob -ScriptBlock { "Hello 1!" } -ThrottleLimit 5
+ $job2 = Start-ThreadJob -ScriptBlock { "Hello 2!" }
+ $job3 = Start-ThreadJob -ScriptBlock { "Hello 3!" }
+ $job4 = Start-ThreadJob -ScriptBlock { "Hello 4!" }
+
+ $job1,$job2,$job3,$job4 | Wait-Job
+
+ # Allow for clean to happen
+ Start-Sleep -Seconds 1
+
+ (Get-Runspace).Count | Should -Be $rsStartCount
+ }
+ finally
+ {
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ }
+ }
+
+ It 'ThreadJob Runspaces should be cleaned up after job removal' {
+
+ try {
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ $rsStartCount = (Get-Runspace).Count
+
+ # Start four thread jobs with ThrottleLimit set to two
+ $Job1 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 2
+ $job2 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 }
+ $job3 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 }
+ $job4 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 }
+
+ # Allow jobs to start
+ Start-Sleep -Seconds 1
+
+ (Get-Runspace).Count | Should -Be ($rsStartCount + 4)
+
+ # Stop two jobs
+ $job1 | Remove-Job -Force
+ $job3 | Remove-Job -Force
+
+ (Get-Runspace).Count | Should -Be ($rsStartCount + 2)
+
+ }
+ finally
+ {
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ }
+
+ (Get-Runspace).Count | Should -Be $rsStartCount
+ }
+
+ It 'ThreadJob jobs should work with Receive-Job -AutoRemoveJob' {
+
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+
+ $job1 = Start-ThreadJob -ScriptBlock { 1..2 | foreach { Start-Sleep -Seconds 1; "Output $_" } } -ThrottleLimit 2
+ $job2 = Start-ThreadJob -ScriptBlock { 1..2 | foreach { Start-Sleep -Seconds 1; "Output $_" } }
+ $job3 = Start-ThreadJob -ScriptBlock { 1..2 | foreach { Start-Sleep -Seconds 1; "Output $_" } }
+ $job4 = Start-ThreadJob -ScriptBlock { 1..2 | foreach { Start-Sleep -Seconds 1; "Output $_" } }
+
+ $null = $job1,$job2,$job3,$job4 | Receive-Job -Wait -AutoRemoveJob
+
+ (Get-Job | where PSJobTypeName -eq "ThreadJob").Count | Should -Be 0
+ }
+
+ It 'ThreadJob jobs should run in FullLanguage mode by default' {
+
+ $result = Start-ThreadJob -ScriptBlock { $ExecutionContext.SessionState.LanguageMode } | Wait-Job | Receive-Job
+ $result | Should -Be "FullLanguage"
+ }
+}
+
+Describe 'Job2 class API tests' -Tags 'CI' {
+
+ AfterEach {
+ Get-Job | where PSJobTypeName -eq "ThreadJob" | Remove-Job -Force
+ }
+
+ It 'Verifies StopJob API' {
+
+ $job = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 5
+ $job.StopJob($true, "No Reason")
+ $job.JobStateInfo.State | Should -Be "Stopped"
+ }
+
+ It 'Verifies StopJobAsync API' {
+
+ $job = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 5
+ $job.StopJobAsync($true, "No Reason")
+ Wait-Job $job
+ $job.JobStateInfo.State | Should -Be "Stopped"
+ }
+
+ It 'Verifies StartJobAsync API' {
+
+ $jobRunning = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 1
+ $jobNotRunning = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 }
+
+ $jobNotRunning.JobStateInfo.State | Should -Be "NotStarted"
+
+ # StartJobAsync starts jobs synchronously for ThreadJob jobs
+ $jobNotRunning.StartJobAsync()
+ $jobNotRunning.JobStateInfo.State | Should -Be "Running"
+ }
+
+ It 'Verifies JobSourceAdapter Get-Jobs' {
+
+ $job = Start-ThreadJob -ScriptBlock { "Hello" } | Wait-Job
+
+ $getJob = Get-Job -InstanceId $job.InstanceId 2>$null
+ $getJob | Should -Be $job
+
+ $getJob = Get-Job -Name $job.Name 2>$null
+ $getJob | Should -Be $job
+
+ $getJob = Get-Job -Command ' "hello" ' 2>$null
+ $getJob | Should -Be $job
+
+ $getJob = Get-Job -State $job.JobStateInfo.State 2>$null
+ $getJob | Should -Be $job
+
+ $getJob = Get-Job -Id $job.Id 2>$null
+ $getJob | Should -Be $job
+
+ # Get-Job -Filter is not supported
+ $result = Get-Job -Filter @{Id = ($job.Id)} 3>$null
+ $result | Should -BeNullOrEmpty
+ }
+
+ It 'Verifies terminating job error' {
+
+ $job = Start-ThreadJob -ScriptBlock { throw "My Job Error!" } | Wait-Job
+ $results = $job | Receive-Job 2>&1
+ $results.ToString() | Should -Be "My Job Error!"
+ }
+}