Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
51 changes: 51 additions & 0 deletions ParallelKit.Harness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,57 @@ try {
}
Write-Host ""

# ==============================================================================
Write-Host "=== X: each worker job starts clean ===" -ForegroundColor Cyan
# Worker runspaces are reused from job to job. Each job should start in the
# caller's location, without anything an earlier job left behind.
Reset-Harness
$workerDir = Join-Path $env:TEMP ("pk-harness-worker-" + $PID)
$null = New-Item -ItemType Directory -Force -Path $workerDir
[System.IO.File]::WriteAllText((Join-Path $workerDir 'data.txt'), 'worker-data')
$cleanWorker = {
param([int]$Id)
$start = [pscustomobject]@{
Id = $Id
Location = (Get-Location).ProviderPath
Leak = (Get-Variable -Name pkHarnessLeak -ValueOnly -ErrorAction SilentlyContinue)
EAP = [string]$ErrorActionPreference
Data = (Get-Content -LiteralPath '.\data.txt' -ErrorAction SilentlyContinue)
}
# Everything that used to carry over to the next job on the same runspace.
Set-Location -LiteralPath $env:SystemRoot
$pkHarnessLeak = "from job " + $Id
$ErrorActionPreference = 'Stop'
$start
}
Push-Location -LiteralPath $workerDir
$workerDir = (Get-Location).ProviderPath # the long form; %TEMP% can be an 8.3 path
try {
$seen = [System.Collections.Generic.List[object]]::new()
$null = Invoke-PkRunspaceJobs -MaxParallel 1 -JobParams (New-JobList -Jobs @(@{ Id = 1 }, @{ Id = 2 }, @{ Id = 3 })) `
-WorkerScript $cleanWorker -ResultHandler { param($r) $seen.Add($r) }.GetNewClosure()
$badLocation = @($seen | Where-Object { $_.Location -ine $workerDir } | ForEach-Object { "job " + $_.Id + "=" + $_.Location })
Test-Assert ($seen.Count -eq 3 -and $badLocation.Count -eq 0) ("each worker job starts in the caller's location, even after an earlier job changed folder (wrong: " + ($badLocation -join ', ') + ")")
Test-Assert (@($seen | Where-Object { $_.Data -ne 'worker-data' }).Count -eq 0) "a relative path in a worker cmdlet is relative to the caller's location"
$leaked = @($seen | Where-Object { $null -ne $_.Leak -or $_.EAP -ne 'Continue' } | ForEach-Object { "job " + $_.Id + ": leak=" + $_.Leak + " EAP=" + $_.EAP })
Test-Assert ($leaked.Count -eq 0) ("variables a job sets, preference variables included, don't carry over to the next job (" + ($leaked -join '; ') + ")")

$null = New-PSDrive -Name PkHarnessJobs -PSProvider FileSystem -Root $workerDir -Scope Script
try {
Push-Location -LiteralPath 'PkHarnessJobs:\'
try {
$onDrive = [System.Collections.Generic.List[object]]::new()
$null = Invoke-PkRunspaceJobs -MaxParallel 1 -JobParams (New-JobList -Jobs @(@{ Id = 1 })) `
-WorkerScript $cleanWorker -ResultHandler { param($r) $onDrive.Add($r) }.GetNewClosure()
} finally { Pop-Location }
} finally { Remove-PSDrive -Name PkHarnessJobs -Scope Script }
Test-Assert ($onDrive.Count -eq 1 -and $onDrive[0].Location -ieq $workerDir) ("from a New-PSDrive drive, workers start in the real folder behind it (" + @($onDrive | ForEach-Object { $_.Location }) + ")")
} finally {
Pop-Location
Remove-Item -LiteralPath $workerDir -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host ""

Set-PkMessageHandler -Handler $null
Reset-PkAbortState

Expand Down
29 changes: 26 additions & 3 deletions ParallelKit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,8 @@ function Invoke-PkRunspaceJobs {
# Each job hashtable's keys become the worker's parameters. A worker runspace
# starts empty apart from cmdlets, installed modules and the kit's own worker
# functions, so name every function of yours it calls in -SharedFunctionNames.
# Each job starts in your current location, in a fresh scope: a folder change
# or a variable from an earlier job doesn't carry over.
# The worker code is checked against the worker rules (see the README)
# before anything starts, and each job's values before that job starts;
# code or values that break them are refused.
Expand Down Expand Up @@ -1983,6 +1985,22 @@ function Invoke-PkRunspaceJobs {
$pool = [RunspaceFactory]::CreateRunspacePool(1, $MaxParallel, $iss, $Host)
$pool.Open()

# Each job starts in the caller's current location, the folder
# Start-PkProcess defaults to as well. The real folder, not a drive
# name: a New-PSDrive drive doesn't exist in the worker.
$workerLocation = $ExecutionContext.SessionState.Path.CurrentFileSystemLocation.ProviderPath
# Every job runs this: move to that folder, then run the worker in a
# scope of its own with the job's values. Set per job because
# runspaces are reused: a job that changed folder would otherwise move
# the next one too. One script rather than a Set-Location statement
# ahead of the worker: with a second statement, a pool runspace's
# pipeline hadn't completed after 10 s in testing, on 5.1 and 7. The
# prefix shares the worker's first line, so error line numbers still
# match its code. If the folder is gone by the time a job starts, the
# job runs wherever its runspace already is, without an error per job.
$jobScript = 'param([string]$PkJobLocation, [hashtable]$PkJobValues) if ($PkJobLocation) { Set-Location -LiteralPath $PkJobLocation -ErrorAction SilentlyContinue }; & {' +
$WorkerScript.ToString() + "`n} @PkJobValues"

while ((-not $scanExhausted -or $handles.Count -gt 0) -and -not $Cts.Token.IsCancellationRequested) {

while (-not $scanExhausted -and $handles.Count -lt $MaxParallel -and -not $Cts.Token.IsCancellationRequested) {
Expand Down Expand Up @@ -2020,11 +2038,16 @@ function Invoke-PkRunspaceJobs {

$ps = [PowerShell]::Create()
$ps.RunspacePool = $pool
$null = $ps.AddScript($WorkerScript)
foreach ($key in $nextJob.Keys) { $null = $ps.AddParameter($key, $nextJob[$key]) }
$jobValues = @{}
foreach ($key in $nextJob.Keys) { $jobValues[$key] = $nextJob[$key] }
if ($declaredParams.Contains('CancelToken') -and -not $nextJob.ContainsKey('CancelToken')) {
$null = $ps.AddParameter("CancelToken", $Cts.Token)
$jobValues['CancelToken'] = $Cts.Token
}
$jobLocation = ""
if ([System.IO.Directory]::Exists($workerLocation)) { $jobLocation = $workerLocation }
# A local scope too, so variables a job sets (preference
# variables included) don't carry over to the next job.
$null = $ps.AddScript($jobScript, $true).AddParameter('PkJobLocation', $jobLocation).AddParameter('PkJobValues', $jobValues)
$handles.Add(@{ PS = $ps; Handle = $ps.BeginInvoke() })
}

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ Each worker runs in its own runspace: a separate PowerShell session inside the s
- If the worker declares a `$CancelToken` parameter, it gets the run's cancellation token.
- `-JobParams` takes a list or array of job hashtables. A single hashtable works too, so a list that happens to hold one job is fine.
- A worker runspace starts empty. Name every one of your functions it calls in `-SharedFunctionNames`. The kit's own worker functions are always there, and cmdlets and functions from installed modules load on their own. A module you imported by path isn't installed, so list its functions too (or put it on `$env:PSModulePath`).
- Each job starts in your current location (the real folder, if you're on a `New-PSDrive` drive), in a fresh scope. A folder change or a variable one job makes, preference variables like `$ErrorActionPreference` included, doesn't carry over to the next job. Relative paths in .NET calls such as `[System.IO.File]::ReadAllText('x.txt')` still go by the process's working directory, as they do outside workers, so give .NET full paths.
- Whatever the worker returns is handed to `-ResultHandler` on the main thread, once per object returned.
- `-NextJobProvider { ... }` can replace `-JobParams` to produce jobs lazily: it's called on the main thread for the next job hashtable, and returns `$null` when there are no more.
- `-PollCallback { ... }` runs on the main thread on every loop pass, for progress display and the like.
Expand Down
Loading